-
- {isDragActive && (
-
-
-
-
- {t("attachment.drag_and_drop")}
-
-
-
- )}
- {issueAttachments?.map((attachmentId) => (
+
+ {attachmentDeleteModalId && (
+
toggleDeleteAttachmentModal(null)}
+ attachmentOperations={attachmentOperations}
+ attachmentId={attachmentDeleteModalId}
+ issueServiceType={issueServiceType}
+ />
+ )}
+
+
+
+
+
+
+
+
+
+ {isDragActive ? "Отпустите файлы здесь" : "Перетащите файлы сюда или нажмите для выбора"}
+
+
+ Любой формат, несколько файлов за раз, до {maxFileSize / 1024 / 1024} МБ на файл
+
+
+
+
+ Выбрать
+
+
+
+ {hasAttachmentRows && (
+
+
+
Вложения
+
{issueAttachments.length}
+
+
+ {uploadStatus?.map((status) => (
+
+ ))}
+ {issueAttachments.map((attachmentId) => (
))}
- >
+
)}
- >
+
);
});
diff --git a/plane-src/apps/web/core/components/issues/attachment/attachment-list-item.tsx b/plane-src/apps/web/core/components/issues/attachment/attachment-list-item.tsx
index ca396aa..2b467fe 100644
--- a/plane-src/apps/web/core/components/issues/attachment/attachment-list-item.tsx
+++ b/plane-src/apps/web/core/components/issues/attachment/attachment-list-item.tsx
@@ -5,6 +5,7 @@
*/
import { observer } from "mobx-react";
+import { useState } from "react";
import { useTranslation } from "@plane/i18n";
import { getIconButtonStyling } from "@plane/propel/icon-button";
@@ -14,8 +15,9 @@ import type { TIssueServiceType } from "@plane/types";
import { EIssueServiceType } from "@plane/types";
// 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 { Download, FileText, ImageIcon, Play, X } from "lucide-react";
// components
//
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
@@ -32,6 +34,18 @@ type TIssueAttachmentsListItem = {
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) {
const { t } = useTranslation();
// props
@@ -46,8 +60,11 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined;
const fileName = getFileName(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 [isPreviewOpen, setIsPreviewOpen] = useState(false);
const menuItems: TContextMenuItem[] = [
{
key: "delete",
@@ -64,56 +81,135 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
if (!attachment) return <>>;
return (
-
{
- e.preventDefault();
- e.stopPropagation();
- window.open(fileURL, "_blank");
- }}
- onKeyDown={(e) => {
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault();
- e.stopPropagation();
- window.open(fileURL, "_blank");
- }
- }}
- >
-
-
-
{fileIcon}
-
- {`${fileName}.${fileExtension}`}
-
-
-
{convertBytesToSize(attachment.attributes.size)}
-
-
-
- {attachment?.created_by && (
+ <>
+
+
+
+
-
+
+
setIsPreviewOpen(false)}
+ position={EModalPosition.CENTER}
+ width={EModalWidth.VIXL}
+ className="overflow-hidden border border-subtle bg-surface-1"
+ >
+
+
+
+
+
{fullFileName}
+
{convertBytesToSize(attachment.attributes.size)}
+
+
+
+ {previewType === "image" && fileURL ? (
+

+ ) : previewType === "video" && fileURL ? (
+
+ ) : previewType === "pdf" && fileURL ? (
+
+ ) : (
+
+
{fileIcon}
+
+ Предпросмотр для этого формата недоступен. Файл можно скачать или открыть в новой вкладке.
+
+
+ )}
+
+
+
+ >
);
});
diff --git a/plane-src/apps/web/core/components/issues/attachment/attachment-list-upload-item.tsx b/plane-src/apps/web/core/components/issues/attachment/attachment-list-upload-item.tsx
index 4b16d8a..cbd6c2f 100644
--- a/plane-src/apps/web/core/components/issues/attachment/attachment-list-upload-item.tsx
+++ b/plane-src/apps/web/core/components/issues/attachment/attachment-list-upload-item.tsx
@@ -32,18 +32,21 @@ export const IssueAttachmentsUploadItem = observer(function IssueAttachmentsUplo
const { isMobile } = usePlatformOS();
return (
-
-
-
{fileIcon}
-
- {fileName}
-
+
+
+
{fileIcon}
+
+
+ {fileName}
+
+
Загрузка
+
-
+
-
{uploadStatus.progress}% done
+
{uploadStatus.progress}%
);
diff --git a/plane-src/apps/web/core/components/issues/issue-detail-widgets/issue-detail-widget-collapsibles.tsx b/plane-src/apps/web/core/components/issues/issue-detail-widgets/issue-detail-widget-collapsibles.tsx
index 8f741ad..7fc036a 100644
--- a/plane-src/apps/web/core/components/issues/issue-detail-widgets/issue-detail-widget-collapsibles.tsx
+++ b/plane-src/apps/web/core/components/issues/issue-detail-widgets/issue-detail-widget-collapsibles.tsx
@@ -49,8 +49,8 @@ export const IssueDetailWidgetCollapsibles = observer(function IssueDetailWidget
const attachmentUploads = getAttachmentsUploadStatusByIssueId(issueId);
const attachmentsCount = getAttachmentsCountByIssueId(issueId);
const shouldRenderAttachments =
- attachmentsCount > 0 ||
- (!!attachmentUploads && attachmentUploads.length > 0 && !hideWidgets?.includes("attachments"));
+ !hideWidgets?.includes("attachments") &&
+ (attachmentsCount > 0 || (!!attachmentUploads && attachmentUploads.length > 0));
return (
diff --git a/plane-src/apps/web/core/components/issues/issue-detail-widgets/root.tsx b/plane-src/apps/web/core/components/issues/issue-detail-widgets/root.tsx
index b918945..8b091c5 100644
--- a/plane-src/apps/web/core/components/issues/issue-detail-widgets/root.tsx
+++ b/plane-src/apps/web/core/components/issues/issue-detail-widgets/root.tsx
@@ -8,6 +8,7 @@ import React from "react";
// plane imports
import type { TIssueServiceType, TWorkItemWidgets } from "@plane/types";
// local imports
+import { IssueAttachmentsCollapsibleContent } from "./attachments/content";
import { IssueDetailWidgetActionButtons } from "./action-buttons";
import { IssueDetailWidgetCollapsibles } from "./issue-detail-widget-collapsibles";
import { IssueDetailWidgetModals } from "./issue-detail-widget-modals";
@@ -34,6 +35,9 @@ export function IssueDetailWidgets(props: Props) {
hideWidgets,
compactView = false,
} = props;
+ const hideWidgetsWithInlineAttachments = hideWidgets?.includes("attachments")
+ ? hideWidgets
+ : ([...(hideWidgets ?? []), "attachments"] as TWorkItemWidgets[]);
return (
<>
@@ -44,16 +48,25 @@ export function IssueDetailWidgets(props: Props) {
issueId={issueId}
disabled={disabled}
issueServiceType={issueServiceType}
- hideWidgets={hideWidgets}
+ hideWidgets={hideWidgetsWithInlineAttachments}
compactView={compactView}
/>
+ {!hideWidgets?.includes("attachments") && (
+
+ )}
{renderWidgetModals && (