mirror of
https://github.com/makeplane/plane.git
synced 2025-12-16 03:47:54 +01:00
[WIKI-804] fix: refactor image uploader (#8210)
* fix: refactor uploader * fix: props * fix: sites fix
This commit is contained in:
@@ -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"])
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
@@ -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,40 +35,35 @@ export const TrackFileDeletionPlugin = (editor: Editor, deleteHandler: TFileHand
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
transactions.forEach((transaction) => {
|
const removedFiles: TFileNode[] = [];
|
||||||
// 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[] = [];
|
// 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
|
removedFiles.forEach(async (node) => {
|
||||||
oldState.doc.descendants((node) => {
|
const nodeType = node.type.name as keyof NodeFileMapType;
|
||||||
const nodeType = node.type.name as keyof NodeFileMapType;
|
const src = node.attrs.src;
|
||||||
const isAValidNode = NODE_FILE_MAP[nodeType];
|
const nodeFileSetDetails = NODE_FILE_MAP[nodeType];
|
||||||
// if the node doesn't match, then return as no point in checking
|
if (!nodeFileSetDetails || !src) return;
|
||||||
if (!isAValidNode) return;
|
try {
|
||||||
// Check if the node has been deleted or replaced
|
editor.storage[nodeType]?.[nodeFileSetDetails.fileSetName]?.set(src, true);
|
||||||
if (!newFileSources[nodeType]?.has(node.attrs.src)) {
|
// update assets list storage value
|
||||||
removedFiles.push(node as TFileNode);
|
editor.commands.updateAssetsList?.({
|
||||||
}
|
idToRemove: node.attrs.id,
|
||||||
});
|
});
|
||||||
|
await deleteHandler(src);
|
||||||
removedFiles.forEach(async (node) => {
|
} catch (error) {
|
||||||
const nodeType = node.type.name as keyof NodeFileMapType;
|
console.error("Error deleting file via delete utility plugin:", error);
|
||||||
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;
|
return null;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user