[WIKI-804] fix: refactor image uploader (#8210)

* fix: refactor uploader

* fix: props

* fix: sites fix
This commit is contained in:
M. Palanikannan
2025-12-05 13:20:15 +05:30
committed by GitHub
parent 392c8cf2e1
commit 82c970ac4b
9 changed files with 63 additions and 56 deletions

View File

@@ -11,11 +11,12 @@ from rest_framework import status
from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
# Module imports from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
from .base import BaseAPIView
from plane.db.models import DeployBoard, FileAsset from plane.db.models import DeployBoard, FileAsset
from plane.settings.storage import S3Storage 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): class EntityAssetEndpoint(BaseAPIView):
@@ -167,7 +168,7 @@ class EntityAssetEndpoint(BaseAPIView):
class AssetRestoreEndpoint(BaseAPIView): class AssetRestoreEndpoint(BaseAPIView):
"""Endpoint to restore a deleted assets.""" """Endpoint to restore a deleted assets."""
def post(self, request, anchor, asset_id): def post(self, request, anchor, pk):
# Get the deploy board # Get the deploy board
deploy_board = DeployBoard.objects.filter(anchor=anchor, entity_name="project").first() deploy_board = DeployBoard.objects.filter(anchor=anchor, entity_name="project").first()
# Check if the project is published # 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) return Response({"error": "Project is not published"}, status=status.HTTP_404_NOT_FOUND)
# Get the asset # 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.is_deleted = False
asset.deleted_at = None asset.deleted_at = None
asset.save(update_fields=["is_deleted", "deleted_at"]) asset.save(update_fields=["is_deleted", "deleted_at"])

View File

@@ -12,6 +12,7 @@ import type { Editor } from "@tiptap/react";
import type { LucideIcon } from "lucide-react"; import type { LucideIcon } from "lucide-react";
import { Copy, Trash2 } from "lucide-react"; import { Copy, Trash2 } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import type { ISvgIcons } from "@plane/propel/icons";
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// constants // constants
import { CORE_EXTENSIONS } from "@/constants/extension"; import { CORE_EXTENSIONS } from "@/constants/extension";
@@ -27,7 +28,7 @@ type Props = {
workItemIdentifier?: IEditorProps["workItemIdentifier"]; workItemIdentifier?: IEditorProps["workItemIdentifier"];
}; };
export type BlockMenuOption = { export type BlockMenuOption = {
icon: LucideIcon; icon: LucideIcon | React.FC<ISvgIcons>;
key: string; key: string;
label: string; label: string;
onClick: (e: React.MouseEvent) => void; onClick: (e: React.MouseEvent) => void;

View File

@@ -53,27 +53,29 @@ export function CalloutBlockLogoSelector(props: Props) {
}; };
if (val.type === "emoji") { if (val.type === "emoji") {
// val.value is now a string in decimal format (e.g. "128512") // val.value is now a string in decimal format (e.g. "128512")
const emojiValue = val.value as string;
newLogoValue = { newLogoValue = {
"data-emoji-unicode": val.value, "data-emoji-unicode": emojiValue,
"data-emoji-url": undefined, "data-emoji-url": undefined,
}; };
newLogoValueToStoreInLocalStorage = { newLogoValueToStoreInLocalStorage = {
in_use: "emoji", in_use: "emoji",
emoji: { emoji: {
value: val.value, value: emojiValue,
url: undefined, url: undefined,
}, },
}; };
} else if (val.type === "icon") { } else if (val.type === "icon") {
const iconValue = val.value as { name: string; color: string };
newLogoValue = { newLogoValue = {
"data-icon-name": val.value.name, "data-icon-name": iconValue.name,
"data-icon-color": val.value.color, "data-icon-color": iconValue.color,
}; };
newLogoValueToStoreInLocalStorage = { newLogoValueToStoreInLocalStorage = {
in_use: "icon", in_use: "icon",
icon: { icon: {
name: val.value.name, name: iconValue.name,
color: val.value.color, color: iconValue.color,
}, },
}; };
} }

View File

@@ -40,6 +40,7 @@ export function CustomImageUploader(props: CustomImageUploaderProps) {
// refs // refs
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const hasTriggeredFilePickerRef = useRef(false); const hasTriggeredFilePickerRef = useRef(false);
const hasTriedUploadingOnMountRef = useRef(false);
const { id: imageEntityId } = node.attrs; const { id: imageEntityId } = node.attrs;
// derived values // derived values
const imageComponentImageFileMap = useMemo(() => getImageComponentImageFileMap(editor), [editor]); const imageComponentImageFileMap = useMemo(() => getImageComponentImageFileMap(editor), [editor]);
@@ -124,17 +125,16 @@ export function CustomImageUploader(props: CustomImageUploaderProps) {
uploader: uploadFile, 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 // after the image component is mounted we start the upload process based on
// it's uploaded // it's uploaded
useEffect(() => { useEffect(() => {
if (hasTriedUploadingOnMountRef.current) return;
// the meta data of the image component
const meta = imageComponentImageFileMap?.get(imageEntityId ?? "");
if (meta) { if (meta) {
if (meta.event === "drop" && "file" in meta) { if (meta.event === "drop" && "file" in meta) {
hasTriedUploadingOnMountRef.current = true;
uploadFile(meta.file); uploadFile(meta.file);
} else if (meta.event === "insert" && fileInputRef.current && !hasTriggeredFilePickerRef.current) { } else if (meta.event === "insert" && fileInputRef.current && !hasTriggeredFilePickerRef.current) {
if (meta.hasOpenedFileInputOnce) return; if (meta.hasOpenedFileInputOnce) return;
@@ -144,8 +144,10 @@ export function CustomImageUploader(props: CustomImageUploaderProps) {
hasTriggeredFilePickerRef.current = true; hasTriggeredFilePickerRef.current = true;
imageComponentImageFileMap?.set(imageEntityId ?? "", { ...meta, hasOpenedFileInputOnce: true }); imageComponentImageFileMap?.set(imageEntityId ?? "", { ...meta, hasOpenedFileInputOnce: true });
} }
} else {
hasTriedUploadingOnMountRef.current = true;
} }
}, [meta, uploadFile, imageComponentImageFileMap, imageEntityId, isTouchDevice]); }, [imageEntityId, isTouchDevice, uploadFile, imageComponentImageFileMap]);
const onFileChange = useCallback( const onFileChange = useCallback(
async (e: ChangeEvent<HTMLInputElement>) => { async (e: ChangeEvent<HTMLInputElement>) => {

View File

@@ -115,6 +115,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
CustomCalloutExtension, CustomCalloutExtension,
UtilityExtension({ UtilityExtension({
disabledExtensions, disabledExtensions,
flaggedExtensions,
fileHandler, fileHandler,
getEditorMetaData, getEditorMetaData,
isEditable: editable, isEditable: editable,

View File

@@ -51,14 +51,14 @@ export type UtilityExtensionStorage = {
isTouchDevice: boolean; isTouchDevice: boolean;
}; };
type Props = Pick<IEditorProps, "disabledExtensions" | "getEditorMetaData"> & { type Props = Pick<IEditorProps, "disabledExtensions" | "flaggedExtensions" | "getEditorMetaData"> & {
fileHandler: TFileHandler; fileHandler: TFileHandler;
isEditable: boolean; isEditable: boolean;
isTouchDevice: boolean; isTouchDevice: boolean;
}; };
export const UtilityExtension = (props: Props) => { export const UtilityExtension = (props: Props) => {
const { disabledExtensions, fileHandler, getEditorMetaData, isEditable, isTouchDevice } = props; const { disabledExtensions, flaggedExtensions, fileHandler, getEditorMetaData, isEditable, isTouchDevice } = props;
const { restore } = fileHandler; const { restore } = fileHandler;
return Extension.create<Record<string, unknown>, UtilityExtensionStorage>({ return Extension.create<Record<string, unknown>, UtilityExtensionStorage>({
@@ -79,6 +79,7 @@ export const UtilityExtension = (props: Props) => {
}), }),
DropHandlerPlugin({ DropHandlerPlugin({
disabledExtensions, disabledExtensions,
flaggedExtensions,
editor: this.editor, editor: this.editor,
}), }),
PasteAssetPlugin(), PasteAssetPlugin(),

View File

@@ -7,11 +7,12 @@ import type { TEditorCommands, TExtensions } from "@/types";
type Props = { type Props = {
disabledExtensions?: TExtensions[]; disabledExtensions?: TExtensions[];
flaggedExtensions?: TExtensions[];
editor: Editor; editor: Editor;
}; };
export const DropHandlerPlugin = (props: Props): Plugin => { export const DropHandlerPlugin = (props: Props): Plugin => {
const { disabledExtensions, editor } = props; const { disabledExtensions, flaggedExtensions, editor } = props;
return new Plugin({ return new Plugin({
key: new PluginKey("drop-handler-plugin"), key: new PluginKey("drop-handler-plugin"),
@@ -33,6 +34,7 @@ export const DropHandlerPlugin = (props: Props): Plugin => {
const pos = view.state.selection.from; const pos = view.state.selection.from;
insertFilesSafely({ insertFilesSafely({
disabledExtensions, disabledExtensions,
flaggedExtensions,
editor, editor,
files: acceptedFiles, files: acceptedFiles,
initialPos: pos, initialPos: pos,
@@ -84,6 +86,7 @@ export const DropHandlerPlugin = (props: Props): Plugin => {
type InsertFilesSafelyArgs = { type InsertFilesSafelyArgs = {
disabledExtensions?: TExtensions[]; disabledExtensions?: TExtensions[];
flaggedExtensions?: TExtensions[];
editor: Editor; editor: Editor;
event: "insert" | "drop"; event: "insert" | "drop";
files: File[]; files: File[];

View File

@@ -21,6 +21,7 @@ export const TrackFileDeletionPlugin = (editor: Editor, deleteHandler: TFileHand
[nodeType: string]: Set<string> | undefined; [nodeType: string]: Set<string> | undefined;
} = {}; } = {};
if (!transactions.some((tr) => tr.docChanged)) return null; 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) => { newState.doc.descendants((node) => {
const nodeType = node.type.name as keyof NodeFileMapType; const nodeType = node.type.name as keyof NodeFileMapType;
@@ -34,10 +35,6 @@ 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 // iterate through all the nodes in the old state
@@ -68,7 +65,6 @@ export const TrackFileDeletionPlugin = (editor: Editor, deleteHandler: TFileHand
console.error("Error deleting file via delete utility plugin:", error); console.error("Error deleting file via delete utility plugin:", error);
} }
}); });
});
return null; return null;
}, },

View File

@@ -98,10 +98,10 @@ export class SitesFileService extends FileService {
* @returns {Promise<void>} Promise resolving to void * @returns {Promise<void>} Promise resolving to void
* @throws {Error} If the request fails * @throws {Error} If the request fails
*/ */
async restoreNewAsset(workspaceSlug: string, src: string): Promise<void> { async restoreNewAsset(anchor: string, src: string): Promise<void> {
// remove the last slash and get the asset id // remove the last slash and get the asset id
const assetId = getAssetIdFromUrl(src); 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) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;