From 82c970ac4bed366f5069751ba7bf1e19832054ad Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Fri, 5 Dec 2025 13:20:15 +0530 Subject: [PATCH] [WIKI-804] fix: refactor image uploader (#8210) * fix: refactor uploader * fix: props * fix: sites fix --- apps/api/plane/space/views/asset.py | 11 ++-- .../src/core/components/menus/block-menu.tsx | 3 +- .../core/extensions/callout/logo-selector.tsx | 14 +++-- .../custom-image/components/uploader.tsx | 16 ++--- .../editor/src/core/extensions/extensions.ts | 1 + .../editor/src/core/extensions/utility.ts | 5 +- packages/editor/src/core/plugins/drop.ts | 5 +- .../editor/src/core/plugins/file/delete.ts | 60 +++++++++---------- .../services/src/file/sites-file.service.ts | 4 +- 9 files changed, 63 insertions(+), 56 deletions(-) diff --git a/apps/api/plane/space/views/asset.py b/apps/api/plane/space/views/asset.py index 6ed5ab9b6a..faabd97ab6 100644 --- a/apps/api/plane/space/views/asset.py +++ b/apps/api/plane/space/views/asset.py @@ -11,11 +11,12 @@ from rest_framework import status from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response -# Module imports -from .base import BaseAPIView +from plane.bgtasks.storage_metadata_task import get_asset_object_metadata from plane.db.models import DeployBoard, FileAsset from plane.settings.storage import S3Storage -from plane.bgtasks.storage_metadata_task import get_asset_object_metadata + +# Module imports +from .base import BaseAPIView class EntityAssetEndpoint(BaseAPIView): @@ -167,7 +168,7 @@ class EntityAssetEndpoint(BaseAPIView): class AssetRestoreEndpoint(BaseAPIView): """Endpoint to restore a deleted assets.""" - def post(self, request, anchor, asset_id): + def post(self, request, anchor, pk): # Get the deploy board deploy_board = DeployBoard.objects.filter(anchor=anchor, entity_name="project").first() # Check if the project is published @@ -175,7 +176,7 @@ class AssetRestoreEndpoint(BaseAPIView): return Response({"error": "Project is not published"}, status=status.HTTP_404_NOT_FOUND) # Get the asset - asset = FileAsset.all_objects.get(id=asset_id, workspace=deploy_board.workspace) + asset = FileAsset.all_objects.get(id=pk, workspace=deploy_board.workspace) asset.is_deleted = False asset.deleted_at = None asset.save(update_fields=["is_deleted", "deleted_at"]) diff --git a/packages/editor/src/core/components/menus/block-menu.tsx b/packages/editor/src/core/components/menus/block-menu.tsx index fa635f1244..e3fc562ae4 100644 --- a/packages/editor/src/core/components/menus/block-menu.tsx +++ b/packages/editor/src/core/components/menus/block-menu.tsx @@ -12,6 +12,7 @@ import type { Editor } from "@tiptap/react"; import type { LucideIcon } from "lucide-react"; import { Copy, Trash2 } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; +import type { ISvgIcons } from "@plane/propel/icons"; import { cn } from "@plane/utils"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; @@ -27,7 +28,7 @@ type Props = { workItemIdentifier?: IEditorProps["workItemIdentifier"]; }; export type BlockMenuOption = { - icon: LucideIcon; + icon: LucideIcon | React.FC; key: string; label: string; onClick: (e: React.MouseEvent) => void; diff --git a/packages/editor/src/core/extensions/callout/logo-selector.tsx b/packages/editor/src/core/extensions/callout/logo-selector.tsx index 4882961c07..4b998c0ad6 100644 --- a/packages/editor/src/core/extensions/callout/logo-selector.tsx +++ b/packages/editor/src/core/extensions/callout/logo-selector.tsx @@ -53,27 +53,29 @@ export function CalloutBlockLogoSelector(props: Props) { }; if (val.type === "emoji") { // val.value is now a string in decimal format (e.g. "128512") + const emojiValue = val.value as string; newLogoValue = { - "data-emoji-unicode": val.value, + "data-emoji-unicode": emojiValue, "data-emoji-url": undefined, }; newLogoValueToStoreInLocalStorage = { in_use: "emoji", emoji: { - value: val.value, + value: emojiValue, url: undefined, }, }; } else if (val.type === "icon") { + const iconValue = val.value as { name: string; color: string }; newLogoValue = { - "data-icon-name": val.value.name, - "data-icon-color": val.value.color, + "data-icon-name": iconValue.name, + "data-icon-color": iconValue.color, }; newLogoValueToStoreInLocalStorage = { in_use: "icon", icon: { - name: val.value.name, - color: val.value.color, + name: iconValue.name, + color: iconValue.color, }, }; } diff --git a/packages/editor/src/core/extensions/custom-image/components/uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/uploader.tsx index 722cefcf82..d7726a81d9 100644 --- a/packages/editor/src/core/extensions/custom-image/components/uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/uploader.tsx @@ -40,6 +40,7 @@ export function CustomImageUploader(props: CustomImageUploaderProps) { // refs const fileInputRef = useRef(null); const hasTriggeredFilePickerRef = useRef(false); + const hasTriedUploadingOnMountRef = useRef(false); const { id: imageEntityId } = node.attrs; // derived values const imageComponentImageFileMap = useMemo(() => getImageComponentImageFileMap(editor), [editor]); @@ -124,17 +125,16 @@ export function CustomImageUploader(props: CustomImageUploaderProps) { uploader: uploadFile, }); - // the meta data of the image component - const meta = useMemo( - () => imageComponentImageFileMap?.get(imageEntityId ?? ""), - [imageComponentImageFileMap, imageEntityId] - ); - // after the image component is mounted we start the upload process based on // it's uploaded useEffect(() => { + if (hasTriedUploadingOnMountRef.current) return; + + // the meta data of the image component + const meta = imageComponentImageFileMap?.get(imageEntityId ?? ""); if (meta) { if (meta.event === "drop" && "file" in meta) { + hasTriedUploadingOnMountRef.current = true; uploadFile(meta.file); } else if (meta.event === "insert" && fileInputRef.current && !hasTriggeredFilePickerRef.current) { if (meta.hasOpenedFileInputOnce) return; @@ -144,8 +144,10 @@ export function CustomImageUploader(props: CustomImageUploaderProps) { hasTriggeredFilePickerRef.current = true; imageComponentImageFileMap?.set(imageEntityId ?? "", { ...meta, hasOpenedFileInputOnce: true }); } + } else { + hasTriedUploadingOnMountRef.current = true; } - }, [meta, uploadFile, imageComponentImageFileMap, imageEntityId, isTouchDevice]); + }, [imageEntityId, isTouchDevice, uploadFile, imageComponentImageFileMap]); const onFileChange = useCallback( async (e: ChangeEvent) => { diff --git a/packages/editor/src/core/extensions/extensions.ts b/packages/editor/src/core/extensions/extensions.ts index 5d6b0284bd..79daaccef6 100644 --- a/packages/editor/src/core/extensions/extensions.ts +++ b/packages/editor/src/core/extensions/extensions.ts @@ -115,6 +115,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { CustomCalloutExtension, UtilityExtension({ disabledExtensions, + flaggedExtensions, fileHandler, getEditorMetaData, isEditable: editable, diff --git a/packages/editor/src/core/extensions/utility.ts b/packages/editor/src/core/extensions/utility.ts index 1bff7589b9..167ba298e0 100644 --- a/packages/editor/src/core/extensions/utility.ts +++ b/packages/editor/src/core/extensions/utility.ts @@ -51,14 +51,14 @@ export type UtilityExtensionStorage = { isTouchDevice: boolean; }; -type Props = Pick & { +type Props = Pick & { fileHandler: TFileHandler; isEditable: boolean; isTouchDevice: boolean; }; export const UtilityExtension = (props: Props) => { - const { disabledExtensions, fileHandler, getEditorMetaData, isEditable, isTouchDevice } = props; + const { disabledExtensions, flaggedExtensions, fileHandler, getEditorMetaData, isEditable, isTouchDevice } = props; const { restore } = fileHandler; return Extension.create, UtilityExtensionStorage>({ @@ -79,6 +79,7 @@ export const UtilityExtension = (props: Props) => { }), DropHandlerPlugin({ disabledExtensions, + flaggedExtensions, editor: this.editor, }), PasteAssetPlugin(), diff --git a/packages/editor/src/core/plugins/drop.ts b/packages/editor/src/core/plugins/drop.ts index f9f5715753..b0cd873cb8 100644 --- a/packages/editor/src/core/plugins/drop.ts +++ b/packages/editor/src/core/plugins/drop.ts @@ -7,11 +7,12 @@ import type { TEditorCommands, TExtensions } from "@/types"; type Props = { disabledExtensions?: TExtensions[]; + flaggedExtensions?: TExtensions[]; editor: Editor; }; export const DropHandlerPlugin = (props: Props): Plugin => { - const { disabledExtensions, editor } = props; + const { disabledExtensions, flaggedExtensions, editor } = props; return new Plugin({ key: new PluginKey("drop-handler-plugin"), @@ -33,6 +34,7 @@ export const DropHandlerPlugin = (props: Props): Plugin => { const pos = view.state.selection.from; insertFilesSafely({ disabledExtensions, + flaggedExtensions, editor, files: acceptedFiles, initialPos: pos, @@ -84,6 +86,7 @@ export const DropHandlerPlugin = (props: Props): Plugin => { type InsertFilesSafelyArgs = { disabledExtensions?: TExtensions[]; + flaggedExtensions?: TExtensions[]; editor: Editor; event: "insert" | "drop"; files: File[]; diff --git a/packages/editor/src/core/plugins/file/delete.ts b/packages/editor/src/core/plugins/file/delete.ts index 47790c4f9d..607ea20447 100644 --- a/packages/editor/src/core/plugins/file/delete.ts +++ b/packages/editor/src/core/plugins/file/delete.ts @@ -21,6 +21,7 @@ export const TrackFileDeletionPlugin = (editor: Editor, deleteHandler: TFileHand [nodeType: string]: Set | undefined; } = {}; if (!transactions.some((tr) => tr.docChanged)) return null; + if (transactions.some((tr) => tr.getMeta(CORE_EDITOR_META.SKIP_FILE_DELETION))) return null; newState.doc.descendants((node) => { const nodeType = node.type.name as keyof NodeFileMapType; @@ -34,40 +35,35 @@ export const TrackFileDeletionPlugin = (editor: Editor, deleteHandler: TFileHand } }); - transactions.forEach((transaction) => { - // if the transaction has meta of skipFileDeletion set to true, then return (like while clearing the editor content programmatically) - if (transaction.getMeta(CORE_EDITOR_META.SKIP_FILE_DELETION)) return; + const removedFiles: TFileNode[] = []; - const removedFiles: TFileNode[] = []; + // iterate through all the nodes in the old state + oldState.doc.descendants((node) => { + const nodeType = node.type.name as keyof NodeFileMapType; + const isAValidNode = NODE_FILE_MAP[nodeType]; + // if the node doesn't match, then return as no point in checking + if (!isAValidNode) return; + // Check if the node has been deleted or replaced + if (!newFileSources[nodeType]?.has(node.attrs.src)) { + removedFiles.push(node as TFileNode); + } + }); - // iterate through all the nodes in the old state - oldState.doc.descendants((node) => { - const nodeType = node.type.name as keyof NodeFileMapType; - const isAValidNode = NODE_FILE_MAP[nodeType]; - // if the node doesn't match, then return as no point in checking - if (!isAValidNode) return; - // Check if the node has been deleted or replaced - if (!newFileSources[nodeType]?.has(node.attrs.src)) { - removedFiles.push(node as TFileNode); - } - }); - - removedFiles.forEach(async (node) => { - const nodeType = node.type.name as keyof NodeFileMapType; - const src = node.attrs.src; - const nodeFileSetDetails = NODE_FILE_MAP[nodeType]; - if (!nodeFileSetDetails || !src) return; - try { - editor.storage[nodeType]?.[nodeFileSetDetails.fileSetName]?.set(src, true); - // update assets list storage value - editor.commands.updateAssetsList?.({ - idToRemove: node.attrs.id, - }); - await deleteHandler(src); - } catch (error) { - console.error("Error deleting file via delete utility plugin:", error); - } - }); + removedFiles.forEach(async (node) => { + const nodeType = node.type.name as keyof NodeFileMapType; + const src = node.attrs.src; + const nodeFileSetDetails = NODE_FILE_MAP[nodeType]; + if (!nodeFileSetDetails || !src) return; + try { + editor.storage[nodeType]?.[nodeFileSetDetails.fileSetName]?.set(src, true); + // update assets list storage value + editor.commands.updateAssetsList?.({ + idToRemove: node.attrs.id, + }); + await deleteHandler(src); + } catch (error) { + console.error("Error deleting file via delete utility plugin:", error); + } }); return null; diff --git a/packages/services/src/file/sites-file.service.ts b/packages/services/src/file/sites-file.service.ts index 4c10198576..fa2713282c 100644 --- a/packages/services/src/file/sites-file.service.ts +++ b/packages/services/src/file/sites-file.service.ts @@ -98,10 +98,10 @@ export class SitesFileService extends FileService { * @returns {Promise} Promise resolving to void * @throws {Error} If the request fails */ - async restoreNewAsset(workspaceSlug: string, src: string): Promise { + async restoreNewAsset(anchor: string, src: string): Promise { // remove the last slash and get the asset id const assetId = getAssetIdFromUrl(src); - return this.post(`/api/public/assets/v2/workspaces/${workspaceSlug}/restore/${assetId}/`) + return this.post(`/api/public/assets/v2/anchor/${anchor}/restore/${assetId}/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data;