Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PE-242, 243] refactor: editor file handling, image upload status #6442

Open
wants to merge 9 commits into
base: preview
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import { getEditorClassNames } from "@/helpers/common";
// hooks
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
// types
import { EditorReadOnlyRefApi, TDisplayConfig, TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types";
import {
EditorReadOnlyRefApi,
TDisplayConfig,
TExtensions,
TReadOnlyFileHandler,
TReadOnlyMentionHandler,
} from "@/types";

interface IDocumentReadOnlyEditor {
disabledExtensions: TExtensions[];
Expand All @@ -20,7 +26,7 @@ interface IDocumentReadOnlyEditor {
displayConfig?: TDisplayConfig;
editorClassName?: string;
embedHandler: any;
fileHandler: Pick<TFileHandler, "getAssetSrc">;
fileHandler: TReadOnlyFileHandler;
tabIndex?: number;
handleEditorReady?: (value: boolean) => void;
mentionHandler: TReadOnlyMentionHandler;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from "react";
import { NodeSelection } from "@tiptap/pm/state";
import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from "react";
// plane utils
import { cn } from "@plane/utils";
// extensions
import { CustoBaseImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image";
import { ImageUploadStatus } from "./upload-status";

const MIN_SIZE = 100;

Expand Down Expand Up @@ -210,6 +211,8 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
// show the image loader if the remote image's src or preview image from filesystem is not set yet (while loading the image post upload) (or)
// if the initial resize (from 35% width and "auto" height attrs to the actual size in px) is not complete
const showImageLoader = !(resolvedImageSrc || imageFromFileSystem) || !initialResizeComplete || hasErroredOnFirstLoad;
// show the image upload status only when the resolvedImageSrc is not ready
const showUploadStatus = !resolvedImageSrc;
// show the image utils only if the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
const showImageUtils = resolvedImageSrc && initialResizeComplete;
// show the image resizer only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
Expand Down Expand Up @@ -270,6 +273,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
...(size.aspectRatio && { aspectRatio: size.aspectRatio }),
}}
/>
{showUploadStatus && <ImageUploadStatus editor={editor} nodeId={node.attrs.id} />}
{showImageUtils && (
<ImageToolbarRoot
containerClassName={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
);
// hooks
const { uploading: isImageBeingUploaded, uploadFile } = useUploader({
blockId: imageEntityId ?? "",
editor,
loadImageFromFileSystem,
maxFileSize,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Editor } from "@tiptap/core";
import { useEditorState } from "@tiptap/react";
import { useEffect, useRef, useState } from "react";

type Props = {
editor: Editor;
nodeId: string;
};

export const ImageUploadStatus: React.FC<Props> = (props) => {
const { editor, nodeId } = props;
// Displayed status that will animate smoothly
const [displayStatus, setDisplayStatus] = useState(0);
// Animation frame ID for cleanup
const animationFrameRef = useRef(null);
// subscribe to image upload status
const uploadStatus: number | undefined = useEditorState({
editor,
selector: ({ editor }) => editor.storage.imageComponent.assetsUploadStatus[nodeId],
});
Comment on lines +10 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify the editor storage structure.

The code assumes the existence of editor.storage.imageComponent.assetsUploadStatus. Please ensure this storage structure is properly initialized.


🏁 Script executed:

#!/bin/bash
# Description: Verify the editor storage structure initialization

# Search for imageComponent storage initialization
ast-grep --pattern 'storage: {
  imageComponent: {
    assetsUploadStatus: $_
  }
}'

# Search for any direct assignments to assetsUploadStatus
rg -A 5 'assetsUploadStatus ='

Length of output: 1235


Discrepancy in storage structure for assetsUploadStatus

It appears that the storage structure is not initialized as expected. In custom-image.ts, assetsUploadStatus is assigned directly on storage (this.storage.assetsUploadStatus) and in use-editor.ts, the assetsUploadStatus is pulled from fileHandler without an imageComponent grouping. Conversely, the upload-status.tsx component expects to access assetsUploadStatus under editor.storage.imageComponent. This mismatch indicates that either the initialization must be updated to nest assetsUploadStatus inside an imageComponent object or the consumer (upload-status.tsx) should be adjusted to match the actual storage layout.


useEffect(() => {
const animateToValue = (start: number, end: number, startTime: number) => {
const duration = 200;

const animation = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);

// Easing function for smooth animation
const easeOutCubic = 1 - Math.pow(1 - progress, 3);

// Calculate current display value
const currentValue = Math.floor(start + (end - start) * easeOutCubic);
setDisplayStatus(currentValue);

// Continue animation if not complete
if (progress < 1) {
animationFrameRef.current = requestAnimationFrame((time) => animation(time));
}
};
animationFrameRef.current = requestAnimationFrame((time) => animation(time));
};
animateToValue(displayStatus, uploadStatus, performance.now());

return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [uploadStatus]);

if (uploadStatus === undefined) return null;

return (
<div className="absolute top-1 right-1 z-20 bg-black/60 rounded text-xs font-medium w-10 text-center">
{displayStatus}%
</div>
);
};
24 changes: 15 additions & 9 deletions packages/editor/src/core/extensions/custom-image/custom-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
import { v4 as uuidv4 } from "uuid";
// extensions
import { CustomImageNode } from "@/extensions/custom-image";
// helpers
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
// plugins
import { TrackImageDeletionPlugin, TrackImageRestorationPlugin, isFileValid } from "@/plugins/image";
// types
import { TFileHandler } from "@/types";
// helpers
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";

export type InsertImageComponentProps = {
file?: File;
Expand All @@ -21,7 +21,8 @@ declare module "@tiptap/core" {
interface Commands<ReturnType> {
imageComponent: {
insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType;
uploadImage: (file: File) => () => Promise<string> | undefined;
uploadImage: (blockId: string, file: File) => () => Promise<string> | undefined;
updateAssetsUploadStatus: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void;
getImageSource?: (path: string) => () => Promise<string>;
restoreImage: (src: string) => () => Promise<void>;
};
Expand All @@ -32,13 +33,15 @@ export const getImageComponentImageFileMap = (editor: Editor) =>
(editor.storage.imageComponent as UploadImageExtensionStorage | undefined)?.fileMap;

export interface UploadImageExtensionStorage {
assetsUploadStatus: TFileHandler["assetsUploadStatus"];
fileMap: Map<string, UploadEntity>;
}

export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & { hasOpenedFileInputOnce?: boolean };

export const CustomImageExtension = (props: TFileHandler) => {
const {
assetsUploadStatus,
getAssetSrc,
upload,
delete: deleteImageFn,
Expand Down Expand Up @@ -105,7 +108,6 @@ export const CustomImageExtension = (props: TFileHandler) => {
this.editor.state.doc.descendants((node) => {
if (node.type.name === this.name) {
if (!node.attrs.src?.startsWith("http")) return;

imageSources.add(node.attrs.src);
}
});
Expand All @@ -128,13 +130,14 @@ export const CustomImageExtension = (props: TFileHandler) => {
markdown: {
serialize() {},
},
assetsUploadStatus,
};
},

addCommands() {
return {
insertImageComponent:
(props: { file?: File; pos?: number; event: "insert" | "drop" }) =>
(props) =>
({ commands }) => {
// Early return if there's an invalid file being dropped
if (
Expand Down Expand Up @@ -182,12 +185,15 @@ export const CustomImageExtension = (props: TFileHandler) => {
attrs: attributes,
});
},
uploadImage: (file: File) => async () => {
const fileUrl = await upload(file);
uploadImage: (blockId, file) => async () => {
const fileUrl = await upload(blockId, file);
return fileUrl;
},
getImageSource: (path: string) => async () => await getAssetSrc(path),
restoreImage: (src: string) => async () => {
updateAssetsUploadStatus: (updatedStatus) => () => {
this.storage.assetsUploadStatus = updatedStatus;
},
aaryan610 marked this conversation as resolved.
Show resolved Hide resolved
getImageSource: (path) => async () => await getAssetSrc(path),
restoreImage: (src) => async () => {
await restoreImageFn(src);
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
// components
import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custom-image";
// types
import { TFileHandler } from "@/types";
import { TReadOnlyFileHandler } from "@/types";

export const CustomReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">) => {
export const CustomReadOnlyImageExtension = (props: TReadOnlyFileHandler) => {
const { getAssetSrc } = props;

return Image.extend<Record<string, unknown>, UploadImageExtensionStorage>({
Expand Down Expand Up @@ -56,6 +56,7 @@ export const CustomReadOnlyImageExtension = (props: Pick<TFileHandler, "getAsset
markdown: {
serialize() {},
},
assetsUploadStatus: {},
};
},

Expand Down
4 changes: 2 additions & 2 deletions packages/editor/src/core/extensions/extensions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ import {
} from "@/extensions";
// helpers
import { isValidHttpUrl } from "@/helpers/common";
// types
import { TExtensions, TFileHandler, TMentionHandler } from "@/types";
// plane editor extensions
import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions";
// types
import { TExtensions, TFileHandler, TMentionHandler } from "@/types";

type TArguments = {
disabledExtensions: TExtensions[];
Expand Down
4 changes: 2 additions & 2 deletions packages/editor/src/core/extensions/image/read-only-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
// extensions
import { CustomImageNode } from "@/extensions";
// types
import { TFileHandler } from "@/types";
import { TReadOnlyFileHandler } from "@/types";

export const ReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">) => {
export const ReadOnlyImageExtension = (props: TReadOnlyFileHandler) => {
const { getAssetSrc } = props;

return Image.extend({
Expand Down
14 changes: 5 additions & 9 deletions packages/editor/src/core/extensions/read-only-extensions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ import {
} from "@/extensions";
// helpers
import { isValidHttpUrl } from "@/helpers/common";
// types
import { TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types";
// plane editor extensions
import { CoreReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions";
// types
import { TExtensions, TReadOnlyFileHandler, TReadOnlyMentionHandler } from "@/types";

type Props = {
disabledExtensions: TExtensions[];
fileHandler: Pick<TFileHandler, "getAssetSrc">;
fileHandler: TReadOnlyFileHandler;
mentionHandler: TReadOnlyMentionHandler;
};

Expand Down Expand Up @@ -94,16 +94,12 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
},
}),
CustomTypographyExtension,
ReadOnlyImageExtension({
getAssetSrc: fileHandler.getAssetSrc,
}).configure({
ReadOnlyImageExtension(fileHandler).configure({
HTMLAttributes: {
class: "rounded-md",
},
}),
CustomReadOnlyImageExtension({
getAssetSrc: fileHandler.getAssetSrc,
}),
CustomReadOnlyImageExtension(fileHandler),
TiptapUnderline,
TextStyle,
TaskList.configure({
Expand Down
7 changes: 7 additions & 0 deletions packages/editor/src/core/hooks/use-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ export const useEditor = (props: CustomEditorProps) => {
}
}, [editor, value, id]);

// update assets upload status
useEffect(() => {
if (!editor) return;
const assetsUploadStatus = fileHandler.assetsUploadStatus;
editor.commands.updateAssetsUploadStatus(assetsUploadStatus);
}, [editor, fileHandler.assetsUploadStatus]);

useImperativeHandle(
forwardedRef,
() => ({
Expand Down
5 changes: 3 additions & 2 deletions packages/editor/src/core/hooks/use-file-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import { insertImagesSafely } from "@/extensions/drop";
import { isFileValid } from "@/plugins/image";

type TUploaderArgs = {
blockId: string;
editor: Editor;
loadImageFromFileSystem: (file: string) => void;
maxFileSize: number;
onUpload: (url: string) => void;
};

export const useUploader = (args: TUploaderArgs) => {
const { editor, loadImageFromFileSystem, maxFileSize, onUpload } = args;
const { blockId, editor, loadImageFromFileSystem, maxFileSize, onUpload } = args;
// states
const [uploading, setUploading] = useState(false);

Expand Down Expand Up @@ -49,7 +50,7 @@ export const useUploader = (args: TUploaderArgs) => {
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(fileWithTrimmedName);
const url: string = await editor?.commands.uploadImage(blockId, fileWithTrimmedName);
aaryan610 marked this conversation as resolved.
Show resolved Hide resolved

if (!url) {
throw new Error("Something went wrong while uploading the image");
Expand Down
4 changes: 2 additions & 2 deletions packages/editor/src/core/hooks/use-read-only-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
// props
import { CoreReadOnlyEditorProps } from "@/props";
// types
import type { EditorReadOnlyRefApi, TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types";
import type { EditorReadOnlyRefApi, TExtensions, TReadOnlyFileHandler, TReadOnlyMentionHandler } from "@/types";

interface CustomReadOnlyEditorProps {
disabledExtensions: TExtensions[];
Expand All @@ -20,7 +20,7 @@ interface CustomReadOnlyEditorProps {
extensions?: Extensions;
forwardedRef?: MutableRefObject<EditorReadOnlyRefApi | null>;
initialValue?: string;
fileHandler: Pick<TFileHandler, "getAssetSrc">;
fileHandler: TReadOnlyFileHandler;
handleEditorReady?: (value: boolean) => void;
mentionHandler: TReadOnlyMentionHandler;
provider?: HocuspocusProvider;
Expand Down
3 changes: 2 additions & 1 deletion packages/editor/src/core/types/collaboration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
TExtensions,
TFileHandler,
TMentionHandler,
TReadOnlyFileHandler,
TReadOnlyMentionHandler,
TRealtimeConfig,
TUserDetails,
Expand Down Expand Up @@ -43,7 +44,7 @@ export type TCollaborativeEditorProps = TCollaborativeEditorHookProps & {
};

export type TReadOnlyCollaborativeEditorProps = TCollaborativeEditorHookProps & {
fileHandler: Pick<TFileHandler, "getAssetSrc">;
fileHandler: TReadOnlyFileHandler;
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
mentionHandler: TReadOnlyMentionHandler;
};
8 changes: 6 additions & 2 deletions packages/editor/src/core/types/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { DeleteImage, RestoreImage, UploadImage } from "@/types";

export type TFileHandler = {
export type TReadOnlyFileHandler = {
getAssetSrc: (path: string) => Promise<string>;
restore: RestoreImage;
};

export type TFileHandler = TReadOnlyFileHandler & {
assetsUploadStatus: Record<string, number>; // blockId => progress percentage
cancel: () => void;
delete: DeleteImage;
upload: UploadImage;
restore: RestoreImage;
validation: {
/**
* @description max file size in bytes
Expand Down
Loading
Loading