[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:
parent
b7198234de
commit
214692f5b2
53 changed files with 602 additions and 315 deletions
|
|
@ -11,7 +11,13 @@ import { getEditorClassNames } from "@/helpers/common";
|
|||
// hooks
|
||||
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
|
||||
// types
|
||||
import { EditorReadOnlyRefApi, TDisplayConfig, TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types";
|
||||
import {
|
||||
EditorReadOnlyRefApi,
|
||||
TDisplayConfig,
|
||||
TExtensions,
|
||||
TReadOnlyFileHandler,
|
||||
TReadOnlyMentionHandler,
|
||||
} from "@/types";
|
||||
|
||||
interface IDocumentReadOnlyEditor {
|
||||
disabledExtensions: TExtensions[];
|
||||
|
|
@ -21,7 +27,7 @@ interface IDocumentReadOnlyEditor {
|
|||
displayConfig?: TDisplayConfig;
|
||||
editorClassName?: string;
|
||||
embedHandler: any;
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
fileHandler: TReadOnlyFileHandler;
|
||||
tabIndex?: number;
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
mentionHandler: TReadOnlyMentionHandler;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from
|
|||
import { cn } from "@plane/utils";
|
||||
// extensions
|
||||
import { CustoBaseImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image";
|
||||
import { ImageUploadStatus } from "./upload-status";
|
||||
|
||||
const MIN_SIZE = 100;
|
||||
|
||||
|
|
@ -210,6 +211,8 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
// show the image loader if the remote image's src or preview image from filesystem is not set yet (while loading the image post upload) (or)
|
||||
// if the initial resize (from 35% width and "auto" height attrs to the actual size in px) is not complete
|
||||
const showImageLoader = !(resolvedImageSrc || imageFromFileSystem) || !initialResizeComplete || hasErroredOnFirstLoad;
|
||||
// show the image upload status only when the resolvedImageSrc is not ready
|
||||
const showUploadStatus = !resolvedImageSrc;
|
||||
// show the image utils only if the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
|
||||
const showImageUtils = resolvedImageSrc && initialResizeComplete;
|
||||
// show the image resizer only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
|
||||
|
|
@ -279,6 +282,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
...(size.aspectRatio && { aspectRatio: size.aspectRatio }),
|
||||
}}
|
||||
/>
|
||||
{showUploadStatus && <ImageUploadStatus editor={editor} nodeId={node.attrs.id} />}
|
||||
{showImageUtils && (
|
||||
<ImageToolbarRoot
|
||||
containerClassName={
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
|||
);
|
||||
// hooks
|
||||
const { uploading: isImageBeingUploaded, uploadFile } = useUploader({
|
||||
blockId: imageEntityId ?? "",
|
||||
editor,
|
||||
loadImageFromFileSystem,
|
||||
maxFileSize,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -4,12 +4,12 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
// extensions
|
||||
import { CustomImageNode } from "@/extensions/custom-image";
|
||||
// helpers
|
||||
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
|
||||
// plugins
|
||||
import { TrackImageDeletionPlugin, TrackImageRestorationPlugin, isFileValid } from "@/plugins/image";
|
||||
// types
|
||||
import { TFileHandler } from "@/types";
|
||||
// helpers
|
||||
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
|
||||
|
||||
export type InsertImageComponentProps = {
|
||||
file?: File;
|
||||
|
|
@ -21,7 +21,8 @@ declare module "@tiptap/core" {
|
|||
interface Commands<ReturnType> {
|
||||
imageComponent: {
|
||||
insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType;
|
||||
uploadImage: (file: File) => () => Promise<string> | undefined;
|
||||
uploadImage: (blockId: string, file: File) => () => Promise<string> | undefined;
|
||||
updateAssetsUploadStatus: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void;
|
||||
getImageSource?: (path: string) => () => Promise<string>;
|
||||
restoreImage: (src: string) => () => Promise<void>;
|
||||
};
|
||||
|
|
@ -32,6 +33,7 @@ export const getImageComponentImageFileMap = (editor: Editor) =>
|
|||
(editor.storage.imageComponent as UploadImageExtensionStorage | undefined)?.fileMap;
|
||||
|
||||
export interface UploadImageExtensionStorage {
|
||||
assetsUploadStatus: TFileHandler["assetsUploadStatus"];
|
||||
fileMap: Map<string, UploadEntity>;
|
||||
}
|
||||
|
||||
|
|
@ -39,6 +41,7 @@ export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File })
|
|||
|
||||
export const CustomImageExtension = (props: TFileHandler) => {
|
||||
const {
|
||||
assetsUploadStatus,
|
||||
getAssetSrc,
|
||||
upload,
|
||||
delete: deleteImageFn,
|
||||
|
|
@ -105,7 +108,6 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
|||
this.editor.state.doc.descendants((node) => {
|
||||
if (node.type.name === this.name) {
|
||||
if (!node.attrs.src?.startsWith("http")) return;
|
||||
|
||||
imageSources.add(node.attrs.src);
|
||||
}
|
||||
});
|
||||
|
|
@ -128,13 +130,14 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
|||
markdown: {
|
||||
serialize() {},
|
||||
},
|
||||
assetsUploadStatus,
|
||||
};
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
insertImageComponent:
|
||||
(props: { file?: File; pos?: number; event: "insert" | "drop" }) =>
|
||||
(props) =>
|
||||
({ commands }) => {
|
||||
// Early return if there's an invalid file being dropped
|
||||
if (
|
||||
|
|
@ -182,12 +185,15 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
|||
attrs: attributes,
|
||||
});
|
||||
},
|
||||
uploadImage: (file: File) => async () => {
|
||||
const fileUrl = await upload(file);
|
||||
uploadImage: (blockId, file) => async () => {
|
||||
const fileUrl = await upload(blockId, file);
|
||||
return fileUrl;
|
||||
},
|
||||
getImageSource: (path: string) => async () => await getAssetSrc(path),
|
||||
restoreImage: (src: string) => async () => {
|
||||
updateAssetsUploadStatus: (updatedStatus) => () => {
|
||||
this.storage.assetsUploadStatus = updatedStatus;
|
||||
},
|
||||
getImageSource: (path) => async () => await getAssetSrc(path),
|
||||
restoreImage: (src) => async () => {
|
||||
await restoreImageFn(src);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
|
|||
// components
|
||||
import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custom-image";
|
||||
// types
|
||||
import { TFileHandler } from "@/types";
|
||||
import { TReadOnlyFileHandler } from "@/types";
|
||||
|
||||
export const CustomReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">) => {
|
||||
export const CustomReadOnlyImageExtension = (props: TReadOnlyFileHandler) => {
|
||||
const { getAssetSrc } = props;
|
||||
|
||||
return Image.extend<Record<string, unknown>, UploadImageExtensionStorage>({
|
||||
|
|
@ -56,6 +56,7 @@ export const CustomReadOnlyImageExtension = (props: Pick<TFileHandler, "getAsset
|
|||
markdown: {
|
||||
serialize() {},
|
||||
},
|
||||
assetsUploadStatus: {},
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
|
|||
// extensions
|
||||
import { CustomImageNode } from "@/extensions";
|
||||
// types
|
||||
import { TFileHandler } from "@/types";
|
||||
import { TReadOnlyFileHandler } from "@/types";
|
||||
|
||||
export const ReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">) => {
|
||||
export const ReadOnlyImageExtension = (props: TReadOnlyFileHandler) => {
|
||||
const { getAssetSrc } = props;
|
||||
|
||||
return Image.extend({
|
||||
|
|
|
|||
|
|
@ -27,14 +27,14 @@ import {
|
|||
} from "@/extensions";
|
||||
// helpers
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
// types
|
||||
import { TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types";
|
||||
// plane editor extensions
|
||||
import { CoreReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions";
|
||||
// types
|
||||
import { TExtensions, TReadOnlyFileHandler, TReadOnlyMentionHandler } from "@/types";
|
||||
|
||||
type Props = {
|
||||
disabledExtensions: TExtensions[];
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
fileHandler: TReadOnlyFileHandler;
|
||||
mentionHandler: TReadOnlyMentionHandler;
|
||||
};
|
||||
|
||||
|
|
@ -94,16 +94,12 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
|||
},
|
||||
}),
|
||||
CustomTypographyExtension,
|
||||
ReadOnlyImageExtension({
|
||||
getAssetSrc: fileHandler.getAssetSrc,
|
||||
}).configure({
|
||||
ReadOnlyImageExtension(fileHandler).configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md",
|
||||
},
|
||||
}),
|
||||
CustomReadOnlyImageExtension({
|
||||
getAssetSrc: fileHandler.getAssetSrc,
|
||||
}),
|
||||
CustomReadOnlyImageExtension(fileHandler),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
TaskList.configure({
|
||||
|
|
|
|||
|
|
@ -125,6 +125,13 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
}
|
||||
}, [editor, value, id]);
|
||||
|
||||
// update assets upload status
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
const assetsUploadStatus = fileHandler.assetsUploadStatus;
|
||||
editor.commands.updateAssetsUploadStatus(assetsUploadStatus);
|
||||
}, [editor, fileHandler.assetsUploadStatus]);
|
||||
|
||||
useImperativeHandle(
|
||||
forwardedRef,
|
||||
() => ({
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { insertImagesSafely } from "@/extensions/drop";
|
|||
import { isFileValid } from "@/plugins/image";
|
||||
|
||||
type TUploaderArgs = {
|
||||
blockId: string;
|
||||
editor: Editor;
|
||||
loadImageFromFileSystem: (file: string) => void;
|
||||
maxFileSize: number;
|
||||
|
|
@ -13,7 +14,7 @@ type TUploaderArgs = {
|
|||
};
|
||||
|
||||
export const useUploader = (args: TUploaderArgs) => {
|
||||
const { editor, loadImageFromFileSystem, maxFileSize, onUpload } = args;
|
||||
const { blockId, editor, loadImageFromFileSystem, maxFileSize, onUpload } = args;
|
||||
// states
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ export const useUploader = (args: TUploaderArgs) => {
|
|||
reader.readAsDataURL(fileWithTrimmedName);
|
||||
// @ts-expect-error - TODO: fix typings, and don't remove await from
|
||||
// here for now
|
||||
const url: string = await editor?.commands.uploadImage(fileWithTrimmedName);
|
||||
const url: string = await editor?.commands.uploadImage(blockId, fileWithTrimmedName);
|
||||
|
||||
if (!url) {
|
||||
throw new Error("Something went wrong while uploading the image");
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
|
|||
// props
|
||||
import { CoreReadOnlyEditorProps } from "@/props";
|
||||
// types
|
||||
import type { EditorReadOnlyRefApi, TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types";
|
||||
import type { EditorReadOnlyRefApi, TExtensions, TReadOnlyFileHandler, TReadOnlyMentionHandler } from "@/types";
|
||||
|
||||
interface CustomReadOnlyEditorProps {
|
||||
disabledExtensions: TExtensions[];
|
||||
|
|
@ -20,7 +20,7 @@ interface CustomReadOnlyEditorProps {
|
|||
extensions?: Extensions;
|
||||
forwardedRef?: MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||
initialValue?: string;
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
fileHandler: TReadOnlyFileHandler;
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
mentionHandler: TReadOnlyMentionHandler;
|
||||
provider?: HocuspocusProvider;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
TExtensions,
|
||||
TFileHandler,
|
||||
TMentionHandler,
|
||||
TReadOnlyFileHandler,
|
||||
TReadOnlyMentionHandler,
|
||||
TRealtimeConfig,
|
||||
TUserDetails,
|
||||
|
|
@ -43,7 +44,7 @@ export type TCollaborativeEditorProps = TCollaborativeEditorHookProps & {
|
|||
};
|
||||
|
||||
export type TReadOnlyCollaborativeEditorProps = TCollaborativeEditorHookProps & {
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
fileHandler: TReadOnlyFileHandler;
|
||||
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||
mentionHandler: TReadOnlyMentionHandler;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import { DeleteImage, RestoreImage, UploadImage } from "@/types";
|
||||
|
||||
export type TFileHandler = {
|
||||
export type TReadOnlyFileHandler = {
|
||||
getAssetSrc: (path: string) => Promise<string>;
|
||||
restore: RestoreImage;
|
||||
};
|
||||
|
||||
export type TFileHandler = TReadOnlyFileHandler & {
|
||||
assetsUploadStatus: Record<string, number>; // blockId => progress percentage
|
||||
cancel: () => void;
|
||||
delete: DeleteImage;
|
||||
upload: UploadImage;
|
||||
restore: RestoreImage;
|
||||
validation: {
|
||||
/**
|
||||
* @description max file size in bytes
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
TExtensions,
|
||||
TFileHandler,
|
||||
TMentionHandler,
|
||||
TReadOnlyFileHandler,
|
||||
TReadOnlyMentionHandler,
|
||||
TServerHandler,
|
||||
} from "@/types";
|
||||
|
|
@ -44,12 +45,16 @@ export type TEditorCommands =
|
|||
| "text-color"
|
||||
| "background-color"
|
||||
| "text-align"
|
||||
| "callout";
|
||||
| "callout"
|
||||
| "attachment";
|
||||
|
||||
export type TCommandExtraProps = {
|
||||
image: {
|
||||
savedSelection: Selection | null;
|
||||
};
|
||||
attachment: {
|
||||
savedSelection: Selection | null;
|
||||
};
|
||||
"text-color": {
|
||||
color: string | undefined;
|
||||
};
|
||||
|
|
@ -155,7 +160,7 @@ export interface IReadOnlyEditorProps {
|
|||
disabledExtensions: TExtensions[];
|
||||
displayConfig?: TDisplayConfig;
|
||||
editorClassName?: string;
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
fileHandler: TReadOnlyFileHandler;
|
||||
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||
id: string;
|
||||
initialValue: string;
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@ export type DeleteImage = (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>;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
// editor
|
||||
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor";
|
||||
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor";
|
||||
// components
|
||||
import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor";
|
||||
// helpers
|
||||
|
|
@ -14,7 +14,7 @@ interface LiteTextEditorWrapperProps
|
|||
workspaceId: string;
|
||||
isSubmitting?: boolean;
|
||||
showSubmitButton?: boolean;
|
||||
uploadFile: (file: File) => Promise<string>;
|
||||
uploadFile: TFileHandler["upload"];
|
||||
}
|
||||
|
||||
export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapperProps>((props, ref) => {
|
||||
|
|
|
|||
|
|
@ -12,15 +12,17 @@ type LiteTextReadOnlyEditorWrapperProps = Omit<
|
|||
"disabledExtensions" | "fileHandler" | "mentionHandler"
|
||||
> & {
|
||||
anchor: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>(
|
||||
({ anchor, ...props }, ref) => (
|
||||
({ anchor, workspaceId, ...props }, ref) => (
|
||||
<LiteTextReadOnlyEditorWithRef
|
||||
ref={ref}
|
||||
disabledExtensions={[]}
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
anchor,
|
||||
workspaceId,
|
||||
})}
|
||||
mentionHandler={{
|
||||
renderComponent: (props) => <EditorMentionsRoot {...props} />,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { forwardRef } from "react";
|
||||
// editor
|
||||
import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef } from "@plane/editor";
|
||||
import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef, TFileHandler } from "@plane/editor";
|
||||
// components
|
||||
import { EditorMentionsRoot } from "@/components/editor";
|
||||
// helpers
|
||||
|
|
@ -8,11 +8,13 @@ import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
|||
|
||||
interface RichTextEditorWrapperProps
|
||||
extends Omit<IRichTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
|
||||
uploadFile: (file: File) => Promise<string>;
|
||||
anchor: string;
|
||||
uploadFile: TFileHandler["upload"];
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => {
|
||||
const { containerClassName, uploadFile, ...rest } = props;
|
||||
const { anchor, containerClassName, uploadFile, workspaceId, ...rest } = props;
|
||||
|
||||
return (
|
||||
<RichTextEditorWithRef
|
||||
|
|
@ -22,9 +24,9 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
|||
ref={ref}
|
||||
disabledExtensions={[]}
|
||||
fileHandler={getEditorFileHandlers({
|
||||
anchor,
|
||||
uploadFile,
|
||||
workspaceId: "",
|
||||
anchor: "",
|
||||
workspaceId,
|
||||
})}
|
||||
{...rest}
|
||||
containerClassName={containerClassName}
|
||||
|
|
|
|||
|
|
@ -12,15 +12,17 @@ type RichTextReadOnlyEditorWrapperProps = Omit<
|
|||
"disabledExtensions" | "fileHandler" | "mentionHandler"
|
||||
> & {
|
||||
anchor: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>(
|
||||
({ anchor, ...props }, ref) => (
|
||||
({ anchor, workspaceId, ...props }, ref) => (
|
||||
<RichTextReadOnlyEditorWithRef
|
||||
ref={ref}
|
||||
disabledExtensions={[]}
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
anchor,
|
||||
workspaceId,
|
||||
})}
|
||||
mentionHandler={{
|
||||
renderComponent: (props) => <EditorMentionsRoot {...props} />,
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export const AddComment: React.FC<Props> = observer((props) => {
|
|||
onChange={(comment_json, comment_html) => onChange(comment_html)}
|
||||
isSubmitting={isSubmitting}
|
||||
placeholder="Add comment..."
|
||||
uploadFile={async (file) => {
|
||||
uploadFile={async (blockId, file) => {
|
||||
const { asset_id } = await uploadCommentAsset(file, anchor);
|
||||
setUploadAssetIds((prev) => [...prev, asset_id]);
|
||||
return asset_id;
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
|||
onChange={(comment_json, comment_html) => onChange(comment_html)}
|
||||
isSubmitting={isSubmitting}
|
||||
showSubmitButton={false}
|
||||
uploadFile={async (file) => {
|
||||
uploadFile={async (blockId, file) => {
|
||||
const { asset_id } = await uploadCommentAsset(file, anchor, comment.id);
|
||||
return asset_id;
|
||||
}}
|
||||
|
|
@ -140,6 +140,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
|||
<div className={`${isEditing ? "hidden" : ""}`}>
|
||||
<LiteTextReadOnlyEditor
|
||||
anchor={anchor}
|
||||
workspaceId={workspaceID?.toString() ?? ""}
|
||||
ref={showEditorRef}
|
||||
id={comment.id}
|
||||
initialValue={comment.comment_html}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ type Props = {
|
|||
|
||||
export const PeekOverviewIssueDetails: React.FC<Props> = observer((props) => {
|
||||
const { anchor, issueDetails } = props;
|
||||
|
||||
const { project_details } = usePublish(anchor);
|
||||
|
||||
// store hooks
|
||||
const { project_details, workspace: workspaceID } = usePublish(anchor);
|
||||
// derived values
|
||||
const description = issueDetails.description_html;
|
||||
|
||||
return (
|
||||
|
|
@ -35,6 +35,7 @@ export const PeekOverviewIssueDetails: React.FC<Props> = observer((props) => {
|
|||
? "<p></p>"
|
||||
: description
|
||||
}
|
||||
workspaceId={workspaceID?.toString() ?? ""}
|
||||
/>
|
||||
)}
|
||||
<IssueReactions anchor={anchor} />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// plane internal
|
||||
import { MAX_FILE_SIZE } from "@plane/constants";
|
||||
import { TFileHandler } from "@plane/editor";
|
||||
import { TFileHandler, TReadOnlyFileHandler } from "@plane/editor";
|
||||
import { SitesFileService } from "@plane/services";
|
||||
// helpers
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
|
|
@ -18,10 +18,35 @@ export const getEditorAssetSrc = (anchor: string, assetId: string): string | und
|
|||
|
||||
type TArgs = {
|
||||
anchor: string;
|
||||
uploadFile: (file: File) => Promise<string>;
|
||||
uploadFile: TFileHandler["upload"];
|
||||
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
|
||||
* @param {TArgs} args
|
||||
|
|
@ -30,14 +55,11 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
|||
const { anchor, uploadFile, workspaceId } = args;
|
||||
|
||||
return {
|
||||
getAssetSrc: async (path) => {
|
||||
if (!path) return "";
|
||||
if (path?.startsWith("http")) {
|
||||
return path;
|
||||
} else {
|
||||
return getEditorAssetSrc(anchor, path) ?? "";
|
||||
}
|
||||
},
|
||||
...getReadOnlyEditorFileHandlers({
|
||||
anchor,
|
||||
workspaceId,
|
||||
}),
|
||||
getAssetUploadStatus: () => 0,
|
||||
upload: uploadFile,
|
||||
delete: async (src: string) => {
|
||||
if (src?.startsWith("http")) {
|
||||
|
|
@ -46,36 +68,9 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
|||
await sitesFileService.deleteNewAsset(getEditorAssetSrc(anchor, src) ?? "");
|
||||
}
|
||||
},
|
||||
restore: async (src: string) => {
|
||||
if (src?.startsWith("http")) {
|
||||
await sitesFileService.restoreOldEditorAsset(workspaceId, src);
|
||||
} else {
|
||||
await sitesFileService.restoreNewAsset(anchor, src);
|
||||
}
|
||||
},
|
||||
cancel: sitesFileService.cancelUpload,
|
||||
validation: {
|
||||
maxFileSize: MAX_FILE_SIZE,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function returns the file handler required by the read-only editors
|
||||
*/
|
||||
export const getReadOnlyEditorFileHandlers = (
|
||||
args: Pick<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) ?? "";
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import Link from "next/link";
|
|||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane types
|
||||
import { TSearchEntityRequestPayload } from "@plane/types";
|
||||
import { TSearchEntityRequestPayload, TWebhookConnectionQueryParams } from "@plane/types";
|
||||
import { EFileAssetType } from "@plane/types/src/enums";
|
||||
// plane ui
|
||||
import { getButtonStyling } from "@plane/ui";
|
||||
|
|
@ -17,19 +17,14 @@ import { LogoSpinner } from "@/components/common";
|
|||
import { PageHead } from "@/components/core";
|
||||
import { IssuePeekOverview } from "@/components/issues";
|
||||
import { PageRoot, TPageRootConfig, TPageRootHandlers } from "@/components/pages";
|
||||
// helpers
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useProjectPage, useProjectPages, useWorkspace } from "@/hooks/store";
|
||||
// plane web hooks
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
import { useEditorConfig } from "@/hooks/editor";
|
||||
import { useEditorAsset, useProjectPage, useProjectPages, useWorkspace } from "@/hooks/store";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
import { ProjectPageService, ProjectPageVersionService } from "@/services/page";
|
||||
const workspaceService = new WorkspaceService();
|
||||
const fileService = new FileService();
|
||||
const projectPageService = new ProjectPageService();
|
||||
const projectPageVersionService = new ProjectPageVersionService();
|
||||
|
||||
|
|
@ -39,6 +34,7 @@ const PageDetailsPage = observer(() => {
|
|||
const { createPage, getPageById } = useProjectPages();
|
||||
const page = useProjectPage(pageId?.toString() ?? "");
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
const { uploadEditorAsset } = useEditorAsset();
|
||||
// derived values
|
||||
const workspaceId = workspaceSlug ? (getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "") : "";
|
||||
const { canCurrentUserAccessPage, id, name, updateDescription } = page;
|
||||
|
|
@ -51,8 +47,8 @@ const PageDetailsPage = observer(() => {
|
|||
}),
|
||||
[projectId, workspaceSlug]
|
||||
);
|
||||
// file size
|
||||
const { maxFileSize } = useFileSize();
|
||||
// editor config
|
||||
const { getEditorFileHandlers } = useEditorConfig();
|
||||
// fetch page details
|
||||
const { error: pageDetailsError } = useSWR(
|
||||
workspaceSlug && projectId && pageId ? `PAGE_DETAILS_${pageId}` : null,
|
||||
|
|
@ -96,30 +92,34 @@ const PageDetailsPage = observer(() => {
|
|||
const pageRootConfig: TPageRootConfig = useMemo(
|
||||
() => ({
|
||||
fileHandler: getEditorFileHandlers({
|
||||
maxFileSize,
|
||||
projectId: projectId?.toString() ?? "",
|
||||
uploadFile: async (file) => {
|
||||
const { asset_id } = await fileService.uploadProjectAsset(
|
||||
workspaceSlug?.toString() ?? "",
|
||||
projectId?.toString() ?? "",
|
||||
{
|
||||
uploadFile: async (blockId, file) => {
|
||||
const { asset_id } = await uploadEditorAsset({
|
||||
blockId,
|
||||
data: {
|
||||
entity_identifier: id ?? "",
|
||||
entity_type: EFileAssetType.PAGE_DESCRIPTION,
|
||||
},
|
||||
file
|
||||
);
|
||||
file,
|
||||
projectId: projectId?.toString() ?? "",
|
||||
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||
});
|
||||
return asset_id;
|
||||
},
|
||||
workspaceId,
|
||||
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||
}),
|
||||
webhookConnectionParams: {
|
||||
documentType: "project_page",
|
||||
projectId: projectId?.toString() ?? "",
|
||||
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||
},
|
||||
}),
|
||||
[id, maxFileSize, projectId, workspaceId, workspaceSlug]
|
||||
[getEditorFileHandlers, id, projectId, uploadEditorAsset, workspaceId, workspaceSlug]
|
||||
);
|
||||
|
||||
const webhookConnectionParams: TWebhookConnectionQueryParams = useMemo(
|
||||
() => ({
|
||||
documentType: "project_page",
|
||||
projectId: projectId?.toString() ?? "",
|
||||
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||
}),
|
||||
[projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
if ((!page || !id) && !pageDetailsError)
|
||||
|
|
@ -154,6 +154,7 @@ const PageDetailsPage = observer(() => {
|
|||
config={pageRootConfig}
|
||||
handlers={pageRootHandlers}
|
||||
page={page}
|
||||
webhookConnectionParams={webhookConnectionParams}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
/>
|
||||
<IssuePeekOverview />
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { Tooltip } from "@plane/ui";
|
|||
import { RichTextReadOnlyEditor } from "@/components/editor";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
handleInsertText: (insertOnNextLine: boolean) => void;
|
||||
|
|
@ -19,6 +21,10 @@ export const AskPiMenu: React.FC<Props> = (props) => {
|
|||
const { handleInsertText, handleRegenerate, isRegenerating, response, workspaceSlug } = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
// store hooks
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? "";
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -40,6 +46,7 @@ export const AskPiMenu: React.FC<Props> = (props) => {
|
|||
initialValue={response}
|
||||
containerClassName="!p-0 border-none"
|
||||
editorClassName="!pl-0"
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
<div className="mt-3 flex items-center gap-4">
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ type Props = {
|
|||
editorRef: RefObject<EditorRefApi>;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
workspaceId: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ const TONES_LIST = [
|
|||
];
|
||||
|
||||
export const EditorAIMenu: React.FC<Props> = (props) => {
|
||||
const { editorRef, isOpen, onClose, workspaceSlug } = props;
|
||||
const { editorRef, isOpen, onClose, workspaceId, workspaceSlug } = props;
|
||||
// states
|
||||
const [activeTask, setActiveTask] = useState<AI_EDITOR_TASKS | null>(null);
|
||||
const [response, setResponse] = useState<string | undefined>(undefined);
|
||||
|
|
@ -215,6 +216,7 @@ export const EditorAIMenu: React.FC<Props> = (props) => {
|
|||
initialValue={response}
|
||||
containerClassName="!p-0 border-none"
|
||||
editorClassName="!pl-0"
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
<div className="mt-3 flex items-center gap-4">
|
||||
|
|
|
|||
|
|
@ -6,12 +6,15 @@ import { Controller, useForm } from "react-hook-form"; // services
|
|||
import { usePopper } from "react-popper";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// plane editor
|
||||
import { EditorReadOnlyRefApi } from "@plane/editor";
|
||||
// ui
|
||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
|
||||
// services
|
||||
import { AIService } from "@/services/ai.service";
|
||||
const aiService = new AIService();
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
|
|
@ -22,6 +25,7 @@ type Props = {
|
|||
prompt?: string;
|
||||
button: JSX.Element;
|
||||
className?: string;
|
||||
workspaceId: string;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
|
@ -31,8 +35,6 @@ type FormData = {
|
|||
task: string;
|
||||
};
|
||||
|
||||
const aiService = new AIService();
|
||||
|
||||
export const GptAssistantPopover: React.FC<Props> = (props) => {
|
||||
const {
|
||||
isOpen,
|
||||
|
|
@ -43,6 +45,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
|
|||
prompt,
|
||||
button,
|
||||
className = "",
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
} = props;
|
||||
|
|
@ -51,7 +54,8 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
|
|||
const [invalidResponse, setInvalidResponse] = useState(false);
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | 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);
|
||||
// popper
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
|
|
@ -218,6 +222,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
|
|||
initialValue={prompt}
|
||||
containerClassName="-m-3"
|
||||
ref={editorRef}
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
|
@ -230,6 +235,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
|
|||
id="ai-assistant-response"
|
||||
initialValue={`<p>${response}</p>`}
|
||||
ref={responseRef}
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,20 +2,18 @@ import React, { useState } from "react";
|
|||
// plane constants
|
||||
import { EIssueCommentAccessSpecifier } from "@plane/constants";
|
||||
// plane editor
|
||||
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor";
|
||||
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
import { isCommentEmpty } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useEditorMention } from "@/hooks/use-editor-mention";
|
||||
import { useEditorConfig, useEditorMention } from "@/hooks/editor";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
|
@ -31,7 +29,7 @@ interface LiteTextEditorWrapperProps
|
|||
showSubmitButton?: boolean;
|
||||
isSubmitting?: boolean;
|
||||
showToolbarInitially?: boolean;
|
||||
uploadFile: (file: File) => Promise<string>;
|
||||
uploadFile: TFileHandler["upload"];
|
||||
issue_id?: string;
|
||||
}
|
||||
|
||||
|
|
@ -66,8 +64,8 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
|||
issue_id: issue_id,
|
||||
}),
|
||||
});
|
||||
// file size
|
||||
const { maxFileSize } = useFileSize();
|
||||
// editor config
|
||||
const { getEditorFileHandlers } = useEditorConfig();
|
||||
function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> {
|
||||
return !!ref && typeof ref === "object" && "current" in ref;
|
||||
}
|
||||
|
|
@ -85,7 +83,6 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
|||
ref={ref}
|
||||
disabledExtensions={disabledExtensions}
|
||||
fileHandler={getEditorFileHandlers({
|
||||
maxFileSize,
|
||||
projectId,
|
||||
uploadFile,
|
||||
workspaceId,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditor, LiteTextReadOnlyEditorWi
|
|||
import { EditorMentionsRoot } from "@/components/editor";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useEditorConfig } from "@/hooks/editor";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
|
||||
|
|
@ -13,14 +14,17 @@ type LiteTextReadOnlyEditorWrapperProps = Omit<
|
|||
ILiteTextReadOnlyEditor,
|
||||
"disabledExtensions" | "fileHandler" | "mentionHandler"
|
||||
> & {
|
||||
workspaceId: string;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>(
|
||||
({ workspaceSlug, projectId, ...props }, ref) => {
|
||||
({ workspaceId, workspaceSlug, projectId, ...props }, ref) => {
|
||||
// editor flaggings
|
||||
const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
|
||||
// editor config
|
||||
const { getReadOnlyEditorFileHandlers } = useEditorConfig();
|
||||
|
||||
return (
|
||||
<LiteTextReadOnlyEditorWithRef
|
||||
|
|
@ -28,6 +32,7 @@ export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Lit
|
|||
disabledExtensions={disabledExtensions}
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
projectId,
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
})}
|
||||
mentionHandler={{
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
import React, { forwardRef } from "react";
|
||||
// editor
|
||||
import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef } from "@plane/editor";
|
||||
import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef, TFileHandler } from "@plane/editor";
|
||||
// plane types
|
||||
import { TSearchEntityRequestPayload, TSearchResponse } from "@plane/types";
|
||||
// components
|
||||
import { EditorMentionsRoot } from "@/components/editor";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useEditorMention } from "@/hooks/use-editor-mention";
|
||||
import { useEditorConfig, useEditorMention } from "@/hooks/editor";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
|
||||
interface RichTextEditorWrapperProps
|
||||
extends Omit<IRichTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
|
||||
|
|
@ -20,7 +18,7 @@ interface RichTextEditorWrapperProps
|
|||
workspaceSlug: string;
|
||||
workspaceId: string;
|
||||
projectId?: string;
|
||||
uploadFile: (file: File) => Promise<string>;
|
||||
uploadFile: TFileHandler["upload"];
|
||||
}
|
||||
|
||||
export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => {
|
||||
|
|
@ -32,15 +30,14 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
|||
const { fetchMentions } = useEditorMention({
|
||||
searchEntity: async (payload) => await searchMentionCallback(payload),
|
||||
});
|
||||
// file size
|
||||
const { maxFileSize } = useFileSize();
|
||||
// editor config
|
||||
const { getEditorFileHandlers } = useEditorConfig();
|
||||
|
||||
return (
|
||||
<RichTextEditorWithRef
|
||||
ref={ref}
|
||||
disabledExtensions={disabledExtensions}
|
||||
fileHandler={getEditorFileHandlers({
|
||||
maxFileSize,
|
||||
projectId,
|
||||
uploadFile,
|
||||
workspaceId,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor, RichTextReadOnlyEditorWi
|
|||
import { EditorMentionsRoot } from "@/components/editor";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useEditorConfig } from "@/hooks/editor";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
|
||||
|
|
@ -13,14 +14,17 @@ type RichTextReadOnlyEditorWrapperProps = Omit<
|
|||
IRichTextReadOnlyEditor,
|
||||
"disabledExtensions" | "fileHandler" | "mentionHandler"
|
||||
> & {
|
||||
workspaceId: string;
|
||||
workspaceSlug: string;
|
||||
projectId?: string;
|
||||
};
|
||||
|
||||
export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>(
|
||||
({ workspaceSlug, projectId, ...props }, ref) => {
|
||||
({ workspaceId, workspaceSlug, projectId, ...props }, ref) => {
|
||||
// editor flaggings
|
||||
const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
|
||||
// editor config
|
||||
const { getReadOnlyEditorFileHandlers } = useEditorConfig();
|
||||
|
||||
return (
|
||||
<RichTextReadOnlyEditorWithRef
|
||||
|
|
@ -28,6 +32,7 @@ export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Ric
|
|||
disabledExtensions={disabledExtensions}
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
projectId,
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
})}
|
||||
mentionHandler={{
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ import React, { useState } from "react";
|
|||
// plane constants
|
||||
import { EIssueCommentAccessSpecifier } from "@plane/constants";
|
||||
// plane editor
|
||||
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor";
|
||||
// plane types
|
||||
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor";
|
||||
// components
|
||||
import { TSticky } from "@plane/types";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useEditorConfig } from "@/hooks/editor";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
import { StickyEditorToolbar } from "./toolbar";
|
||||
|
||||
interface StickyEditorWrapperProps
|
||||
|
|
@ -25,7 +25,7 @@ interface StickyEditorWrapperProps
|
|||
isSubmitting?: boolean;
|
||||
showToolbarInitially?: boolean;
|
||||
showToolbar?: boolean;
|
||||
uploadFile: (file: File) => Promise<string>;
|
||||
uploadFile: TFileHandler["upload"];
|
||||
parentClassName?: string;
|
||||
handleColorChange: (data: Partial<TSticky>) => Promise<void>;
|
||||
handleDelete: () => void;
|
||||
|
|
@ -49,8 +49,8 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
|
|||
const [isFocused, setIsFocused] = useState(showToolbarInitially);
|
||||
// editor flaggings
|
||||
const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
|
||||
// file size
|
||||
const { maxFileSize } = useFileSize();
|
||||
// editor config
|
||||
const { getEditorFileHandlers } = useEditorConfig();
|
||||
function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> {
|
||||
return !!ref && typeof ref === "object" && "current" in ref;
|
||||
}
|
||||
|
|
@ -67,7 +67,6 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
|
|||
ref={ref}
|
||||
disabledExtensions={[...disabledExtensions, "enter-key"]}
|
||||
fileHandler={getEditorFileHandlers({
|
||||
maxFileSize,
|
||||
projectId,
|
||||
uploadFile,
|
||||
workspaceId,
|
||||
|
|
|
|||
|
|
@ -19,13 +19,10 @@ import { RichTextEditor } from "@/components/editor/rich-text-editor/rich-text-e
|
|||
import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper";
|
||||
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
||||
// hooks
|
||||
import { useProjectInbox } from "@/hooks/store";
|
||||
import { useEditorAsset, useProjectInbox } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
const fileService = new FileService();
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
type TInboxIssueDescription = {
|
||||
|
|
@ -53,11 +50,10 @@ export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props
|
|||
onEnterKeyPress,
|
||||
onAssetUpload,
|
||||
} = props;
|
||||
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
|
||||
// hooks
|
||||
// store hooks
|
||||
const { uploadEditorAsset } = useEditorAsset();
|
||||
const { loader } = useProjectInbox();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
|
|
@ -90,17 +86,18 @@ export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props
|
|||
containerClassName={containerClassName}
|
||||
onEnterKeyPress={onEnterKeyPress}
|
||||
tabIndex={getIndex("description_html")}
|
||||
uploadFile={async (file) => {
|
||||
uploadFile={async (blockId, file) => {
|
||||
try {
|
||||
const { asset_id } = await fileService.uploadProjectAsset(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
{
|
||||
const { asset_id } = await uploadEditorAsset({
|
||||
blockId,
|
||||
data: {
|
||||
entity_identifier: data.id ?? "",
|
||||
entity_type: EFileAssetType.ISSUE_DESCRIPTION,
|
||||
},
|
||||
file
|
||||
);
|
||||
file,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
});
|
||||
onAssetUpload?.(asset_id);
|
||||
return asset_id;
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -17,13 +17,10 @@ import { TIssueOperations } from "@/components/issues/issue-detail";
|
|||
// helpers
|
||||
import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
import { useEditorAsset, useWorkspace } from "@/hooks/store";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
const workspaceService = new WorkspaceService();
|
||||
const fileService = new FileService();
|
||||
|
||||
export type IssueDescriptionInputProps = {
|
||||
containerClassName?: string;
|
||||
|
|
@ -51,6 +48,14 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
|||
setIsSubmitting,
|
||||
placeholder,
|
||||
} = props;
|
||||
// states
|
||||
const [localIssueDescription, setLocalIssueDescription] = useState({
|
||||
id: issueId,
|
||||
description_html: initialValue,
|
||||
});
|
||||
// store hooks
|
||||
const { uploadEditorAsset } = useEditorAsset();
|
||||
// form info
|
||||
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -61,11 +66,6 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
|||
},
|
||||
});
|
||||
|
||||
const [localIssueDescription, setLocalIssueDescription] = useState({
|
||||
id: issueId,
|
||||
description_html: initialValue,
|
||||
});
|
||||
|
||||
const handleDescriptionFormSubmit = useCallback(
|
||||
async (formData: Partial<TIssue>) => {
|
||||
await issueOperations.update(workspaceSlug, projectId, issueId, {
|
||||
|
|
@ -136,17 +136,18 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
|||
})
|
||||
}
|
||||
containerClassName={containerClassName}
|
||||
uploadFile={async (file) => {
|
||||
uploadFile={async (blockId, file) => {
|
||||
try {
|
||||
const { asset_id } = await fileService.uploadProjectAsset(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
{
|
||||
const { asset_id } = await uploadEditorAsset({
|
||||
blockId,
|
||||
data: {
|
||||
entity_identifier: issueId,
|
||||
entity_type: EFileAssetType.ISSUE_DESCRIPTION,
|
||||
},
|
||||
file
|
||||
);
|
||||
file,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
});
|
||||
return asset_id;
|
||||
} catch (error) {
|
||||
console.log("Error in uploading work item asset:", error);
|
||||
|
|
@ -159,6 +160,7 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
|||
id={issueId}
|
||||
initialValue={localIssueDescription.description_html ?? ""}
|
||||
containerClassName={containerClassName}
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -170,8 +170,8 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
|
|||
}
|
||||
}}
|
||||
showSubmitButton={false}
|
||||
uploadFile={async (file) => {
|
||||
const { asset_id } = await activityOperations.uploadCommentAsset(file, comment.id);
|
||||
uploadFile={async (blockId, file) => {
|
||||
const { asset_id } = await activityOperations.uploadCommentAsset(blockId, file, comment.id);
|
||||
return asset_id;
|
||||
}}
|
||||
/>
|
||||
|
|
@ -215,6 +215,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
|
|||
ref={showEditorRef}
|
||||
id={comment.id}
|
||||
initialValue={comment.comment_html ?? ""}
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -110,8 +110,8 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
|||
handleAccessChange={onAccessChange}
|
||||
showAccessSpecifier={showAccessSpecifier}
|
||||
isSubmitting={isSubmitting}
|
||||
uploadFile={async (file) => {
|
||||
const { asset_id } = await activityOperations.uploadCommentAsset(file);
|
||||
uploadFile={async (blockId, file) => {
|
||||
const { asset_id } = await activityOperations.uploadCommentAsset(blockId, file);
|
||||
setUploadedAssetIds((prev) => [...prev, asset_id]);
|
||||
return asset_id;
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { FC, useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane package imports
|
||||
import { E_SORT_ORDER, TActivityFilters, defaultActivityFilters,EUserPermissions } from "@plane/constants";
|
||||
import { E_SORT_ORDER, TActivityFilters, defaultActivityFilters, EUserPermissions } from "@plane/constants";
|
||||
import { useLocalStorage } from "@plane/hooks";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
|
|
@ -16,12 +16,9 @@ import { IssueCommentCreate } from "@/components/issues";
|
|||
import { ActivitySortRoot, IssueActivityCommentRoot } from "@/components/issues/issue-detail";
|
||||
// constants
|
||||
// hooks
|
||||
import { useIssueDetail, useProject, useUser, useUserPermissions } from "@/hooks/store";
|
||||
import { useEditorAsset, useIssueDetail, useProject, useUser, useUserPermissions } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { ActivityFilterRoot, IssueActivityWorklogCreateButton } from "@/plane-web/components/issues/worklog";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
const fileService = new FileService();
|
||||
|
||||
type TIssueActivity = {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -35,7 +32,7 @@ export type TActivityOperations = {
|
|||
createComment: (data: Partial<TIssueComment>) => Promise<TIssueComment>;
|
||||
updateComment: (commentId: string, data: Partial<TIssueComment>) => 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) => {
|
||||
|
|
@ -48,6 +45,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||
defaultActivityFilters
|
||||
);
|
||||
const { setValue: setSortOrder, storedValue: sortOrder } = useLocalStorage("activity_sort_order", E_SORT_ORDER.ASC);
|
||||
// store hooks
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
createComment,
|
||||
|
|
@ -57,7 +55,8 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||
const { projectPermissionsByWorkspaceSlugAndProjectId } = useUserPermissions();
|
||||
const { getProjectById } = useProject();
|
||||
const { data: currentUser } = useUser();
|
||||
//derived values
|
||||
const { uploadEditorAsset } = useEditorAsset();
|
||||
// derived values
|
||||
const issue = issueId ? getIssueById(issueId) : undefined;
|
||||
const currentUserProjectRole = projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
|
||||
const isAdmin = (currentUserProjectRole ?? EUserPermissions.GUEST) === EUserPermissions.ADMIN;
|
||||
|
|
@ -94,7 +93,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||
message: t("issue.comments.create.success"),
|
||||
});
|
||||
return comment;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setToast({
|
||||
title: t("common.error.label"),
|
||||
type: TOAST_TYPE.ERROR,
|
||||
|
|
@ -111,7 +110,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||
type: TOAST_TYPE.SUCCESS,
|
||||
message: t("issue.comments.update.success"),
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setToast({
|
||||
title: t("common.error.label"),
|
||||
type: TOAST_TYPE.ERROR,
|
||||
|
|
@ -128,7 +127,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||
type: TOAST_TYPE.SUCCESS,
|
||||
message: t("issue.comments.remove.success"),
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setToast({
|
||||
title: t("common.error.label"),
|
||||
type: TOAST_TYPE.ERROR,
|
||||
|
|
@ -136,18 +135,19 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||
});
|
||||
}
|
||||
},
|
||||
uploadCommentAsset: async (file, commentId) => {
|
||||
uploadCommentAsset: async (blockId, file, commentId) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId) throw new Error("Missing fields");
|
||||
const res = await fileService.uploadProjectAsset(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
{
|
||||
const res = await uploadEditorAsset({
|
||||
blockId,
|
||||
data: {
|
||||
entity_identifier: commentId ?? "",
|
||||
entity_type: EFileAssetType.COMMENT_DESCRIPTION,
|
||||
},
|
||||
file
|
||||
);
|
||||
file,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
});
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.log("Error in uploading comment asset:", error);
|
||||
|
|
@ -155,7 +155,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||
}
|
||||
},
|
||||
}),
|
||||
[workspaceSlug, projectId, issueId, createComment, updateComment, removeComment]
|
||||
[workspaceSlug, projectId, issueId, createComment, updateComment, uploadEditorAsset, removeComment]
|
||||
);
|
||||
|
||||
const project = getProjectById(projectId);
|
||||
|
|
|
|||
|
|
@ -22,14 +22,15 @@ import { RichTextEditor } from "@/components/editor";
|
|||
import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper";
|
||||
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
||||
// hooks
|
||||
import { useInstance, useWorkspace } from "@/hooks/store";
|
||||
import { useEditorAsset, useInstance, useWorkspace } from "@/hooks/store";
|
||||
import useKeypress from "@/hooks/use-keypress";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { AIService } from "@/services/ai.service";
|
||||
import { FileService } from "@/services/file.service";
|
||||
const workspaceService = new WorkspaceService();
|
||||
const aiService = new AIService();
|
||||
|
||||
type TIssueDescriptionEditorProps = {
|
||||
control: Control<TIssue>;
|
||||
|
|
@ -50,11 +51,6 @@ type TIssueDescriptionEditorProps = {
|
|||
onClose: () => void;
|
||||
};
|
||||
|
||||
// services
|
||||
const workspaceService = new WorkspaceService();
|
||||
const aiService = new AIService();
|
||||
const fileService = new FileService();
|
||||
|
||||
export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = observer((props) => {
|
||||
const {
|
||||
control,
|
||||
|
|
@ -80,8 +76,10 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = ob
|
|||
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||
// store hooks
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString())?.id as string;
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString())?.id ?? "";
|
||||
const { config } = useInstance();
|
||||
const { uploadEditorAsset } = useEditorAsset();
|
||||
// platform
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
|
||||
|
|
@ -202,19 +200,20 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = ob
|
|||
})
|
||||
}
|
||||
containerClassName="pt-3 min-h-[120px]"
|
||||
uploadFile={async (file) => {
|
||||
uploadFile={async (blockId, file) => {
|
||||
try {
|
||||
const { asset_id } = await fileService.uploadProjectAsset(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
{
|
||||
const { asset_id } = await uploadEditorAsset({
|
||||
blockId,
|
||||
data: {
|
||||
entity_identifier: issueId ?? "",
|
||||
entity_type: isDraft
|
||||
? EFileAssetType.DRAFT_ISSUE_DESCRIPTION
|
||||
: EFileAssetType.ISSUE_DESCRIPTION,
|
||||
},
|
||||
file
|
||||
);
|
||||
file,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
});
|
||||
onAssetUpload(asset_id);
|
||||
return asset_id;
|
||||
} catch (error) {
|
||||
|
|
@ -268,6 +267,7 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = ob
|
|||
AI
|
||||
</button>
|
||||
}
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ import { PageContentBrowser, PageContentLoader, PageEditorTitle } from "@/compon
|
|||
import { cn, LIVE_BASE_PATH, LIVE_BASE_URL } from "@/helpers/common.helper";
|
||||
import { generateRandomColor } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
import { useEditorMention } from "@/hooks/use-editor-mention";
|
||||
import { useEditorMention } from "@/hooks/editor";
|
||||
import { useUser, useWorkspace } from "@/hooks/store";
|
||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
// plane web components
|
||||
import { EditorAIMenu } from "@/plane-web/components/pages";
|
||||
|
|
@ -34,7 +34,6 @@ import { TPageInstance } from "@/store/pages/base-page";
|
|||
|
||||
export type TEditorBodyConfig = {
|
||||
fileHandler: TFileHandler;
|
||||
webhookConnectionParams: TWebhookConnectionQueryParams;
|
||||
};
|
||||
|
||||
export type TEditorBodyHandlers = {
|
||||
|
|
@ -50,6 +49,7 @@ type Props = {
|
|||
handlers: TEditorBodyHandlers;
|
||||
page: TPageInstance;
|
||||
sidePeekVisible: boolean;
|
||||
webhookConnectionParams: TWebhookConnectionQueryParams;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
|
|
@ -62,12 +62,15 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
|||
handlers,
|
||||
page,
|
||||
sidePeekVisible,
|
||||
webhookConnectionParams,
|
||||
workspaceSlug,
|
||||
} = props;
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
const { id: pageId, name: pageTitle, isContentEditable, updateTitle } = page;
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? "";
|
||||
// issue-embed
|
||||
const { issueEmbedProps } = useIssueEmbed({
|
||||
fetchEmbedSuggestions: handlers.fetchEntity,
|
||||
|
|
@ -96,10 +99,11 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
|||
editorRef={editorRef}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
/>
|
||||
),
|
||||
[editorRef, workspaceSlug]
|
||||
[editorRef, workspaceId, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleServerConnect = useCallback(() => {
|
||||
|
|
@ -129,13 +133,13 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
|||
// Construct realtime config
|
||||
return {
|
||||
url: WS_LIVE_URL.toString(),
|
||||
queryParams: config.webhookConnectionParams,
|
||||
queryParams: webhookConnectionParams,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating realtime config", error);
|
||||
return undefined;
|
||||
}
|
||||
}, [config.webhookConnectionParams]);
|
||||
}, [webhookConnectionParams]);
|
||||
|
||||
const userConfig = useMemo(
|
||||
() => ({
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useSearchParams } from "next/navigation";
|
|||
// editor
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
// types
|
||||
import { TDocumentPayload, TPage, TPageVersion } from "@plane/types";
|
||||
import { TDocumentPayload, TPage, TPageVersion, TWebhookConnectionQueryParams } from "@plane/types";
|
||||
// components
|
||||
import {
|
||||
PageEditorHeaderRoot,
|
||||
|
|
@ -36,11 +36,12 @@ type TPageRootProps = {
|
|||
config: TPageRootConfig;
|
||||
handlers: TPageRootHandlers;
|
||||
page: TPageInstance;
|
||||
webhookConnectionParams: TWebhookConnectionQueryParams;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const PageRoot = observer((props: TPageRootProps) => {
|
||||
const { config, handlers, page, workspaceSlug } = props;
|
||||
const { config, handlers, page, webhookConnectionParams, workspaceSlug } = props;
|
||||
// states
|
||||
const [editorReady, setEditorReady] = useState(false);
|
||||
const [hasConnectionFailed, setHasConnectionFailed] = useState(false);
|
||||
|
|
@ -116,6 +117,7 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
|||
handlers={handlers}
|
||||
page={page}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
webhookConnectionParams={webhookConnectionParams}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import { TPageVersion } from "@plane/types";
|
|||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { EditorMentionsRoot } from "@/components/editor";
|
||||
// helpers
|
||||
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useEditorConfig } from "@/hooks/editor";
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
|
|
@ -27,8 +27,14 @@ export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props
|
|||
const { activeVersion, currentVersionDescription, isCurrentVersionActive, versionDetails } = props;
|
||||
// params
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
const workspaceDetails = getWorkspaceBySlug(workspaceSlug?.toString() ?? "");
|
||||
// editor flaggings
|
||||
const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString() ?? "");
|
||||
// editor config
|
||||
const { getReadOnlyEditorFileHandlers } = useEditorConfig();
|
||||
// issue-embed
|
||||
const { issueEmbedProps } = useIssueEmbed({
|
||||
projectId: projectId?.toString() ?? "",
|
||||
|
|
@ -97,6 +103,7 @@ export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props
|
|||
editorClassName="pl-10"
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
projectId: projectId?.toString() ?? "",
|
||||
workspaceId: workspaceDetails?.id ?? "",
|
||||
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||
})}
|
||||
mentionHandler={{
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { ActivitySettingsLoader } from "@/components/ui";
|
|||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
import { useUser, useWorkspace } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
activity: IUserActivityResponse | undefined;
|
||||
|
|
@ -27,6 +27,9 @@ export const ActivityList: React.FC<Props> = observer((props) => {
|
|||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString() ?? "")?.id ?? "";
|
||||
|
||||
// TODO: refactor this component
|
||||
return (
|
||||
|
|
@ -79,6 +82,7 @@ export const ActivityList: React.FC<Props> = observer((props) => {
|
|||
: (activityItem.old_value?.toString() as string)
|
||||
}
|
||||
containerClassName="text-xs bg-custom-background-100"
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
projectId={activityItem.project}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -103,7 +103,8 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
|
|||
activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value
|
||||
}
|
||||
containerClassName="text-xs bg-custom-background-100"
|
||||
workspaceSlug={activityItem?.workspace_detail?.slug.toString() ?? ""}
|
||||
workspaceId={activityItem?.workspace_detail?.id?.toString() ?? ""}
|
||||
workspaceSlug={activityItem?.workspace_detail?.slug?.toString() ?? ""}
|
||||
projectId={activityItem.project ?? ""}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
2
web/core/hooks/editor/index.ts
Normal file
2
web/core/hooks/editor/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./use-editor-config";
|
||||
export * from "./use-editor-mention";
|
||||
96
web/core/hooks/editor/use-editor-config.ts
Normal file
96
web/core/hooks/editor/use-editor-config.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
|
|
@ -7,6 +7,7 @@ export * from "./use-command-palette";
|
|||
export * from "./use-cycle";
|
||||
export * from "./use-cycle-filter";
|
||||
export * from "./use-dashboard";
|
||||
export * from "./use-editor-asset";
|
||||
export * from "./use-event-tracker";
|
||||
export * from "./use-global-view";
|
||||
export * from "./use-inbox-issues";
|
||||
|
|
|
|||
10
web/core/hooks/store/use-editor-asset.ts
Normal file
10
web/core/hooks/store/use-editor-asset.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { AxiosRequestConfig } from "axios";
|
||||
// plane types
|
||||
import { TFileEntityInfo, TFileSignedURLResponse } from "@plane/types";
|
||||
// helpers
|
||||
|
|
@ -64,7 +65,8 @@ export class FileService extends APIService {
|
|||
async uploadWorkspaceAsset(
|
||||
workspaceSlug: string,
|
||||
data: TFileEntityInfo,
|
||||
file: File
|
||||
file: File,
|
||||
uploadProgressHandler?: AxiosRequestConfig["onUploadProgress"]
|
||||
): Promise<TFileSignedURLResponse> {
|
||||
const fileMetaData = getFileMetaDataForUpload(file);
|
||||
return this.post(`/api/assets/v2/workspaces/${workspaceSlug}/`, {
|
||||
|
|
@ -74,7 +76,11 @@ export class FileService extends APIService {
|
|||
.then(async (response) => {
|
||||
const signedURLResponse: TFileSignedURLResponse = response?.data;
|
||||
const fileUploadPayload = generateFileUploadPayload(signedURLResponse, file);
|
||||
await this.fileUploadService.uploadFile(signedURLResponse.upload_data.url, fileUploadPayload);
|
||||
await this.fileUploadService.uploadFile(
|
||||
signedURLResponse.upload_data.url,
|
||||
fileUploadPayload,
|
||||
uploadProgressHandler
|
||||
);
|
||||
await this.updateWorkspaceAssetUploadStatus(workspaceSlug.toString(), signedURLResponse.asset_id);
|
||||
return signedURLResponse;
|
||||
})
|
||||
|
|
@ -122,7 +128,8 @@ export class FileService extends APIService {
|
|||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: TFileEntityInfo,
|
||||
file: File
|
||||
file: File,
|
||||
uploadProgressHandler?: AxiosRequestConfig["onUploadProgress"]
|
||||
): Promise<TFileSignedURLResponse> {
|
||||
const fileMetaData = getFileMetaDataForUpload(file);
|
||||
return this.post(`/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/`, {
|
||||
|
|
@ -132,7 +139,11 @@ export class FileService extends APIService {
|
|||
.then(async (response) => {
|
||||
const signedURLResponse: TFileSignedURLResponse = response?.data;
|
||||
const fileUploadPayload = generateFileUploadPayload(signedURLResponse, file);
|
||||
await this.fileUploadService.uploadFile(signedURLResponse.upload_data.url, fileUploadPayload);
|
||||
await this.fileUploadService.uploadFile(
|
||||
signedURLResponse.upload_data.url,
|
||||
fileUploadPayload,
|
||||
uploadProgressHandler
|
||||
);
|
||||
await this.updateProjectAssetUploadStatus(workspaceSlug, projectId, signedURLResponse.asset_id);
|
||||
return signedURLResponse;
|
||||
})
|
||||
|
|
|
|||
121
web/core/store/editor/asset.store.ts
Normal file
121
web/core/store/editor/asset.store.ts
Normal 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];
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -126,7 +126,7 @@ export class IssueAttachmentStore implements IIssueAttachmentStore {
|
|||
return response;
|
||||
};
|
||||
|
||||
debouncedUpdateProgress = debounce((issueId: string, tempId: string, progress: number) => {
|
||||
private debouncedUpdateProgress = debounce((issueId: string, tempId: string, progress: number) => {
|
||||
runInAction(() => {
|
||||
set(this.attachmentsUploadStatusMap, [issueId, tempId, "progress"], progress);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { enableStaticRendering } from "mobx-react";
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
// plane web store
|
||||
import { CommandPaletteStore, ICommandPaletteStore } from "@/plane-web/store/command-palette.store";
|
||||
import { RootStore } from "@/plane-web/store/root.store";
|
||||
|
|
@ -8,6 +7,7 @@ import { IStateStore, StateStore } from "@/plane-web/store/state.store";
|
|||
import { CycleStore, ICycleStore } from "./cycle.store";
|
||||
import { CycleFilterStore, ICycleFilterStore } from "./cycle_filter.store";
|
||||
import { DashboardStore, IDashboardStore } from "./dashboard.store";
|
||||
import { EditorAssetStore, IEditorAssetStore } from "./editor/asset.store";
|
||||
import { IProjectEstimateStore, ProjectEstimateStore } from "./estimates/project-estimate.store";
|
||||
import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store";
|
||||
import { FavoriteStore, IFavoriteStore } from "./favorite.store";
|
||||
|
|
@ -61,6 +61,7 @@ export class CoreRootStore {
|
|||
favorite: IFavoriteStore;
|
||||
transient: ITransientStore;
|
||||
stickyStore: IStickyStore;
|
||||
editorAssetStore: IEditorAssetStore;
|
||||
|
||||
constructor() {
|
||||
this.router = new RouterStore();
|
||||
|
|
@ -90,6 +91,7 @@ export class CoreRootStore {
|
|||
this.favorite = new FavoriteStore(this);
|
||||
this.transient = new TransientStore();
|
||||
this.stickyStore = new StickyStore();
|
||||
this.editorAssetStore = new EditorAssetStore();
|
||||
}
|
||||
|
||||
resetOnSignOut() {
|
||||
|
|
@ -122,5 +124,6 @@ export class CoreRootStore {
|
|||
this.favorite = new FavoriteStore(this);
|
||||
this.transient = new TransientStore();
|
||||
this.stickyStore = new StickyStore();
|
||||
this.editorAssetStore = new EditorAssetStore();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
// plane editor
|
||||
import { TFileHandler } from "@plane/editor";
|
||||
// helpers
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
const fileService = new FileService();
|
||||
|
||||
type TEditorSrcArgs = {
|
||||
assetId: string;
|
||||
|
|
@ -27,90 +22,6 @@ export const getEditorAssetSrc = (args: TEditorSrcArgs): string | undefined => {
|
|||
return url;
|
||||
};
|
||||
|
||||
type TArgs = {
|
||||
maxFileSize: number;
|
||||
projectId?: string;
|
||||
uploadFile: (file: File) => Promise<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 => {
|
||||
if (!jsx) return "";
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue