[PE-242, 243] refactor: editor file handling, image upload status (#6442)

* refactor: editor file handling

* refactor: asset store

* refactor: space app file handlers

* fix: separate webhook connection params

* chore: handle undefined status

* chore: add type to upload status

* chore: added transition for upload status update
This commit is contained in:
Aaryan Khandelwal 2025-02-19 15:18:01 +05:30 committed by GitHub
parent b7198234de
commit 214692f5b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 602 additions and 315 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,96 @@
import { useCallback } from "react";
// plane editor
import { TFileHandler, TReadOnlyFileHandler } from "@plane/editor";
// helpers
import { getEditorAssetSrc } from "@/helpers/editor.helper";
// hooks
import { useEditorAsset } from "@/hooks/store";
// plane web hooks
import { useFileSize } from "@/plane-web/hooks/use-file-size";
// services
import { FileService } from "@/services/file.service";
const fileService = new FileService();
type TArgs = {
projectId?: string;
uploadFile: TFileHandler["upload"];
workspaceId: string;
workspaceSlug: string;
};
export const useEditorConfig = () => {
// store hooks
const { assetsUploadPercentage } = useEditorAsset();
// file size
const { maxFileSize } = useFileSize();
const getReadOnlyEditorFileHandlers = useCallback(
(args: Pick<TArgs, "projectId" | "workspaceId" | "workspaceSlug">): TReadOnlyFileHandler => {
const { projectId, workspaceId, workspaceSlug } = args;
return {
getAssetSrc: async (path) => {
if (!path) return "";
if (path?.startsWith("http")) {
return path;
} else {
return (
getEditorAssetSrc({
assetId: path,
projectId,
workspaceSlug,
}) ?? ""
);
}
},
restore: async (src: string) => {
if (src?.startsWith("http")) {
await fileService.restoreOldEditorAsset(workspaceId, src);
} else {
await fileService.restoreNewAsset(workspaceSlug, src);
}
},
};
},
[]
);
const getEditorFileHandlers = useCallback(
(args: TArgs): TFileHandler => {
const { projectId, uploadFile, workspaceId, workspaceSlug } = args;
return {
...getReadOnlyEditorFileHandlers({
projectId,
workspaceId,
workspaceSlug,
}),
assetsUploadStatus: assetsUploadPercentage,
upload: uploadFile,
delete: async (src: string) => {
if (src?.startsWith("http")) {
await fileService.deleteOldWorkspaceAsset(workspaceId, src);
} else {
await fileService.deleteNewAsset(
getEditorAssetSrc({
assetId: src,
projectId,
workspaceSlug,
}) ?? ""
);
}
},
cancel: fileService.cancelUpload,
validation: {
maxFileSize,
},
};
},
[assetsUploadPercentage, getReadOnlyEditorFileHandlers, maxFileSize]
);
return {
getEditorFileHandlers,
getReadOnlyEditorFileHandlers,
};
};

View file

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

View file

@ -0,0 +1,10 @@
import { useContext } from "react";
// store
import { StoreContext } from "@/lib/store-context";
import { IEditorAssetStore } from "@/store/editor/asset.store";
export const useEditorAsset = (): IEditorAssetStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useEditorAsset must be used within StoreProvider");
return context.editorAssetStore;
};

View file

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

View file

@ -0,0 +1,121 @@
import debounce from "lodash/debounce";
import set from "lodash/set";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import { v4 as uuidv4 } from "uuid";
// plane types
import { TFileEntityInfo, TFileSignedURLResponse } from "@plane/types";
// services
import { FileService } from "@/services/file.service";
import { TAttachmentUploadStatus } from "../issue/issue-details/attachment.store";
export interface IEditorAssetStore {
// computed
assetsUploadPercentage: Record<string, number>;
// helper methods
getAssetUploadStatusByEditorBlockId: (blockId: string) => TAttachmentUploadStatus | undefined;
// actions
uploadEditorAsset: ({
blockId,
data,
file,
projectId,
workspaceSlug,
}: {
blockId: string;
data: TFileEntityInfo;
file: File;
projectId?: string;
workspaceSlug: string;
}) => Promise<TFileSignedURLResponse>;
}
export class EditorAssetStore implements IEditorAssetStore {
// observables
assetsUploadStatus: Record<string, TAttachmentUploadStatus> = {};
// services
fileService: FileService;
constructor() {
makeObservable(this, {
// observables
assetsUploadStatus: observable,
// computed
assetsUploadPercentage: computed,
// actions
uploadEditorAsset: action,
});
// services
this.fileService = new FileService();
}
get assetsUploadPercentage() {
const assetsStatus = this.assetsUploadStatus;
const assetsPercentage: Record<string, number> = {};
Object.keys(assetsStatus).forEach((blockId) => {
const asset = assetsStatus[blockId];
if (asset) assetsPercentage[blockId] = asset.progress;
});
return assetsPercentage;
}
// helper methods
getAssetUploadStatusByEditorBlockId: IEditorAssetStore["getAssetUploadStatusByEditorBlockId"] = computedFn(
(blockId) => {
const blockDetails = this.assetsUploadStatus[blockId];
if (!blockDetails) return undefined;
return blockDetails;
}
);
// actions
private debouncedUpdateProgress = debounce((blockId: string, progress: number) => {
runInAction(() => {
set(this.assetsUploadStatus, [blockId, "progress"], progress);
});
}, 16);
uploadEditorAsset: IEditorAssetStore["uploadEditorAsset"] = async (args) => {
const { blockId, data, file, projectId, workspaceSlug } = args;
const tempId = uuidv4();
try {
// update attachment upload status
runInAction(() => {
set(this.assetsUploadStatus, [blockId], {
id: tempId,
name: file.name,
progress: 0,
size: file.size,
type: file.type,
});
});
if (projectId) {
const response = await this.fileService.uploadProjectAsset(
workspaceSlug,
projectId,
data,
file,
(progressEvent) => {
const progressPercentage = Math.round((progressEvent.progress ?? 0) * 100);
this.debouncedUpdateProgress(blockId, progressPercentage);
}
);
return response;
} else {
const response = await this.fileService.uploadWorkspaceAsset(workspaceSlug, data, file, (progressEvent) => {
const progressPercentage = Math.round((progressEvent.progress ?? 0) * 100);
this.debouncedUpdateProgress(blockId, progressPercentage);
});
return response;
}
} catch (error) {
console.error("Error in uploading page asset:", error);
throw error;
} finally {
runInAction(() => {
delete this.assetsUploadStatus[blockId];
});
}
};
}

View file

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

View file

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

View file

@ -1,10 +1,5 @@
// plane editor
import { TFileHandler } from "@plane/editor";
// helpers
import { getFileURL } from "@/helpers/file.helper";
// services
import { FileService } from "@/services/file.service";
const fileService = new FileService();
type TEditorSrcArgs = {
assetId: string;
@ -27,90 +22,6 @@ export const getEditorAssetSrc = (args: TEditorSrcArgs): string | undefined => {
return url;
};
type TArgs = {
maxFileSize: number;
projectId?: string;
uploadFile: (file: File) => Promise<string>;
workspaceId: string;
workspaceSlug: string;
};
/**
* @description this function returns the file handler required by the editors
* @param {TArgs} args
*/
export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
const { maxFileSize, projectId, uploadFile, workspaceId, workspaceSlug } = args;
return {
getAssetSrc: async (path) => {
if (!path) return "";
if (path?.startsWith("http")) {
return path;
} else {
return (
getEditorAssetSrc({
assetId: path,
projectId,
workspaceSlug,
}) ?? ""
);
}
},
upload: uploadFile,
delete: async (src: string) => {
if (src?.startsWith("http")) {
await fileService.deleteOldWorkspaceAsset(workspaceId, src);
} else {
await fileService.deleteNewAsset(
getEditorAssetSrc({
assetId: src,
projectId,
workspaceSlug,
}) ?? ""
);
}
},
restore: async (src: string) => {
if (src?.startsWith("http")) {
await fileService.restoreOldEditorAsset(workspaceId, src);
} else {
await fileService.restoreNewAsset(workspaceSlug, src);
}
},
cancel: fileService.cancelUpload,
validation: {
maxFileSize,
},
};
};
/**
* @description this function returns the file handler required by the read-only editors
*/
export const getReadOnlyEditorFileHandlers = (
args: Pick<TArgs, "projectId" | "workspaceSlug">
): { getAssetSrc: TFileHandler["getAssetSrc"] } => {
const { projectId, workspaceSlug } = args;
return {
getAssetSrc: async (path) => {
if (!path) return "";
if (path?.startsWith("http")) {
return path;
} else {
return (
getEditorAssetSrc({
assetId: path,
projectId,
workspaceSlug,
}) ?? ""
);
}
},
};
};
export const getTextContent = (jsx: JSX.Element | React.ReactNode | null | undefined): string => {
if (!jsx) return "";