From dc16f2862e749373e4ffa1604d8eb5cb11755957 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 12 May 2025 18:37:36 +0530 Subject: [PATCH] [WIKI-181] refactor: make file handling generic in editor (#7046) * refactor: make file handling generic * fix: useeffect dependency array * chore: remove mime type to extension conversion --- .../src/ce/extensions/core/extensions.ts | 3 +- packages/editor/src/core/constants/config.ts | 54 +++++- .../components/image-uploader.tsx | 30 ++-- .../extensions/custom-image/custom-image.ts | 6 +- packages/editor/src/core/extensions/drop.ts | 127 ++++++++++++++ packages/editor/src/core/extensions/drop.tsx | 94 ---------- .../editor/src/core/extensions/extensions.tsx | 1 + .../src/core/helpers/editor-commands.ts | 4 +- .../validate-file.ts => helpers/file.ts} | 8 +- .../editor/src/core/hooks/use-file-upload.ts | 164 +++++++++--------- .../editor/src/core/plugins/drag-handle.ts | 4 +- .../src/core/plugins/image/constants.ts | 7 - .../src/core/plugins/image/delete-image.ts | 18 +- .../editor/src/core/plugins/image/index.ts | 2 - .../src/core/plugins/image/utils/index.ts | 1 - packages/utils/src/attachment.ts | 32 ++++ packages/utils/src/index.ts | 2 +- 17 files changed, 334 insertions(+), 223 deletions(-) create mode 100644 packages/editor/src/core/extensions/drop.ts delete mode 100644 packages/editor/src/core/extensions/drop.tsx rename packages/editor/src/core/{plugins/image/utils/validate-file.ts => helpers/file.ts} (74%) delete mode 100644 packages/editor/src/core/plugins/image/constants.ts delete mode 100644 packages/editor/src/core/plugins/image/utils/index.ts create mode 100644 packages/utils/src/attachment.ts diff --git a/packages/editor/src/ce/extensions/core/extensions.ts b/packages/editor/src/ce/extensions/core/extensions.ts index d03229133..cecfb38b4 100644 --- a/packages/editor/src/ce/extensions/core/extensions.ts +++ b/packages/editor/src/ce/extensions/core/extensions.ts @@ -1,9 +1,10 @@ import { Extensions } from "@tiptap/core"; // types -import { TExtensions } from "@/types"; +import { TExtensions, TFileHandler } from "@/types"; type Props = { disabledExtensions: TExtensions[]; + fileHandler: TFileHandler; }; export const CoreEditorAdditionalExtensions = (props: Props): Extensions => { diff --git a/packages/editor/src/core/constants/config.ts b/packages/editor/src/core/constants/config.ts index ac6d63dd1..922be9ef9 100644 --- a/packages/editor/src/core/constants/config.ts +++ b/packages/editor/src/core/constants/config.ts @@ -8,5 +8,55 @@ export const DEFAULT_DISPLAY_CONFIG: TDisplayConfig = { wideLayout: false, }; -export const ACCEPTED_FILE_MIME_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"]; -export const ACCEPTED_FILE_EXTENSIONS = ACCEPTED_FILE_MIME_TYPES.map((type) => `.${type.split("/")[1]}`); +export const ACCEPTED_IMAGE_MIME_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"]; + +export const ACCEPTED_ATTACHMENT_MIME_TYPES = [ + "image/jpeg", + "image/png", + "image/gif", + "image/svg+xml", + "image/webp", + "image/tiff", + "image/bmp", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "text/plain", + "application/rtf", + "audio/mpeg", + "audio/wav", + "audio/ogg", + "audio/midi", + "audio/x-midi", + "audio/aac", + "audio/flac", + "audio/x-m4a", + "video/mp4", + "video/mpeg", + "video/ogg", + "video/webm", + "video/quicktime", + "video/x-msvideo", + "video/x-ms-wmv", + "application/zip", + "application/x-rar-compressed", + "application/x-tar", + "application/gzip", + "model/gltf-binary", + "model/gltf+json", + "application/octet-stream", + "font/ttf", + "font/otf", + "font/woff", + "font/woff2", + "text/css", + "text/javascript", + "application/json", + "text/xml", + "text/csv", + "application/xml", +]; diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index 0fd0e6dd4..0a3ee1a1c 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -3,11 +3,11 @@ import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react"; // plane utils import { cn } from "@plane/utils"; // constants -import { ACCEPTED_FILE_EXTENSIONS } from "@/constants/config"; +import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; // extensions import { CustoBaseImageNodeViewProps, getImageComponentImageFileMap } from "@/extensions/custom-image"; // hooks -import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload"; +import { useUploader, useDropZone, uploadFirstFileAndInsertRemaining } from "@/hooks/use-file-upload"; type CustomImageUploaderProps = CustoBaseImageNodeViewProps & { maxFileSize: number; @@ -41,7 +41,9 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { if (!imageEntityId) return; setIsUploaded(true); // Update the node view's src attribute post upload - updateAttributes({ src: url }); + updateAttributes({ + src: url, + }); imageComponentImageFileMap?.delete(imageEntityId); const pos = getPos(); @@ -51,7 +53,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { // only if the cursor is at the current image component, manipulate // the cursor position - if (currentNode && currentNode.type.name === "imageComponent" && currentNode.attrs.src === url) { + if (currentNode && currentNode.type.name === node.type.name && currentNode.attrs.src === url) { // control cursor position after upload const nextNode = editor.state.doc.nodeAt(pos + 1); @@ -68,17 +70,23 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { [imageComponentImageFileMap, imageEntityId, updateAttributes, getPos] ); // hooks - const { uploading: isImageBeingUploaded, uploadFile } = useUploader({ - blockId: imageEntityId ?? "", - editor, - loadImageFromFileSystem, + const { isUploading: isImageBeingUploaded, uploadFile } = useUploader({ + acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, + // @ts-expect-error - TODO: fix typings, and don't remove await from here for now + editorCommand: async (file) => await editor?.commands.uploadImage(imageEntityId, file), + handleProgressStatus: (isUploading) => { + editor.storage.imageComponent.uploadInProgress = isUploading; + }, + loadFileFromFileSystem: loadImageFromFileSystem, maxFileSize, onUpload, }); const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({ + acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, editor, maxFileSize, pos: getPos(), + type: "image", uploader: uploadFile, }); @@ -110,11 +118,13 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { if (!filesList) { return; } - await uploadFirstImageAndInsertRemaining({ + await uploadFirstFileAndInsertRemaining({ + acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, editor, filesList, maxFileSize, pos: getPos(), + type: "image", uploader: uploadFile, }); }, @@ -170,7 +180,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { ref={fileInputRef} hidden type="file" - accept={ACCEPTED_FILE_EXTENSIONS.join(",")} + accept={ACCEPTED_IMAGE_MIME_TYPES.join(",")} onChange={onFileChange} multiple /> diff --git a/packages/editor/src/core/extensions/custom-image/custom-image.ts b/packages/editor/src/core/extensions/custom-image/custom-image.ts index 4f1b3c8db..a9a69fa60 100644 --- a/packages/editor/src/core/extensions/custom-image/custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/custom-image.ts @@ -2,12 +2,15 @@ import { Editor, mergeAttributes } from "@tiptap/core"; import { Image } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; import { v4 as uuidv4 } from "uuid"; +// constants +import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; // extensions import { CustomImageNode } from "@/extensions/custom-image"; // helpers +import { isFileValid } from "@/helpers/file"; import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary"; // plugins -import { TrackImageDeletionPlugin, TrackImageRestorationPlugin, isFileValid } from "@/plugins/image"; +import { TrackImageDeletionPlugin, TrackImageRestorationPlugin } from "@/plugins/image"; // types import { TFileHandler } from "@/types"; @@ -146,6 +149,7 @@ export const CustomImageExtension = (props: TFileHandler) => { if ( props?.file && !isFileValid({ + acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, file: props.file, maxFileSize, }) diff --git a/packages/editor/src/core/extensions/drop.ts b/packages/editor/src/core/extensions/drop.ts new file mode 100644 index 000000000..2a5a994f8 --- /dev/null +++ b/packages/editor/src/core/extensions/drop.ts @@ -0,0 +1,127 @@ +import { Extension, Editor } from "@tiptap/core"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +// constants +import { ACCEPTED_ATTACHMENT_MIME_TYPES, ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; +// types +import { TEditorCommands } from "@/types"; + +export const DropHandlerExtension = Extension.create({ + name: "dropHandler", + priority: 1000, + + addProseMirrorPlugins() { + const editor = this.editor; + return [ + new Plugin({ + key: new PluginKey("drop-handler-plugin"), + props: { + handlePaste: (view, event) => { + if ( + editor.isEditable && + event.clipboardData && + event.clipboardData.files && + event.clipboardData.files.length > 0 + ) { + event.preventDefault(); + const files = Array.from(event.clipboardData.files); + const acceptedFiles = files.filter( + (f) => ACCEPTED_IMAGE_MIME_TYPES.includes(f.type) || ACCEPTED_ATTACHMENT_MIME_TYPES.includes(f.type) + ); + + if (acceptedFiles.length) { + const pos = view.state.selection.from; + insertFilesSafely({ + editor, + files: acceptedFiles, + initialPos: pos, + event: "drop", + }); + } + return true; + } + return false; + }, + handleDrop: (view, event, _slice, moved) => { + if ( + editor.isEditable && + !moved && + event.dataTransfer && + event.dataTransfer.files && + event.dataTransfer.files.length > 0 + ) { + event.preventDefault(); + const files = Array.from(event.dataTransfer.files); + const acceptedFiles = files.filter( + (f) => ACCEPTED_IMAGE_MIME_TYPES.includes(f.type) || ACCEPTED_ATTACHMENT_MIME_TYPES.includes(f.type) + ); + + if (acceptedFiles.length) { + const coordinates = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + + if (coordinates) { + const pos = coordinates.pos; + insertFilesSafely({ + editor, + files: acceptedFiles, + initialPos: pos, + event: "drop", + }); + } + return true; + } + } + return false; + }, + }, + }), + ]; + }, +}); + +type InsertFilesSafelyArgs = { + editor: Editor; + event: "insert" | "drop"; + files: File[]; + initialPos: number; + type?: Extract; +}; + +export const insertFilesSafely = async (args: InsertFilesSafelyArgs) => { + const { editor, event, files, initialPos, type } = args; + let pos = initialPos; + + for (const file of files) { + // safe insertion + const docSize = editor.state.doc.content.size; + pos = Math.min(pos, docSize); + + let fileType: "image" | "attachment" | null = null; + + try { + if (type) { + if (["image", "attachment"].includes(type)) fileType = type; + else throw new Error("Wrong file type passed"); + } else { + if (ACCEPTED_IMAGE_MIME_TYPES.includes(file.type)) fileType = "image"; + else if (ACCEPTED_ATTACHMENT_MIME_TYPES.includes(file.type)) fileType = "attachment"; + } + // insert file depending on the type at the current position + if (fileType === "image") { + editor.commands.insertImageComponent({ + file, + pos, + event, + }); + } else if (fileType === "attachment") { + } + } catch (error) { + console.error(`Error while ${event}ing file:`, error); + } + + // Move to the next position + pos += 1; + } +}; diff --git a/packages/editor/src/core/extensions/drop.tsx b/packages/editor/src/core/extensions/drop.tsx deleted file mode 100644 index 0d578770a..000000000 --- a/packages/editor/src/core/extensions/drop.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Extension, Editor } from "@tiptap/core"; -import { Plugin, PluginKey } from "@tiptap/pm/state"; -import { EditorView } from "@tiptap/pm/view"; - -export const DropHandlerExtension = Extension.create({ - name: "dropHandler", - priority: 1000, - - addProseMirrorPlugins() { - const editor = this.editor; - return [ - new Plugin({ - key: new PluginKey("drop-handler-plugin"), - props: { - handlePaste: (view: EditorView, event: ClipboardEvent) => { - if ( - editor.isEditable && - event.clipboardData && - event.clipboardData.files && - event.clipboardData.files.length > 0 - ) { - event.preventDefault(); - const files = Array.from(event.clipboardData.files); - const imageFiles = files.filter((file) => file.type.startsWith("image")); - - if (imageFiles.length > 0) { - const pos = view.state.selection.from; - insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" }); - } - return true; - } - return false; - }, - handleDrop: (view: EditorView, event: DragEvent, _slice: any, moved: boolean) => { - if ( - editor.isEditable && - !moved && - event.dataTransfer && - event.dataTransfer.files && - event.dataTransfer.files.length > 0 - ) { - event.preventDefault(); - const files = Array.from(event.dataTransfer.files); - const imageFiles = files.filter((file) => file.type.startsWith("image")); - - if (imageFiles.length > 0) { - const coordinates = view.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - - if (coordinates) { - const pos = coordinates.pos; - insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" }); - } - return true; - } - } - return false; - }, - }, - }), - ]; - }, -}); -export const insertImagesSafely = async ({ - editor, - files, - initialPos, - event, -}: { - editor: Editor; - files: File[]; - initialPos: number; - event: "insert" | "drop"; -}) => { - let pos = initialPos; - - for (const file of files) { - // safe insertion - const docSize = editor.state.doc.content.size; - pos = Math.min(pos, docSize); - - try { - // Insert the image at the current position - editor.commands.insertImageComponent({ file, pos, event }); - } catch (error) { - console.error(`Error while ${event}ing image:`, error); - } - - // Move to the next position - pos += 1; - } -}; diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index ff200cd32..1ef0a3b15 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -172,6 +172,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { CustomColorExtension, ...CoreEditorAdditionalExtensions({ disabledExtensions, + fileHandler, }), ]; diff --git a/packages/editor/src/core/helpers/editor-commands.ts b/packages/editor/src/core/helpers/editor-commands.ts index 39796ac24..e8c98ada5 100644 --- a/packages/editor/src/core/helpers/editor-commands.ts +++ b/packages/editor/src/core/helpers/editor-commands.ts @@ -1,7 +1,6 @@ import { Editor, Range } from "@tiptap/core"; -// types -import { InsertImageComponentProps } from "@/extensions"; // extensions +import { InsertImageComponentProps } from "@/extensions"; import { replaceCodeWithText } from "@/extensions/code/utils/replace-code-block-with-text"; // helpers import { findTableAncestor } from "@/helpers/common"; @@ -206,6 +205,7 @@ export const insertHorizontalRule = (editor: Editor, range?: Range) => { if (range) editor.chain().focus().deleteRange(range).setHorizontalRule().run(); else editor.chain().focus().setHorizontalRule().run(); }; + export const insertCallout = (editor: Editor, range?: Range) => { if (range) editor.chain().focus().deleteRange(range).insertCallout().run(); else editor.chain().focus().insertCallout().run(); diff --git a/packages/editor/src/core/plugins/image/utils/validate-file.ts b/packages/editor/src/core/helpers/file.ts similarity index 74% rename from packages/editor/src/core/plugins/image/utils/validate-file.ts rename to packages/editor/src/core/helpers/file.ts index 703bb2bf0..f2c9968f0 100644 --- a/packages/editor/src/core/plugins/image/utils/validate-file.ts +++ b/packages/editor/src/core/helpers/file.ts @@ -1,20 +1,18 @@ -// constants -import { ACCEPTED_FILE_MIME_TYPES } from "@/constants/config"; - type TArgs = { + acceptedMimeTypes: string[]; file: File; maxFileSize: number; }; export const isFileValid = (args: TArgs): boolean => { - const { file, maxFileSize } = args; + const { acceptedMimeTypes, file, maxFileSize } = args; if (!file) { alert("No file selected. Please select a file to upload."); return false; } - if (!ACCEPTED_FILE_MIME_TYPES.includes(file.type)) { + if (!acceptedMimeTypes.includes(file.type)) { alert("Invalid file type. Please select a JPEG, JPG, PNG, WEBP or GIF file."); return false; } diff --git a/packages/editor/src/core/hooks/use-file-upload.ts b/packages/editor/src/core/hooks/use-file-upload.ts index e57c811a0..b707824f2 100644 --- a/packages/editor/src/core/hooks/use-file-upload.ts +++ b/packages/editor/src/core/hooks/use-file-upload.ts @@ -1,87 +1,87 @@ -import { DragEvent, useCallback, useEffect, useState } from "react"; import { Editor } from "@tiptap/core"; +import { DragEvent, useCallback, useEffect, useState } from "react"; // extensions -import { insertImagesSafely } from "@/extensions/drop"; +import { insertFilesSafely } from "@/extensions/drop"; // plugins -import { isFileValid } from "@/plugins/image"; +import { isFileValid } from "@/helpers/file"; +// types +import { TEditorCommands } from "@/types"; type TUploaderArgs = { - blockId: string; - editor: Editor; - loadImageFromFileSystem: (file: string) => void; + acceptedMimeTypes: string[]; + editorCommand: (file: File) => Promise; + handleProgressStatus?: (isUploading: boolean) => void; + loadFileFromFileSystem?: (file: string) => void; maxFileSize: number; - onUpload: (url: string) => void; + onUpload: (url: string, file: File) => void; }; export const useUploader = (args: TUploaderArgs) => { - const { blockId, editor, loadImageFromFileSystem, maxFileSize, onUpload } = args; + const { acceptedMimeTypes, editorCommand, handleProgressStatus, loadFileFromFileSystem, maxFileSize, onUpload } = + args; // states - const [uploading, setUploading] = useState(false); + const [isUploading, setIsUploading] = useState(false); const uploadFile = useCallback( async (file: File) => { - const setImageUploadInProgress = (isUploading: boolean) => { - if (editor.storage.imageComponent) { - editor.storage.imageComponent.uploadInProgress = isUploading; - } - }; - setImageUploadInProgress(true); - setUploading(true); - const fileNameTrimmed = trimFileName(file.name); - const fileWithTrimmedName = new File([file], fileNameTrimmed, { type: file.type }); + handleProgressStatus?.(true); + setIsUploading(true); const isValid = isFileValid({ - file: fileWithTrimmedName, + acceptedMimeTypes, + file, maxFileSize, }); if (!isValid) { - setImageUploadInProgress(false); + handleProgressStatus?.(false); + setIsUploading(false); return; } try { - const reader = new FileReader(); - reader.onload = () => { - if (reader.result) { - loadImageFromFileSystem(reader.result as string); - } else { - console.error("Failed to read the file: reader.result is null"); - } - }; - reader.onerror = () => { - console.error("Error reading file"); - }; - reader.readAsDataURL(fileWithTrimmedName); - // @ts-expect-error - TODO: fix typings, and don't remove await from - // here for now - const url: string = await editor?.commands.uploadImage(blockId, fileWithTrimmedName); + if (loadFileFromFileSystem) { + const reader = new FileReader(); + reader.onload = () => { + if (reader.result) { + loadFileFromFileSystem(reader.result as string); + } else { + console.error("Failed to read the file: reader.result is null"); + } + }; + reader.onerror = () => { + console.error("Error reading file"); + }; + reader.readAsDataURL(file); + } + const url: string = await editorCommand(file); if (!url) { - throw new Error("Something went wrong while uploading the image"); + throw new Error("Something went wrong while uploading the file."); } - onUpload(url); - } catch (errPayload: any) { - console.log(errPayload); + onUpload(url, file); + } catch (errPayload) { const error = errPayload?.response?.data?.error || "Something went wrong"; console.error(error); } finally { - setImageUploadInProgress(false); - setUploading(false); + handleProgressStatus?.(false); + setIsUploading(false); } }, - [onUpload] + [acceptedMimeTypes, editorCommand, handleProgressStatus, loadFileFromFileSystem, maxFileSize, onUpload] ); - return { uploading, uploadFile }; + return { isUploading, uploadFile }; }; type TDropzoneArgs = { + acceptedMimeTypes: string[]; editor: Editor; maxFileSize: number; pos: number; + type: Extract; uploader: (file: File) => Promise; }; export const useDropZone = (args: TDropzoneArgs) => { - const { editor, maxFileSize, pos, uploader } = args; + const { acceptedMimeTypes, editor, maxFileSize, pos, type, uploader } = args; // states const [isDragging, setIsDragging] = useState(false); const [draggedInside, setDraggedInside] = useState(false); @@ -112,83 +112,79 @@ export const useDropZone = (args: TDropzoneArgs) => { return; } const filesList = e.dataTransfer.files; - await uploadFirstImageAndInsertRemaining({ + await uploadFirstFileAndInsertRemaining({ + acceptedMimeTypes, editor, filesList, maxFileSize, pos, + type, uploader, }); }, - [uploader, editor, pos] + [acceptedMimeTypes, editor, maxFileSize, pos, type, uploader] ); + const onDragEnter = useCallback(() => setDraggedInside(true), []); + const onDragLeave = useCallback(() => setDraggedInside(false), []); - const onDragEnter = () => { - setDraggedInside(true); + return { + isDragging, + draggedInside, + onDragEnter, + onDragLeave, + onDrop, }; - - const onDragLeave = () => { - setDraggedInside(false); - }; - - return { isDragging, draggedInside, onDragEnter, onDragLeave, onDrop }; }; -function trimFileName(fileName: string, maxLength = 100) { - if (fileName.length > maxLength) { - const extension = fileName.split(".").pop(); - const nameWithoutExtension = fileName.slice(0, -(extension?.length ?? 0 + 1)); - const allowedNameLength = maxLength - (extension?.length ?? 0) - 1; // -1 for the dot - return `${nameWithoutExtension.slice(0, allowedNameLength)}.${extension}`; - } - - return fileName; -} - -type TMultipleImagesArgs = { +type TMultipleFileArgs = { + acceptedMimeTypes: string[]; editor: Editor; filesList: FileList; maxFileSize: number; pos: number; + type: Extract; uploader: (file: File) => Promise; }; -// Upload the first image and insert the remaining images for uploading multiple image -// post insertion of image-component -export async function uploadFirstImageAndInsertRemaining(args: TMultipleImagesArgs) { - const { editor, filesList, maxFileSize, pos, uploader } = args; +// Upload the first file and insert the remaining ones for uploading multiple files +export const uploadFirstFileAndInsertRemaining = async (args: TMultipleFileArgs) => { + const { acceptedMimeTypes, editor, filesList, maxFileSize, pos, type, uploader } = args; const filteredFiles: File[] = []; for (let i = 0; i < filesList.length; i += 1) { - const item = filesList.item(i); + const file = filesList.item(i); if ( - item && - item.type.indexOf("image") !== -1 && + file && isFileValid({ - file: item, + acceptedMimeTypes, + file, maxFileSize, }) ) { - filteredFiles.push(item); + filteredFiles.push(file); } } if (filteredFiles.length !== filesList.length) { - console.warn("Some files were not images and have been ignored."); + console.warn("Some files were invalid and have been ignored."); } if (filteredFiles.length === 0) { - console.error("No image files found to upload"); + console.error("No files found to upload."); return; } - // Upload the first image + // Upload the first file const firstFile = filteredFiles[0]; uploader(firstFile); - - // Insert the remaining images + // Insert the remaining files const remainingFiles = filteredFiles.slice(1); - if (remainingFiles.length > 0) { const docSize = editor.state.doc.content.size; - const posOfNextImageToBeInserted = Math.min(pos + 1, docSize); - insertImagesSafely({ editor, files: remainingFiles, initialPos: posOfNextImageToBeInserted, event: "drop" }); + const posOfNextFileToBeInserted = Math.min(pos + 1, docSize); + insertFilesSafely({ + editor, + files: remainingFiles, + initialPos: posOfNextFileToBeInserted, + event: "drop", + type, + }); } -} +}; diff --git a/packages/editor/src/core/plugins/drag-handle.ts b/packages/editor/src/core/plugins/drag-handle.ts index e71c38b30..f9a60a48c 100644 --- a/packages/editor/src/core/plugins/drag-handle.ts +++ b/packages/editor/src/core/plugins/drag-handle.ts @@ -10,10 +10,10 @@ const verticalEllipsisIcon = const generalSelectors = [ "li", - "p:not(:first-child)", + "p.editor-paragraph-block:not(:first-child)", ".code-block", "blockquote", - "h1, h2, h3, h4, h5, h6", + "h1.editor-heading-block, h2.editor-heading-block, h3.editor-heading-block, h4.editor-heading-block, h5.editor-heading-block, h6.editor-heading-block", "[data-type=horizontalRule]", ".table-wrapper", ".issue-embed", diff --git a/packages/editor/src/core/plugins/image/constants.ts b/packages/editor/src/core/plugins/image/constants.ts deleted file mode 100644 index 72fae6710..000000000 --- a/packages/editor/src/core/plugins/image/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { PluginKey } from "@tiptap/pm/state"; - -export const uploadKey = new PluginKey("upload-image"); -export const deleteKey = new PluginKey("delete-image"); -export const restoreKey = new PluginKey("restore-image"); - -export const IMAGE_NODE_TYPE = "image"; diff --git a/packages/editor/src/core/plugins/image/delete-image.ts b/packages/editor/src/core/plugins/image/delete-image.ts index bcede7707..459d9fd70 100644 --- a/packages/editor/src/core/plugins/image/delete-image.ts +++ b/packages/editor/src/core/plugins/image/delete-image.ts @@ -37,20 +37,16 @@ export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImag removedImages.forEach(async (node) => { const src = node.attrs.src; - editor.storage[nodeType].deletedImageSet.set(src, true); - await onNodeDeleted(src, deleteImage); + editor.storage[nodeType].deletedImageSet?.set(src, true); + if (!src) return; + try { + await deleteImage(src); + } catch (error) { + console.error("Error deleting image:", error); + } }); }); return null; }, }); - -async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise { - if (!src) return; - try { - await deleteImage(src); - } catch (error) { - console.error("Error deleting image: ", error); - } -} diff --git a/packages/editor/src/core/plugins/image/index.ts b/packages/editor/src/core/plugins/image/index.ts index dfb787873..c0dc631c5 100644 --- a/packages/editor/src/core/plugins/image/index.ts +++ b/packages/editor/src/core/plugins/image/index.ts @@ -1,5 +1,3 @@ export * from "./types"; -export * from "./utils"; -export * from "./constants"; export * from "./delete-image"; export * from "./restore-image"; diff --git a/packages/editor/src/core/plugins/image/utils/index.ts b/packages/editor/src/core/plugins/image/utils/index.ts deleted file mode 100644 index 08d377a83..000000000 --- a/packages/editor/src/core/plugins/image/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./validate-file"; diff --git a/packages/utils/src/attachment.ts b/packages/utils/src/attachment.ts new file mode 100644 index 000000000..1f9f4f5a3 --- /dev/null +++ b/packages/utils/src/attachment.ts @@ -0,0 +1,32 @@ +export const generateFileName = (fileName: string) => { + const date = new Date(); + const timestamp = date.getTime(); + + const _fileName = getFileName(fileName); + const nameWithoutExtension = _fileName.length > 80 ? _fileName.substring(0, 80) : _fileName; + const extension = getFileExtension(fileName); + + return `${nameWithoutExtension}-${timestamp}.${extension}`; +}; + +export const getFileExtension = (filename: string) => filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2); + +export const getFileName = (fileName: string) => { + const dotIndex = fileName.lastIndexOf("."); + + const nameWithoutExtension = fileName.substring(0, dotIndex); + + return nameWithoutExtension; +}; + +export const convertBytesToSize = (bytes: number) => { + let size; + + if (bytes < 1024 * 1024) { + size = Math.round(bytes / 1024) + " KB"; + } else { + size = Math.round(bytes / (1024 * 1024)) + " MB"; + } + + return size; +}; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 765dce49d..495d065df 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,4 +1,5 @@ export * from "./array"; +export * from "./attachment"; export * from "./auth"; export * from "./datetime"; export * from "./color"; @@ -16,4 +17,3 @@ export * from "./work-item"; export * from "./get-icon-for-link"; export * from "./subscription"; -