diff --git a/apps/web/core/components/editor/document/editor.tsx b/apps/web/core/components/editor/document/editor.tsx new file mode 100644 index 00000000000..a579430d9a1 --- /dev/null +++ b/apps/web/core/components/editor/document/editor.tsx @@ -0,0 +1,92 @@ +import React, { forwardRef } from "react"; +// plane imports +import { DocumentEditorWithRef, EditorRefApi, IDocumentEditorProps, TFileHandler } from "@plane/editor"; +import { MakeOptional, TSearchEntityRequestPayload, TSearchResponse } from "@plane/types"; +import { cn } from "@plane/utils"; +// components +import { EditorMentionsRoot } from "@/components/editor"; +// hooks +import { useEditorConfig, useEditorMention } from "@/hooks/editor"; +import { useMember } from "@/hooks/store"; +// plane web hooks +import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; +import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed"; + +type DocumentEditorWrapperProps = MakeOptional< + Omit, + "disabledExtensions" | "editable" | "flaggedExtensions" +> & { + embedHandler?: Partial; + workspaceSlug: string; + workspaceId: string; + projectId?: string; +} & ( + | { + editable: false; + } + | { + editable: true; + searchMentionCallback: (payload: TSearchEntityRequestPayload) => Promise; + uploadFile: TFileHandler["upload"]; + } + ); + +export const DocumentEditor = forwardRef((props, ref) => { + const { + containerClassName, + editable, + embedHandler, + workspaceSlug, + workspaceId, + projectId, + disabledExtensions: additionalDisabledExtensions = [], + ...rest + } = props; + // store hooks + const { getUserDetails } = useMember(); + // editor flaggings + const { document: documentEditorExtensions } = useEditorFlagging(workspaceSlug); + // use editor mention + const { fetchMentions } = useEditorMention({ + searchEntity: editable ? async (payload) => await props.searchMentionCallback(payload) : async () => ({}), + }); + // editor config + const { getEditorFileHandlers } = useEditorConfig(); + // issue-embed + const { issueEmbedProps } = useIssueEmbed({ + projectId, + workspaceSlug, + }); + + return ( + "", + workspaceId, + workspaceSlug, + })} + mentionHandler={{ + searchCallback: async (query) => { + const res = await fetchMentions(query); + if (!res) throw new Error("Failed in fetching mentions"); + return res; + }, + renderComponent: EditorMentionsRoot, + getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }), + }} + embedHandler={{ + issue: issueEmbedProps, + ...embedHandler, + }} + {...rest} + containerClassName={cn("relative pl-3 pb-3", containerClassName)} + /> + ); +}); + +DocumentEditor.displayName = "DocumentEditor"; diff --git a/apps/web/core/components/editor/index.ts b/apps/web/core/components/editor/index.ts index 674bbdf1582..c80c733527f 100644 --- a/apps/web/core/components/editor/index.ts +++ b/apps/web/core/components/editor/index.ts @@ -1,5 +1,5 @@ export * from "./embeds"; -export * from "./lite-text-editor"; +export * from "./lite-text"; export * from "./pdf"; -export * from "./rich-text-editor"; +export * from "./rich-text"; export * from "./sticky-editor"; diff --git a/apps/web/core/components/editor/lite-text-editor/index.ts b/apps/web/core/components/editor/lite-text-editor/index.ts deleted file mode 100644 index 661c8e75541..00000000000 --- a/apps/web/core/components/editor/lite-text-editor/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./lite-text-editor"; -export * from "./lite-text-read-only-editor"; -export * from "./toolbar"; diff --git a/apps/web/core/components/editor/lite-text-editor/lite-text-editor.tsx b/apps/web/core/components/editor/lite-text/editor.tsx similarity index 100% rename from apps/web/core/components/editor/lite-text-editor/lite-text-editor.tsx rename to apps/web/core/components/editor/lite-text/editor.tsx diff --git a/apps/web/core/components/editor/lite-text/index.ts b/apps/web/core/components/editor/lite-text/index.ts new file mode 100644 index 00000000000..a53feeb8277 --- /dev/null +++ b/apps/web/core/components/editor/lite-text/index.ts @@ -0,0 +1,3 @@ +export * from "./editor"; +export * from "./read-only-editor"; +export * from "./toolbar"; diff --git a/apps/web/core/components/editor/lite-text-editor/lite-text-read-only-editor.tsx b/apps/web/core/components/editor/lite-text/read-only-editor.tsx similarity index 100% rename from apps/web/core/components/editor/lite-text-editor/lite-text-read-only-editor.tsx rename to apps/web/core/components/editor/lite-text/read-only-editor.tsx diff --git a/apps/web/core/components/editor/lite-text-editor/toolbar.tsx b/apps/web/core/components/editor/lite-text/toolbar.tsx similarity index 100% rename from apps/web/core/components/editor/lite-text-editor/toolbar.tsx rename to apps/web/core/components/editor/lite-text/toolbar.tsx diff --git a/apps/web/core/components/editor/rich-text-editor/index.ts b/apps/web/core/components/editor/rich-text-editor/index.ts deleted file mode 100644 index 49fdb69dd1c..00000000000 --- a/apps/web/core/components/editor/rich-text-editor/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./rich-text-editor"; diff --git a/apps/web/core/components/editor/rich-text-editor/rich-text-editor.tsx b/apps/web/core/components/editor/rich-text/editor.tsx similarity index 100% rename from apps/web/core/components/editor/rich-text-editor/rich-text-editor.tsx rename to apps/web/core/components/editor/rich-text/editor.tsx diff --git a/apps/web/core/components/editor/rich-text/index.ts b/apps/web/core/components/editor/rich-text/index.ts new file mode 100644 index 00000000000..8b1fd904bb0 --- /dev/null +++ b/apps/web/core/components/editor/rich-text/index.ts @@ -0,0 +1 @@ +export * from "./editor"; diff --git a/apps/web/core/components/inbox/modals/create-modal/issue-description.tsx b/apps/web/core/components/inbox/modals/create-modal/issue-description.tsx index 907f48e700f..09679cf313f 100644 --- a/apps/web/core/components/inbox/modals/create-modal/issue-description.tsx +++ b/apps/web/core/components/inbox/modals/create-modal/issue-description.tsx @@ -10,7 +10,7 @@ import { EFileAssetType, TIssue } from "@plane/types"; import { Loader } from "@plane/ui"; import { getDescriptionPlaceholderI18n, getTabIndex } from "@plane/utils"; // components -import { RichTextEditor } from "@/components/editor/rich-text-editor/rich-text-editor"; +import { RichTextEditor } from "@/components/editor/rich-text/editor"; // hooks import { useEditorAsset, useProjectInbox } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/apps/web/core/components/pages/version/editor.tsx b/apps/web/core/components/pages/version/editor.tsx index 1a2c23e2872..b4aec1bb5ba 100644 --- a/apps/web/core/components/pages/version/editor.tsx +++ b/apps/web/core/components/pages/version/editor.tsx @@ -1,18 +1,14 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // plane imports -import { DocumentReadOnlyEditorWithRef, TDisplayConfig } from "@plane/editor"; +import { TDisplayConfig } from "@plane/editor"; import { TPageVersion } from "@plane/types"; import { Loader } from "@plane/ui"; // components -import { EditorMentionsRoot } from "@/components/editor"; +import { DocumentEditor } from "@/components/editor/document/editor"; // hooks -import { useEditorConfig } from "@/hooks/editor"; -import { useMember, useWorkspace } from "@/hooks/store"; +import { useWorkspace } from "@/hooks/store"; import { usePageFilters } from "@/hooks/use-page-filters"; -// plane web hooks -import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; -import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed"; export type TVersionEditorProps = { activeVersion: string | null; @@ -21,23 +17,12 @@ export type TVersionEditorProps = { export const PagesVersionEditor: React.FC = observer((props) => { const { activeVersion, versionDetails } = props; - // store hooks - const { getUserDetails } = useMember(); // params const { workspaceSlug, projectId } = useParams(); // store hooks const { getWorkspaceBySlug } = useWorkspace(); // derived values const workspaceDetails = getWorkspaceBySlug(workspaceSlug?.toString() ?? ""); - // editor flaggings - const { document: documentEditorExtensions } = useEditorFlagging(workspaceSlug?.toString() ?? ""); - // editor config - const { getReadOnlyEditorFileHandlers } = useEditorConfig(); - // issue-embed - const { issueEmbedProps } = useIssueEmbed({ - projectId: projectId?.toString() ?? "", - workspaceSlug: workspaceSlug?.toString() ?? "", - }); // page filters const { fontSize, fontStyle } = usePageFilters(); @@ -89,32 +74,21 @@ export const PagesVersionEditor: React.FC = observer((props ); - const description = versionDetails?.description_html; - if (description === undefined || description?.trim() === "") return null; + const description = versionDetails?.description_json; + if (!description) return null; return ( -

"} + value={description} containerClassName="p-0 pb-64 border-none" - disabledExtensions={documentEditorExtensions.disabled} - flaggedExtensions={documentEditorExtensions.flagged} displayConfig={displayConfig} editorClassName="pl-10" - fileHandler={getReadOnlyEditorFileHandlers({ - projectId: projectId?.toString() ?? "", - workspaceId: workspaceDetails?.id ?? "", - workspaceSlug: workspaceSlug?.toString() ?? "", - })} - mentionHandler={{ - renderComponent: (props) => , - getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }), - }} - embedHandler={{ - issue: { - widgetCallback: issueEmbedProps.widgetCallback, - }, - }} + projectId={projectId?.toString()} + workspaceId={workspaceDetails?.id ?? ""} + workspaceSlug={workspaceSlug?.toString() ?? ""} /> ); }); diff --git a/packages/editor/src/ce/extensions/document-extensions.tsx b/packages/editor/src/ce/extensions/document-extensions.tsx index 8815e2d26e9..8260da7d434 100644 --- a/packages/editor/src/ce/extensions/document-extensions.tsx +++ b/packages/editor/src/ce/extensions/document-extensions.tsx @@ -11,6 +11,7 @@ export type TDocumentEditorAdditionalExtensionsProps = Pick< "disabledExtensions" | "flaggedExtensions" | "fileHandler" > & { embedConfig: TEmbedConfig | undefined; + isEditable: boolean; provider?: HocuspocusProvider; userDetails: TUserDetails; }; diff --git a/packages/editor/src/core/components/editors/document/editor.tsx b/packages/editor/src/core/components/editors/document/editor.tsx new file mode 100644 index 00000000000..150d943fdfd --- /dev/null +++ b/packages/editor/src/core/components/editors/document/editor.tsx @@ -0,0 +1,109 @@ +import { Extensions } from "@tiptap/core"; +import { forwardRef, MutableRefObject, useMemo } from "react"; +// plane imports +import { cn } from "@plane/utils"; +// components +import { PageRenderer } from "@/components/editors"; +// constants +import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config"; +// extensions +import { HeadingListExtension, WorkItemEmbedExtension, SideMenuExtension } from "@/extensions"; +// helpers +import { getEditorClassNames } from "@/helpers/common"; +// hooks +import { useEditor } from "@/hooks/use-editor"; +// plane editor extensions +import { DocumentEditorAdditionalExtensions } from "@/plane-editor/extensions"; +// types +import { EditorRefApi, IDocumentEditorProps } from "@/types"; + +const DocumentEditor = (props: IDocumentEditorProps) => { + const { + bubbleMenuEnabled = false, + containerClassName, + disabledExtensions, + displayConfig = DEFAULT_DISPLAY_CONFIG, + editable, + editorClassName = "", + embedHandler, + fileHandler, + flaggedExtensions, + forwardedRef, + id, + handleEditorReady, + mentionHandler, + onChange, + user, + value, + } = props; + const extensions: Extensions = useMemo(() => { + const additionalExtensions: Extensions = []; + if (embedHandler?.issue) { + additionalExtensions.push( + WorkItemEmbedExtension({ + widgetCallback: embedHandler.issue.widgetCallback, + }) + ); + } + additionalExtensions.push( + SideMenuExtension({ + aiEnabled: !disabledExtensions?.includes("ai"), + dragDropEnabled: true, + }), + HeadingListExtension, + ...DocumentEditorAdditionalExtensions({ + disabledExtensions, + embedConfig: embedHandler, + flaggedExtensions, + isEditable: editable, + fileHandler, + userDetails: user ?? { + id: "", + name: "", + color: "", + }, + }) + ); + return additionalExtensions; + }, []); + + const editor = useEditor({ + disabledExtensions, + editable, + editorClassName, + enableHistory: true, + extensions, + fileHandler, + flaggedExtensions, + forwardedRef, + handleEditorReady, + id, + initialValue: value, + mentionHandler, + onChange, + }); + + const editorContainerClassName = getEditorClassNames({ + containerClassName, + }); + + if (!editor) return null; + + return ( + + ); +}; + +const DocumentEditorWithRef = forwardRef((props, ref) => ( + } /> +)); + +DocumentEditorWithRef.displayName = "DocumentEditorWithRef"; + +export { DocumentEditorWithRef }; diff --git a/packages/editor/src/core/components/editors/document/index.ts b/packages/editor/src/core/components/editors/document/index.ts index 571cb7e9a1f..8a5bffd178b 100644 --- a/packages/editor/src/core/components/editors/document/index.ts +++ b/packages/editor/src/core/components/editors/document/index.ts @@ -1,4 +1,4 @@ export * from "./collaborative-editor"; +export * from "./editor"; export * from "./loader"; export * from "./page-renderer"; -export * from "./read-only-editor"; diff --git a/packages/editor/src/core/components/editors/document/read-only-editor.tsx b/packages/editor/src/core/components/editors/document/read-only-editor.tsx deleted file mode 100644 index 8f0d67ddc0d..00000000000 --- a/packages/editor/src/core/components/editors/document/read-only-editor.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Extensions } from "@tiptap/core"; -import React, { forwardRef, MutableRefObject } from "react"; -// plane imports -import { cn } from "@plane/utils"; -// components -import { PageRenderer } from "@/components/editors"; -// constants -import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config"; -// extensions -import { WorkItemEmbedExtension } from "@/extensions"; -// helpers -import { getEditorClassNames } from "@/helpers/common"; -// hooks -import { useReadOnlyEditor } from "@/hooks/use-read-only-editor"; -// types -import { EditorReadOnlyRefApi, IDocumentReadOnlyEditorProps } from "@/types"; - -const DocumentReadOnlyEditor: React.FC = (props) => { - const { - containerClassName, - disabledExtensions, - displayConfig = DEFAULT_DISPLAY_CONFIG, - editorClassName = "", - embedHandler, - fileHandler, - flaggedExtensions, - id, - forwardedRef, - handleEditorReady, - initialValue, - mentionHandler, - } = props; - const extensions: Extensions = []; - if (embedHandler?.issue) { - extensions.push( - WorkItemEmbedExtension({ - widgetCallback: embedHandler.issue.widgetCallback, - }) - ); - } - - const editor = useReadOnlyEditor({ - disabledExtensions, - editorClassName, - extensions, - fileHandler, - flaggedExtensions, - forwardedRef, - handleEditorReady, - initialValue, - mentionHandler, - }); - - const editorContainerClassName = getEditorClassNames({ - containerClassName, - }); - - if (!editor) return null; - - return ( - - ); -}; - -const DocumentReadOnlyEditorWithRef = forwardRef((props, ref) => ( - } /> -)); - -DocumentReadOnlyEditorWithRef.displayName = "DocumentReadOnlyEditorWithRef"; - -export { DocumentReadOnlyEditorWithRef }; diff --git a/packages/editor/src/core/helpers/scroll-to-node.ts b/packages/editor/src/core/helpers/scroll-to-node.ts index d74e7532c58..7e5aa0979a0 100644 --- a/packages/editor/src/core/helpers/scroll-to-node.ts +++ b/packages/editor/src/core/helpers/scroll-to-node.ts @@ -32,11 +32,8 @@ function scrollToNode(editor: Editor, pos: number): void { } } -export function scrollToNodeViaDOMCoordinates( - editor: Editor, - pos: number, - behavior?: "auto" | "smooth" | "instant" -): void { +// eslint-disable-next-line no-undef +export function scrollToNodeViaDOMCoordinates(editor: Editor, pos: number, behavior?: ScrollBehavior): void { const view = editor.view; // Get the coordinates of the position diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index 3b4b333e6af..e6c20b13f7b 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -98,6 +98,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) => embedConfig: embedHandler, fileHandler, flaggedExtensions, + isEditable: editable, provider, userDetails: user, }), diff --git a/packages/editor/src/core/hooks/use-editor-markings.tsx b/packages/editor/src/core/hooks/use-editor-markings.tsx deleted file mode 100644 index 76d02cd68e2..00000000000 --- a/packages/editor/src/core/hooks/use-editor-markings.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useCallback, useState } from "react"; - -export interface IMarking { - type: "heading"; - level: number; - text: string; - sequence: number; -} - -export const useEditorMarkings = () => { - const [markings, setMarkings] = useState([]); - - const updateMarkings = useCallback((html: string) => { - const parser = new DOMParser(); - const doc = parser.parseFromString(html, "text/html"); - const headings = doc.querySelectorAll("h1, h2, h3"); - const tempMarkings: IMarking[] = []; - let h1Sequence: number = 0; - let h2Sequence: number = 0; - let h3Sequence: number = 0; - - headings.forEach((heading) => { - const level = parseInt(heading.tagName[1]); // Extract the number from h1, h2, h3 - tempMarkings.push({ - type: "heading", - level: level, - text: heading.textContent || "", - sequence: level === 1 ? ++h1Sequence : level === 2 ? ++h2Sequence : ++h3Sequence, - }); - }); - - setMarkings(tempMarkings); - }, []); - - return { - updateMarkings, - markings, - }; -}; diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 1b82e0246ce..954ef61b84d 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -70,7 +70,7 @@ export const useEditor = (props: TEditorHookProps) => { }), ...extensions, ], - content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "

", + content: initialValue, onCreate: () => handleEditorReady?.(true), onTransaction: () => { onTransaction?.(); diff --git a/packages/editor/src/core/types/config.ts b/packages/editor/src/core/types/config.ts index 7ef685ad02d..8c1903cf2dc 100644 --- a/packages/editor/src/core/types/config.ts +++ b/packages/editor/src/core/types/config.ts @@ -46,3 +46,10 @@ export type TRealtimeConfig = { url: string; queryParams: TWebhookConnectionQueryParams; }; + +export type IMarking = { + type: "heading"; + level: number; + text: string; + sequence: number; +}; diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index d409b88283c..d511bb7f122 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -1,5 +1,5 @@ -import { Extensions, JSONContent } from "@tiptap/core"; -import { Selection } from "@tiptap/pm/state"; +import type { Content, Extensions, JSONContent } from "@tiptap/core"; +import type { Selection } from "@tiptap/pm/state"; // extension types import type { TTextAlign } from "@/extensions"; // helpers @@ -111,7 +111,8 @@ export interface EditorRefApi extends EditorReadOnlyRefApi { onDocumentInfoChange: (callback: (documentInfo: TDocumentInfo) => void) => () => void; onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void; onStateChange: (callback: () => void) => () => void; - scrollToNodeViaDOMCoordinates: (behavior?: "auto" | "smooth" | "instant", position?: number) => void; + // eslint-disable-next-line no-undef + scrollToNodeViaDOMCoordinates: (behavior?: ScrollBehavior, position?: number) => void; setEditorValueAtCursorPosition: (content: string) => void; setFocusAtPosition: (position: number) => void; setProviderDocument: (value: Uint8Array) => void; @@ -159,6 +160,14 @@ export interface ICollaborativeDocumentEditorProps user: TUserDetails; } +export interface IDocumentEditorProps extends Omit { + aiHandler?: TAIHandler; + editable: boolean; + embedHandler: TEmbedConfig; + user?: TUserDetails; + value: Content; +} + // read only editor props export interface IReadOnlyEditorProps extends Pick< @@ -180,10 +189,6 @@ export interface IReadOnlyEditorProps export type ILiteTextReadOnlyEditorProps = IReadOnlyEditorProps; -export interface IDocumentReadOnlyEditorProps extends IReadOnlyEditorProps { - embedHandler: TEmbedConfig; -} - export interface EditorEvents { beforeCreate: never; create: never; diff --git a/packages/editor/src/core/types/hook.ts b/packages/editor/src/core/types/hook.ts index 40974981b7d..fa014ceb905 100644 --- a/packages/editor/src/core/types/hook.ts +++ b/packages/editor/src/core/types/hook.ts @@ -1,4 +1,5 @@ import type { HocuspocusProvider } from "@hocuspocus/provider"; +import type { Content } from "@tiptap/core"; import type { EditorProps } from "@tiptap/pm/view"; // local imports import type { ICollaborativeDocumentEditorProps, IEditorProps, IReadOnlyEditorProps } from "./editor"; @@ -27,7 +28,7 @@ export type TEditorHookProps = TCoreHookProps & > & { editable: boolean; enableHistory: boolean; - initialValue?: string; + initialValue?: Content; provider?: HocuspocusProvider; }; diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 9cb374dce24..2cfc6891588 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -9,30 +9,18 @@ import "./styles/drag-drop.css"; // editors export { CollaborativeDocumentEditorWithRef, - DocumentReadOnlyEditorWithRef, + DocumentEditorWithRef, LiteTextEditorWithRef, LiteTextReadOnlyEditorWithRef, RichTextEditorWithRef, } from "@/components/editors"; -export { isCellSelection } from "@/extensions/table/table/utilities/helpers"; - // constants export * from "@/constants/common"; // helpers export * from "@/helpers/common"; -export * from "@/helpers/editor-commands"; export * from "@/helpers/yjs-utils"; -export * from "@/extensions/table/table"; - -// components -export * from "@/components/menus"; - -// hooks -export { useEditor } from "@/hooks/use-editor"; -export { type IMarking, useEditorMarkings } from "@/hooks/use-editor-markings"; -export { useReadOnlyEditor } from "@/hooks/use-read-only-editor"; export { CORE_EXTENSIONS } from "@/constants/extension"; export { ADDITIONAL_EXTENSIONS } from "@/plane-editor/constants/extensions";