From 214692f5b2eb72d4f6b48ff8af9bff69c68708f8 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:18:01 +0530 Subject: [PATCH] [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 --- .../editors/document/read-only-editor.tsx | 10 +- .../custom-image/components/image-block.tsx | 4 + .../components/image-uploader.tsx | 1 + .../custom-image/components/upload-status.tsx | 60 +++++++++ .../extensions/custom-image/custom-image.ts | 24 ++-- .../custom-image/read-only-custom-image.ts | 5 +- .../core/extensions/image/read-only-image.tsx | 4 +- .../core/extensions/read-only-extensions.tsx | 14 +- packages/editor/src/core/hooks/use-editor.ts | 7 + .../editor/src/core/hooks/use-file-upload.ts | 5 +- .../src/core/hooks/use-read-only-editor.ts | 4 +- .../editor/src/core/types/collaboration.ts | 3 +- packages/editor/src/core/types/config.ts | 8 +- packages/editor/src/core/types/editor.ts | 9 +- packages/editor/src/core/types/image.ts | 2 +- .../components/editor/lite-text-editor.tsx | 4 +- .../editor/lite-text-read-only-editor.tsx | 4 +- .../components/editor/rich-text-editor.tsx | 12 +- .../editor/rich-text-read-only-editor.tsx | 4 +- .../peek-overview/comment/add-comment.tsx | 2 +- .../comment/comment-detail-card.tsx | 3 +- .../issues/peek-overview/issue-details.tsx | 7 +- space/helpers/editor.helper.ts | 69 +++++----- .../pages/(detail)/[pageId]/page.tsx | 49 +++---- .../pages/editor/ai/ask-pi-menu.tsx | 7 + web/ce/components/pages/editor/ai/menu.tsx | 4 +- .../core/modals/gpt-assistant-popover.tsx | 12 +- .../lite-text-editor/lite-text-editor.tsx | 13 +- .../lite-text-read-only-editor.tsx | 9 +- .../rich-text-editor/rich-text-editor.tsx | 13 +- .../rich-text-read-only-editor.tsx | 9 +- .../editor/sticky-editor/editor.tsx | 15 +-- .../modals/create-modal/issue-description.tsx | 25 ++-- .../components/issues/description-input.tsx | 34 ++--- .../issue-activity/comments/comment-card.tsx | 5 +- .../comments/comment-create.tsx | 4 +- .../issue-detail/issue-activity/root.tsx | 36 +++--- .../components/description-editor.tsx | 30 ++--- .../components/pages/editor/editor-body.tsx | 16 ++- .../components/pages/editor/page-root.tsx | 6 +- web/core/components/pages/version/editor.tsx | 11 +- .../profile/activity/activity-list.tsx | 6 +- .../activity/profile-activity-list.tsx | 3 +- web/core/hooks/editor/index.ts | 2 + web/core/hooks/editor/use-editor-config.ts | 96 ++++++++++++++ .../hooks/{ => editor}/use-editor-mention.tsx | 0 web/core/hooks/store/index.ts | 1 + web/core/hooks/store/use-editor-asset.ts | 10 ++ web/core/services/file.service.ts | 19 ++- web/core/store/editor/asset.store.ts | 121 ++++++++++++++++++ .../issue/issue-details/attachment.store.ts | 2 +- web/core/store/root.store.ts | 5 +- web/helpers/editor.helper.ts | 89 ------------- 53 files changed, 602 insertions(+), 315 deletions(-) create mode 100644 packages/editor/src/core/extensions/custom-image/components/upload-status.tsx create mode 100644 web/core/hooks/editor/index.ts create mode 100644 web/core/hooks/editor/use-editor-config.ts rename web/core/hooks/{ => editor}/use-editor-mention.tsx (100%) create mode 100644 web/core/hooks/store/use-editor-asset.ts create mode 100644 web/core/store/editor/asset.store.ts diff --git a/packages/editor/src/core/components/editors/document/read-only-editor.tsx b/packages/editor/src/core/components/editors/document/read-only-editor.tsx index 58e834e71..fa1770f0c 100644 --- a/packages/editor/src/core/components/editors/document/read-only-editor.tsx +++ b/packages/editor/src/core/components/editors/document/read-only-editor.tsx @@ -11,7 +11,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[]; @@ -21,7 +27,7 @@ interface IDocumentReadOnlyEditor { displayConfig?: TDisplayConfig; editorClassName?: string; embedHandler: any; - fileHandler: Pick; + fileHandler: TReadOnlyFileHandler; tabIndex?: number; handleEditorReady?: (value: boolean) => void; mentionHandler: TReadOnlyMentionHandler; diff --git a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx index 961764081..bae6d3dae 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx @@ -4,6 +4,7 @@ import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from import { cn } from "@plane/utils"; // extensions import { CustoBaseImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image"; +import { ImageUploadStatus } from "./upload-status"; const MIN_SIZE = 100; @@ -210,6 +211,8 @@ export const CustomImageBlock: React.FC = (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) @@ -279,6 +282,7 @@ export const CustomImageBlock: React.FC = (props) => { ...(size.aspectRatio && { aspectRatio: size.aspectRatio }), }} /> + {showUploadStatus && } {showImageUtils && ( { ); // hooks const { uploading: isImageBeingUploaded, uploadFile } = useUploader({ + blockId: imageEntityId ?? "", editor, loadImageFromFileSystem, maxFileSize, diff --git a/packages/editor/src/core/extensions/custom-image/components/upload-status.tsx b/packages/editor/src/core/extensions/custom-image/components/upload-status.tsx new file mode 100644 index 000000000..8492d5194 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/components/upload-status.tsx @@ -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) => { + 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], + }); + + 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 ( +
+ {displayStatus}% +
+ ); +}; diff --git a/packages/editor/src/core/extensions/custom-image/custom-image.ts b/packages/editor/src/core/extensions/custom-image/custom-image.ts index 3b64db8d0..a4d38ceb5 100644 --- a/packages/editor/src/core/extensions/custom-image/custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/custom-image.ts @@ -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; @@ -21,7 +21,8 @@ declare module "@tiptap/core" { interface Commands { imageComponent: { insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType; - uploadImage: (file: File) => () => Promise | undefined; + uploadImage: (blockId: string, file: File) => () => Promise | undefined; + updateAssetsUploadStatus: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void; getImageSource?: (path: string) => () => Promise; restoreImage: (src: string) => () => Promise; }; @@ -32,6 +33,7 @@ export const getImageComponentImageFileMap = (editor: Editor) => (editor.storage.imageComponent as UploadImageExtensionStorage | undefined)?.fileMap; export interface UploadImageExtensionStorage { + assetsUploadStatus: TFileHandler["assetsUploadStatus"]; fileMap: Map; } @@ -39,6 +41,7 @@ export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) export const CustomImageExtension = (props: TFileHandler) => { const { + assetsUploadStatus, getAssetSrc, upload, delete: deleteImageFn, @@ -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); } }); @@ -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 ( @@ -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; + }, + getImageSource: (path) => async () => await getAssetSrc(path), + restoreImage: (src) => async () => { await restoreImageFn(src); }, }; diff --git a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts index c27970d92..78237d678 100644 --- a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts @@ -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) => { +export const CustomReadOnlyImageExtension = (props: TReadOnlyFileHandler) => { const { getAssetSrc } = props; return Image.extend, UploadImageExtensionStorage>({ @@ -56,6 +56,7 @@ export const CustomReadOnlyImageExtension = (props: Pick) => { +export const ReadOnlyImageExtension = (props: TReadOnlyFileHandler) => { const { getAssetSrc } = props; return Image.extend({ diff --git a/packages/editor/src/core/extensions/read-only-extensions.tsx b/packages/editor/src/core/extensions/read-only-extensions.tsx index e39973f9c..8711b2cb3 100644 --- a/packages/editor/src/core/extensions/read-only-extensions.tsx +++ b/packages/editor/src/core/extensions/read-only-extensions.tsx @@ -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; + fileHandler: TReadOnlyFileHandler; mentionHandler: TReadOnlyMentionHandler; }; @@ -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({ diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 03e9353f4..16df9151c 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -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, () => ({ diff --git a/packages/editor/src/core/hooks/use-file-upload.ts b/packages/editor/src/core/hooks/use-file-upload.ts index 65daa2f8e..7d3dc7eae 100644 --- a/packages/editor/src/core/hooks/use-file-upload.ts +++ b/packages/editor/src/core/hooks/use-file-upload.ts @@ -6,6 +6,7 @@ import { insertImagesSafely } from "@/extensions/drop"; import { isFileValid } from "@/plugins/image"; type TUploaderArgs = { + blockId: string; editor: Editor; loadImageFromFileSystem: (file: string) => void; maxFileSize: number; @@ -13,7 +14,7 @@ type TUploaderArgs = { }; export const useUploader = (args: TUploaderArgs) => { - const { editor, loadImageFromFileSystem, maxFileSize, onUpload } = args; + const { blockId, editor, loadImageFromFileSystem, maxFileSize, onUpload } = args; // states const [uploading, setUploading] = useState(false); @@ -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); if (!url) { throw new Error("Something went wrong while uploading the image"); diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index a0fa9f90c..6d33c0f8a 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -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[]; @@ -20,7 +20,7 @@ interface CustomReadOnlyEditorProps { extensions?: Extensions; forwardedRef?: MutableRefObject; initialValue?: string; - fileHandler: Pick; + fileHandler: TReadOnlyFileHandler; handleEditorReady?: (value: boolean) => void; mentionHandler: TReadOnlyMentionHandler; provider?: HocuspocusProvider; diff --git a/packages/editor/src/core/types/collaboration.ts b/packages/editor/src/core/types/collaboration.ts index c69a003fc..82e2f81f9 100644 --- a/packages/editor/src/core/types/collaboration.ts +++ b/packages/editor/src/core/types/collaboration.ts @@ -9,6 +9,7 @@ import { TExtensions, TFileHandler, TMentionHandler, + TReadOnlyFileHandler, TReadOnlyMentionHandler, TRealtimeConfig, TUserDetails, @@ -43,7 +44,7 @@ export type TCollaborativeEditorProps = TCollaborativeEditorHookProps & { }; export type TReadOnlyCollaborativeEditorProps = TCollaborativeEditorHookProps & { - fileHandler: Pick; + fileHandler: TReadOnlyFileHandler; forwardedRef?: React.MutableRefObject; mentionHandler: TReadOnlyMentionHandler; }; diff --git a/packages/editor/src/core/types/config.ts b/packages/editor/src/core/types/config.ts index 3bb4d1af2..d4d8ca901 100644 --- a/packages/editor/src/core/types/config.ts +++ b/packages/editor/src/core/types/config.ts @@ -1,11 +1,15 @@ import { DeleteImage, RestoreImage, UploadImage } from "@/types"; -export type TFileHandler = { +export type TReadOnlyFileHandler = { getAssetSrc: (path: string) => Promise; + restore: RestoreImage; +}; + +export type TFileHandler = TReadOnlyFileHandler & { + assetsUploadStatus: Record; // blockId => progress percentage cancel: () => void; delete: DeleteImage; upload: UploadImage; - restore: RestoreImage; validation: { /** * @description max file size in bytes diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index fd64dc46a..edf696ab8 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -16,6 +16,7 @@ import { TExtensions, TFileHandler, TMentionHandler, + TReadOnlyFileHandler, TReadOnlyMentionHandler, TServerHandler, } from "@/types"; @@ -44,12 +45,16 @@ export type TEditorCommands = | "text-color" | "background-color" | "text-align" - | "callout"; + | "callout" + | "attachment"; export type TCommandExtraProps = { image: { savedSelection: Selection | null; }; + attachment: { + savedSelection: Selection | null; + }; "text-color": { color: string | undefined; }; @@ -155,7 +160,7 @@ export interface IReadOnlyEditorProps { disabledExtensions: TExtensions[]; displayConfig?: TDisplayConfig; editorClassName?: string; - fileHandler: Pick; + fileHandler: TReadOnlyFileHandler; forwardedRef?: React.MutableRefObject; id: string; initialValue: string; diff --git a/packages/editor/src/core/types/image.ts b/packages/editor/src/core/types/image.ts index 5c707bf33..ca6f76fb1 100644 --- a/packages/editor/src/core/types/image.ts +++ b/packages/editor/src/core/types/image.ts @@ -2,4 +2,4 @@ export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise; export type RestoreImage = (assetUrlWithWorkspaceId: string) => Promise; -export type UploadImage = (file: File) => Promise; +export type UploadImage = (blockId: string, file: File) => Promise; diff --git a/space/core/components/editor/lite-text-editor.tsx b/space/core/components/editor/lite-text-editor.tsx index ac0a0633a..9f2cda4ad 100644 --- a/space/core/components/editor/lite-text-editor.tsx +++ b/space/core/components/editor/lite-text-editor.tsx @@ -1,6 +1,6 @@ import React from "react"; // editor -import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor"; +import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor"; // components import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor"; // helpers @@ -14,7 +14,7 @@ interface LiteTextEditorWrapperProps workspaceId: string; isSubmitting?: boolean; showSubmitButton?: boolean; - uploadFile: (file: File) => Promise; + uploadFile: TFileHandler["upload"]; } export const LiteTextEditor = React.forwardRef((props, ref) => { diff --git a/space/core/components/editor/lite-text-read-only-editor.tsx b/space/core/components/editor/lite-text-read-only-editor.tsx index 5f936baec..f9889f2ab 100644 --- a/space/core/components/editor/lite-text-read-only-editor.tsx +++ b/space/core/components/editor/lite-text-read-only-editor.tsx @@ -12,15 +12,17 @@ type LiteTextReadOnlyEditorWrapperProps = Omit< "disabledExtensions" | "fileHandler" | "mentionHandler" > & { anchor: string; + workspaceId: string; }; export const LiteTextReadOnlyEditor = React.forwardRef( - ({ anchor, ...props }, ref) => ( + ({ anchor, workspaceId, ...props }, ref) => ( , diff --git a/space/core/components/editor/rich-text-editor.tsx b/space/core/components/editor/rich-text-editor.tsx index 96f490054..c9e795d7b 100644 --- a/space/core/components/editor/rich-text-editor.tsx +++ b/space/core/components/editor/rich-text-editor.tsx @@ -1,6 +1,6 @@ import React, { forwardRef } from "react"; // editor -import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef } from "@plane/editor"; +import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef, TFileHandler } from "@plane/editor"; // components import { EditorMentionsRoot } from "@/components/editor"; // helpers @@ -8,11 +8,13 @@ import { getEditorFileHandlers } from "@/helpers/editor.helper"; interface RichTextEditorWrapperProps extends Omit { - uploadFile: (file: File) => Promise; + anchor: string; + uploadFile: TFileHandler["upload"]; + workspaceId: string; } export const RichTextEditor = forwardRef((props, ref) => { - const { containerClassName, uploadFile, ...rest } = props; + const { anchor, containerClassName, uploadFile, workspaceId, ...rest } = props; return ( & { anchor: string; + workspaceId: string; }; export const RichTextReadOnlyEditor = React.forwardRef( - ({ anchor, ...props }, ref) => ( + ({ anchor, workspaceId, ...props }, ref) => ( , diff --git a/space/core/components/issues/peek-overview/comment/add-comment.tsx b/space/core/components/issues/peek-overview/comment/add-comment.tsx index 9712b64e6..d746d7766 100644 --- a/space/core/components/issues/peek-overview/comment/add-comment.tsx +++ b/space/core/components/issues/peek-overview/comment/add-comment.tsx @@ -90,7 +90,7 @@ export const AddComment: React.FC = observer((props) => { onChange={(comment_json, comment_html) => onChange(comment_html)} isSubmitting={isSubmitting} placeholder="Add comment..." - uploadFile={async (file) => { + uploadFile={async (blockId, file) => { const { asset_id } = await uploadCommentAsset(file, anchor); setUploadAssetIds((prev) => [...prev, asset_id]); return asset_id; diff --git a/space/core/components/issues/peek-overview/comment/comment-detail-card.tsx b/space/core/components/issues/peek-overview/comment/comment-detail-card.tsx index e229ac21e..70fcedd0a 100644 --- a/space/core/components/issues/peek-overview/comment/comment-detail-card.tsx +++ b/space/core/components/issues/peek-overview/comment/comment-detail-card.tsx @@ -112,7 +112,7 @@ export const CommentCard: React.FC = observer((props) => { onChange={(comment_json, comment_html) => onChange(comment_html)} isSubmitting={isSubmitting} showSubmitButton={false} - uploadFile={async (file) => { + uploadFile={async (blockId, file) => { const { asset_id } = await uploadCommentAsset(file, anchor, comment.id); return asset_id; }} @@ -140,6 +140,7 @@ export const CommentCard: React.FC = observer((props) => {
= observer((props) => { const { anchor, issueDetails } = props; - - const { project_details } = usePublish(anchor); - + // store hooks + const { project_details, workspace: workspaceID } = usePublish(anchor); + // derived values const description = issueDetails.description_html; return ( @@ -35,6 +35,7 @@ export const PeekOverviewIssueDetails: React.FC = observer((props) => { ? "

" : description } + workspaceId={workspaceID?.toString() ?? ""} /> )} diff --git a/space/helpers/editor.helper.ts b/space/helpers/editor.helper.ts index 52d83ccc9..0907b421e 100644 --- a/space/helpers/editor.helper.ts +++ b/space/helpers/editor.helper.ts @@ -1,6 +1,6 @@ // plane internal import { MAX_FILE_SIZE } from "@plane/constants"; -import { TFileHandler } from "@plane/editor"; +import { TFileHandler, TReadOnlyFileHandler } from "@plane/editor"; import { SitesFileService } from "@plane/services"; // helpers import { getFileURL } from "@/helpers/file.helper"; @@ -18,10 +18,35 @@ export const getEditorAssetSrc = (anchor: string, assetId: string): string | und type TArgs = { anchor: string; - uploadFile: (file: File) => Promise; + uploadFile: TFileHandler["upload"]; workspaceId: string; }; +/** + * @description this function returns the file handler required by the read-only editors + */ +export const getReadOnlyEditorFileHandlers = (args: Pick): TReadOnlyFileHandler => { + const { anchor, workspaceId } = args; + + return { + getAssetSrc: async (path) => { + if (!path) return ""; + if (path?.startsWith("http")) { + return path; + } else { + return getEditorAssetSrc(anchor, path) ?? ""; + } + }, + restore: async (src: string) => { + if (src?.startsWith("http")) { + await sitesFileService.restoreOldEditorAsset(workspaceId, src); + } else { + await sitesFileService.restoreNewAsset(anchor, src); + } + }, + }; +}; + /** * @description this function returns the file handler required by the editors * @param {TArgs} args @@ -30,14 +55,11 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => { const { anchor, uploadFile, workspaceId } = args; return { - getAssetSrc: async (path) => { - if (!path) return ""; - if (path?.startsWith("http")) { - return path; - } else { - return getEditorAssetSrc(anchor, path) ?? ""; - } - }, + ...getReadOnlyEditorFileHandlers({ + anchor, + workspaceId, + }), + getAssetUploadStatus: () => 0, upload: uploadFile, delete: async (src: string) => { if (src?.startsWith("http")) { @@ -46,36 +68,9 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => { await sitesFileService.deleteNewAsset(getEditorAssetSrc(anchor, src) ?? ""); } }, - restore: async (src: string) => { - if (src?.startsWith("http")) { - await sitesFileService.restoreOldEditorAsset(workspaceId, src); - } else { - await sitesFileService.restoreNewAsset(anchor, src); - } - }, cancel: sitesFileService.cancelUpload, validation: { maxFileSize: MAX_FILE_SIZE, }, }; }; - -/** - * @description this function returns the file handler required by the read-only editors - */ -export const getReadOnlyEditorFileHandlers = ( - args: Pick -): { getAssetSrc: TFileHandler["getAssetSrc"] } => { - const { anchor } = args; - - return { - getAssetSrc: async (path) => { - if (!path) return ""; - if (path?.startsWith("http")) { - return path; - } else { - return getEditorAssetSrc(anchor, path) ?? ""; - } - }, - }; -}; diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx index 8d7c51357..4a87a9cbc 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx @@ -6,7 +6,7 @@ import Link from "next/link"; import { useParams } from "next/navigation"; import useSWR from "swr"; // plane types -import { TSearchEntityRequestPayload } from "@plane/types"; +import { TSearchEntityRequestPayload, TWebhookConnectionQueryParams } from "@plane/types"; import { EFileAssetType } from "@plane/types/src/enums"; // plane ui import { getButtonStyling } from "@plane/ui"; @@ -17,19 +17,14 @@ import { LogoSpinner } from "@/components/common"; import { PageHead } from "@/components/core"; import { IssuePeekOverview } from "@/components/issues"; import { PageRoot, TPageRootConfig, TPageRootHandlers } from "@/components/pages"; -// helpers -import { getEditorFileHandlers } from "@/helpers/editor.helper"; // hooks -import { useProjectPage, useProjectPages, useWorkspace } from "@/hooks/store"; -// plane web hooks -import { useFileSize } from "@/plane-web/hooks/use-file-size"; +import { useEditorConfig } from "@/hooks/editor"; +import { useEditorAsset, useProjectPage, useProjectPages, useWorkspace } from "@/hooks/store"; // plane web services import { WorkspaceService } from "@/plane-web/services"; // services -import { FileService } from "@/services/file.service"; import { ProjectPageService, ProjectPageVersionService } from "@/services/page"; const workspaceService = new WorkspaceService(); -const fileService = new FileService(); const projectPageService = new ProjectPageService(); const projectPageVersionService = new ProjectPageVersionService(); @@ -39,6 +34,7 @@ const PageDetailsPage = observer(() => { const { createPage, getPageById } = useProjectPages(); const page = useProjectPage(pageId?.toString() ?? ""); const { getWorkspaceBySlug } = useWorkspace(); + const { uploadEditorAsset } = useEditorAsset(); // derived values const workspaceId = workspaceSlug ? (getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "") : ""; const { canCurrentUserAccessPage, id, name, updateDescription } = page; @@ -51,8 +47,8 @@ const PageDetailsPage = observer(() => { }), [projectId, workspaceSlug] ); - // file size - const { maxFileSize } = useFileSize(); + // editor config + const { getEditorFileHandlers } = useEditorConfig(); // fetch page details const { error: pageDetailsError } = useSWR( workspaceSlug && projectId && pageId ? `PAGE_DETAILS_${pageId}` : null, @@ -96,30 +92,34 @@ const PageDetailsPage = observer(() => { const pageRootConfig: TPageRootConfig = useMemo( () => ({ fileHandler: getEditorFileHandlers({ - maxFileSize, projectId: projectId?.toString() ?? "", - uploadFile: async (file) => { - const { asset_id } = await fileService.uploadProjectAsset( - workspaceSlug?.toString() ?? "", - projectId?.toString() ?? "", - { + uploadFile: async (blockId, file) => { + const { asset_id } = await uploadEditorAsset({ + blockId, + data: { entity_identifier: id ?? "", entity_type: EFileAssetType.PAGE_DESCRIPTION, }, - file - ); + file, + projectId: projectId?.toString() ?? "", + workspaceSlug: workspaceSlug?.toString() ?? "", + }); return asset_id; }, workspaceId, workspaceSlug: workspaceSlug?.toString() ?? "", }), - webhookConnectionParams: { - documentType: "project_page", - projectId: projectId?.toString() ?? "", - workspaceSlug: workspaceSlug?.toString() ?? "", - }, }), - [id, maxFileSize, projectId, workspaceId, workspaceSlug] + [getEditorFileHandlers, id, projectId, uploadEditorAsset, workspaceId, workspaceSlug] + ); + + const webhookConnectionParams: TWebhookConnectionQueryParams = useMemo( + () => ({ + documentType: "project_page", + projectId: projectId?.toString() ?? "", + workspaceSlug: workspaceSlug?.toString() ?? "", + }), + [projectId, workspaceSlug] ); if ((!page || !id) && !pageDetailsError) @@ -154,6 +154,7 @@ const PageDetailsPage = observer(() => { config={pageRootConfig} handlers={pageRootHandlers} page={page} + webhookConnectionParams={webhookConnectionParams} workspaceSlug={workspaceSlug?.toString() ?? ""} /> diff --git a/web/ce/components/pages/editor/ai/ask-pi-menu.tsx b/web/ce/components/pages/editor/ai/ask-pi-menu.tsx index b9d6c85ef..bd49942ef 100644 --- a/web/ce/components/pages/editor/ai/ask-pi-menu.tsx +++ b/web/ce/components/pages/editor/ai/ask-pi-menu.tsx @@ -6,6 +6,8 @@ import { Tooltip } from "@plane/ui"; import { RichTextReadOnlyEditor } from "@/components/editor"; // helpers import { cn } from "@/helpers/common.helper"; +// hooks +import { useWorkspace } from "@/hooks/store"; type Props = { handleInsertText: (insertOnNextLine: boolean) => void; @@ -19,6 +21,10 @@ export const AskPiMenu: React.FC = (props) => { const { handleInsertText, handleRegenerate, isRegenerating, response, workspaceSlug } = props; // states const [query, setQuery] = useState(""); + // store hooks + const { getWorkspaceBySlug } = useWorkspace(); + // derived values + const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? ""; return ( <> @@ -40,6 +46,7 @@ export const AskPiMenu: React.FC = (props) => { initialValue={response} containerClassName="!p-0 border-none" editorClassName="!pl-0" + workspaceId={workspaceId} workspaceSlug={workspaceSlug} />
diff --git a/web/ce/components/pages/editor/ai/menu.tsx b/web/ce/components/pages/editor/ai/menu.tsx index 036eaf283..cef586a77 100644 --- a/web/ce/components/pages/editor/ai/menu.tsx +++ b/web/ce/components/pages/editor/ai/menu.tsx @@ -21,6 +21,7 @@ type Props = { editorRef: RefObject; isOpen: boolean; onClose: () => void; + workspaceId: string; workspaceSlug: string; }; @@ -58,7 +59,7 @@ const TONES_LIST = [ ]; export const EditorAIMenu: React.FC = (props) => { - const { editorRef, isOpen, onClose, workspaceSlug } = props; + const { editorRef, isOpen, onClose, workspaceId, workspaceSlug } = props; // states const [activeTask, setActiveTask] = useState(null); const [response, setResponse] = useState(undefined); @@ -215,6 +216,7 @@ export const EditorAIMenu: React.FC = (props) => { initialValue={response} containerClassName="!p-0 border-none" editorClassName="!pl-0" + workspaceId={workspaceId} workspaceSlug={workspaceSlug} />
diff --git a/web/core/components/core/modals/gpt-assistant-popover.tsx b/web/core/components/core/modals/gpt-assistant-popover.tsx index 747723c29..676da4624 100644 --- a/web/core/components/core/modals/gpt-assistant-popover.tsx +++ b/web/core/components/core/modals/gpt-assistant-popover.tsx @@ -6,12 +6,15 @@ import { Controller, useForm } from "react-hook-form"; // services import { usePopper } from "react-popper"; import { AlertCircle } from "lucide-react"; import { Popover, Transition } from "@headlessui/react"; +// plane editor +import { EditorReadOnlyRefApi } from "@plane/editor"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // components import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor"; // services import { AIService } from "@/services/ai.service"; +const aiService = new AIService(); type Props = { isOpen: boolean; @@ -22,6 +25,7 @@ type Props = { prompt?: string; button: JSX.Element; className?: string; + workspaceId: string; workspaceSlug: string; projectId: string; }; @@ -31,8 +35,6 @@ type FormData = { task: string; }; -const aiService = new AIService(); - export const GptAssistantPopover: React.FC = (props) => { const { isOpen, @@ -43,6 +45,7 @@ export const GptAssistantPopover: React.FC = (props) => { prompt, button, className = "", + workspaceId, workspaceSlug, projectId, } = props; @@ -51,7 +54,8 @@ export const GptAssistantPopover: React.FC = (props) => { const [invalidResponse, setInvalidResponse] = useState(false); const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); - const editorRef = useRef(null); + // refs + const editorRef = useRef(null); const responseRef = useRef(null); // popper const { styles, attributes } = usePopper(referenceElement, popperElement, { @@ -218,6 +222,7 @@ export const GptAssistantPopover: React.FC = (props) => { initialValue={prompt} containerClassName="-m-3" ref={editorRef} + workspaceId={workspaceId} workspaceSlug={workspaceSlug} projectId={projectId} /> @@ -230,6 +235,7 @@ export const GptAssistantPopover: React.FC = (props) => { id="ai-assistant-response" initialValue={`

${response}

`} ref={responseRef} + workspaceId={workspaceId} workspaceSlug={workspaceSlug} projectId={projectId} /> diff --git a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx index 82cbd1225..63de65bc6 100644 --- a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx +++ b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx @@ -2,20 +2,18 @@ import React, { useState } from "react"; // plane constants import { EIssueCommentAccessSpecifier } from "@plane/constants"; // plane editor -import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor"; +import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor"; // i18n import { useTranslation } from "@plane/i18n"; // components import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor"; // helpers import { cn } from "@/helpers/common.helper"; -import { getEditorFileHandlers } from "@/helpers/editor.helper"; import { isCommentEmpty } from "@/helpers/string.helper"; // hooks -import { useEditorMention } from "@/hooks/use-editor-mention"; +import { useEditorConfig, useEditorMention } from "@/hooks/editor"; // plane web hooks import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; -import { useFileSize } from "@/plane-web/hooks/use-file-size"; // plane web services import { WorkspaceService } from "@/plane-web/services"; const workspaceService = new WorkspaceService(); @@ -31,7 +29,7 @@ interface LiteTextEditorWrapperProps showSubmitButton?: boolean; isSubmitting?: boolean; showToolbarInitially?: boolean; - uploadFile: (file: File) => Promise; + uploadFile: TFileHandler["upload"]; issue_id?: string; } @@ -66,8 +64,8 @@ export const LiteTextEditor = React.forwardRef(ref: React.ForwardedRef): ref is React.MutableRefObject { return !!ref && typeof ref === "object" && "current" in ref; } @@ -85,7 +83,6 @@ export const LiteTextEditor = React.forwardRef & { + workspaceId: string; workspaceSlug: string; projectId: string; }; export const LiteTextReadOnlyEditor = React.forwardRef( - ({ workspaceSlug, projectId, ...props }, ref) => { + ({ workspaceId, workspaceSlug, projectId, ...props }, ref) => { // editor flaggings const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString()); + // editor config + const { getReadOnlyEditorFileHandlers } = useEditorConfig(); return ( { @@ -20,7 +18,7 @@ interface RichTextEditorWrapperProps workspaceSlug: string; workspaceId: string; projectId?: string; - uploadFile: (file: File) => Promise; + uploadFile: TFileHandler["upload"]; } export const RichTextEditor = forwardRef((props, ref) => { @@ -32,15 +30,14 @@ export const RichTextEditor = forwardRef await searchMentionCallback(payload), }); - // file size - const { maxFileSize } = useFileSize(); + // editor config + const { getEditorFileHandlers } = useEditorConfig(); return ( & { + workspaceId: string; workspaceSlug: string; projectId?: string; }; export const RichTextReadOnlyEditor = React.forwardRef( - ({ workspaceSlug, projectId, ...props }, ref) => { + ({ workspaceId, workspaceSlug, projectId, ...props }, ref) => { // editor flaggings const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString()); + // editor config + const { getReadOnlyEditorFileHandlers } = useEditorConfig(); return ( Promise; + uploadFile: TFileHandler["upload"]; parentClassName?: string; handleColorChange: (data: Partial) => Promise; handleDelete: () => void; @@ -49,8 +49,8 @@ export const StickyEditor = React.forwardRef(ref: React.ForwardedRef): ref is React.MutableRefObject { return !!ref && typeof ref === "object" && "current" in ref; } @@ -67,7 +67,6 @@ export const StickyEditor = React.forwardRef = observer((props onEnterKeyPress, onAssetUpload, } = props; - // i18n const { t } = useTranslation(); - - // hooks + // store hooks + const { uploadEditorAsset } = useEditorAsset(); const { loader } = useProjectInbox(); const { isMobile } = usePlatformOS(); @@ -90,17 +86,18 @@ export const InboxIssueDescription: FC = observer((props containerClassName={containerClassName} onEnterKeyPress={onEnterKeyPress} tabIndex={getIndex("description_html")} - uploadFile={async (file) => { + uploadFile={async (blockId, file) => { try { - const { asset_id } = await fileService.uploadProjectAsset( - workspaceSlug, - projectId, - { + const { asset_id } = await uploadEditorAsset({ + blockId, + data: { entity_identifier: data.id ?? "", entity_type: EFileAssetType.ISSUE_DESCRIPTION, }, - file - ); + file, + projectId, + workspaceSlug, + }); onAssetUpload?.(asset_id); return asset_id; } catch (error) { diff --git a/web/core/components/issues/description-input.tsx b/web/core/components/issues/description-input.tsx index 54573fbba..df1276dc9 100644 --- a/web/core/components/issues/description-input.tsx +++ b/web/core/components/issues/description-input.tsx @@ -17,13 +17,10 @@ import { TIssueOperations } from "@/components/issues/issue-detail"; // helpers import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper"; // hooks -import { useWorkspace } from "@/hooks/store"; +import { useEditorAsset, useWorkspace } from "@/hooks/store"; // plane web services import { WorkspaceService } from "@/plane-web/services"; -// services -import { FileService } from "@/services/file.service"; const workspaceService = new WorkspaceService(); -const fileService = new FileService(); export type IssueDescriptionInputProps = { containerClassName?: string; @@ -51,6 +48,14 @@ export const IssueDescriptionInput: FC = observer((p setIsSubmitting, placeholder, } = props; + // states + const [localIssueDescription, setLocalIssueDescription] = useState({ + id: issueId, + description_html: initialValue, + }); + // store hooks + const { uploadEditorAsset } = useEditorAsset(); + // form info // i18n const { t } = useTranslation(); @@ -61,11 +66,6 @@ export const IssueDescriptionInput: FC = observer((p }, }); - const [localIssueDescription, setLocalIssueDescription] = useState({ - id: issueId, - description_html: initialValue, - }); - const handleDescriptionFormSubmit = useCallback( async (formData: Partial) => { await issueOperations.update(workspaceSlug, projectId, issueId, { @@ -136,17 +136,18 @@ export const IssueDescriptionInput: FC = observer((p }) } containerClassName={containerClassName} - uploadFile={async (file) => { + uploadFile={async (blockId, file) => { try { - const { asset_id } = await fileService.uploadProjectAsset( - workspaceSlug, - projectId, - { + const { asset_id } = await uploadEditorAsset({ + blockId, + data: { entity_identifier: issueId, entity_type: EFileAssetType.ISSUE_DESCRIPTION, }, - file - ); + file, + projectId, + workspaceSlug, + }); return asset_id; } catch (error) { console.log("Error in uploading work item asset:", error); @@ -159,6 +160,7 @@ export const IssueDescriptionInput: FC = observer((p id={issueId} initialValue={localIssueDescription.description_html ?? ""} containerClassName={containerClassName} + workspaceId={workspaceId} workspaceSlug={workspaceSlug} projectId={projectId} /> diff --git a/web/core/components/issues/issue-detail/issue-activity/comments/comment-card.tsx b/web/core/components/issues/issue-detail/issue-activity/comments/comment-card.tsx index c8f33b626..96f0960b6 100644 --- a/web/core/components/issues/issue-detail/issue-activity/comments/comment-card.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/comments/comment-card.tsx @@ -170,8 +170,8 @@ export const IssueCommentCard: FC = observer((props) => { } }} showSubmitButton={false} - uploadFile={async (file) => { - const { asset_id } = await activityOperations.uploadCommentAsset(file, comment.id); + uploadFile={async (blockId, file) => { + const { asset_id } = await activityOperations.uploadCommentAsset(blockId, file, comment.id); return asset_id; }} /> @@ -215,6 +215,7 @@ export const IssueCommentCard: FC = observer((props) => { ref={showEditorRef} id={comment.id} initialValue={comment.comment_html ?? ""} + workspaceId={workspaceId} workspaceSlug={workspaceSlug} projectId={projectId} /> diff --git a/web/core/components/issues/issue-detail/issue-activity/comments/comment-create.tsx b/web/core/components/issues/issue-detail/issue-activity/comments/comment-create.tsx index 5727714f2..6abd2d6e4 100644 --- a/web/core/components/issues/issue-detail/issue-activity/comments/comment-create.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/comments/comment-create.tsx @@ -110,8 +110,8 @@ export const IssueCommentCreate: FC = (props) => { handleAccessChange={onAccessChange} showAccessSpecifier={showAccessSpecifier} isSubmitting={isSubmitting} - uploadFile={async (file) => { - const { asset_id } = await activityOperations.uploadCommentAsset(file); + uploadFile={async (blockId, file) => { + const { asset_id } = await activityOperations.uploadCommentAsset(blockId, file); setUploadedAssetIds((prev) => [...prev, asset_id]); return asset_id; }} diff --git a/web/core/components/issues/issue-detail/issue-activity/root.tsx b/web/core/components/issues/issue-detail/issue-activity/root.tsx index 6e45b12d4..0133b5e61 100644 --- a/web/core/components/issues/issue-detail/issue-activity/root.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/root.tsx @@ -3,7 +3,7 @@ import { FC, useMemo } from "react"; import { observer } from "mobx-react"; // plane package imports -import { E_SORT_ORDER, TActivityFilters, defaultActivityFilters,EUserPermissions } from "@plane/constants"; +import { E_SORT_ORDER, TActivityFilters, defaultActivityFilters, EUserPermissions } from "@plane/constants"; import { useLocalStorage } from "@plane/hooks"; // i18n import { useTranslation } from "@plane/i18n"; @@ -16,12 +16,9 @@ import { IssueCommentCreate } from "@/components/issues"; import { ActivitySortRoot, IssueActivityCommentRoot } from "@/components/issues/issue-detail"; // constants // hooks -import { useIssueDetail, useProject, useUser, useUserPermissions } from "@/hooks/store"; +import { useEditorAsset, useIssueDetail, useProject, useUser, useUserPermissions } from "@/hooks/store"; // plane web components import { ActivityFilterRoot, IssueActivityWorklogCreateButton } from "@/plane-web/components/issues/worklog"; -// services -import { FileService } from "@/services/file.service"; -const fileService = new FileService(); type TIssueActivity = { workspaceSlug: string; @@ -35,7 +32,7 @@ export type TActivityOperations = { createComment: (data: Partial) => Promise; updateComment: (commentId: string, data: Partial) => Promise; removeComment: (commentId: string) => Promise; - uploadCommentAsset: (file: File, commentId?: string) => Promise; + uploadCommentAsset: (blockId: string, file: File, commentId?: string) => Promise; }; export const IssueActivity: FC = observer((props) => { @@ -48,6 +45,7 @@ export const IssueActivity: FC = observer((props) => { defaultActivityFilters ); const { setValue: setSortOrder, storedValue: sortOrder } = useLocalStorage("activity_sort_order", E_SORT_ORDER.ASC); + // store hooks const { issue: { getIssueById }, createComment, @@ -57,7 +55,8 @@ export const IssueActivity: FC = observer((props) => { const { projectPermissionsByWorkspaceSlugAndProjectId } = useUserPermissions(); const { getProjectById } = useProject(); const { data: currentUser } = useUser(); - //derived values + const { uploadEditorAsset } = useEditorAsset(); + // derived values const issue = issueId ? getIssueById(issueId) : undefined; const currentUserProjectRole = projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId); const isAdmin = (currentUserProjectRole ?? EUserPermissions.GUEST) === EUserPermissions.ADMIN; @@ -94,7 +93,7 @@ export const IssueActivity: FC = observer((props) => { message: t("issue.comments.create.success"), }); return comment; - } catch (error) { + } catch { setToast({ title: t("common.error.label"), type: TOAST_TYPE.ERROR, @@ -111,7 +110,7 @@ export const IssueActivity: FC = observer((props) => { type: TOAST_TYPE.SUCCESS, message: t("issue.comments.update.success"), }); - } catch (error) { + } catch { setToast({ title: t("common.error.label"), type: TOAST_TYPE.ERROR, @@ -128,7 +127,7 @@ export const IssueActivity: FC = observer((props) => { type: TOAST_TYPE.SUCCESS, message: t("issue.comments.remove.success"), }); - } catch (error) { + } catch { setToast({ title: t("common.error.label"), type: TOAST_TYPE.ERROR, @@ -136,18 +135,19 @@ export const IssueActivity: FC = observer((props) => { }); } }, - uploadCommentAsset: async (file, commentId) => { + uploadCommentAsset: async (blockId, file, commentId) => { try { if (!workspaceSlug || !projectId) throw new Error("Missing fields"); - const res = await fileService.uploadProjectAsset( - workspaceSlug, - projectId, - { + const res = await uploadEditorAsset({ + blockId, + data: { entity_identifier: commentId ?? "", entity_type: EFileAssetType.COMMENT_DESCRIPTION, }, - file - ); + file, + projectId, + workspaceSlug, + }); return res; } catch (error) { console.log("Error in uploading comment asset:", error); @@ -155,7 +155,7 @@ export const IssueActivity: FC = observer((props) => { } }, }), - [workspaceSlug, projectId, issueId, createComment, updateComment, removeComment] + [workspaceSlug, projectId, issueId, createComment, updateComment, uploadEditorAsset, removeComment] ); const project = getProjectById(projectId); diff --git a/web/core/components/issues/issue-modal/components/description-editor.tsx b/web/core/components/issues/issue-modal/components/description-editor.tsx index 8e5d51f78..b19fc62db 100644 --- a/web/core/components/issues/issue-modal/components/description-editor.tsx +++ b/web/core/components/issues/issue-modal/components/description-editor.tsx @@ -22,14 +22,15 @@ import { RichTextEditor } from "@/components/editor"; import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper"; import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks -import { useInstance, useWorkspace } from "@/hooks/store"; +import { useEditorAsset, useInstance, useWorkspace } from "@/hooks/store"; import useKeypress from "@/hooks/use-keypress"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web services import { WorkspaceService } from "@/plane-web/services"; // services import { AIService } from "@/services/ai.service"; -import { FileService } from "@/services/file.service"; +const workspaceService = new WorkspaceService(); +const aiService = new AIService(); type TIssueDescriptionEditorProps = { control: Control; @@ -50,11 +51,6 @@ type TIssueDescriptionEditorProps = { onClose: () => void; }; -// services -const workspaceService = new WorkspaceService(); -const aiService = new AIService(); -const fileService = new FileService(); - export const IssueDescriptionEditor: React.FC = observer((props) => { const { control, @@ -80,8 +76,10 @@ export const IssueDescriptionEditor: React.FC = ob const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); // store hooks const { getWorkspaceBySlug } = useWorkspace(); - const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString())?.id as string; + const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString())?.id ?? ""; const { config } = useInstance(); + const { uploadEditorAsset } = useEditorAsset(); + // platform const { isMobile } = usePlatformOS(); const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile); @@ -202,19 +200,20 @@ export const IssueDescriptionEditor: React.FC = ob }) } containerClassName="pt-3 min-h-[120px]" - uploadFile={async (file) => { + uploadFile={async (blockId, file) => { try { - const { asset_id } = await fileService.uploadProjectAsset( - workspaceSlug, - projectId, - { + const { asset_id } = await uploadEditorAsset({ + blockId, + data: { entity_identifier: issueId ?? "", entity_type: isDraft ? EFileAssetType.DRAFT_ISSUE_DESCRIPTION : EFileAssetType.ISSUE_DESCRIPTION, }, - file - ); + file, + projectId, + workspaceSlug, + }); onAssetUpload(asset_id); return asset_id; } catch (error) { @@ -268,6 +267,7 @@ export const IssueDescriptionEditor: React.FC = ob AI } + workspaceId={workspaceId} workspaceSlug={workspaceSlug} projectId={projectId} /> diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 161d63c47..72533c3dc 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -21,8 +21,8 @@ import { PageContentBrowser, PageContentLoader, PageEditorTitle } from "@/compon import { cn, LIVE_BASE_PATH, LIVE_BASE_URL } from "@/helpers/common.helper"; import { generateRandomColor } from "@/helpers/string.helper"; // hooks -import { useUser } from "@/hooks/store"; -import { useEditorMention } from "@/hooks/use-editor-mention"; +import { useEditorMention } from "@/hooks/editor"; +import { useUser, useWorkspace } from "@/hooks/store"; import { usePageFilters } from "@/hooks/use-page-filters"; // plane web components import { EditorAIMenu } from "@/plane-web/components/pages"; @@ -34,7 +34,6 @@ import { TPageInstance } from "@/store/pages/base-page"; export type TEditorBodyConfig = { fileHandler: TFileHandler; - webhookConnectionParams: TWebhookConnectionQueryParams; }; export type TEditorBodyHandlers = { @@ -50,6 +49,7 @@ type Props = { handlers: TEditorBodyHandlers; page: TPageInstance; sidePeekVisible: boolean; + webhookConnectionParams: TWebhookConnectionQueryParams; workspaceSlug: string; }; @@ -62,12 +62,15 @@ export const PageEditorBody: React.FC = observer((props) => { handlers, page, sidePeekVisible, + webhookConnectionParams, workspaceSlug, } = props; // store hooks const { data: currentUser } = useUser(); + const { getWorkspaceBySlug } = useWorkspace(); // derived values const { id: pageId, name: pageTitle, isContentEditable, updateTitle } = page; + const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? ""; // issue-embed const { issueEmbedProps } = useIssueEmbed({ fetchEmbedSuggestions: handlers.fetchEntity, @@ -96,10 +99,11 @@ export const PageEditorBody: React.FC = observer((props) => { editorRef={editorRef} isOpen={isOpen} onClose={onClose} + workspaceId={workspaceId} workspaceSlug={workspaceSlug?.toString() ?? ""} /> ), - [editorRef, workspaceSlug] + [editorRef, workspaceId, workspaceSlug] ); const handleServerConnect = useCallback(() => { @@ -129,13 +133,13 @@ export const PageEditorBody: React.FC = observer((props) => { // Construct realtime config return { url: WS_LIVE_URL.toString(), - queryParams: config.webhookConnectionParams, + queryParams: webhookConnectionParams, }; } catch (error) { console.error("Error creating realtime config", error); return undefined; } - }, [config.webhookConnectionParams]); + }, [webhookConnectionParams]); const userConfig = useMemo( () => ({ diff --git a/web/core/components/pages/editor/page-root.tsx b/web/core/components/pages/editor/page-root.tsx index 64aef6912..bc9c49780 100644 --- a/web/core/components/pages/editor/page-root.tsx +++ b/web/core/components/pages/editor/page-root.tsx @@ -4,7 +4,7 @@ import { useSearchParams } from "next/navigation"; // editor import { EditorRefApi } from "@plane/editor"; // types -import { TDocumentPayload, TPage, TPageVersion } from "@plane/types"; +import { TDocumentPayload, TPage, TPageVersion, TWebhookConnectionQueryParams } from "@plane/types"; // components import { PageEditorHeaderRoot, @@ -36,11 +36,12 @@ type TPageRootProps = { config: TPageRootConfig; handlers: TPageRootHandlers; page: TPageInstance; + webhookConnectionParams: TWebhookConnectionQueryParams; workspaceSlug: string; }; export const PageRoot = observer((props: TPageRootProps) => { - const { config, handlers, page, workspaceSlug } = props; + const { config, handlers, page, webhookConnectionParams, workspaceSlug } = props; // states const [editorReady, setEditorReady] = useState(false); const [hasConnectionFailed, setHasConnectionFailed] = useState(false); @@ -116,6 +117,7 @@ export const PageRoot = observer((props: TPageRootProps) => { handlers={handlers} page={page} sidePeekVisible={sidePeekVisible} + webhookConnectionParams={webhookConnectionParams} workspaceSlug={workspaceSlug} /> diff --git a/web/core/components/pages/version/editor.tsx b/web/core/components/pages/version/editor.tsx index 0beffa6f3..d20123290 100644 --- a/web/core/components/pages/version/editor.tsx +++ b/web/core/components/pages/version/editor.tsx @@ -8,9 +8,9 @@ import { TPageVersion } from "@plane/types"; import { Loader } from "@plane/ui"; // components import { EditorMentionsRoot } from "@/components/editor"; -// helpers -import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper"; // hooks +import { useEditorConfig } from "@/hooks/editor"; +import { useWorkspace } from "@/hooks/store"; import { usePageFilters } from "@/hooks/use-page-filters"; // plane web hooks import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; @@ -27,8 +27,14 @@ export const PagesVersionEditor: React.FC = observer((props const { activeVersion, currentVersionDescription, isCurrentVersionActive, versionDetails } = props; // params const { workspaceSlug, projectId } = useParams(); + // store hooks + const { getWorkspaceBySlug } = useWorkspace(); + // derived values + const workspaceDetails = getWorkspaceBySlug(workspaceSlug?.toString() ?? ""); // editor flaggings const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString() ?? ""); + // editor config + const { getReadOnlyEditorFileHandlers } = useEditorConfig(); // issue-embed const { issueEmbedProps } = useIssueEmbed({ projectId: projectId?.toString() ?? "", @@ -97,6 +103,7 @@ export const PagesVersionEditor: React.FC = observer((props editorClassName="pl-10" fileHandler={getReadOnlyEditorFileHandlers({ projectId: projectId?.toString() ?? "", + workspaceId: workspaceDetails?.id ?? "", workspaceSlug: workspaceSlug?.toString() ?? "", })} mentionHandler={{ diff --git a/web/core/components/profile/activity/activity-list.tsx b/web/core/components/profile/activity/activity-list.tsx index bdb6c6f93..671fd9dc2 100644 --- a/web/core/components/profile/activity/activity-list.tsx +++ b/web/core/components/profile/activity/activity-list.tsx @@ -15,7 +15,7 @@ import { ActivitySettingsLoader } from "@/components/ui"; import { calculateTimeAgo } from "@/helpers/date-time.helper"; import { getFileURL } from "@/helpers/file.helper"; // hooks -import { useUser } from "@/hooks/store"; +import { useUser, useWorkspace } from "@/hooks/store"; type Props = { activity: IUserActivityResponse | undefined; @@ -27,6 +27,9 @@ export const ActivityList: React.FC = observer((props) => { const { workspaceSlug } = useParams(); // store hooks const { data: currentUser } = useUser(); + const { getWorkspaceBySlug } = useWorkspace(); + // derived values + const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString() ?? "")?.id ?? ""; // TODO: refactor this component return ( @@ -79,6 +82,7 @@ export const ActivityList: React.FC = observer((props) => { : (activityItem.old_value?.toString() as string) } containerClassName="text-xs bg-custom-background-100" + workspaceId={workspaceId} workspaceSlug={workspaceSlug?.toString() ?? ""} projectId={activityItem.project} /> diff --git a/web/core/components/profile/activity/profile-activity-list.tsx b/web/core/components/profile/activity/profile-activity-list.tsx index 5046948c0..4d10ce2b1 100644 --- a/web/core/components/profile/activity/profile-activity-list.tsx +++ b/web/core/components/profile/activity/profile-activity-list.tsx @@ -103,7 +103,8 @@ export const ProfileActivityListPage: React.FC = observer((props) => { activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value } containerClassName="text-xs bg-custom-background-100" - workspaceSlug={activityItem?.workspace_detail?.slug.toString() ?? ""} + workspaceId={activityItem?.workspace_detail?.id?.toString() ?? ""} + workspaceSlug={activityItem?.workspace_detail?.slug?.toString() ?? ""} projectId={activityItem.project ?? ""} />
diff --git a/web/core/hooks/editor/index.ts b/web/core/hooks/editor/index.ts new file mode 100644 index 000000000..532916cd4 --- /dev/null +++ b/web/core/hooks/editor/index.ts @@ -0,0 +1,2 @@ +export * from "./use-editor-config"; +export * from "./use-editor-mention"; diff --git a/web/core/hooks/editor/use-editor-config.ts b/web/core/hooks/editor/use-editor-config.ts new file mode 100644 index 000000000..166df1d5b --- /dev/null +++ b/web/core/hooks/editor/use-editor-config.ts @@ -0,0 +1,96 @@ +import { useCallback } from "react"; +// plane editor +import { TFileHandler, TReadOnlyFileHandler } from "@plane/editor"; +// helpers +import { getEditorAssetSrc } from "@/helpers/editor.helper"; +// hooks +import { useEditorAsset } from "@/hooks/store"; +// plane web hooks +import { useFileSize } from "@/plane-web/hooks/use-file-size"; +// services +import { FileService } from "@/services/file.service"; +const fileService = new FileService(); + +type TArgs = { + projectId?: string; + uploadFile: TFileHandler["upload"]; + workspaceId: string; + workspaceSlug: string; +}; + +export const useEditorConfig = () => { + // store hooks + const { assetsUploadPercentage } = useEditorAsset(); + // file size + const { maxFileSize } = useFileSize(); + + const getReadOnlyEditorFileHandlers = useCallback( + (args: Pick): TReadOnlyFileHandler => { + const { projectId, workspaceId, workspaceSlug } = args; + + return { + getAssetSrc: async (path) => { + if (!path) return ""; + if (path?.startsWith("http")) { + return path; + } else { + return ( + getEditorAssetSrc({ + assetId: path, + projectId, + workspaceSlug, + }) ?? "" + ); + } + }, + restore: async (src: string) => { + if (src?.startsWith("http")) { + await fileService.restoreOldEditorAsset(workspaceId, src); + } else { + await fileService.restoreNewAsset(workspaceSlug, src); + } + }, + }; + }, + [] + ); + + const getEditorFileHandlers = useCallback( + (args: TArgs): TFileHandler => { + const { projectId, uploadFile, workspaceId, workspaceSlug } = args; + + return { + ...getReadOnlyEditorFileHandlers({ + projectId, + workspaceId, + workspaceSlug, + }), + assetsUploadStatus: assetsUploadPercentage, + upload: uploadFile, + delete: async (src: string) => { + if (src?.startsWith("http")) { + await fileService.deleteOldWorkspaceAsset(workspaceId, src); + } else { + await fileService.deleteNewAsset( + getEditorAssetSrc({ + assetId: src, + projectId, + workspaceSlug, + }) ?? "" + ); + } + }, + cancel: fileService.cancelUpload, + validation: { + maxFileSize, + }, + }; + }, + [assetsUploadPercentage, getReadOnlyEditorFileHandlers, maxFileSize] + ); + + return { + getEditorFileHandlers, + getReadOnlyEditorFileHandlers, + }; +}; diff --git a/web/core/hooks/use-editor-mention.tsx b/web/core/hooks/editor/use-editor-mention.tsx similarity index 100% rename from web/core/hooks/use-editor-mention.tsx rename to web/core/hooks/editor/use-editor-mention.tsx diff --git a/web/core/hooks/store/index.ts b/web/core/hooks/store/index.ts index 266efea32..fa9ff44cc 100644 --- a/web/core/hooks/store/index.ts +++ b/web/core/hooks/store/index.ts @@ -7,6 +7,7 @@ export * from "./use-command-palette"; export * from "./use-cycle"; export * from "./use-cycle-filter"; export * from "./use-dashboard"; +export * from "./use-editor-asset"; export * from "./use-event-tracker"; export * from "./use-global-view"; export * from "./use-inbox-issues"; diff --git a/web/core/hooks/store/use-editor-asset.ts b/web/core/hooks/store/use-editor-asset.ts new file mode 100644 index 000000000..7c5af3696 --- /dev/null +++ b/web/core/hooks/store/use-editor-asset.ts @@ -0,0 +1,10 @@ +import { useContext } from "react"; +// store +import { StoreContext } from "@/lib/store-context"; +import { IEditorAssetStore } from "@/store/editor/asset.store"; + +export const useEditorAsset = (): IEditorAssetStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useEditorAsset must be used within StoreProvider"); + return context.editorAssetStore; +}; diff --git a/web/core/services/file.service.ts b/web/core/services/file.service.ts index d1f654ba7..dba3027c5 100644 --- a/web/core/services/file.service.ts +++ b/web/core/services/file.service.ts @@ -1,3 +1,4 @@ +import { AxiosRequestConfig } from "axios"; // plane types import { TFileEntityInfo, TFileSignedURLResponse } from "@plane/types"; // helpers @@ -64,7 +65,8 @@ export class FileService extends APIService { async uploadWorkspaceAsset( workspaceSlug: string, data: TFileEntityInfo, - file: File + file: File, + uploadProgressHandler?: AxiosRequestConfig["onUploadProgress"] ): Promise { const fileMetaData = getFileMetaDataForUpload(file); return this.post(`/api/assets/v2/workspaces/${workspaceSlug}/`, { @@ -74,7 +76,11 @@ export class FileService extends APIService { .then(async (response) => { const signedURLResponse: TFileSignedURLResponse = response?.data; const fileUploadPayload = generateFileUploadPayload(signedURLResponse, file); - await this.fileUploadService.uploadFile(signedURLResponse.upload_data.url, fileUploadPayload); + await this.fileUploadService.uploadFile( + signedURLResponse.upload_data.url, + fileUploadPayload, + uploadProgressHandler + ); await this.updateWorkspaceAssetUploadStatus(workspaceSlug.toString(), signedURLResponse.asset_id); return signedURLResponse; }) @@ -122,7 +128,8 @@ export class FileService extends APIService { workspaceSlug: string, projectId: string, data: TFileEntityInfo, - file: File + file: File, + uploadProgressHandler?: AxiosRequestConfig["onUploadProgress"] ): Promise { const fileMetaData = getFileMetaDataForUpload(file); return this.post(`/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/`, { @@ -132,7 +139,11 @@ export class FileService extends APIService { .then(async (response) => { const signedURLResponse: TFileSignedURLResponse = response?.data; const fileUploadPayload = generateFileUploadPayload(signedURLResponse, file); - await this.fileUploadService.uploadFile(signedURLResponse.upload_data.url, fileUploadPayload); + await this.fileUploadService.uploadFile( + signedURLResponse.upload_data.url, + fileUploadPayload, + uploadProgressHandler + ); await this.updateProjectAssetUploadStatus(workspaceSlug, projectId, signedURLResponse.asset_id); return signedURLResponse; }) diff --git a/web/core/store/editor/asset.store.ts b/web/core/store/editor/asset.store.ts new file mode 100644 index 000000000..587acba02 --- /dev/null +++ b/web/core/store/editor/asset.store.ts @@ -0,0 +1,121 @@ +import debounce from "lodash/debounce"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; +import { v4 as uuidv4 } from "uuid"; +// plane types +import { TFileEntityInfo, TFileSignedURLResponse } from "@plane/types"; +// services +import { FileService } from "@/services/file.service"; +import { TAttachmentUploadStatus } from "../issue/issue-details/attachment.store"; + +export interface IEditorAssetStore { + // computed + assetsUploadPercentage: Record; + // helper methods + getAssetUploadStatusByEditorBlockId: (blockId: string) => TAttachmentUploadStatus | undefined; + // actions + uploadEditorAsset: ({ + blockId, + data, + file, + projectId, + workspaceSlug, + }: { + blockId: string; + data: TFileEntityInfo; + file: File; + projectId?: string; + workspaceSlug: string; + }) => Promise; +} + +export class EditorAssetStore implements IEditorAssetStore { + // observables + assetsUploadStatus: Record = {}; + // services + fileService: FileService; + + constructor() { + makeObservable(this, { + // observables + assetsUploadStatus: observable, + // computed + assetsUploadPercentage: computed, + // actions + uploadEditorAsset: action, + }); + // services + this.fileService = new FileService(); + } + + get assetsUploadPercentage() { + const assetsStatus = this.assetsUploadStatus; + const assetsPercentage: Record = {}; + Object.keys(assetsStatus).forEach((blockId) => { + const asset = assetsStatus[blockId]; + if (asset) assetsPercentage[blockId] = asset.progress; + }); + return assetsPercentage; + } + + // helper methods + getAssetUploadStatusByEditorBlockId: IEditorAssetStore["getAssetUploadStatusByEditorBlockId"] = computedFn( + (blockId) => { + const blockDetails = this.assetsUploadStatus[blockId]; + if (!blockDetails) return undefined; + return blockDetails; + } + ); + + // actions + private debouncedUpdateProgress = debounce((blockId: string, progress: number) => { + runInAction(() => { + set(this.assetsUploadStatus, [blockId, "progress"], progress); + }); + }, 16); + + uploadEditorAsset: IEditorAssetStore["uploadEditorAsset"] = async (args) => { + const { blockId, data, file, projectId, workspaceSlug } = args; + const tempId = uuidv4(); + + try { + // update attachment upload status + runInAction(() => { + set(this.assetsUploadStatus, [blockId], { + id: tempId, + name: file.name, + progress: 0, + size: file.size, + type: file.type, + }); + }); + if (projectId) { + const response = await this.fileService.uploadProjectAsset( + workspaceSlug, + projectId, + data, + file, + (progressEvent) => { + const progressPercentage = Math.round((progressEvent.progress ?? 0) * 100); + this.debouncedUpdateProgress(blockId, progressPercentage); + } + ); + return response; + } else { + const response = await this.fileService.uploadWorkspaceAsset(workspaceSlug, data, file, (progressEvent) => { + const progressPercentage = Math.round((progressEvent.progress ?? 0) * 100); + this.debouncedUpdateProgress(blockId, progressPercentage); + }); + return response; + } + } catch (error) { + console.error("Error in uploading page asset:", error); + throw error; + } finally { + runInAction(() => { + delete this.assetsUploadStatus[blockId]; + }); + } + }; +} diff --git a/web/core/store/issue/issue-details/attachment.store.ts b/web/core/store/issue/issue-details/attachment.store.ts index f9e47a560..6f4977dbb 100644 --- a/web/core/store/issue/issue-details/attachment.store.ts +++ b/web/core/store/issue/issue-details/attachment.store.ts @@ -126,7 +126,7 @@ export class IssueAttachmentStore implements IIssueAttachmentStore { return response; }; - debouncedUpdateProgress = debounce((issueId: string, tempId: string, progress: number) => { + private debouncedUpdateProgress = debounce((issueId: string, tempId: string, progress: number) => { runInAction(() => { set(this.attachmentsUploadStatusMap, [issueId, tempId, "progress"], progress); }); diff --git a/web/core/store/root.store.ts b/web/core/store/root.store.ts index 03f1acdf4..5a10df45b 100644 --- a/web/core/store/root.store.ts +++ b/web/core/store/root.store.ts @@ -1,5 +1,4 @@ import { enableStaticRendering } from "mobx-react"; -import { EIssueServiceType } from "@plane/constants"; // plane web store import { CommandPaletteStore, ICommandPaletteStore } from "@/plane-web/store/command-palette.store"; import { RootStore } from "@/plane-web/store/root.store"; @@ -8,6 +7,7 @@ import { IStateStore, StateStore } from "@/plane-web/store/state.store"; import { CycleStore, ICycleStore } from "./cycle.store"; import { CycleFilterStore, ICycleFilterStore } from "./cycle_filter.store"; import { DashboardStore, IDashboardStore } from "./dashboard.store"; +import { EditorAssetStore, IEditorAssetStore } from "./editor/asset.store"; import { IProjectEstimateStore, ProjectEstimateStore } from "./estimates/project-estimate.store"; import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store"; import { FavoriteStore, IFavoriteStore } from "./favorite.store"; @@ -61,6 +61,7 @@ export class CoreRootStore { favorite: IFavoriteStore; transient: ITransientStore; stickyStore: IStickyStore; + editorAssetStore: IEditorAssetStore; constructor() { this.router = new RouterStore(); @@ -90,6 +91,7 @@ export class CoreRootStore { this.favorite = new FavoriteStore(this); this.transient = new TransientStore(); this.stickyStore = new StickyStore(); + this.editorAssetStore = new EditorAssetStore(); } resetOnSignOut() { @@ -122,5 +124,6 @@ export class CoreRootStore { this.favorite = new FavoriteStore(this); this.transient = new TransientStore(); this.stickyStore = new StickyStore(); + this.editorAssetStore = new EditorAssetStore(); } } diff --git a/web/helpers/editor.helper.ts b/web/helpers/editor.helper.ts index a3b05041d..90a431190 100644 --- a/web/helpers/editor.helper.ts +++ b/web/helpers/editor.helper.ts @@ -1,10 +1,5 @@ -// plane editor -import { TFileHandler } from "@plane/editor"; // helpers import { getFileURL } from "@/helpers/file.helper"; -// services -import { FileService } from "@/services/file.service"; -const fileService = new FileService(); type TEditorSrcArgs = { assetId: string; @@ -27,90 +22,6 @@ export const getEditorAssetSrc = (args: TEditorSrcArgs): string | undefined => { return url; }; -type TArgs = { - maxFileSize: number; - projectId?: string; - uploadFile: (file: File) => Promise; - workspaceId: string; - workspaceSlug: string; -}; - -/** - * @description this function returns the file handler required by the editors - * @param {TArgs} args - */ -export const getEditorFileHandlers = (args: TArgs): TFileHandler => { - const { maxFileSize, projectId, uploadFile, workspaceId, workspaceSlug } = args; - - return { - getAssetSrc: async (path) => { - if (!path) return ""; - if (path?.startsWith("http")) { - return path; - } else { - return ( - getEditorAssetSrc({ - assetId: path, - projectId, - workspaceSlug, - }) ?? "" - ); - } - }, - upload: uploadFile, - delete: async (src: string) => { - if (src?.startsWith("http")) { - await fileService.deleteOldWorkspaceAsset(workspaceId, src); - } else { - await fileService.deleteNewAsset( - getEditorAssetSrc({ - assetId: src, - projectId, - workspaceSlug, - }) ?? "" - ); - } - }, - restore: async (src: string) => { - if (src?.startsWith("http")) { - await fileService.restoreOldEditorAsset(workspaceId, src); - } else { - await fileService.restoreNewAsset(workspaceSlug, src); - } - }, - cancel: fileService.cancelUpload, - validation: { - maxFileSize, - }, - }; -}; - -/** - * @description this function returns the file handler required by the read-only editors - */ -export const getReadOnlyEditorFileHandlers = ( - args: Pick -): { getAssetSrc: TFileHandler["getAssetSrc"] } => { - const { projectId, workspaceSlug } = args; - - return { - getAssetSrc: async (path) => { - if (!path) return ""; - if (path?.startsWith("http")) { - return path; - } else { - return ( - getEditorAssetSrc({ - assetId: path, - projectId, - workspaceSlug, - }) ?? "" - ); - } - }, - }; -}; - export const getTextContent = (jsx: JSX.Element | React.ReactNode | null | undefined): string => { if (!jsx) return "";