[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
|
// 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;
|
||||||
|
|
|
||||||
|
|
@ -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={
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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";
|
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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
() => ({
|
() => ({
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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} />,
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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} />,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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) ?? "";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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={{
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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={{
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -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={{
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
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";
|
||||||
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";
|
||||||
|
|
|
||||||
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
|
// 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;
|
||||||
})
|
})
|
||||||
|
|
|
||||||
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;
|
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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 "";
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue