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 c8753fe42fd..146cf36c0a5 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 @@ -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[]; @@ -20,7 +26,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 89194aae0c3..4817d2d17f6 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 @@ -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; @@ -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) @@ -270,6 +273,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 00000000000..8492d51944b --- /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 3b64db8d098..a4d38ceb59b 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 c27970d9287..78237d67835 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 e39973f9c90..8711b2cb38a 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 23bb5088417..b73a69a1731 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 65daa2f8e49..7d3dc7eaed9 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 12d4960b690..87b9c335d42 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 c69a003fc9c..82e2f81f9a3 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 3bb4d1af292..d4d8ca9010c 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 fd64dc46a61..edf696ab8d8 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 5c707bf33dd..ca6f76fb1b8 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 ac0a0633a45..9f2cda4ad51 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 5f936baec5a..f9889f2ab6d 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 96f4900548c..c9e795d7bdc 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 9712b64e690..d746d776622 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 e229ac21e81..70fcedd0ac1 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 52d83ccc9c7..0907b421e09 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,16 +18,15 @@ 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 editors - * @param {TArgs} args + * @description this function returns the file handler required by the read-only editors */ -export const getEditorFileHandlers = (args: TArgs): TFileHandler => { - const { anchor, uploadFile, workspaceId } = args; +export const getReadOnlyEditorFileHandlers = (args: Pick): TReadOnlyFileHandler => { + const { anchor, workspaceId } = args; return { getAssetSrc: async (path) => { @@ -38,14 +37,6 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => { return getEditorAssetSrc(anchor, path) ?? ""; } }, - upload: uploadFile, - delete: async (src: string) => { - if (src?.startsWith("http")) { - await sitesFileService.deleteOldEditorAsset(workspaceId, src); - } else { - await sitesFileService.deleteNewAsset(getEditorAssetSrc(anchor, src) ?? ""); - } - }, restore: async (src: string) => { if (src?.startsWith("http")) { await sitesFileService.restoreOldEditorAsset(workspaceId, src); @@ -53,29 +44,33 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => { 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 + * @description this function returns the file handler required by the editors + * @param {TArgs} args */ -export const getReadOnlyEditorFileHandlers = ( - args: Pick -): { getAssetSrc: TFileHandler["getAssetSrc"] } => { - const { anchor } = args; +export const getEditorFileHandlers = (args: TArgs): TFileHandler => { + const { anchor, uploadFile, workspaceId } = args; return { - getAssetSrc: async (path) => { - if (!path) return ""; - if (path?.startsWith("http")) { - return path; + ...getReadOnlyEditorFileHandlers({ + anchor, + workspaceId, + }), + getAssetUploadStatus: () => 0, + upload: uploadFile, + delete: async (src: string) => { + if (src?.startsWith("http")) { + await sitesFileService.deleteOldEditorAsset(workspaceId, src); } else { - return getEditorAssetSrc(anchor, path) ?? ""; + await sitesFileService.deleteNewAsset(getEditorAssetSrc(anchor, src) ?? ""); } }, + cancel: sitesFileService.cancelUpload, + validation: { + maxFileSize: MAX_FILE_SIZE, + }, }; }; 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 8d7c5135725..4a87a9cbc4b 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 b9d6c85ef57..bd49942ef34 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 036eaf28395..cef586a7712 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 747723c29d6..676da4624f3 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 82cbd12257b..63de65bc62a 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 54573fbba6c..df1276dc9fa 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 c8f33b62649..96f0960b605 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 5727714f29b..6abd2d6e4e9 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 6e45b12d4cb..0133b5e6154 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 8e5d51f788b..b19fc62dbb0 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 161d63c47cc..72533c3dc04 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 64aef6912c0..bc9c497806f 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 0beffa6f358..d201232904d 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 bdb6c6f9356..671fd9dc27f 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 5046948c00d..4d10ce2b1e4 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 00000000000..532916cd480 --- /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 00000000000..166df1d5b32 --- /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 266efea32c5..fa9ff44cc9b 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 00000000000..7c5af36960a --- /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 d1f654ba717..dba3027c552 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 00000000000..587acba023a --- /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 f9e47a56055..6f4977dbb30 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 03f1acdf497..5a10df45be0 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 a3b05041d23..90a431190f5 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 "";