Skip to content

Commit 214692f

Browse files
authored
[PE-242, 243] refactor: editor file handling, image upload status (#6442)
* refactor: editor file handling * refactor: asset store * refactor: space app file handlers * fix: separate webhook connection params * chore: handle undefined status * chore: add type to upload status * chore: added transition for upload status update
1 parent b719823 commit 214692f

File tree

53 files changed

+593
-306
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+593
-306
lines changed

packages/editor/src/core/components/editors/document/read-only-editor.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import { getEditorClassNames } from "@/helpers/common";
1111
// hooks
1212
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
1313
// types
14-
import { EditorReadOnlyRefApi, TDisplayConfig, TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types";
14+
import {
15+
EditorReadOnlyRefApi,
16+
TDisplayConfig,
17+
TExtensions,
18+
TReadOnlyFileHandler,
19+
TReadOnlyMentionHandler,
20+
} from "@/types";
1521

1622
interface IDocumentReadOnlyEditor {
1723
disabledExtensions: TExtensions[];
@@ -21,7 +27,7 @@ interface IDocumentReadOnlyEditor {
2127
displayConfig?: TDisplayConfig;
2228
editorClassName?: string;
2329
embedHandler: any;
24-
fileHandler: Pick<TFileHandler, "getAssetSrc">;
30+
fileHandler: TReadOnlyFileHandler;
2531
tabIndex?: number;
2632
handleEditorReady?: (value: boolean) => void;
2733
mentionHandler: TReadOnlyMentionHandler;

packages/editor/src/core/extensions/custom-image/components/image-block.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from
44
import { cn } from "@plane/utils";
55
// extensions
66
import { CustoBaseImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image";
7+
import { ImageUploadStatus } from "./upload-status";
78

89
const MIN_SIZE = 100;
910

@@ -210,6 +211,8 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
210211
// 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)
211212
// if the initial resize (from 35% width and "auto" height attrs to the actual size in px) is not complete
212213
const showImageLoader = !(resolvedImageSrc || imageFromFileSystem) || !initialResizeComplete || hasErroredOnFirstLoad;
214+
// show the image upload status only when the resolvedImageSrc is not ready
215+
const showUploadStatus = !resolvedImageSrc;
213216
// 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)
214217
const showImageUtils = resolvedImageSrc && initialResizeComplete;
215218
// 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)
@@ -279,6 +282,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
279282
...(size.aspectRatio && { aspectRatio: size.aspectRatio }),
280283
}}
281284
/>
285+
{showUploadStatus && <ImageUploadStatus editor={editor} nodeId={node.attrs.id} />}
282286
{showImageUtils && (
283287
<ImageToolbarRoot
284288
containerClassName={

packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
6969
);
7070
// hooks
7171
const { uploading: isImageBeingUploaded, uploadFile } = useUploader({
72+
blockId: imageEntityId ?? "",
7273
editor,
7374
loadImageFromFileSystem,
7475
maxFileSize,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Editor } from "@tiptap/core";
2+
import { useEditorState } from "@tiptap/react";
3+
import { useEffect, useRef, useState } from "react";
4+
5+
type Props = {
6+
editor: Editor;
7+
nodeId: string;
8+
};
9+
10+
export const ImageUploadStatus: React.FC<Props> = (props) => {
11+
const { editor, nodeId } = props;
12+
// Displayed status that will animate smoothly
13+
const [displayStatus, setDisplayStatus] = useState(0);
14+
// Animation frame ID for cleanup
15+
const animationFrameRef = useRef(null);
16+
// subscribe to image upload status
17+
const uploadStatus: number | undefined = useEditorState({
18+
editor,
19+
selector: ({ editor }) => editor.storage.imageComponent.assetsUploadStatus[nodeId],
20+
});
21+
22+
useEffect(() => {
23+
const animateToValue = (start: number, end: number, startTime: number) => {
24+
const duration = 200;
25+
26+
const animation = (currentTime: number) => {
27+
const elapsed = currentTime - startTime;
28+
const progress = Math.min(elapsed / duration, 1);
29+
30+
// Easing function for smooth animation
31+
const easeOutCubic = 1 - Math.pow(1 - progress, 3);
32+
33+
// Calculate current display value
34+
const currentValue = Math.floor(start + (end - start) * easeOutCubic);
35+
setDisplayStatus(currentValue);
36+
37+
// Continue animation if not complete
38+
if (progress < 1) {
39+
animationFrameRef.current = requestAnimationFrame((time) => animation(time));
40+
}
41+
};
42+
animationFrameRef.current = requestAnimationFrame((time) => animation(time));
43+
};
44+
animateToValue(displayStatus, uploadStatus, performance.now());
45+
46+
return () => {
47+
if (animationFrameRef.current) {
48+
cancelAnimationFrame(animationFrameRef.current);
49+
}
50+
};
51+
}, [uploadStatus]);
52+
53+
if (uploadStatus === undefined) return null;
54+
55+
return (
56+
<div className="absolute top-1 right-1 z-20 bg-black/60 rounded text-xs font-medium w-10 text-center">
57+
{displayStatus}%
58+
</div>
59+
);
60+
};

packages/editor/src/core/extensions/custom-image/custom-image.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
44
import { v4 as uuidv4 } from "uuid";
55
// extensions
66
import { CustomImageNode } from "@/extensions/custom-image";
7+
// helpers
8+
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
79
// plugins
810
import { TrackImageDeletionPlugin, TrackImageRestorationPlugin, isFileValid } from "@/plugins/image";
911
// types
1012
import { TFileHandler } from "@/types";
11-
// helpers
12-
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
1313

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

3435
export interface UploadImageExtensionStorage {
36+
assetsUploadStatus: TFileHandler["assetsUploadStatus"];
3537
fileMap: Map<string, UploadEntity>;
3638
}
3739

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

4042
export const CustomImageExtension = (props: TFileHandler) => {
4143
const {
44+
assetsUploadStatus,
4245
getAssetSrc,
4346
upload,
4447
delete: deleteImageFn,
@@ -105,7 +108,6 @@ export const CustomImageExtension = (props: TFileHandler) => {
105108
this.editor.state.doc.descendants((node) => {
106109
if (node.type.name === this.name) {
107110
if (!node.attrs.src?.startsWith("http")) return;
108-
109111
imageSources.add(node.attrs.src);
110112
}
111113
});
@@ -128,13 +130,14 @@ export const CustomImageExtension = (props: TFileHandler) => {
128130
markdown: {
129131
serialize() {},
130132
},
133+
assetsUploadStatus,
131134
};
132135
},
133136

134137
addCommands() {
135138
return {
136139
insertImageComponent:
137-
(props: { file?: File; pos?: number; event: "insert" | "drop" }) =>
140+
(props) =>
138141
({ commands }) => {
139142
// Early return if there's an invalid file being dropped
140143
if (
@@ -182,12 +185,15 @@ export const CustomImageExtension = (props: TFileHandler) => {
182185
attrs: attributes,
183186
});
184187
},
185-
uploadImage: (file: File) => async () => {
186-
const fileUrl = await upload(file);
188+
uploadImage: (blockId, file) => async () => {
189+
const fileUrl = await upload(blockId, file);
187190
return fileUrl;
188191
},
189-
getImageSource: (path: string) => async () => await getAssetSrc(path),
190-
restoreImage: (src: string) => async () => {
192+
updateAssetsUploadStatus: (updatedStatus) => () => {
193+
this.storage.assetsUploadStatus = updatedStatus;
194+
},
195+
getImageSource: (path) => async () => await getAssetSrc(path),
196+
restoreImage: (src) => async () => {
191197
await restoreImageFn(src);
192198
},
193199
};

packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
44
// components
55
import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custom-image";
66
// types
7-
import { TFileHandler } from "@/types";
7+
import { TReadOnlyFileHandler } from "@/types";
88

9-
export const CustomReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">) => {
9+
export const CustomReadOnlyImageExtension = (props: TReadOnlyFileHandler) => {
1010
const { getAssetSrc } = props;
1111

1212
return Image.extend<Record<string, unknown>, UploadImageExtensionStorage>({
@@ -56,6 +56,7 @@ export const CustomReadOnlyImageExtension = (props: Pick<TFileHandler, "getAsset
5656
markdown: {
5757
serialize() {},
5858
},
59+
assetsUploadStatus: {},
5960
};
6061
},
6162

packages/editor/src/core/extensions/image/read-only-image.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
33
// extensions
44
import { CustomImageNode } from "@/extensions";
55
// types
6-
import { TFileHandler } from "@/types";
6+
import { TReadOnlyFileHandler } from "@/types";
77

8-
export const ReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">) => {
8+
export const ReadOnlyImageExtension = (props: TReadOnlyFileHandler) => {
99
const { getAssetSrc } = props;
1010

1111
return Image.extend({

packages/editor/src/core/extensions/read-only-extensions.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ import {
2727
} from "@/extensions";
2828
// helpers
2929
import { isValidHttpUrl } from "@/helpers/common";
30-
// types
31-
import { TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types";
3230
// plane editor extensions
3331
import { CoreReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions";
32+
// types
33+
import { TExtensions, TReadOnlyFileHandler, TReadOnlyMentionHandler } from "@/types";
3434

3535
type Props = {
3636
disabledExtensions: TExtensions[];
37-
fileHandler: Pick<TFileHandler, "getAssetSrc">;
37+
fileHandler: TReadOnlyFileHandler;
3838
mentionHandler: TReadOnlyMentionHandler;
3939
};
4040

@@ -94,16 +94,12 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
9494
},
9595
}),
9696
CustomTypographyExtension,
97-
ReadOnlyImageExtension({
98-
getAssetSrc: fileHandler.getAssetSrc,
99-
}).configure({
97+
ReadOnlyImageExtension(fileHandler).configure({
10098
HTMLAttributes: {
10199
class: "rounded-md",
102100
},
103101
}),
104-
CustomReadOnlyImageExtension({
105-
getAssetSrc: fileHandler.getAssetSrc,
106-
}),
102+
CustomReadOnlyImageExtension(fileHandler),
107103
TiptapUnderline,
108104
TextStyle,
109105
TaskList.configure({

packages/editor/src/core/hooks/use-editor.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@ export const useEditor = (props: CustomEditorProps) => {
125125
}
126126
}, [editor, value, id]);
127127

128+
// update assets upload status
129+
useEffect(() => {
130+
if (!editor) return;
131+
const assetsUploadStatus = fileHandler.assetsUploadStatus;
132+
editor.commands.updateAssetsUploadStatus(assetsUploadStatus);
133+
}, [editor, fileHandler.assetsUploadStatus]);
134+
128135
useImperativeHandle(
129136
forwardedRef,
130137
() => ({

packages/editor/src/core/hooks/use-file-upload.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import { insertImagesSafely } from "@/extensions/drop";
66
import { isFileValid } from "@/plugins/image";
77

88
type TUploaderArgs = {
9+
blockId: string;
910
editor: Editor;
1011
loadImageFromFileSystem: (file: string) => void;
1112
maxFileSize: number;
1213
onUpload: (url: string) => void;
1314
};
1415

1516
export const useUploader = (args: TUploaderArgs) => {
16-
const { editor, loadImageFromFileSystem, maxFileSize, onUpload } = args;
17+
const { blockId, editor, loadImageFromFileSystem, maxFileSize, onUpload } = args;
1718
// states
1819
const [uploading, setUploading] = useState(false);
1920

@@ -49,7 +50,7 @@ export const useUploader = (args: TUploaderArgs) => {
4950
reader.readAsDataURL(fileWithTrimmedName);
5051
// @ts-expect-error - TODO: fix typings, and don't remove await from
5152
// here for now
52-
const url: string = await editor?.commands.uploadImage(fileWithTrimmedName);
53+
const url: string = await editor?.commands.uploadImage(blockId, fileWithTrimmedName);
5354

5455
if (!url) {
5556
throw new Error("Something went wrong while uploading the image");

0 commit comments

Comments
 (0)