[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
This commit is contained in:
Aaryan Khandelwal 2025-02-19 15:18:01 +05:30 committed by GitHub
parent b7198234de
commit 214692f5b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 602 additions and 315 deletions

View file

@ -11,7 +11,13 @@ import { getEditorClassNames } from "@/helpers/common";
// hooks // hooks
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor"; import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
// types // types
import { EditorReadOnlyRefApi, TDisplayConfig, TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types"; import {
EditorReadOnlyRefApi,
TDisplayConfig,
TExtensions,
TReadOnlyFileHandler,
TReadOnlyMentionHandler,
} from "@/types";
interface IDocumentReadOnlyEditor { interface IDocumentReadOnlyEditor {
disabledExtensions: TExtensions[]; disabledExtensions: TExtensions[];
@ -21,7 +27,7 @@ interface IDocumentReadOnlyEditor {
displayConfig?: TDisplayConfig; displayConfig?: TDisplayConfig;
editorClassName?: string; editorClassName?: string;
embedHandler: any; embedHandler: any;
fileHandler: Pick<TFileHandler, "getAssetSrc">; fileHandler: TReadOnlyFileHandler;
tabIndex?: number; tabIndex?: number;
handleEditorReady?: (value: boolean) => void; handleEditorReady?: (value: boolean) => void;
mentionHandler: TReadOnlyMentionHandler; mentionHandler: TReadOnlyMentionHandler;

View file

@ -4,6 +4,7 @@ import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// extensions // extensions
import { CustoBaseImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image"; import { CustoBaseImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image";
import { ImageUploadStatus } from "./upload-status";
const MIN_SIZE = 100; const MIN_SIZE = 100;
@ -210,6 +211,8 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
// show the image loader if the remote image's src or preview image from filesystem is not set yet (while loading the image post upload) (or) // 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 // 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; 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) // 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; 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) // show the image resizer only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
@ -279,6 +282,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
...(size.aspectRatio && { aspectRatio: size.aspectRatio }), ...(size.aspectRatio && { aspectRatio: size.aspectRatio }),
}} }}
/> />
{showUploadStatus && <ImageUploadStatus editor={editor} nodeId={node.attrs.id} />}
{showImageUtils && ( {showImageUtils && (
<ImageToolbarRoot <ImageToolbarRoot
containerClassName={ containerClassName={

View file

@ -69,6 +69,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
); );
// hooks // hooks
const { uploading: isImageBeingUploaded, uploadFile } = useUploader({ const { uploading: isImageBeingUploaded, uploadFile } = useUploader({
blockId: imageEntityId ?? "",
editor, editor,
loadImageFromFileSystem, loadImageFromFileSystem,
maxFileSize, maxFileSize,

View file

@ -0,0 +1,60 @@
import { Editor } from "@tiptap/core";
import { useEditorState } from "@tiptap/react";
import { useEffect, useRef, useState } from "react";
type Props = {
editor: Editor;
nodeId: string;
};
export const ImageUploadStatus: React.FC<Props> = (props) => {
const { editor, nodeId } = props;
// Displayed status that will animate smoothly
const [displayStatus, setDisplayStatus] = useState(0);
// Animation frame ID for cleanup
const animationFrameRef = useRef(null);
// subscribe to image upload status
const uploadStatus: number | undefined = useEditorState({
editor,
selector: ({ editor }) => editor.storage.imageComponent.assetsUploadStatus[nodeId],
});
useEffect(() => {
const animateToValue = (start: number, end: number, startTime: number) => {
const duration = 200;
const animation = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Easing function for smooth animation
const easeOutCubic = 1 - Math.pow(1 - progress, 3);
// Calculate current display value
const currentValue = Math.floor(start + (end - start) * easeOutCubic);
setDisplayStatus(currentValue);
// Continue animation if not complete
if (progress < 1) {
animationFrameRef.current = requestAnimationFrame((time) => animation(time));
}
};
animationFrameRef.current = requestAnimationFrame((time) => animation(time));
};
animateToValue(displayStatus, uploadStatus, performance.now());
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [uploadStatus]);
if (uploadStatus === undefined) return null;
return (
<div className="absolute top-1 right-1 z-20 bg-black/60 rounded text-xs font-medium w-10 text-center">
{displayStatus}%
</div>
);
};

View file

@ -4,12 +4,12 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
// extensions // extensions
import { CustomImageNode } from "@/extensions/custom-image"; import { CustomImageNode } from "@/extensions/custom-image";
// helpers
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
// plugins // plugins
import { TrackImageDeletionPlugin, TrackImageRestorationPlugin, isFileValid } from "@/plugins/image"; import { TrackImageDeletionPlugin, TrackImageRestorationPlugin, isFileValid } from "@/plugins/image";
// types // types
import { TFileHandler } from "@/types"; import { TFileHandler } from "@/types";
// helpers
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
export type InsertImageComponentProps = { export type InsertImageComponentProps = {
file?: File; file?: File;
@ -21,7 +21,8 @@ declare module "@tiptap/core" {
interface Commands<ReturnType> { interface Commands<ReturnType> {
imageComponent: { imageComponent: {
insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType; insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType;
uploadImage: (file: File) => () => Promise<string> | undefined; uploadImage: (blockId: string, file: File) => () => Promise<string> | undefined;
updateAssetsUploadStatus: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void;
getImageSource?: (path: string) => () => Promise<string>; getImageSource?: (path: string) => () => Promise<string>;
restoreImage: (src: string) => () => Promise<void>; restoreImage: (src: string) => () => Promise<void>;
}; };
@ -32,6 +33,7 @@ export const getImageComponentImageFileMap = (editor: Editor) =>
(editor.storage.imageComponent as UploadImageExtensionStorage | undefined)?.fileMap; (editor.storage.imageComponent as UploadImageExtensionStorage | undefined)?.fileMap;
export interface UploadImageExtensionStorage { export interface UploadImageExtensionStorage {
assetsUploadStatus: TFileHandler["assetsUploadStatus"];
fileMap: Map<string, UploadEntity>; fileMap: Map<string, UploadEntity>;
} }
@ -39,6 +41,7 @@ export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File })
export const CustomImageExtension = (props: TFileHandler) => { export const CustomImageExtension = (props: TFileHandler) => {
const { const {
assetsUploadStatus,
getAssetSrc, getAssetSrc,
upload, upload,
delete: deleteImageFn, delete: deleteImageFn,
@ -105,7 +108,6 @@ export const CustomImageExtension = (props: TFileHandler) => {
this.editor.state.doc.descendants((node) => { this.editor.state.doc.descendants((node) => {
if (node.type.name === this.name) { if (node.type.name === this.name) {
if (!node.attrs.src?.startsWith("http")) return; if (!node.attrs.src?.startsWith("http")) return;
imageSources.add(node.attrs.src); imageSources.add(node.attrs.src);
} }
}); });
@ -128,13 +130,14 @@ export const CustomImageExtension = (props: TFileHandler) => {
markdown: { markdown: {
serialize() {}, serialize() {},
}, },
assetsUploadStatus,
}; };
}, },
addCommands() { addCommands() {
return { return {
insertImageComponent: insertImageComponent:
(props: { file?: File; pos?: number; event: "insert" | "drop" }) => (props) =>
({ commands }) => { ({ commands }) => {
// Early return if there's an invalid file being dropped // Early return if there's an invalid file being dropped
if ( if (
@ -182,12 +185,15 @@ export const CustomImageExtension = (props: TFileHandler) => {
attrs: attributes, attrs: attributes,
}); });
}, },
uploadImage: (file: File) => async () => { uploadImage: (blockId, file) => async () => {
const fileUrl = await upload(file); const fileUrl = await upload(blockId, file);
return fileUrl; return fileUrl;
}, },
getImageSource: (path: string) => async () => await getAssetSrc(path), updateAssetsUploadStatus: (updatedStatus) => () => {
restoreImage: (src: string) => async () => { this.storage.assetsUploadStatus = updatedStatus;
},
getImageSource: (path) => async () => await getAssetSrc(path),
restoreImage: (src) => async () => {
await restoreImageFn(src); await restoreImageFn(src);
}, },
}; };

View file

@ -4,9 +4,9 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
// components // components
import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custom-image"; import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custom-image";
// types // types
import { TFileHandler } from "@/types"; import { TReadOnlyFileHandler } from "@/types";
export const CustomReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">) => { export const CustomReadOnlyImageExtension = (props: TReadOnlyFileHandler) => {
const { getAssetSrc } = props; const { getAssetSrc } = props;
return Image.extend<Record<string, unknown>, UploadImageExtensionStorage>({ return Image.extend<Record<string, unknown>, UploadImageExtensionStorage>({
@ -56,6 +56,7 @@ export const CustomReadOnlyImageExtension = (props: Pick<TFileHandler, "getAsset
markdown: { markdown: {
serialize() {}, serialize() {},
}, },
assetsUploadStatus: {},
}; };
}, },

View file

@ -3,9 +3,9 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
// extensions // extensions
import { CustomImageNode } from "@/extensions"; import { CustomImageNode } from "@/extensions";
// types // types
import { TFileHandler } from "@/types"; import { TReadOnlyFileHandler } from "@/types";
export const ReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">) => { export const ReadOnlyImageExtension = (props: TReadOnlyFileHandler) => {
const { getAssetSrc } = props; const { getAssetSrc } = props;
return Image.extend({ return Image.extend({

View file

@ -27,14 +27,14 @@ import {
} from "@/extensions"; } from "@/extensions";
// helpers // helpers
import { isValidHttpUrl } from "@/helpers/common"; import { isValidHttpUrl } from "@/helpers/common";
// types
import { TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types";
// plane editor extensions // plane editor extensions
import { CoreReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions"; import { CoreReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions";
// types
import { TExtensions, TReadOnlyFileHandler, TReadOnlyMentionHandler } from "@/types";
type Props = { type Props = {
disabledExtensions: TExtensions[]; disabledExtensions: TExtensions[];
fileHandler: Pick<TFileHandler, "getAssetSrc">; fileHandler: TReadOnlyFileHandler;
mentionHandler: TReadOnlyMentionHandler; mentionHandler: TReadOnlyMentionHandler;
}; };
@ -94,16 +94,12 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
}, },
}), }),
CustomTypographyExtension, CustomTypographyExtension,
ReadOnlyImageExtension({ ReadOnlyImageExtension(fileHandler).configure({
getAssetSrc: fileHandler.getAssetSrc,
}).configure({
HTMLAttributes: { HTMLAttributes: {
class: "rounded-md", class: "rounded-md",
}, },
}), }),
CustomReadOnlyImageExtension({ CustomReadOnlyImageExtension(fileHandler),
getAssetSrc: fileHandler.getAssetSrc,
}),
TiptapUnderline, TiptapUnderline,
TextStyle, TextStyle,
TaskList.configure({ TaskList.configure({

View file

@ -125,6 +125,13 @@ export const useEditor = (props: CustomEditorProps) => {
} }
}, [editor, value, id]); }, [editor, value, id]);
// update assets upload status
useEffect(() => {
if (!editor) return;
const assetsUploadStatus = fileHandler.assetsUploadStatus;
editor.commands.updateAssetsUploadStatus(assetsUploadStatus);
}, [editor, fileHandler.assetsUploadStatus]);
useImperativeHandle( useImperativeHandle(
forwardedRef, forwardedRef,
() => ({ () => ({

View file

@ -6,6 +6,7 @@ import { insertImagesSafely } from "@/extensions/drop";
import { isFileValid } from "@/plugins/image"; import { isFileValid } from "@/plugins/image";
type TUploaderArgs = { type TUploaderArgs = {
blockId: string;
editor: Editor; editor: Editor;
loadImageFromFileSystem: (file: string) => void; loadImageFromFileSystem: (file: string) => void;
maxFileSize: number; maxFileSize: number;
@ -13,7 +14,7 @@ type TUploaderArgs = {
}; };
export const useUploader = (args: TUploaderArgs) => { export const useUploader = (args: TUploaderArgs) => {
const { editor, loadImageFromFileSystem, maxFileSize, onUpload } = args; const { blockId, editor, loadImageFromFileSystem, maxFileSize, onUpload } = args;
// states // states
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
@ -49,7 +50,7 @@ export const useUploader = (args: TUploaderArgs) => {
reader.readAsDataURL(fileWithTrimmedName); reader.readAsDataURL(fileWithTrimmedName);
// @ts-expect-error - TODO: fix typings, and don't remove await from // @ts-expect-error - TODO: fix typings, and don't remove await from
// here for now // here for now
const url: string = await editor?.commands.uploadImage(fileWithTrimmedName); const url: string = await editor?.commands.uploadImage(blockId, fileWithTrimmedName);
if (!url) { if (!url) {
throw new Error("Something went wrong while uploading the image"); throw new Error("Something went wrong while uploading the image");

View file

@ -11,7 +11,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
// props // props
import { CoreReadOnlyEditorProps } from "@/props"; import { CoreReadOnlyEditorProps } from "@/props";
// types // types
import type { EditorReadOnlyRefApi, TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types"; import type { EditorReadOnlyRefApi, TExtensions, TReadOnlyFileHandler, TReadOnlyMentionHandler } from "@/types";
interface CustomReadOnlyEditorProps { interface CustomReadOnlyEditorProps {
disabledExtensions: TExtensions[]; disabledExtensions: TExtensions[];
@ -20,7 +20,7 @@ interface CustomReadOnlyEditorProps {
extensions?: Extensions; extensions?: Extensions;
forwardedRef?: MutableRefObject<EditorReadOnlyRefApi | null>; forwardedRef?: MutableRefObject<EditorReadOnlyRefApi | null>;
initialValue?: string; initialValue?: string;
fileHandler: Pick<TFileHandler, "getAssetSrc">; fileHandler: TReadOnlyFileHandler;
handleEditorReady?: (value: boolean) => void; handleEditorReady?: (value: boolean) => void;
mentionHandler: TReadOnlyMentionHandler; mentionHandler: TReadOnlyMentionHandler;
provider?: HocuspocusProvider; provider?: HocuspocusProvider;

View file

@ -9,6 +9,7 @@ import {
TExtensions, TExtensions,
TFileHandler, TFileHandler,
TMentionHandler, TMentionHandler,
TReadOnlyFileHandler,
TReadOnlyMentionHandler, TReadOnlyMentionHandler,
TRealtimeConfig, TRealtimeConfig,
TUserDetails, TUserDetails,
@ -43,7 +44,7 @@ export type TCollaborativeEditorProps = TCollaborativeEditorHookProps & {
}; };
export type TReadOnlyCollaborativeEditorProps = TCollaborativeEditorHookProps & { export type TReadOnlyCollaborativeEditorProps = TCollaborativeEditorHookProps & {
fileHandler: Pick<TFileHandler, "getAssetSrc">; fileHandler: TReadOnlyFileHandler;
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>; forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
mentionHandler: TReadOnlyMentionHandler; mentionHandler: TReadOnlyMentionHandler;
}; };

View file

@ -1,11 +1,15 @@
import { DeleteImage, RestoreImage, UploadImage } from "@/types"; import { DeleteImage, RestoreImage, UploadImage } from "@/types";
export type TFileHandler = { export type TReadOnlyFileHandler = {
getAssetSrc: (path: string) => Promise<string>; getAssetSrc: (path: string) => Promise<string>;
restore: RestoreImage;
};
export type TFileHandler = TReadOnlyFileHandler & {
assetsUploadStatus: Record<string, number>; // blockId => progress percentage
cancel: () => void; cancel: () => void;
delete: DeleteImage; delete: DeleteImage;
upload: UploadImage; upload: UploadImage;
restore: RestoreImage;
validation: { validation: {
/** /**
* @description max file size in bytes * @description max file size in bytes

View file

@ -16,6 +16,7 @@ import {
TExtensions, TExtensions,
TFileHandler, TFileHandler,
TMentionHandler, TMentionHandler,
TReadOnlyFileHandler,
TReadOnlyMentionHandler, TReadOnlyMentionHandler,
TServerHandler, TServerHandler,
} from "@/types"; } from "@/types";
@ -44,12 +45,16 @@ export type TEditorCommands =
| "text-color" | "text-color"
| "background-color" | "background-color"
| "text-align" | "text-align"
| "callout"; | "callout"
| "attachment";
export type TCommandExtraProps = { export type TCommandExtraProps = {
image: { image: {
savedSelection: Selection | null; savedSelection: Selection | null;
}; };
attachment: {
savedSelection: Selection | null;
};
"text-color": { "text-color": {
color: string | undefined; color: string | undefined;
}; };
@ -155,7 +160,7 @@ export interface IReadOnlyEditorProps {
disabledExtensions: TExtensions[]; disabledExtensions: TExtensions[];
displayConfig?: TDisplayConfig; displayConfig?: TDisplayConfig;
editorClassName?: string; editorClassName?: string;
fileHandler: Pick<TFileHandler, "getAssetSrc">; fileHandler: TReadOnlyFileHandler;
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>; forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
id: string; id: string;
initialValue: string; initialValue: string;

View file

@ -2,4 +2,4 @@ export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<void>;
export type RestoreImage = (assetUrlWithWorkspaceId: string) => Promise<void>; export type RestoreImage = (assetUrlWithWorkspaceId: string) => Promise<void>;
export type UploadImage = (file: File) => Promise<string>; export type UploadImage = (blockId: string, file: File) => Promise<string>;

View file

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
// editor // editor
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor"; import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor";
// components // components
import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor"; import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor";
// helpers // helpers
@ -14,7 +14,7 @@ interface LiteTextEditorWrapperProps
workspaceId: string; workspaceId: string;
isSubmitting?: boolean; isSubmitting?: boolean;
showSubmitButton?: boolean; showSubmitButton?: boolean;
uploadFile: (file: File) => Promise<string>; uploadFile: TFileHandler["upload"];
} }
export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapperProps>((props, ref) => { export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapperProps>((props, ref) => {

View file

@ -12,15 +12,17 @@ type LiteTextReadOnlyEditorWrapperProps = Omit<
"disabledExtensions" | "fileHandler" | "mentionHandler" "disabledExtensions" | "fileHandler" | "mentionHandler"
> & { > & {
anchor: string; anchor: string;
workspaceId: string;
}; };
export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>( export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>(
({ anchor, ...props }, ref) => ( ({ anchor, workspaceId, ...props }, ref) => (
<LiteTextReadOnlyEditorWithRef <LiteTextReadOnlyEditorWithRef
ref={ref} ref={ref}
disabledExtensions={[]} disabledExtensions={[]}
fileHandler={getReadOnlyEditorFileHandlers({ fileHandler={getReadOnlyEditorFileHandlers({
anchor, anchor,
workspaceId,
})} })}
mentionHandler={{ mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />, renderComponent: (props) => <EditorMentionsRoot {...props} />,

View file

@ -1,6 +1,6 @@
import React, { forwardRef } from "react"; import React, { forwardRef } from "react";
// editor // editor
import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef } from "@plane/editor"; import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef, TFileHandler } from "@plane/editor";
// components // components
import { EditorMentionsRoot } from "@/components/editor"; import { EditorMentionsRoot } from "@/components/editor";
// helpers // helpers
@ -8,11 +8,13 @@ import { getEditorFileHandlers } from "@/helpers/editor.helper";
interface RichTextEditorWrapperProps interface RichTextEditorWrapperProps
extends Omit<IRichTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> { extends Omit<IRichTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
uploadFile: (file: File) => Promise<string>; anchor: string;
uploadFile: TFileHandler["upload"];
workspaceId: string;
} }
export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => { export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => {
const { containerClassName, uploadFile, ...rest } = props; const { anchor, containerClassName, uploadFile, workspaceId, ...rest } = props;
return ( return (
<RichTextEditorWithRef <RichTextEditorWithRef
@ -22,9 +24,9 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
ref={ref} ref={ref}
disabledExtensions={[]} disabledExtensions={[]}
fileHandler={getEditorFileHandlers({ fileHandler={getEditorFileHandlers({
anchor,
uploadFile, uploadFile,
workspaceId: "", workspaceId,
anchor: "",
})} })}
{...rest} {...rest}
containerClassName={containerClassName} containerClassName={containerClassName}

View file

@ -12,15 +12,17 @@ type RichTextReadOnlyEditorWrapperProps = Omit<
"disabledExtensions" | "fileHandler" | "mentionHandler" "disabledExtensions" | "fileHandler" | "mentionHandler"
> & { > & {
anchor: string; anchor: string;
workspaceId: string;
}; };
export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>( export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>(
({ anchor, ...props }, ref) => ( ({ anchor, workspaceId, ...props }, ref) => (
<RichTextReadOnlyEditorWithRef <RichTextReadOnlyEditorWithRef
ref={ref} ref={ref}
disabledExtensions={[]} disabledExtensions={[]}
fileHandler={getReadOnlyEditorFileHandlers({ fileHandler={getReadOnlyEditorFileHandlers({
anchor, anchor,
workspaceId,
})} })}
mentionHandler={{ mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />, renderComponent: (props) => <EditorMentionsRoot {...props} />,

View file

@ -90,7 +90,7 @@ export const AddComment: React.FC<Props> = observer((props) => {
onChange={(comment_json, comment_html) => onChange(comment_html)} onChange={(comment_json, comment_html) => onChange(comment_html)}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
placeholder="Add comment..." placeholder="Add comment..."
uploadFile={async (file) => { uploadFile={async (blockId, file) => {
const { asset_id } = await uploadCommentAsset(file, anchor); const { asset_id } = await uploadCommentAsset(file, anchor);
setUploadAssetIds((prev) => [...prev, asset_id]); setUploadAssetIds((prev) => [...prev, asset_id]);
return asset_id; return asset_id;

View file

@ -112,7 +112,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
onChange={(comment_json, comment_html) => onChange(comment_html)} onChange={(comment_json, comment_html) => onChange(comment_html)}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
showSubmitButton={false} showSubmitButton={false}
uploadFile={async (file) => { uploadFile={async (blockId, file) => {
const { asset_id } = await uploadCommentAsset(file, anchor, comment.id); const { asset_id } = await uploadCommentAsset(file, anchor, comment.id);
return asset_id; return asset_id;
}} }}
@ -140,6 +140,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
<div className={`${isEditing ? "hidden" : ""}`}> <div className={`${isEditing ? "hidden" : ""}`}>
<LiteTextReadOnlyEditor <LiteTextReadOnlyEditor
anchor={anchor} anchor={anchor}
workspaceId={workspaceID?.toString() ?? ""}
ref={showEditorRef} ref={showEditorRef}
id={comment.id} id={comment.id}
initialValue={comment.comment_html} initialValue={comment.comment_html}

View file

@ -13,9 +13,9 @@ type Props = {
export const PeekOverviewIssueDetails: React.FC<Props> = observer((props) => { export const PeekOverviewIssueDetails: React.FC<Props> = observer((props) => {
const { anchor, issueDetails } = props; const { anchor, issueDetails } = props;
// store hooks
const { project_details } = usePublish(anchor); const { project_details, workspace: workspaceID } = usePublish(anchor);
// derived values
const description = issueDetails.description_html; const description = issueDetails.description_html;
return ( return (
@ -35,6 +35,7 @@ export const PeekOverviewIssueDetails: React.FC<Props> = observer((props) => {
? "<p></p>" ? "<p></p>"
: description : description
} }
workspaceId={workspaceID?.toString() ?? ""}
/> />
)} )}
<IssueReactions anchor={anchor} /> <IssueReactions anchor={anchor} />

View file

@ -1,6 +1,6 @@
// plane internal // plane internal
import { MAX_FILE_SIZE } from "@plane/constants"; import { MAX_FILE_SIZE } from "@plane/constants";
import { TFileHandler } from "@plane/editor"; import { TFileHandler, TReadOnlyFileHandler } from "@plane/editor";
import { SitesFileService } from "@plane/services"; import { SitesFileService } from "@plane/services";
// helpers // helpers
import { getFileURL } from "@/helpers/file.helper"; import { getFileURL } from "@/helpers/file.helper";
@ -18,10 +18,35 @@ export const getEditorAssetSrc = (anchor: string, assetId: string): string | und
type TArgs = { type TArgs = {
anchor: string; anchor: string;
uploadFile: (file: File) => Promise<string>; uploadFile: TFileHandler["upload"];
workspaceId: string; workspaceId: string;
}; };
/**
* @description this function returns the file handler required by the read-only editors
*/
export const getReadOnlyEditorFileHandlers = (args: Pick<TArgs, "anchor" | "workspaceId">): 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 * @description this function returns the file handler required by the editors
* @param {TArgs} args * @param {TArgs} args
@ -30,14 +55,11 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
const { anchor, uploadFile, workspaceId } = args; const { anchor, uploadFile, workspaceId } = args;
return { return {
getAssetSrc: async (path) => { ...getReadOnlyEditorFileHandlers({
if (!path) return ""; anchor,
if (path?.startsWith("http")) { workspaceId,
return path; }),
} else { getAssetUploadStatus: () => 0,
return getEditorAssetSrc(anchor, path) ?? "";
}
},
upload: uploadFile, upload: uploadFile,
delete: async (src: string) => { delete: async (src: string) => {
if (src?.startsWith("http")) { if (src?.startsWith("http")) {
@ -46,36 +68,9 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
await sitesFileService.deleteNewAsset(getEditorAssetSrc(anchor, src) ?? ""); 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, cancel: sitesFileService.cancelUpload,
validation: { validation: {
maxFileSize: MAX_FILE_SIZE, maxFileSize: MAX_FILE_SIZE,
}, },
}; };
}; };
/**
* @description this function returns the file handler required by the read-only editors
*/
export const getReadOnlyEditorFileHandlers = (
args: Pick<TArgs, "anchor">
): { getAssetSrc: TFileHandler["getAssetSrc"] } => {
const { anchor } = args;
return {
getAssetSrc: async (path) => {
if (!path) return "";
if (path?.startsWith("http")) {
return path;
} else {
return getEditorAssetSrc(anchor, path) ?? "";
}
},
};
};

View file

@ -6,7 +6,7 @@ import Link from "next/link";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import useSWR from "swr"; import useSWR from "swr";
// plane types // plane types
import { TSearchEntityRequestPayload } from "@plane/types"; import { TSearchEntityRequestPayload, TWebhookConnectionQueryParams } from "@plane/types";
import { EFileAssetType } from "@plane/types/src/enums"; import { EFileAssetType } from "@plane/types/src/enums";
// plane ui // plane ui
import { getButtonStyling } from "@plane/ui"; import { getButtonStyling } from "@plane/ui";
@ -17,19 +17,14 @@ import { LogoSpinner } from "@/components/common";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { IssuePeekOverview } from "@/components/issues"; import { IssuePeekOverview } from "@/components/issues";
import { PageRoot, TPageRootConfig, TPageRootHandlers } from "@/components/pages"; import { PageRoot, TPageRootConfig, TPageRootHandlers } from "@/components/pages";
// helpers
import { getEditorFileHandlers } from "@/helpers/editor.helper";
// hooks // hooks
import { useProjectPage, useProjectPages, useWorkspace } from "@/hooks/store"; import { useEditorConfig } from "@/hooks/editor";
// plane web hooks import { useEditorAsset, useProjectPage, useProjectPages, useWorkspace } from "@/hooks/store";
import { useFileSize } from "@/plane-web/hooks/use-file-size";
// plane web services // plane web services
import { WorkspaceService } from "@/plane-web/services"; import { WorkspaceService } from "@/plane-web/services";
// services // services
import { FileService } from "@/services/file.service";
import { ProjectPageService, ProjectPageVersionService } from "@/services/page"; import { ProjectPageService, ProjectPageVersionService } from "@/services/page";
const workspaceService = new WorkspaceService(); const workspaceService = new WorkspaceService();
const fileService = new FileService();
const projectPageService = new ProjectPageService(); const projectPageService = new ProjectPageService();
const projectPageVersionService = new ProjectPageVersionService(); const projectPageVersionService = new ProjectPageVersionService();
@ -39,6 +34,7 @@ const PageDetailsPage = observer(() => {
const { createPage, getPageById } = useProjectPages(); const { createPage, getPageById } = useProjectPages();
const page = useProjectPage(pageId?.toString() ?? ""); const page = useProjectPage(pageId?.toString() ?? "");
const { getWorkspaceBySlug } = useWorkspace(); const { getWorkspaceBySlug } = useWorkspace();
const { uploadEditorAsset } = useEditorAsset();
// derived values // derived values
const workspaceId = workspaceSlug ? (getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "") : ""; const workspaceId = workspaceSlug ? (getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "") : "";
const { canCurrentUserAccessPage, id, name, updateDescription } = page; const { canCurrentUserAccessPage, id, name, updateDescription } = page;
@ -51,8 +47,8 @@ const PageDetailsPage = observer(() => {
}), }),
[projectId, workspaceSlug] [projectId, workspaceSlug]
); );
// file size // editor config
const { maxFileSize } = useFileSize(); const { getEditorFileHandlers } = useEditorConfig();
// fetch page details // fetch page details
const { error: pageDetailsError } = useSWR( const { error: pageDetailsError } = useSWR(
workspaceSlug && projectId && pageId ? `PAGE_DETAILS_${pageId}` : null, workspaceSlug && projectId && pageId ? `PAGE_DETAILS_${pageId}` : null,
@ -96,30 +92,34 @@ const PageDetailsPage = observer(() => {
const pageRootConfig: TPageRootConfig = useMemo( const pageRootConfig: TPageRootConfig = useMemo(
() => ({ () => ({
fileHandler: getEditorFileHandlers({ fileHandler: getEditorFileHandlers({
maxFileSize,
projectId: projectId?.toString() ?? "", projectId: projectId?.toString() ?? "",
uploadFile: async (file) => { uploadFile: async (blockId, file) => {
const { asset_id } = await fileService.uploadProjectAsset( const { asset_id } = await uploadEditorAsset({
workspaceSlug?.toString() ?? "", blockId,
projectId?.toString() ?? "", data: {
{
entity_identifier: id ?? "", entity_identifier: id ?? "",
entity_type: EFileAssetType.PAGE_DESCRIPTION, entity_type: EFileAssetType.PAGE_DESCRIPTION,
}, },
file file,
); projectId: projectId?.toString() ?? "",
workspaceSlug: workspaceSlug?.toString() ?? "",
});
return asset_id; return asset_id;
}, },
workspaceId, workspaceId,
workspaceSlug: workspaceSlug?.toString() ?? "", workspaceSlug: workspaceSlug?.toString() ?? "",
}), }),
webhookConnectionParams: { }),
[getEditorFileHandlers, id, projectId, uploadEditorAsset, workspaceId, workspaceSlug]
);
const webhookConnectionParams: TWebhookConnectionQueryParams = useMemo(
() => ({
documentType: "project_page", documentType: "project_page",
projectId: projectId?.toString() ?? "", projectId: projectId?.toString() ?? "",
workspaceSlug: workspaceSlug?.toString() ?? "", workspaceSlug: workspaceSlug?.toString() ?? "",
},
}), }),
[id, maxFileSize, projectId, workspaceId, workspaceSlug] [projectId, workspaceSlug]
); );
if ((!page || !id) && !pageDetailsError) if ((!page || !id) && !pageDetailsError)
@ -154,6 +154,7 @@ const PageDetailsPage = observer(() => {
config={pageRootConfig} config={pageRootConfig}
handlers={pageRootHandlers} handlers={pageRootHandlers}
page={page} page={page}
webhookConnectionParams={webhookConnectionParams}
workspaceSlug={workspaceSlug?.toString() ?? ""} workspaceSlug={workspaceSlug?.toString() ?? ""}
/> />
<IssuePeekOverview /> <IssuePeekOverview />

View file

@ -6,6 +6,8 @@ import { Tooltip } from "@plane/ui";
import { RichTextReadOnlyEditor } from "@/components/editor"; import { RichTextReadOnlyEditor } from "@/components/editor";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
// hooks
import { useWorkspace } from "@/hooks/store";
type Props = { type Props = {
handleInsertText: (insertOnNextLine: boolean) => void; handleInsertText: (insertOnNextLine: boolean) => void;
@ -19,6 +21,10 @@ export const AskPiMenu: React.FC<Props> = (props) => {
const { handleInsertText, handleRegenerate, isRegenerating, response, workspaceSlug } = props; const { handleInsertText, handleRegenerate, isRegenerating, response, workspaceSlug } = props;
// states // states
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
// store hooks
const { getWorkspaceBySlug } = useWorkspace();
// derived values
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? "";
return ( return (
<> <>
@ -40,6 +46,7 @@ export const AskPiMenu: React.FC<Props> = (props) => {
initialValue={response} initialValue={response}
containerClassName="!p-0 border-none" containerClassName="!p-0 border-none"
editorClassName="!pl-0" editorClassName="!pl-0"
workspaceId={workspaceId}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
/> />
<div className="mt-3 flex items-center gap-4"> <div className="mt-3 flex items-center gap-4">

View file

@ -21,6 +21,7 @@ type Props = {
editorRef: RefObject<EditorRefApi>; editorRef: RefObject<EditorRefApi>;
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
workspaceId: string;
workspaceSlug: string; workspaceSlug: string;
}; };
@ -58,7 +59,7 @@ const TONES_LIST = [
]; ];
export const EditorAIMenu: React.FC<Props> = (props) => { export const EditorAIMenu: React.FC<Props> = (props) => {
const { editorRef, isOpen, onClose, workspaceSlug } = props; const { editorRef, isOpen, onClose, workspaceId, workspaceSlug } = props;
// states // states
const [activeTask, setActiveTask] = useState<AI_EDITOR_TASKS | null>(null); const [activeTask, setActiveTask] = useState<AI_EDITOR_TASKS | null>(null);
const [response, setResponse] = useState<string | undefined>(undefined); const [response, setResponse] = useState<string | undefined>(undefined);
@ -215,6 +216,7 @@ export const EditorAIMenu: React.FC<Props> = (props) => {
initialValue={response} initialValue={response}
containerClassName="!p-0 border-none" containerClassName="!p-0 border-none"
editorClassName="!pl-0" editorClassName="!pl-0"
workspaceId={workspaceId}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
/> />
<div className="mt-3 flex items-center gap-4"> <div className="mt-3 flex items-center gap-4">

View file

@ -6,12 +6,15 @@ import { Controller, useForm } from "react-hook-form"; // services
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
import { Popover, Transition } from "@headlessui/react"; import { Popover, Transition } from "@headlessui/react";
// plane editor
import { EditorReadOnlyRefApi } from "@plane/editor";
// ui // ui
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
// components // components
import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor"; import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
// services // services
import { AIService } from "@/services/ai.service"; import { AIService } from "@/services/ai.service";
const aiService = new AIService();
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -22,6 +25,7 @@ type Props = {
prompt?: string; prompt?: string;
button: JSX.Element; button: JSX.Element;
className?: string; className?: string;
workspaceId: string;
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
}; };
@ -31,8 +35,6 @@ type FormData = {
task: string; task: string;
}; };
const aiService = new AIService();
export const GptAssistantPopover: React.FC<Props> = (props) => { export const GptAssistantPopover: React.FC<Props> = (props) => {
const { const {
isOpen, isOpen,
@ -43,6 +45,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
prompt, prompt,
button, button,
className = "", className = "",
workspaceId,
workspaceSlug, workspaceSlug,
projectId, projectId,
} = props; } = props;
@ -51,7 +54,8 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
const [invalidResponse, setInvalidResponse] = useState(false); const [invalidResponse, setInvalidResponse] = useState(false);
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null); const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null); const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const editorRef = useRef<any>(null); // refs
const editorRef = useRef<EditorReadOnlyRefApi>(null);
const responseRef = useRef<any>(null); const responseRef = useRef<any>(null);
// popper // popper
const { styles, attributes } = usePopper(referenceElement, popperElement, { const { styles, attributes } = usePopper(referenceElement, popperElement, {
@ -218,6 +222,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
initialValue={prompt} initialValue={prompt}
containerClassName="-m-3" containerClassName="-m-3"
ref={editorRef} ref={editorRef}
workspaceId={workspaceId}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
/> />
@ -230,6 +235,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
id="ai-assistant-response" id="ai-assistant-response"
initialValue={`<p>${response}</p>`} initialValue={`<p>${response}</p>`}
ref={responseRef} ref={responseRef}
workspaceId={workspaceId}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
/> />

View file

@ -2,20 +2,18 @@ import React, { useState } from "react";
// plane constants // plane constants
import { EIssueCommentAccessSpecifier } from "@plane/constants"; import { EIssueCommentAccessSpecifier } from "@plane/constants";
// plane editor // plane editor
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor"; import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor";
// i18n // i18n
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// components // components
import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor"; import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { getEditorFileHandlers } from "@/helpers/editor.helper";
import { isCommentEmpty } from "@/helpers/string.helper"; import { isCommentEmpty } from "@/helpers/string.helper";
// hooks // hooks
import { useEditorMention } from "@/hooks/use-editor-mention"; import { useEditorConfig, useEditorMention } from "@/hooks/editor";
// plane web hooks // plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
import { useFileSize } from "@/plane-web/hooks/use-file-size";
// plane web services // plane web services
import { WorkspaceService } from "@/plane-web/services"; import { WorkspaceService } from "@/plane-web/services";
const workspaceService = new WorkspaceService(); const workspaceService = new WorkspaceService();
@ -31,7 +29,7 @@ interface LiteTextEditorWrapperProps
showSubmitButton?: boolean; showSubmitButton?: boolean;
isSubmitting?: boolean; isSubmitting?: boolean;
showToolbarInitially?: boolean; showToolbarInitially?: boolean;
uploadFile: (file: File) => Promise<string>; uploadFile: TFileHandler["upload"];
issue_id?: string; issue_id?: string;
} }
@ -66,8 +64,8 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
issue_id: issue_id, issue_id: issue_id,
}), }),
}); });
// file size // editor config
const { maxFileSize } = useFileSize(); const { getEditorFileHandlers } = useEditorConfig();
function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> { function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> {
return !!ref && typeof ref === "object" && "current" in ref; return !!ref && typeof ref === "object" && "current" in ref;
} }
@ -85,7 +83,6 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
ref={ref} ref={ref}
disabledExtensions={disabledExtensions} disabledExtensions={disabledExtensions}
fileHandler={getEditorFileHandlers({ fileHandler={getEditorFileHandlers({
maxFileSize,
projectId, projectId,
uploadFile, uploadFile,
workspaceId, workspaceId,

View file

@ -5,7 +5,8 @@ import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditor, LiteTextReadOnlyEditorWi
import { EditorMentionsRoot } from "@/components/editor"; import { EditorMentionsRoot } from "@/components/editor";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper"; // hooks
import { useEditorConfig } from "@/hooks/editor";
// plane web hooks // plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
@ -13,14 +14,17 @@ type LiteTextReadOnlyEditorWrapperProps = Omit<
ILiteTextReadOnlyEditor, ILiteTextReadOnlyEditor,
"disabledExtensions" | "fileHandler" | "mentionHandler" "disabledExtensions" | "fileHandler" | "mentionHandler"
> & { > & {
workspaceId: string;
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
}; };
export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>( export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>(
({ workspaceSlug, projectId, ...props }, ref) => { ({ workspaceId, workspaceSlug, projectId, ...props }, ref) => {
// editor flaggings // editor flaggings
const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString()); const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
// editor config
const { getReadOnlyEditorFileHandlers } = useEditorConfig();
return ( return (
<LiteTextReadOnlyEditorWithRef <LiteTextReadOnlyEditorWithRef
@ -28,6 +32,7 @@ export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Lit
disabledExtensions={disabledExtensions} disabledExtensions={disabledExtensions}
fileHandler={getReadOnlyEditorFileHandlers({ fileHandler={getReadOnlyEditorFileHandlers({
projectId, projectId,
workspaceId,
workspaceSlug, workspaceSlug,
})} })}
mentionHandler={{ mentionHandler={{

View file

@ -1,18 +1,16 @@
import React, { forwardRef } from "react"; import React, { forwardRef } from "react";
// editor // editor
import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef } from "@plane/editor"; import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef, TFileHandler } from "@plane/editor";
// plane types // plane types
import { TSearchEntityRequestPayload, TSearchResponse } from "@plane/types"; import { TSearchEntityRequestPayload, TSearchResponse } from "@plane/types";
// components // components
import { EditorMentionsRoot } from "@/components/editor"; import { EditorMentionsRoot } from "@/components/editor";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { getEditorFileHandlers } from "@/helpers/editor.helper";
// hooks // hooks
import { useEditorMention } from "@/hooks/use-editor-mention"; import { useEditorConfig, useEditorMention } from "@/hooks/editor";
// plane web hooks // plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
import { useFileSize } from "@/plane-web/hooks/use-file-size";
interface RichTextEditorWrapperProps interface RichTextEditorWrapperProps
extends Omit<IRichTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> { extends Omit<IRichTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
@ -20,7 +18,7 @@ interface RichTextEditorWrapperProps
workspaceSlug: string; workspaceSlug: string;
workspaceId: string; workspaceId: string;
projectId?: string; projectId?: string;
uploadFile: (file: File) => Promise<string>; uploadFile: TFileHandler["upload"];
} }
export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => { export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => {
@ -32,15 +30,14 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
const { fetchMentions } = useEditorMention({ const { fetchMentions } = useEditorMention({
searchEntity: async (payload) => await searchMentionCallback(payload), searchEntity: async (payload) => await searchMentionCallback(payload),
}); });
// file size // editor config
const { maxFileSize } = useFileSize(); const { getEditorFileHandlers } = useEditorConfig();
return ( return (
<RichTextEditorWithRef <RichTextEditorWithRef
ref={ref} ref={ref}
disabledExtensions={disabledExtensions} disabledExtensions={disabledExtensions}
fileHandler={getEditorFileHandlers({ fileHandler={getEditorFileHandlers({
maxFileSize,
projectId, projectId,
uploadFile, uploadFile,
workspaceId, workspaceId,

View file

@ -5,7 +5,8 @@ import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor, RichTextReadOnlyEditorWi
import { EditorMentionsRoot } from "@/components/editor"; import { EditorMentionsRoot } from "@/components/editor";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper"; // hooks
import { useEditorConfig } from "@/hooks/editor";
// plane web hooks // plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
@ -13,14 +14,17 @@ type RichTextReadOnlyEditorWrapperProps = Omit<
IRichTextReadOnlyEditor, IRichTextReadOnlyEditor,
"disabledExtensions" | "fileHandler" | "mentionHandler" "disabledExtensions" | "fileHandler" | "mentionHandler"
> & { > & {
workspaceId: string;
workspaceSlug: string; workspaceSlug: string;
projectId?: string; projectId?: string;
}; };
export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>( export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>(
({ workspaceSlug, projectId, ...props }, ref) => { ({ workspaceId, workspaceSlug, projectId, ...props }, ref) => {
// editor flaggings // editor flaggings
const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString()); const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
// editor config
const { getReadOnlyEditorFileHandlers } = useEditorConfig();
return ( return (
<RichTextReadOnlyEditorWithRef <RichTextReadOnlyEditorWithRef
@ -28,6 +32,7 @@ export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Ric
disabledExtensions={disabledExtensions} disabledExtensions={disabledExtensions}
fileHandler={getReadOnlyEditorFileHandlers({ fileHandler={getReadOnlyEditorFileHandlers({
projectId, projectId,
workspaceId,
workspaceSlug, workspaceSlug,
})} })}
mentionHandler={{ mentionHandler={{

View file

@ -2,15 +2,15 @@ import React, { useState } from "react";
// plane constants // plane constants
import { EIssueCommentAccessSpecifier } from "@plane/constants"; import { EIssueCommentAccessSpecifier } from "@plane/constants";
// plane editor // plane editor
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor"; import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor";
// plane types // components
import { TSticky } from "@plane/types"; import { TSticky } from "@plane/types";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { getEditorFileHandlers } from "@/helpers/editor.helper"; // hooks
import { useEditorConfig } from "@/hooks/editor";
// plane web hooks // plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
import { useFileSize } from "@/plane-web/hooks/use-file-size";
import { StickyEditorToolbar } from "./toolbar"; import { StickyEditorToolbar } from "./toolbar";
interface StickyEditorWrapperProps interface StickyEditorWrapperProps
@ -25,7 +25,7 @@ interface StickyEditorWrapperProps
isSubmitting?: boolean; isSubmitting?: boolean;
showToolbarInitially?: boolean; showToolbarInitially?: boolean;
showToolbar?: boolean; showToolbar?: boolean;
uploadFile: (file: File) => Promise<string>; uploadFile: TFileHandler["upload"];
parentClassName?: string; parentClassName?: string;
handleColorChange: (data: Partial<TSticky>) => Promise<void>; handleColorChange: (data: Partial<TSticky>) => Promise<void>;
handleDelete: () => void; handleDelete: () => void;
@ -49,8 +49,8 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
const [isFocused, setIsFocused] = useState(showToolbarInitially); const [isFocused, setIsFocused] = useState(showToolbarInitially);
// editor flaggings // editor flaggings
const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString()); const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
// file size // editor config
const { maxFileSize } = useFileSize(); const { getEditorFileHandlers } = useEditorConfig();
function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> { function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> {
return !!ref && typeof ref === "object" && "current" in ref; return !!ref && typeof ref === "object" && "current" in ref;
} }
@ -67,7 +67,6 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
ref={ref} ref={ref}
disabledExtensions={[...disabledExtensions, "enter-key"]} disabledExtensions={[...disabledExtensions, "enter-key"]}
fileHandler={getEditorFileHandlers({ fileHandler={getEditorFileHandlers({
maxFileSize,
projectId, projectId,
uploadFile, uploadFile,
workspaceId, workspaceId,

View file

@ -19,13 +19,10 @@ import { RichTextEditor } from "@/components/editor/rich-text-editor/rich-text-e
import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper"; import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper"; import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks // hooks
import { useProjectInbox } from "@/hooks/store"; import { useEditorAsset, useProjectInbox } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web services // plane web services
import { WorkspaceService } from "@/plane-web/services"; import { WorkspaceService } from "@/plane-web/services";
// services
import { FileService } from "@/services/file.service";
const fileService = new FileService();
const workspaceService = new WorkspaceService(); const workspaceService = new WorkspaceService();
type TInboxIssueDescription = { type TInboxIssueDescription = {
@ -53,11 +50,10 @@ export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props
onEnterKeyPress, onEnterKeyPress,
onAssetUpload, onAssetUpload,
} = props; } = props;
// i18n // i18n
const { t } = useTranslation(); const { t } = useTranslation();
// store hooks
// hooks const { uploadEditorAsset } = useEditorAsset();
const { loader } = useProjectInbox(); const { loader } = useProjectInbox();
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
@ -90,17 +86,18 @@ export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props
containerClassName={containerClassName} containerClassName={containerClassName}
onEnterKeyPress={onEnterKeyPress} onEnterKeyPress={onEnterKeyPress}
tabIndex={getIndex("description_html")} tabIndex={getIndex("description_html")}
uploadFile={async (file) => { uploadFile={async (blockId, file) => {
try { try {
const { asset_id } = await fileService.uploadProjectAsset( const { asset_id } = await uploadEditorAsset({
workspaceSlug, blockId,
projectId, data: {
{
entity_identifier: data.id ?? "", entity_identifier: data.id ?? "",
entity_type: EFileAssetType.ISSUE_DESCRIPTION, entity_type: EFileAssetType.ISSUE_DESCRIPTION,
}, },
file file,
); projectId,
workspaceSlug,
});
onAssetUpload?.(asset_id); onAssetUpload?.(asset_id);
return asset_id; return asset_id;
} catch (error) { } catch (error) {

View file

@ -17,13 +17,10 @@ import { TIssueOperations } from "@/components/issues/issue-detail";
// helpers // helpers
import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper"; import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper";
// hooks // hooks
import { useWorkspace } from "@/hooks/store"; import { useEditorAsset, useWorkspace } from "@/hooks/store";
// plane web services // plane web services
import { WorkspaceService } from "@/plane-web/services"; import { WorkspaceService } from "@/plane-web/services";
// services
import { FileService } from "@/services/file.service";
const workspaceService = new WorkspaceService(); const workspaceService = new WorkspaceService();
const fileService = new FileService();
export type IssueDescriptionInputProps = { export type IssueDescriptionInputProps = {
containerClassName?: string; containerClassName?: string;
@ -51,6 +48,14 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
setIsSubmitting, setIsSubmitting,
placeholder, placeholder,
} = props; } = props;
// states
const [localIssueDescription, setLocalIssueDescription] = useState({
id: issueId,
description_html: initialValue,
});
// store hooks
const { uploadEditorAsset } = useEditorAsset();
// form info
// i18n // i18n
const { t } = useTranslation(); const { t } = useTranslation();
@ -61,11 +66,6 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
}, },
}); });
const [localIssueDescription, setLocalIssueDescription] = useState({
id: issueId,
description_html: initialValue,
});
const handleDescriptionFormSubmit = useCallback( const handleDescriptionFormSubmit = useCallback(
async (formData: Partial<TIssue>) => { async (formData: Partial<TIssue>) => {
await issueOperations.update(workspaceSlug, projectId, issueId, { await issueOperations.update(workspaceSlug, projectId, issueId, {
@ -136,17 +136,18 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
}) })
} }
containerClassName={containerClassName} containerClassName={containerClassName}
uploadFile={async (file) => { uploadFile={async (blockId, file) => {
try { try {
const { asset_id } = await fileService.uploadProjectAsset( const { asset_id } = await uploadEditorAsset({
workspaceSlug, blockId,
projectId, data: {
{
entity_identifier: issueId, entity_identifier: issueId,
entity_type: EFileAssetType.ISSUE_DESCRIPTION, entity_type: EFileAssetType.ISSUE_DESCRIPTION,
}, },
file file,
); projectId,
workspaceSlug,
});
return asset_id; return asset_id;
} catch (error) { } catch (error) {
console.log("Error in uploading work item asset:", error); console.log("Error in uploading work item asset:", error);
@ -159,6 +160,7 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
id={issueId} id={issueId}
initialValue={localIssueDescription.description_html ?? ""} initialValue={localIssueDescription.description_html ?? ""}
containerClassName={containerClassName} containerClassName={containerClassName}
workspaceId={workspaceId}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
/> />

View file

@ -170,8 +170,8 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
} }
}} }}
showSubmitButton={false} showSubmitButton={false}
uploadFile={async (file) => { uploadFile={async (blockId, file) => {
const { asset_id } = await activityOperations.uploadCommentAsset(file, comment.id); const { asset_id } = await activityOperations.uploadCommentAsset(blockId, file, comment.id);
return asset_id; return asset_id;
}} }}
/> />
@ -215,6 +215,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
ref={showEditorRef} ref={showEditorRef}
id={comment.id} id={comment.id}
initialValue={comment.comment_html ?? ""} initialValue={comment.comment_html ?? ""}
workspaceId={workspaceId}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
/> />

View file

@ -110,8 +110,8 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
handleAccessChange={onAccessChange} handleAccessChange={onAccessChange}
showAccessSpecifier={showAccessSpecifier} showAccessSpecifier={showAccessSpecifier}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
uploadFile={async (file) => { uploadFile={async (blockId, file) => {
const { asset_id } = await activityOperations.uploadCommentAsset(file); const { asset_id } = await activityOperations.uploadCommentAsset(blockId, file);
setUploadedAssetIds((prev) => [...prev, asset_id]); setUploadedAssetIds((prev) => [...prev, asset_id]);
return asset_id; return asset_id;
}} }}

View file

@ -16,12 +16,9 @@ import { IssueCommentCreate } from "@/components/issues";
import { ActivitySortRoot, IssueActivityCommentRoot } from "@/components/issues/issue-detail"; import { ActivitySortRoot, IssueActivityCommentRoot } from "@/components/issues/issue-detail";
// constants // constants
// hooks // hooks
import { useIssueDetail, useProject, useUser, useUserPermissions } from "@/hooks/store"; import { useEditorAsset, useIssueDetail, useProject, useUser, useUserPermissions } from "@/hooks/store";
// plane web components // plane web components
import { ActivityFilterRoot, IssueActivityWorklogCreateButton } from "@/plane-web/components/issues/worklog"; import { ActivityFilterRoot, IssueActivityWorklogCreateButton } from "@/plane-web/components/issues/worklog";
// services
import { FileService } from "@/services/file.service";
const fileService = new FileService();
type TIssueActivity = { type TIssueActivity = {
workspaceSlug: string; workspaceSlug: string;
@ -35,7 +32,7 @@ export type TActivityOperations = {
createComment: (data: Partial<TIssueComment>) => Promise<TIssueComment>; createComment: (data: Partial<TIssueComment>) => Promise<TIssueComment>;
updateComment: (commentId: string, data: Partial<TIssueComment>) => Promise<void>; updateComment: (commentId: string, data: Partial<TIssueComment>) => Promise<void>;
removeComment: (commentId: string) => Promise<void>; removeComment: (commentId: string) => Promise<void>;
uploadCommentAsset: (file: File, commentId?: string) => Promise<TFileSignedURLResponse>; uploadCommentAsset: (blockId: string, file: File, commentId?: string) => Promise<TFileSignedURLResponse>;
}; };
export const IssueActivity: FC<TIssueActivity> = observer((props) => { export const IssueActivity: FC<TIssueActivity> = observer((props) => {
@ -48,6 +45,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
defaultActivityFilters defaultActivityFilters
); );
const { setValue: setSortOrder, storedValue: sortOrder } = useLocalStorage("activity_sort_order", E_SORT_ORDER.ASC); const { setValue: setSortOrder, storedValue: sortOrder } = useLocalStorage("activity_sort_order", E_SORT_ORDER.ASC);
// store hooks
const { const {
issue: { getIssueById }, issue: { getIssueById },
createComment, createComment,
@ -57,6 +55,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
const { projectPermissionsByWorkspaceSlugAndProjectId } = useUserPermissions(); const { projectPermissionsByWorkspaceSlugAndProjectId } = useUserPermissions();
const { getProjectById } = useProject(); const { getProjectById } = useProject();
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
const { uploadEditorAsset } = useEditorAsset();
// derived values // derived values
const issue = issueId ? getIssueById(issueId) : undefined; const issue = issueId ? getIssueById(issueId) : undefined;
const currentUserProjectRole = projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId); const currentUserProjectRole = projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
@ -94,7 +93,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
message: t("issue.comments.create.success"), message: t("issue.comments.create.success"),
}); });
return comment; return comment;
} catch (error) { } catch {
setToast({ setToast({
title: t("common.error.label"), title: t("common.error.label"),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
@ -111,7 +110,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS,
message: t("issue.comments.update.success"), message: t("issue.comments.update.success"),
}); });
} catch (error) { } catch {
setToast({ setToast({
title: t("common.error.label"), title: t("common.error.label"),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
@ -128,7 +127,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS,
message: t("issue.comments.remove.success"), message: t("issue.comments.remove.success"),
}); });
} catch (error) { } catch {
setToast({ setToast({
title: t("common.error.label"), title: t("common.error.label"),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
@ -136,18 +135,19 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
}); });
} }
}, },
uploadCommentAsset: async (file, commentId) => { uploadCommentAsset: async (blockId, file, commentId) => {
try { try {
if (!workspaceSlug || !projectId) throw new Error("Missing fields"); if (!workspaceSlug || !projectId) throw new Error("Missing fields");
const res = await fileService.uploadProjectAsset( const res = await uploadEditorAsset({
workspaceSlug, blockId,
projectId, data: {
{
entity_identifier: commentId ?? "", entity_identifier: commentId ?? "",
entity_type: EFileAssetType.COMMENT_DESCRIPTION, entity_type: EFileAssetType.COMMENT_DESCRIPTION,
}, },
file file,
); projectId,
workspaceSlug,
});
return res; return res;
} catch (error) { } catch (error) {
console.log("Error in uploading comment asset:", error); console.log("Error in uploading comment asset:", error);
@ -155,7 +155,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
} }
}, },
}), }),
[workspaceSlug, projectId, issueId, createComment, updateComment, removeComment] [workspaceSlug, projectId, issueId, createComment, updateComment, uploadEditorAsset, removeComment]
); );
const project = getProjectById(projectId); const project = getProjectById(projectId);

View file

@ -22,14 +22,15 @@ import { RichTextEditor } from "@/components/editor";
import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper"; import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper"; import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks // hooks
import { useInstance, useWorkspace } from "@/hooks/store"; import { useEditorAsset, useInstance, useWorkspace } from "@/hooks/store";
import useKeypress from "@/hooks/use-keypress"; import useKeypress from "@/hooks/use-keypress";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web services // plane web services
import { WorkspaceService } from "@/plane-web/services"; import { WorkspaceService } from "@/plane-web/services";
// services // services
import { AIService } from "@/services/ai.service"; import { AIService } from "@/services/ai.service";
import { FileService } from "@/services/file.service"; const workspaceService = new WorkspaceService();
const aiService = new AIService();
type TIssueDescriptionEditorProps = { type TIssueDescriptionEditorProps = {
control: Control<TIssue>; control: Control<TIssue>;
@ -50,11 +51,6 @@ type TIssueDescriptionEditorProps = {
onClose: () => void; onClose: () => void;
}; };
// services
const workspaceService = new WorkspaceService();
const aiService = new AIService();
const fileService = new FileService();
export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = observer((props) => { export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = observer((props) => {
const { const {
control, control,
@ -80,8 +76,10 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = ob
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
// store hooks // store hooks
const { getWorkspaceBySlug } = useWorkspace(); const { getWorkspaceBySlug } = useWorkspace();
const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString())?.id as string; const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString())?.id ?? "";
const { config } = useInstance(); const { config } = useInstance();
const { uploadEditorAsset } = useEditorAsset();
// platform
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile); const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
@ -202,19 +200,20 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = ob
}) })
} }
containerClassName="pt-3 min-h-[120px]" containerClassName="pt-3 min-h-[120px]"
uploadFile={async (file) => { uploadFile={async (blockId, file) => {
try { try {
const { asset_id } = await fileService.uploadProjectAsset( const { asset_id } = await uploadEditorAsset({
workspaceSlug, blockId,
projectId, data: {
{
entity_identifier: issueId ?? "", entity_identifier: issueId ?? "",
entity_type: isDraft entity_type: isDraft
? EFileAssetType.DRAFT_ISSUE_DESCRIPTION ? EFileAssetType.DRAFT_ISSUE_DESCRIPTION
: EFileAssetType.ISSUE_DESCRIPTION, : EFileAssetType.ISSUE_DESCRIPTION,
}, },
file file,
); projectId,
workspaceSlug,
});
onAssetUpload(asset_id); onAssetUpload(asset_id);
return asset_id; return asset_id;
} catch (error) { } catch (error) {
@ -268,6 +267,7 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = ob
AI AI
</button> </button>
} }
workspaceId={workspaceId}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
/> />

View file

@ -21,8 +21,8 @@ import { PageContentBrowser, PageContentLoader, PageEditorTitle } from "@/compon
import { cn, LIVE_BASE_PATH, LIVE_BASE_URL } from "@/helpers/common.helper"; import { cn, LIVE_BASE_PATH, LIVE_BASE_URL } from "@/helpers/common.helper";
import { generateRandomColor } from "@/helpers/string.helper"; import { generateRandomColor } from "@/helpers/string.helper";
// hooks // hooks
import { useUser } from "@/hooks/store"; import { useEditorMention } from "@/hooks/editor";
import { useEditorMention } from "@/hooks/use-editor-mention"; import { useUser, useWorkspace } from "@/hooks/store";
import { usePageFilters } from "@/hooks/use-page-filters"; import { usePageFilters } from "@/hooks/use-page-filters";
// plane web components // plane web components
import { EditorAIMenu } from "@/plane-web/components/pages"; import { EditorAIMenu } from "@/plane-web/components/pages";
@ -34,7 +34,6 @@ import { TPageInstance } from "@/store/pages/base-page";
export type TEditorBodyConfig = { export type TEditorBodyConfig = {
fileHandler: TFileHandler; fileHandler: TFileHandler;
webhookConnectionParams: TWebhookConnectionQueryParams;
}; };
export type TEditorBodyHandlers = { export type TEditorBodyHandlers = {
@ -50,6 +49,7 @@ type Props = {
handlers: TEditorBodyHandlers; handlers: TEditorBodyHandlers;
page: TPageInstance; page: TPageInstance;
sidePeekVisible: boolean; sidePeekVisible: boolean;
webhookConnectionParams: TWebhookConnectionQueryParams;
workspaceSlug: string; workspaceSlug: string;
}; };
@ -62,12 +62,15 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
handlers, handlers,
page, page,
sidePeekVisible, sidePeekVisible,
webhookConnectionParams,
workspaceSlug, workspaceSlug,
} = props; } = props;
// store hooks // store hooks
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
const { getWorkspaceBySlug } = useWorkspace();
// derived values // derived values
const { id: pageId, name: pageTitle, isContentEditable, updateTitle } = page; const { id: pageId, name: pageTitle, isContentEditable, updateTitle } = page;
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? "";
// issue-embed // issue-embed
const { issueEmbedProps } = useIssueEmbed({ const { issueEmbedProps } = useIssueEmbed({
fetchEmbedSuggestions: handlers.fetchEntity, fetchEmbedSuggestions: handlers.fetchEntity,
@ -96,10 +99,11 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
editorRef={editorRef} editorRef={editorRef}
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
workspaceId={workspaceId}
workspaceSlug={workspaceSlug?.toString() ?? ""} workspaceSlug={workspaceSlug?.toString() ?? ""}
/> />
), ),
[editorRef, workspaceSlug] [editorRef, workspaceId, workspaceSlug]
); );
const handleServerConnect = useCallback(() => { const handleServerConnect = useCallback(() => {
@ -129,13 +133,13 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
// Construct realtime config // Construct realtime config
return { return {
url: WS_LIVE_URL.toString(), url: WS_LIVE_URL.toString(),
queryParams: config.webhookConnectionParams, queryParams: webhookConnectionParams,
}; };
} catch (error) { } catch (error) {
console.error("Error creating realtime config", error); console.error("Error creating realtime config", error);
return undefined; return undefined;
} }
}, [config.webhookConnectionParams]); }, [webhookConnectionParams]);
const userConfig = useMemo( const userConfig = useMemo(
() => ({ () => ({

View file

@ -4,7 +4,7 @@ import { useSearchParams } from "next/navigation";
// editor // editor
import { EditorRefApi } from "@plane/editor"; import { EditorRefApi } from "@plane/editor";
// types // types
import { TDocumentPayload, TPage, TPageVersion } from "@plane/types"; import { TDocumentPayload, TPage, TPageVersion, TWebhookConnectionQueryParams } from "@plane/types";
// components // components
import { import {
PageEditorHeaderRoot, PageEditorHeaderRoot,
@ -36,11 +36,12 @@ type TPageRootProps = {
config: TPageRootConfig; config: TPageRootConfig;
handlers: TPageRootHandlers; handlers: TPageRootHandlers;
page: TPageInstance; page: TPageInstance;
webhookConnectionParams: TWebhookConnectionQueryParams;
workspaceSlug: string; workspaceSlug: string;
}; };
export const PageRoot = observer((props: TPageRootProps) => { export const PageRoot = observer((props: TPageRootProps) => {
const { config, handlers, page, workspaceSlug } = props; const { config, handlers, page, webhookConnectionParams, workspaceSlug } = props;
// states // states
const [editorReady, setEditorReady] = useState(false); const [editorReady, setEditorReady] = useState(false);
const [hasConnectionFailed, setHasConnectionFailed] = useState(false); const [hasConnectionFailed, setHasConnectionFailed] = useState(false);
@ -116,6 +117,7 @@ export const PageRoot = observer((props: TPageRootProps) => {
handlers={handlers} handlers={handlers}
page={page} page={page}
sidePeekVisible={sidePeekVisible} sidePeekVisible={sidePeekVisible}
webhookConnectionParams={webhookConnectionParams}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
/> />
</> </>

View file

@ -8,9 +8,9 @@ import { TPageVersion } from "@plane/types";
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// components // components
import { EditorMentionsRoot } from "@/components/editor"; import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
// hooks // hooks
import { useEditorConfig } from "@/hooks/editor";
import { useWorkspace } from "@/hooks/store";
import { usePageFilters } from "@/hooks/use-page-filters"; import { usePageFilters } from "@/hooks/use-page-filters";
// plane web hooks // plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
@ -27,8 +27,14 @@ export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props
const { activeVersion, currentVersionDescription, isCurrentVersionActive, versionDetails } = props; const { activeVersion, currentVersionDescription, isCurrentVersionActive, versionDetails } = props;
// params // params
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
// store hooks
const { getWorkspaceBySlug } = useWorkspace();
// derived values
const workspaceDetails = getWorkspaceBySlug(workspaceSlug?.toString() ?? "");
// editor flaggings // editor flaggings
const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString() ?? ""); const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString() ?? "");
// editor config
const { getReadOnlyEditorFileHandlers } = useEditorConfig();
// issue-embed // issue-embed
const { issueEmbedProps } = useIssueEmbed({ const { issueEmbedProps } = useIssueEmbed({
projectId: projectId?.toString() ?? "", projectId: projectId?.toString() ?? "",
@ -97,6 +103,7 @@ export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props
editorClassName="pl-10" editorClassName="pl-10"
fileHandler={getReadOnlyEditorFileHandlers({ fileHandler={getReadOnlyEditorFileHandlers({
projectId: projectId?.toString() ?? "", projectId: projectId?.toString() ?? "",
workspaceId: workspaceDetails?.id ?? "",
workspaceSlug: workspaceSlug?.toString() ?? "", workspaceSlug: workspaceSlug?.toString() ?? "",
})} })}
mentionHandler={{ mentionHandler={{

View file

@ -15,7 +15,7 @@ import { ActivitySettingsLoader } from "@/components/ui";
import { calculateTimeAgo } from "@/helpers/date-time.helper"; import { calculateTimeAgo } from "@/helpers/date-time.helper";
import { getFileURL } from "@/helpers/file.helper"; import { getFileURL } from "@/helpers/file.helper";
// hooks // hooks
import { useUser } from "@/hooks/store"; import { useUser, useWorkspace } from "@/hooks/store";
type Props = { type Props = {
activity: IUserActivityResponse | undefined; activity: IUserActivityResponse | undefined;
@ -27,6 +27,9 @@ export const ActivityList: React.FC<Props> = observer((props) => {
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
// store hooks // store hooks
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
const { getWorkspaceBySlug } = useWorkspace();
// derived values
const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString() ?? "")?.id ?? "";
// TODO: refactor this component // TODO: refactor this component
return ( return (
@ -79,6 +82,7 @@ export const ActivityList: React.FC<Props> = observer((props) => {
: (activityItem.old_value?.toString() as string) : (activityItem.old_value?.toString() as string)
} }
containerClassName="text-xs bg-custom-background-100" containerClassName="text-xs bg-custom-background-100"
workspaceId={workspaceId}
workspaceSlug={workspaceSlug?.toString() ?? ""} workspaceSlug={workspaceSlug?.toString() ?? ""}
projectId={activityItem.project} projectId={activityItem.project}
/> />

View file

@ -103,7 +103,8 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value
} }
containerClassName="text-xs bg-custom-background-100" 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 ?? ""} projectId={activityItem.project ?? ""}
/> />
</div> </div>

View file

@ -0,0 +1,2 @@
export * from "./use-editor-config";
export * from "./use-editor-mention";

View file

@ -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<TArgs, "projectId" | "workspaceId" | "workspaceSlug">): 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,
};
};

View file

@ -7,6 +7,7 @@ export * from "./use-command-palette";
export * from "./use-cycle"; export * from "./use-cycle";
export * from "./use-cycle-filter"; export * from "./use-cycle-filter";
export * from "./use-dashboard"; export * from "./use-dashboard";
export * from "./use-editor-asset";
export * from "./use-event-tracker"; export * from "./use-event-tracker";
export * from "./use-global-view"; export * from "./use-global-view";
export * from "./use-inbox-issues"; export * from "./use-inbox-issues";

View file

@ -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;
};

View file

@ -1,3 +1,4 @@
import { AxiosRequestConfig } from "axios";
// plane types // plane types
import { TFileEntityInfo, TFileSignedURLResponse } from "@plane/types"; import { TFileEntityInfo, TFileSignedURLResponse } from "@plane/types";
// helpers // helpers
@ -64,7 +65,8 @@ export class FileService extends APIService {
async uploadWorkspaceAsset( async uploadWorkspaceAsset(
workspaceSlug: string, workspaceSlug: string,
data: TFileEntityInfo, data: TFileEntityInfo,
file: File file: File,
uploadProgressHandler?: AxiosRequestConfig["onUploadProgress"]
): Promise<TFileSignedURLResponse> { ): Promise<TFileSignedURLResponse> {
const fileMetaData = getFileMetaDataForUpload(file); const fileMetaData = getFileMetaDataForUpload(file);
return this.post(`/api/assets/v2/workspaces/${workspaceSlug}/`, { return this.post(`/api/assets/v2/workspaces/${workspaceSlug}/`, {
@ -74,7 +76,11 @@ export class FileService extends APIService {
.then(async (response) => { .then(async (response) => {
const signedURLResponse: TFileSignedURLResponse = response?.data; const signedURLResponse: TFileSignedURLResponse = response?.data;
const fileUploadPayload = generateFileUploadPayload(signedURLResponse, file); 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); await this.updateWorkspaceAssetUploadStatus(workspaceSlug.toString(), signedURLResponse.asset_id);
return signedURLResponse; return signedURLResponse;
}) })
@ -122,7 +128,8 @@ export class FileService extends APIService {
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
data: TFileEntityInfo, data: TFileEntityInfo,
file: File file: File,
uploadProgressHandler?: AxiosRequestConfig["onUploadProgress"]
): Promise<TFileSignedURLResponse> { ): Promise<TFileSignedURLResponse> {
const fileMetaData = getFileMetaDataForUpload(file); const fileMetaData = getFileMetaDataForUpload(file);
return this.post(`/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/`, { return this.post(`/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/`, {
@ -132,7 +139,11 @@ export class FileService extends APIService {
.then(async (response) => { .then(async (response) => {
const signedURLResponse: TFileSignedURLResponse = response?.data; const signedURLResponse: TFileSignedURLResponse = response?.data;
const fileUploadPayload = generateFileUploadPayload(signedURLResponse, file); 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); await this.updateProjectAssetUploadStatus(workspaceSlug, projectId, signedURLResponse.asset_id);
return signedURLResponse; return signedURLResponse;
}) })

View file

@ -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<string, number>;
// 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<TFileSignedURLResponse>;
}
export class EditorAssetStore implements IEditorAssetStore {
// observables
assetsUploadStatus: Record<string, TAttachmentUploadStatus> = {};
// 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<string, number> = {};
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];
});
}
};
}

View file

@ -126,7 +126,7 @@ export class IssueAttachmentStore implements IIssueAttachmentStore {
return response; return response;
}; };
debouncedUpdateProgress = debounce((issueId: string, tempId: string, progress: number) => { private debouncedUpdateProgress = debounce((issueId: string, tempId: string, progress: number) => {
runInAction(() => { runInAction(() => {
set(this.attachmentsUploadStatusMap, [issueId, tempId, "progress"], progress); set(this.attachmentsUploadStatusMap, [issueId, tempId, "progress"], progress);
}); });

View file

@ -1,5 +1,4 @@
import { enableStaticRendering } from "mobx-react"; import { enableStaticRendering } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
// plane web store // plane web store
import { CommandPaletteStore, ICommandPaletteStore } from "@/plane-web/store/command-palette.store"; import { CommandPaletteStore, ICommandPaletteStore } from "@/plane-web/store/command-palette.store";
import { RootStore } from "@/plane-web/store/root.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 { CycleStore, ICycleStore } from "./cycle.store";
import { CycleFilterStore, ICycleFilterStore } from "./cycle_filter.store"; import { CycleFilterStore, ICycleFilterStore } from "./cycle_filter.store";
import { DashboardStore, IDashboardStore } from "./dashboard.store"; import { DashboardStore, IDashboardStore } from "./dashboard.store";
import { EditorAssetStore, IEditorAssetStore } from "./editor/asset.store";
import { IProjectEstimateStore, ProjectEstimateStore } from "./estimates/project-estimate.store"; import { IProjectEstimateStore, ProjectEstimateStore } from "./estimates/project-estimate.store";
import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store"; import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store";
import { FavoriteStore, IFavoriteStore } from "./favorite.store"; import { FavoriteStore, IFavoriteStore } from "./favorite.store";
@ -61,6 +61,7 @@ export class CoreRootStore {
favorite: IFavoriteStore; favorite: IFavoriteStore;
transient: ITransientStore; transient: ITransientStore;
stickyStore: IStickyStore; stickyStore: IStickyStore;
editorAssetStore: IEditorAssetStore;
constructor() { constructor() {
this.router = new RouterStore(); this.router = new RouterStore();
@ -90,6 +91,7 @@ export class CoreRootStore {
this.favorite = new FavoriteStore(this); this.favorite = new FavoriteStore(this);
this.transient = new TransientStore(); this.transient = new TransientStore();
this.stickyStore = new StickyStore(); this.stickyStore = new StickyStore();
this.editorAssetStore = new EditorAssetStore();
} }
resetOnSignOut() { resetOnSignOut() {
@ -122,5 +124,6 @@ export class CoreRootStore {
this.favorite = new FavoriteStore(this); this.favorite = new FavoriteStore(this);
this.transient = new TransientStore(); this.transient = new TransientStore();
this.stickyStore = new StickyStore(); this.stickyStore = new StickyStore();
this.editorAssetStore = new EditorAssetStore();
} }
} }

View file

@ -1,10 +1,5 @@
// plane editor
import { TFileHandler } from "@plane/editor";
// helpers // helpers
import { getFileURL } from "@/helpers/file.helper"; import { getFileURL } from "@/helpers/file.helper";
// services
import { FileService } from "@/services/file.service";
const fileService = new FileService();
type TEditorSrcArgs = { type TEditorSrcArgs = {
assetId: string; assetId: string;
@ -27,90 +22,6 @@ export const getEditorAssetSrc = (args: TEditorSrcArgs): string | undefined => {
return url; return url;
}; };
type TArgs = {
maxFileSize: number;
projectId?: string;
uploadFile: (file: File) => Promise<string>;
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<TArgs, "projectId" | "workspaceSlug">
): { 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 => { export const getTextContent = (jsx: JSX.Element | React.ReactNode | null | undefined): string => {
if (!jsx) return ""; if (!jsx) return "";