[WIKI-537] [WIKI-539] refactor: document editor (#3758)

* refactor: document editor

* chore: update user prop

* fix: type warning

* refactor: document read only editor

* chore: update value prop name

* chore: remove unnecessary exports

* hore: update initialValue type

* chore: revert initialValue type
This commit is contained in:
Aaryan Khandelwal
2025-07-25 16:41:12 +05:30
committed by GitHub
parent 580729dc20
commit dd4e733fa9
8 changed files with 71 additions and 156 deletions

View File

@@ -2,14 +2,13 @@ import { useRef } from "react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { FileText } from "lucide-react";
// editor
import { DocumentReadOnlyEditorWithRef, EditorRefApi } from "@plane/editor";
// ui
// plane imports
import { DocumentEditorWithRef, EditorRefApi } from "@plane/editor";
import { ERowVariant, Logo, Row } from "@plane/ui";
// components
import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
import { getEditorFileHandlers } from "@/helpers/editor.helper";
// hooks
import { usePublish } from "@/hooks/store";
// plane web components
@@ -66,16 +65,18 @@ export const PageDetailsMainContent: React.FC<Props> = observer((props) => {
</div>
</div>
<div className="size-full">
<DocumentReadOnlyEditorWithRef
<DocumentEditorWithRef
editable={false}
ref={editorRef}
id={pageDetails.id}
disabledExtensions={[]}
flaggedExtensions={[]}
initialValue={pageDetails.description_html ?? "<p></p>"}
value={pageDetails.description_html ?? "<p></p>"}
containerClassName="p-0 pb-64 border-none"
fileHandler={getReadOnlyEditorFileHandlers({
fileHandler={getEditorFileHandlers({
anchor,
workspaceId: publishSettings.workspace ?? "",
uploadFile: async () => "",
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
@@ -89,6 +90,7 @@ export const PageDetailsMainContent: React.FC<Props> = observer((props) => {
workspaceSlug: "",
},
}}
isSmoothCursorEnabled={false}
/>
</div>
</div>

View File

@@ -13,8 +13,8 @@ import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed";
type DocumentEditorWrapperProps = MakeOptional<
Omit<IDocumentEditorProps, "fileHandler" | "mentionHandler" | "embedHandler" | "isSmoothCursorEnabled" | "user">,
"disabledExtensions" | "editable" | "flaggedExtensions"
Omit<IDocumentEditorProps, "fileHandler" | "mentionHandler" | "embedHandler" | "user">,
"disabledExtensions" | "editable" | "flaggedExtensions" | "isSmoothCursorEnabled"
> & {
embedHandler?: Partial<IDocumentEditorProps["embedHandler"]>;
workspaceSlug: string;

View File

@@ -43,11 +43,9 @@ export const ProjectFeaturesList: FC<Props> = observer((props) => {
[featureProperty]: !currentProjectDetails?.[featureProperty as keyof IProject],
};
const updateProjectPromise = updateProject(workspaceSlug, projectId, settingsPayload);
if (featureProperty === "is_time_tracking_enabled") {
toggleProjectFeatures(workspaceSlug, projectId, settingsPayload, false);
}
setPromiseToast(updateProjectPromise, {
loading: "Updating project feature...",
success: {

View File

@@ -1,23 +1,18 @@
import React, { useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane editor
import { DocumentReadOnlyEditorWithRef } from "@plane/editor";
// plane types
// plane imports
import { TPage } from "@plane/types";
import { Avatar, Loader } from "@plane/ui";
import { calculateTimeAgo, cn, getFileURL, getPageName } from "@plane/utils";
// components
import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { DocumentEditor } from "@/components/editor/document/editor";
// hooks
import { useEditorConfig } from "@/hooks/editor";
import { useMember, useWorkspace } from "@/hooks/store";
// plane web components
// plane web imports
import { PageEmbedCardRoot } from "@/plane-web/components/pages";
// plane web store
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
// plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed";
@@ -163,9 +158,10 @@ export const PageEmbedPreview: React.FC<Props> = observer((props) => {
)}
>
<div className="h-full overflow-hidden" id={`content-container-${id}`}>
<DocumentReadOnlyEditorWithRef
<DocumentEditor
editable={false}
id={id ?? ""}
initialValue={description_html ?? "<p></p>"}
value={description_html ?? "<p></p>"}
containerClassName="p-0 pl-3 border-none"
editorClassName="p-2.5 text-xs"
disabledExtensions={documentEditorExtensions.disabled}
@@ -173,18 +169,7 @@ export const PageEmbedPreview: React.FC<Props> = observer((props) => {
displayConfig={{
fontSize: "small-font",
}}
fileHandler={getReadOnlyEditorFileHandlers({
projectId: projectId?.toString() ?? "",
workspaceSlug: workspaceSlug?.toString() ?? "",
workspaceId: workspaceDetails?.id?.toString() ?? "",
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
}}
embedHandler={{
issue: {
widgetCallback: issueEmbedProps.widgetCallback,
},
page: {
widgetCallback: ({ pageId: pageIdFromNode }) => (
<PageEmbedCardRoot
@@ -197,6 +182,9 @@ export const PageEmbedPreview: React.FC<Props> = observer((props) => {
workspaceSlug: workspaceSlug.toString(),
},
}}
projectId={projectId?.toString() ?? ""}
workspaceId={workspaceDetails?.id ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
/>
</div>

View File

@@ -1,40 +1,39 @@
import { useMemo } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane editor
import { DocumentReadOnlyEditorWithRef, TDisplayConfig } from "@plane/editor";
import type { TDisplayConfig } from "@plane/editor";
// plane ui
import { Loader } from "@plane/ui";
// components
import { EditorMentionsRoot } from "@/components/editor";
import { DocumentEditor } from "@/components/editor/document/editor";
import { TVersionEditorProps } from "@/components/pages";
// hooks
import { useEditorConfig } from "@/hooks/editor";
import { useWorkspace } from "@/hooks/store";
import { usePageFilters } from "@/hooks/use-page-filters";
// plane web components
import { IssueEmbedCard } from "@/plane-web/components/pages";
// plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
export const WorkspacePagesVersionEditor: React.FC<TVersionEditorProps> = observer((props) => {
const { activeVersion, versionDetails } = props;
// params
// navigation
const { workspaceSlug } = useParams();
// store hooks
const { getWorkspaceBySlug } = useWorkspace();
// derived values
const workspaceDetails = getWorkspaceBySlug(workspaceSlug?.toString() ?? "");
// editor config
const { getReadOnlyEditorFileHandlers } = useEditorConfig();
// editor flagging
const { document: documentEditorExtensions } = useEditorFlagging(workspaceSlug?.toString() ?? "");
// page filters
const { fontSize, fontStyle } = usePageFilters();
const displayConfig: TDisplayConfig = {
fontSize,
fontStyle,
};
const displayConfig: TDisplayConfig = useMemo(
() => ({
fontSize,
fontStyle,
}),
[fontSize, fontStyle]
);
if (!versionDetails)
return (
@@ -78,36 +77,21 @@ export const WorkspacePagesVersionEditor: React.FC<TVersionEditorProps> = observ
</div>
);
const description = versionDetails?.description_html;
if (description === undefined || description?.trim() === "") return null;
const description = versionDetails?.description_json;
if (!description) return null;
return (
<DocumentReadOnlyEditorWithRef
<DocumentEditor
editable={false}
id={activeVersion ?? ""}
initialValue={description ?? "<p></p>"}
value={description}
containerClassName="p-0 pb-64 border-none"
disabledExtensions={documentEditorExtensions.disabled}
flaggedExtensions={documentEditorExtensions.flagged}
displayConfig={displayConfig}
editorClassName="pl-10"
fileHandler={getReadOnlyEditorFileHandlers({
workspaceId: workspaceDetails?.id ?? "",
workspaceSlug: workspaceSlug?.toString() ?? "",
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
}}
embedHandler={{
issue: {
widgetCallback: ({ issueId, projectId: projectIdFromEmbed, workspaceSlug: workspaceSlugFromEmbed }) => {
const resolvedProjectId = projectIdFromEmbed ?? "";
const resolvedWorkspaceSlug = workspaceSlugFromEmbed ?? workspaceSlug?.toString() ?? "";
return (
<IssueEmbedCard issueId={issueId} projectId={resolvedProjectId} workspaceSlug={resolvedWorkspaceSlug} />
);
},
},
}}
workspaceId={workspaceDetails?.id ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
/>
);
});

View File

@@ -1,17 +1,11 @@
import { useCallback, useMemo } from "react";
import { useMemo } from "react";
import { observer } from "mobx-react";
// plane imports
import { DocumentEditorWithRef, TExtensions } from "@plane/editor";
import { EFileAssetType, TSearchEntityRequestPayload } from "@plane/types";
import { generateRandomColor, hslToHex } from "@plane/utils";
// components
import { EditorMentionsRoot } from "@/components/editor";
import { DocumentEditor } from "@/components/editor/document/editor";
// hooks
import { useEditorConfig, useEditorMention } from "@/hooks/editor";
import { useEditorAsset, useMember, useUser, useUserProfile, useWorkspace } from "@/hooks/store";
// plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed";
import { useEditorAsset, useWorkspace } from "@/hooks/store";
// services
import { WorkspaceService } from "@/plane-web/services";
const workspaceService = new WorkspaceService();
@@ -26,97 +20,44 @@ type Props = {
export const PageTemplateEditor = observer((props: Props) => {
const { workspaceSlug, projectId, templateId, initialValue, onChange } = props;
// store hooks
const { data: currentUser } = useUser();
const {
data: { is_smooth_cursor_enabled },
} = useUserProfile();
const { getUserDetails } = useMember();
const { getWorkspaceBySlug } = useWorkspace();
const { uploadEditorAsset } = useEditorAsset();
const { getEditorFileHandlers } = useEditorConfig();
const { document: documentEditorExtensions } = useEditorFlagging(workspaceSlug);
// derived values
const additionalDisabledExtensions: TExtensions[] = ["issue-embed"];
const disabledExtensions = [...documentEditorExtensions.disabled, ...additionalDisabledExtensions];
const workspaceId = useMemo(
() => (workspaceSlug ? (getWorkspaceBySlug(workspaceSlug)?.id ?? "") : ""),
[getWorkspaceBySlug, workspaceSlug]
);
// user config
const userConfig = useMemo(
() => ({
id: currentUser?.id ?? "",
name: currentUser?.display_name ?? "",
color: hslToHex(generateRandomColor(currentUser?.id ?? "")),
}),
[currentUser?.display_name, currentUser?.id]
);
// editor configs
// file handler
const fileHandler = useMemo(
() =>
getEditorFileHandlers({
uploadFile: async (blockId, file) => {
const { asset_id } = await uploadEditorAsset({
blockId,
file,
data: {
entity_identifier: templateId ?? "",
entity_type: EFileAssetType.PAGE_TEMPLATE_DESCRIPTION,
},
workspaceSlug: workspaceSlug?.toString() ?? "",
});
return asset_id;
},
workspaceId,
workspaceSlug: workspaceSlug?.toString() ?? "",
}),
[getEditorFileHandlers, templateId, uploadEditorAsset, workspaceId, workspaceSlug]
);
// entity search handler
const fetchEntityCallback = useCallback(
async (payload: TSearchEntityRequestPayload) =>
await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
...payload,
project_id: projectId?.toString() ?? "",
}),
[projectId, workspaceSlug]
);
// issue-embed
const { issueEmbedProps } = useIssueEmbed({
fetchEmbedSuggestions: fetchEntityCallback,
workspaceSlug,
});
// use editor mention
const { fetchMentions } = useEditorMention({
searchEntity: fetchEntityCallback,
});
return (
<DocumentEditorWithRef
disabledExtensions={disabledExtensions}
flaggedExtensions={documentEditorExtensions.flagged}
fileHandler={fileHandler}
<DocumentEditor
disabledExtensions={["issue-embed"]}
editable
id="page-template-editor"
initialValue={initialValue}
value={initialValue}
onChange={(json, html) => onChange(json, html)}
user={userConfig}
embedHandler={{
issue: issueEmbedProps,
}}
mentionHandler={{
searchCallback: async (query) => {
const res = await fetchMentions(query);
if (!res) throw new Error("Failed in fetching mentions");
return res;
},
renderComponent: (props) => <EditorMentionsRoot {...props} />,
getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }),
}}
containerClassName="min-h-[120px] border-none pl-4 -ml-4"
isSmoothCursorEnabled={is_smooth_cursor_enabled}
uploadFile={async (blockId, file) => {
const { asset_id } = await uploadEditorAsset({
blockId,
file,
data: {
entity_identifier: templateId ?? "",
entity_type: EFileAssetType.PAGE_TEMPLATE_DESCRIPTION,
},
workspaceSlug: workspaceSlug?.toString() ?? "",
});
return asset_id;
}}
searchMentionCallback={async (payload: TSearchEntityRequestPayload) =>
await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
...payload,
project_id: projectId?.toString() ?? "",
})
}
bubbleMenuEnabled
projectId={projectId}
workspaceId={workspaceId}
workspaceSlug={workspaceSlug}
/>
);
});

View File

@@ -30,8 +30,8 @@ const DocumentEditor = (props: IDocumentEditorProps) => {
flaggedExtensions,
forwardedRef,
id,
isSmoothCursorEnabled,
handleEditorReady,
isSmoothCursorEnabled,
mentionHandler,
onChange,
user,

View File

@@ -188,10 +188,12 @@ export interface ICollaborativeDocumentEditorProps
titleRef?: React.MutableRefObject<EditorTitleRefApi | null>;
}
export interface IDocumentEditor extends Omit<IEditorProps, "onEnterKeyPress" | "value"> {
export interface IDocumentEditorProps extends Omit<IEditorProps, "initialValue" | "onEnterKeyPress" | "value"> {
aiHandler?: TAIHandler;
editable: boolean;
embedHandler: TEmbedConfig;
user: TUserDetails;
user?: TUserDetails;
value: Content;
}
export interface IDocumentEditorProps extends Omit<IEditorProps, "initialValue" | "onEnterKeyPress" | "value"> {