UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: зона вложений в карточке деталей
This commit is contained in:
parent
21a9d2b809
commit
5f2d543cab
|
|
@ -59,7 +59,8 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList
|
||||||
// file size
|
// file size
|
||||||
const { maxFileSize } = useFileSize();
|
const { maxFileSize } = useFileSize();
|
||||||
// derived values
|
// derived values
|
||||||
const issueAttachments = getAttachmentsByIssueId(issueId);
|
const issueAttachments = getAttachmentsByIssueId(issueId) ?? [];
|
||||||
|
const hasAttachmentRows = issueAttachments.length > 0 || !!uploadStatus?.length;
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
const handleFetchPropertyActivities = useCallback(() => {
|
const handleFetchPropertyActivities = useCallback(() => {
|
||||||
|
|
@ -68,80 +69,96 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList
|
||||||
|
|
||||||
const onDrop = useCallback(
|
const onDrop = useCallback(
|
||||||
(acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
|
(acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
|
||||||
const totalAttachedFiles = acceptedFiles.length + rejectedFiles.length;
|
if (acceptedFiles.length > 0) {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
if (rejectedFiles.length === 0) {
|
|
||||||
const currentFile: File = acceptedFiles[0];
|
|
||||||
if (!currentFile || !workspaceSlug) return;
|
|
||||||
|
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
createAttachment(currentFile)
|
|
||||||
.catch(() => {
|
Promise.allSettled(acceptedFiles.map((file) => createAttachment(file)))
|
||||||
setToast({
|
.then((results) => {
|
||||||
type: TOAST_TYPE.ERROR,
|
const failedUploads = results.filter((result) => result.status === "rejected").length;
|
||||||
title: t("toast.error"),
|
if (failedUploads > 0)
|
||||||
message: t("attachment.error"),
|
setToast({
|
||||||
});
|
type: TOAST_TYPE.ERROR,
|
||||||
|
title: t("toast.error"),
|
||||||
|
message: t("attachment.error"),
|
||||||
|
});
|
||||||
|
return undefined;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
handleFetchPropertyActivities();
|
handleFetchPropertyActivities();
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setToast({
|
if (rejectedFiles.length > 0)
|
||||||
type: TOAST_TYPE.ERROR,
|
setToast({
|
||||||
title: t("toast.error"),
|
type: TOAST_TYPE.ERROR,
|
||||||
message:
|
title: t("toast.error"),
|
||||||
totalAttachedFiles > 1
|
message: t("attachment.file_size_limit", { size: maxFileSize / 1024 / 1024 }),
|
||||||
? t("attachment.only_one_file_allowed")
|
});
|
||||||
: t("attachment.file_size_limit", { size: maxFileSize / 1024 / 1024 }),
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
[createAttachment, maxFileSize, workspaceSlug, handleFetchPropertyActivities]
|
[createAttachment, maxFileSize, workspaceSlug, handleFetchPropertyActivities, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive, isDragReject } = useDropzone({
|
||||||
onDrop,
|
onDrop,
|
||||||
maxSize: maxFileSize,
|
maxSize: maxFileSize,
|
||||||
multiple: false,
|
multiple: true,
|
||||||
disabled: isUploading || disabled,
|
disabled: isUploading || disabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="space-y-3">
|
||||||
{uploadStatus?.map((uploadStatus) => (
|
{attachmentDeleteModalId && (
|
||||||
<IssueAttachmentsUploadItem key={uploadStatus.id} uploadStatus={uploadStatus} />
|
<IssueAttachmentDeleteModal
|
||||||
))}
|
isOpen={Boolean(attachmentDeleteModalId)}
|
||||||
{issueAttachments && (
|
onClose={() => toggleDeleteAttachmentModal(null)}
|
||||||
<>
|
attachmentOperations={attachmentOperations}
|
||||||
{attachmentDeleteModalId && (
|
attachmentId={attachmentDeleteModalId}
|
||||||
<IssueAttachmentDeleteModal
|
issueServiceType={issueServiceType}
|
||||||
isOpen={Boolean(attachmentDeleteModalId)}
|
/>
|
||||||
onClose={() => toggleDeleteAttachmentModal(null)}
|
)}
|
||||||
attachmentOperations={attachmentOperations}
|
|
||||||
attachmentId={attachmentDeleteModalId}
|
<div
|
||||||
issueServiceType={issueServiceType}
|
{...getRootProps()}
|
||||||
/>
|
data-drag-active={isDragActive ? "true" : "false"}
|
||||||
)}
|
data-drag-reject={isDragReject ? "true" : "false"}
|
||||||
<div
|
className={`nodedc-attachment-upload flex w-full items-center justify-between gap-4 px-5 py-4 ${
|
||||||
{...getRootProps()}
|
disabled ? "cursor-not-allowed opacity-70" : "cursor-pointer"
|
||||||
className={`relative flex flex-col ${isDragActive && issueAttachments.length < 3 ? "min-h-[200px]" : ""} ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`}
|
}`}
|
||||||
>
|
>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
{isDragActive && (
|
<div className="flex min-w-0 items-center gap-4">
|
||||||
<div className="absolute top-0 left-0 z-30 flex h-full w-full items-center justify-center bg-surface-2/75">
|
<div className="grid size-11 flex-shrink-0 place-items-center rounded-2xl bg-[rgba(var(--nodedc-accent-rgb),0.14)] text-[rgb(var(--nodedc-accent-rgb))]">
|
||||||
<div className="flex items-center justify-center rounded-md bg-surface-1 p-1">
|
<UploadCloud className="size-5" />
|
||||||
<div className="flex flex-col items-center justify-center rounded-md border border-dashed border-strong px-5 py-6">
|
</div>
|
||||||
<UploadCloud className="size-7" />
|
<div className="min-w-0">
|
||||||
<span className="text-13 text-tertiary">{t("attachment.drag_and_drop")}</span>
|
<div className="truncate text-14 font-semibold text-primary">
|
||||||
</div>
|
{isDragActive ? "Отпустите файлы здесь" : "Перетащите файлы сюда или нажмите для выбора"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mt-1 truncate text-12 text-tertiary">
|
||||||
)}
|
Любой формат, несколько файлов за раз, до {maxFileSize / 1024 / 1024} МБ на файл
|
||||||
{issueAttachments?.map((attachmentId) => (
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="hidden flex-shrink-0 rounded-full bg-[rgba(var(--nodedc-accent-rgb),0.12)] px-3 py-1 text-12 font-semibold text-[rgb(var(--nodedc-accent-rgb))] sm:block">
|
||||||
|
Выбрать
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasAttachmentRows && (
|
||||||
|
<div className="rounded-[1.35rem] border border-subtle bg-surface-1/60 p-3 shadow-[0_12px_28px_rgba(0,0,0,0.08)]">
|
||||||
|
<div className="mb-2 flex items-center justify-between gap-3 px-1">
|
||||||
|
<div className="text-12 font-semibold uppercase tracking-[0.08em] text-tertiary">Вложения</div>
|
||||||
|
<div className="text-12 text-tertiary">{issueAttachments.length}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 overflow-x-auto pb-1">
|
||||||
|
{uploadStatus?.map((status) => (
|
||||||
|
<IssueAttachmentsUploadItem key={status.id} uploadStatus={status} />
|
||||||
|
))}
|
||||||
|
{issueAttachments.map((attachmentId) => (
|
||||||
<IssueAttachmentsListItem
|
<IssueAttachmentsListItem
|
||||||
key={attachmentId}
|
key={attachmentId}
|
||||||
attachmentId={attachmentId}
|
attachmentId={attachmentId}
|
||||||
|
|
@ -150,8 +167,8 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
||||||
|
|
@ -14,8 +15,9 @@ import type { TIssueServiceType } from "@plane/types";
|
||||||
import { EIssueServiceType } from "@plane/types";
|
import { EIssueServiceType } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import type { TContextMenuItem } from "@plane/ui";
|
import type { TContextMenuItem } from "@plane/ui";
|
||||||
import { ActionDropdown } from "@plane/ui";
|
import { ActionDropdown, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||||
import { convertBytesToSize, getFileExtension, getFileName, getFileURL, renderFormattedDate } from "@plane/utils";
|
import { convertBytesToSize, getFileExtension, getFileName, getFileURL, renderFormattedDate } from "@plane/utils";
|
||||||
|
import { Download, FileText, ImageIcon, Play, X } from "lucide-react";
|
||||||
// components
|
// components
|
||||||
//
|
//
|
||||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||||
|
|
@ -32,6 +34,18 @@ type TIssueAttachmentsListItem = {
|
||||||
issueServiceType?: TIssueServiceType;
|
issueServiceType?: TIssueServiceType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const IMAGE_EXTENSIONS = new Set(["apng", "avif", "bmp", "gif", "jpg", "jpeg", "png", "svg", "webp"]);
|
||||||
|
const VIDEO_EXTENSIONS = new Set(["avi", "m4v", "mov", "mp4", "mpeg", "mpg", "ogv", "webm"]);
|
||||||
|
const PDF_EXTENSIONS = new Set(["pdf"]);
|
||||||
|
|
||||||
|
const getPreviewType = (extension: string) => {
|
||||||
|
const normalizedExtension = extension.toLowerCase();
|
||||||
|
if (IMAGE_EXTENSIONS.has(normalizedExtension)) return "image";
|
||||||
|
if (VIDEO_EXTENSIONS.has(normalizedExtension)) return "video";
|
||||||
|
if (PDF_EXTENSIONS.has(normalizedExtension)) return "pdf";
|
||||||
|
return "file";
|
||||||
|
};
|
||||||
|
|
||||||
export const IssueAttachmentsListItem = observer(function IssueAttachmentsListItem(props: TIssueAttachmentsListItem) {
|
export const IssueAttachmentsListItem = observer(function IssueAttachmentsListItem(props: TIssueAttachmentsListItem) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// props
|
// props
|
||||||
|
|
@ -46,8 +60,11 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
||||||
const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined;
|
const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined;
|
||||||
const fileName = getFileName(attachment?.attributes.name ?? "");
|
const fileName = getFileName(attachment?.attributes.name ?? "");
|
||||||
const fileExtension = getFileExtension(attachment?.attributes.name ?? "");
|
const fileExtension = getFileExtension(attachment?.attributes.name ?? "");
|
||||||
const fileIcon = getFileIcon(fileExtension, 18);
|
const fullFileName = fileExtension ? `${fileName}.${fileExtension}` : fileName;
|
||||||
|
const previewType = getPreviewType(fileExtension);
|
||||||
|
const fileIcon = getFileIcon(fileExtension, 32);
|
||||||
const fileURL = getFileURL(attachment?.asset_url ?? "");
|
const fileURL = getFileURL(attachment?.asset_url ?? "");
|
||||||
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||||
const menuItems: TContextMenuItem[] = [
|
const menuItems: TContextMenuItem[] = [
|
||||||
{
|
{
|
||||||
key: "delete",
|
key: "delete",
|
||||||
|
|
@ -64,56 +81,135 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
||||||
if (!attachment) return <></>;
|
if (!attachment) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
role="button"
|
<div className="group flex h-32 w-72 flex-shrink-0 overflow-hidden rounded-2xl border border-subtle bg-surface-2/80 transition hover:border-[rgba(var(--nodedc-accent-rgb),0.45)] hover:bg-surface-2">
|
||||||
tabIndex={0}
|
<button
|
||||||
onClick={(e) => {
|
type="button"
|
||||||
e.preventDefault();
|
className="flex min-w-0 flex-1 items-stretch text-left"
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
window.open(fileURL, "_blank");
|
e.preventDefault();
|
||||||
}}
|
e.stopPropagation();
|
||||||
onKeyDown={(e) => {
|
setIsPreviewOpen(true);
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
}}
|
||||||
e.preventDefault();
|
>
|
||||||
e.stopPropagation();
|
<div className="relative h-full w-28 flex-shrink-0 overflow-hidden bg-surface-1">
|
||||||
window.open(fileURL, "_blank");
|
{previewType === "image" && fileURL ? (
|
||||||
}
|
<img src={fileURL} alt={fullFileName} className="h-full w-full object-cover" loading="lazy" />
|
||||||
}}
|
) : previewType === "video" && fileURL ? (
|
||||||
>
|
|
||||||
<div className="group flex h-11 items-center justify-between gap-3 pr-2 pl-9 hover:bg-surface-2">
|
|
||||||
<div className="flex items-center gap-3 truncate text-13">
|
|
||||||
<div className="flex items-center gap-3">{fileIcon}</div>
|
|
||||||
<Tooltip tooltipContent={`${fileName}.${fileExtension}`} isMobile={isMobile}>
|
|
||||||
<p className="truncate font-medium text-secondary">{`${fileName}.${fileExtension}`}</p>
|
|
||||||
</Tooltip>
|
|
||||||
<span className="flex size-1.5 rounded-full bg-layer-1" />
|
|
||||||
<span className="flex-shrink-0 text-placeholder">{convertBytesToSize(attachment.attributes.size)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{attachment?.created_by && (
|
|
||||||
<>
|
<>
|
||||||
<Tooltip
|
<video src={fileURL} className="h-full w-full object-cover" muted playsInline preload="metadata" />
|
||||||
isMobile={isMobile}
|
<div className="absolute inset-0 grid place-items-center bg-black/20 text-white">
|
||||||
tooltipContent={`${
|
<Play className="size-6 fill-current" />
|
||||||
getUserDetails(attachment?.created_by)?.display_name ?? ""
|
</div>
|
||||||
} uploaded on ${renderFormattedDate(attachment.updated_at)}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<ButtonAvatars showTooltip userIds={attachment?.created_by} />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
</>
|
||||||
|
) : previewType === "pdf" && fileURL ? (
|
||||||
|
<div className="flex h-full w-full items-center justify-center bg-white text-red-500">
|
||||||
|
<FileText className="size-9" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
{previewType === "file" ? fileIcon : <ImageIcon className="size-8 text-tertiary" />}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ActionDropdown
|
|
||||||
items={menuItems}
|
|
||||||
buttonClassName={getIconButtonStyling("ghost", "sm")}
|
|
||||||
placement="bottom-end"
|
|
||||||
disabled={!!disabled}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col justify-between p-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<Tooltip tooltipContent={fullFileName} isMobile={isMobile}>
|
||||||
|
<p className="line-clamp-2 text-13 font-semibold text-secondary">{fullFileName}</p>
|
||||||
|
</Tooltip>
|
||||||
|
<div className="mt-2 flex items-center gap-2 text-11 text-tertiary">
|
||||||
|
<span>{fileExtension ? fileExtension.toUpperCase() : "FILE"}</span>
|
||||||
|
<span className="size-1 rounded-full bg-layer-1" />
|
||||||
|
<span>{convertBytesToSize(attachment.attributes.size)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{attachment?.created_by && (
|
||||||
|
<Tooltip
|
||||||
|
isMobile={isMobile}
|
||||||
|
tooltipContent={`${
|
||||||
|
getUserDetails(attachment?.created_by)?.display_name ?? ""
|
||||||
|
} uploaded on ${renderFormattedDate(attachment.updated_at)}`}
|
||||||
|
>
|
||||||
|
<div className="flex w-fit items-center justify-center">
|
||||||
|
<ButtonAvatars showTooltip userIds={attachment?.created_by} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex w-10 flex-shrink-0 items-start justify-center pt-2">
|
||||||
|
<ActionDropdown
|
||||||
|
items={menuItems}
|
||||||
|
buttonClassName={getIconButtonStyling("ghost", "sm")}
|
||||||
|
placement="bottom-end"
|
||||||
|
disabled={!!disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<ModalCore
|
||||||
|
isOpen={isPreviewOpen}
|
||||||
|
handleClose={() => setIsPreviewOpen(false)}
|
||||||
|
position={EModalPosition.CENTER}
|
||||||
|
width={EModalWidth.VIXL}
|
||||||
|
className="overflow-hidden border border-subtle bg-surface-1"
|
||||||
|
>
|
||||||
|
<div className="relative min-h-[55vh] bg-surface-1 p-4">
|
||||||
|
<div className="absolute top-4 right-4 z-10 flex items-center gap-2">
|
||||||
|
{fileURL && (
|
||||||
|
<a
|
||||||
|
href={fileURL}
|
||||||
|
download={fullFileName}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="grid size-10 place-items-center rounded-full bg-[rgb(var(--nodedc-accent-rgb))] text-[rgb(var(--nodedc-on-accent-rgb))] shadow-[0_14px_30px_rgba(0,0,0,0.3)] transition hover:brightness-110"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Download className="size-5" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid size-10 place-items-center rounded-full bg-black/35 text-white backdrop-blur-md transition hover:bg-black/50"
|
||||||
|
onClick={() => setIsPreviewOpen(false)}
|
||||||
|
>
|
||||||
|
<X className="size-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pr-24">
|
||||||
|
<div className="text-15 font-semibold text-primary">{fullFileName}</div>
|
||||||
|
<div className="mt-1 text-12 text-tertiary">{convertBytesToSize(attachment.attributes.size)}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex h-[70vh] max-h-[70vh] items-center justify-center overflow-hidden rounded-2xl bg-black/20">
|
||||||
|
{previewType === "image" && fileURL ? (
|
||||||
|
<img src={fileURL} alt={fullFileName} className="max-h-full max-w-full object-contain" />
|
||||||
|
) : previewType === "video" && fileURL ? (
|
||||||
|
<video src={fileURL} className="max-h-full max-w-full" controls autoPlay>
|
||||||
|
<track kind="captions" />
|
||||||
|
</video>
|
||||||
|
) : previewType === "pdf" && fileURL ? (
|
||||||
|
<iframe
|
||||||
|
title={fullFileName}
|
||||||
|
src={fileURL}
|
||||||
|
sandbox="allow-downloads allow-same-origin"
|
||||||
|
className="h-full w-full rounded-2xl bg-white"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center gap-4 p-8 text-center">
|
||||||
|
<div className="grid size-20 place-items-center rounded-3xl bg-surface-2">{fileIcon}</div>
|
||||||
|
<div className="max-w-md text-14 text-secondary">
|
||||||
|
Предпросмотр для этого формата недоступен. Файл можно скачать или открыть в новой вкладке.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalCore>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -32,18 +32,21 @@ export const IssueAttachmentsUploadItem = observer(function IssueAttachmentsUplo
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-none flex h-11 items-center justify-between gap-3 bg-surface-2 pr-2 pl-9">
|
<div className="pointer-events-none flex h-24 w-64 flex-shrink-0 items-center justify-between gap-3 rounded-2xl border border-subtle bg-surface-2 px-3 py-2">
|
||||||
<div className="flex items-center gap-3 truncate text-13">
|
<div className="flex min-w-0 items-center gap-3 text-13">
|
||||||
<div className="flex-shrink-0">{fileIcon}</div>
|
<div className="grid size-12 flex-shrink-0 place-items-center rounded-xl bg-surface-1">{fileIcon}</div>
|
||||||
<Tooltip tooltipContent={fileName} isMobile={isMobile}>
|
<div className="min-w-0">
|
||||||
<p className="truncate font-medium text-secondary">{fileName}</p>
|
<Tooltip tooltipContent={fileName} isMobile={isMobile}>
|
||||||
</Tooltip>
|
<p className="truncate font-medium text-secondary">{fileName}</p>
|
||||||
|
</Tooltip>
|
||||||
|
<p className="mt-1 text-11 text-tertiary">Загрузка</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-shrink-0 items-center gap-2">
|
<div className="flex flex-shrink-0 flex-col items-center gap-1">
|
||||||
<span className="flex-shrink-0">
|
<span className="flex-shrink-0">
|
||||||
<CircularProgressIndicator size={20} strokeWidth={3} percentage={uploadStatus.progress} />
|
<CircularProgressIndicator size={20} strokeWidth={3} percentage={uploadStatus.progress} />
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-shrink-0 text-13 font-medium">{uploadStatus.progress}% done</div>
|
<div className="flex-shrink-0 text-11 font-medium text-secondary">{uploadStatus.progress}%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,8 @@ export const IssueDetailWidgetCollapsibles = observer(function IssueDetailWidget
|
||||||
const attachmentUploads = getAttachmentsUploadStatusByIssueId(issueId);
|
const attachmentUploads = getAttachmentsUploadStatusByIssueId(issueId);
|
||||||
const attachmentsCount = getAttachmentsCountByIssueId(issueId);
|
const attachmentsCount = getAttachmentsCountByIssueId(issueId);
|
||||||
const shouldRenderAttachments =
|
const shouldRenderAttachments =
|
||||||
attachmentsCount > 0 ||
|
!hideWidgets?.includes("attachments") &&
|
||||||
(!!attachmentUploads && attachmentUploads.length > 0 && !hideWidgets?.includes("attachments"));
|
(attachmentsCount > 0 || (!!attachmentUploads && attachmentUploads.length > 0));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import React from "react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import type { TIssueServiceType, TWorkItemWidgets } from "@plane/types";
|
import type { TIssueServiceType, TWorkItemWidgets } from "@plane/types";
|
||||||
// local imports
|
// local imports
|
||||||
|
import { IssueAttachmentsCollapsibleContent } from "./attachments/content";
|
||||||
import { IssueDetailWidgetActionButtons } from "./action-buttons";
|
import { IssueDetailWidgetActionButtons } from "./action-buttons";
|
||||||
import { IssueDetailWidgetCollapsibles } from "./issue-detail-widget-collapsibles";
|
import { IssueDetailWidgetCollapsibles } from "./issue-detail-widget-collapsibles";
|
||||||
import { IssueDetailWidgetModals } from "./issue-detail-widget-modals";
|
import { IssueDetailWidgetModals } from "./issue-detail-widget-modals";
|
||||||
|
|
@ -34,6 +35,9 @@ export function IssueDetailWidgets(props: Props) {
|
||||||
hideWidgets,
|
hideWidgets,
|
||||||
compactView = false,
|
compactView = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
const hideWidgetsWithInlineAttachments = hideWidgets?.includes("attachments")
|
||||||
|
? hideWidgets
|
||||||
|
: ([...(hideWidgets ?? []), "attachments"] as TWorkItemWidgets[]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -44,16 +48,25 @@ export function IssueDetailWidgets(props: Props) {
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
issueServiceType={issueServiceType}
|
issueServiceType={issueServiceType}
|
||||||
hideWidgets={hideWidgets}
|
hideWidgets={hideWidgetsWithInlineAttachments}
|
||||||
compactView={compactView}
|
compactView={compactView}
|
||||||
/>
|
/>
|
||||||
|
{!hideWidgets?.includes("attachments") && (
|
||||||
|
<IssueAttachmentsCollapsibleContent
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
|
disabled={disabled}
|
||||||
|
issueServiceType={issueServiceType}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<IssueDetailWidgetCollapsibles
|
<IssueDetailWidgetCollapsibles
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
issueServiceType={issueServiceType}
|
issueServiceType={issueServiceType}
|
||||||
hideWidgets={hideWidgets}
|
hideWidgets={hideWidgetsWithInlineAttachments}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{renderWidgetModals && (
|
{renderWidgetModals && (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue