diff --git a/packages/editor/src/ce/extensions/document-extensions.tsx b/packages/editor/src/ce/extensions/document-extensions.tsx index 29072b41c36..da2790b2a4f 100644 --- a/packages/editor/src/ce/extensions/document-extensions.tsx +++ b/packages/editor/src/ce/extensions/document-extensions.tsx @@ -2,30 +2,31 @@ import { HocuspocusProvider } from "@hocuspocus/provider"; import { AnyExtension } from "@tiptap/core"; import { SlashCommands } from "@/extensions"; // plane editor types -import { TIssueEmbedConfig } from "@/plane-editor/types"; +import { TEmbedConfig } from "@/plane-editor/types"; // types -import { TExtensions, TUserDetails } from "@/types"; +import { TExtensions, TFileHandler, TUserDetails } from "@/types"; -type Props = { - disabledExtensions?: TExtensions[]; - issueEmbedConfig: TIssueEmbedConfig | undefined; - provider: HocuspocusProvider; +export type TDocumentEditorAdditionalExtensionsProps = { + disabledExtensions: TExtensions[]; + embedConfig: TEmbedConfig | undefined; + fileHandler: TFileHandler; + provider?: HocuspocusProvider; userDetails: TUserDetails; }; -type ExtensionConfig = { +export type TDocumentEditorAdditionalExtensionsRegistry = { isEnabled: (disabledExtensions: TExtensions[]) => boolean; - getExtension: (props: Props) => AnyExtension; + getExtension: (props: TDocumentEditorAdditionalExtensionsProps) => AnyExtension; }; -const extensionRegistry: ExtensionConfig[] = [ +const extensionRegistry: TDocumentEditorAdditionalExtensionsRegistry[] = [ { isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"), - getExtension: () => SlashCommands({}), + getExtension: ({ disabledExtensions }) => SlashCommands({ disabledExtensions }), }, ]; -export const DocumentEditorAdditionalExtensions = (_props: Props) => { +export const DocumentEditorAdditionalExtensions = (_props: TDocumentEditorAdditionalExtensionsProps) => { const { disabledExtensions = [] } = _props; const documentExtensions = extensionRegistry diff --git a/packages/editor/src/ce/extensions/rich-text/extensions.tsx b/packages/editor/src/ce/extensions/rich-text/extensions.tsx new file mode 100644 index 00000000000..0eedd1e87fe --- /dev/null +++ b/packages/editor/src/ce/extensions/rich-text/extensions.tsx @@ -0,0 +1,41 @@ +import { AnyExtension, Extensions } from "@tiptap/core"; +// extensions +import { SlashCommands } from "@/extensions/slash-commands/root"; +// types +import { TExtensions, TFileHandler } from "@/types"; + +export type TRichTextEditorAdditionalExtensionsProps = { + disabledExtensions: TExtensions[]; + fileHandler: TFileHandler; +}; + +/** + * Registry entry configuration for extensions + */ +export type TRichTextEditorAdditionalExtensionsRegistry = { + /** Determines if the extension should be enabled based on disabled extensions */ + isEnabled: (disabledExtensions: TExtensions[]) => boolean; + /** Returns the extension instance(s) when enabled */ + getExtension: (props: TRichTextEditorAdditionalExtensionsProps) => AnyExtension | undefined; +}; + +const extensionRegistry: TRichTextEditorAdditionalExtensionsRegistry[] = [ + { + isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"), + getExtension: ({ disabledExtensions }) => + SlashCommands({ + disabledExtensions, + }), + }, +]; + +export const RichTextEditorAdditionalExtensions = (props: TRichTextEditorAdditionalExtensionsProps) => { + const { disabledExtensions } = props; + + const extensions: Extensions = extensionRegistry + .filter((config) => config.isEnabled(disabledExtensions)) + .map((config) => config.getExtension(props)) + .filter((extension): extension is AnyExtension => extension !== undefined); + + return extensions; +}; diff --git a/packages/editor/src/ce/extensions/rich-text/read-only-extensions.tsx b/packages/editor/src/ce/extensions/rich-text/read-only-extensions.tsx new file mode 100644 index 00000000000..01563011707 --- /dev/null +++ b/packages/editor/src/ce/extensions/rich-text/read-only-extensions.tsx @@ -0,0 +1,31 @@ +import { AnyExtension, Extensions } from "@tiptap/core"; +// types +import { TExtensions, TReadOnlyFileHandler } from "@/types"; + +export type TRichTextReadOnlyEditorAdditionalExtensionsProps = { + disabledExtensions: TExtensions[]; + fileHandler: TReadOnlyFileHandler; +}; + +/** + * Registry entry configuration for extensions + */ +export type TRichTextReadOnlyEditorAdditionalExtensionsRegistry = { + /** Determines if the extension should be enabled based on disabled extensions */ + isEnabled: (disabledExtensions: TExtensions[]) => boolean; + /** Returns the extension instance(s) when enabled */ + getExtension: (props: TRichTextReadOnlyEditorAdditionalExtensionsProps) => AnyExtension | undefined; +}; + +const extensionRegistry: TRichTextReadOnlyEditorAdditionalExtensionsRegistry[] = []; + +export const RichTextReadOnlyEditorAdditionalExtensions = (props: TRichTextReadOnlyEditorAdditionalExtensionsProps) => { + const { disabledExtensions } = props; + + const extensions: Extensions = extensionRegistry + .filter((config) => config.isEnabled(disabledExtensions)) + .map((config) => config.getExtension(props)) + .filter((extension): extension is AnyExtension => extension !== undefined); + + return extensions; +}; diff --git a/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx b/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx index 6cd360ac0d7..ffed8ba8bd3 100644 --- a/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx +++ b/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx @@ -15,6 +15,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => { disabledExtensions, displayConfig = DEFAULT_DISPLAY_CONFIG, editorClassName = "", + extensions, fileHandler, forwardedRef, id, @@ -25,6 +26,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => { const editor = useReadOnlyEditor({ disabledExtensions, editorClassName, + extensions, fileHandler, forwardedRef, initialValue, diff --git a/packages/editor/src/core/components/editors/rich-text/editor.tsx b/packages/editor/src/core/components/editors/rich-text/editor.tsx index ffcc21da6c7..2334d9babaf 100644 --- a/packages/editor/src/core/components/editors/rich-text/editor.tsx +++ b/packages/editor/src/core/components/editors/rich-text/editor.tsx @@ -3,12 +3,20 @@ import { forwardRef, useCallback } from "react"; import { EditorWrapper } from "@/components/editors"; import { EditorBubbleMenu } from "@/components/menus"; // extensions -import { SideMenuExtension, SlashCommands } from "@/extensions"; +import { SideMenuExtension } from "@/extensions"; +// plane editor imports +import { RichTextEditorAdditionalExtensions } from "@/plane-editor/extensions/rich-text/extensions"; // types import { EditorRefApi, IRichTextEditor } from "@/types"; const RichTextEditor = (props: IRichTextEditor) => { - const { disabledExtensions, dragDropEnabled, bubbleMenuEnabled = true, extensions: externalExtensions = [] } = props; + const { + disabledExtensions, + dragDropEnabled, + fileHandler, + bubbleMenuEnabled = true, + extensions: externalExtensions = [], + } = props; const getExtensions = useCallback(() => { const extensions = [ @@ -17,17 +25,14 @@ const RichTextEditor = (props: IRichTextEditor) => { aiEnabled: false, dragDropEnabled: !!dragDropEnabled, }), + ...RichTextEditorAdditionalExtensions({ + disabledExtensions, + fileHandler, + }), ]; - if (!disabledExtensions?.includes("slash-commands")) { - extensions.push( - SlashCommands({ - disabledExtensions, - }) - ); - } return extensions; - }, [dragDropEnabled, disabledExtensions, externalExtensions]); + }, [dragDropEnabled, disabledExtensions, externalExtensions, fileHandler]); return ( diff --git a/packages/editor/src/core/components/editors/rich-text/read-only-editor.tsx b/packages/editor/src/core/components/editors/rich-text/read-only-editor.tsx index 8bd7a837a53..18d960ca53f 100644 --- a/packages/editor/src/core/components/editors/rich-text/read-only-editor.tsx +++ b/packages/editor/src/core/components/editors/rich-text/read-only-editor.tsx @@ -1,11 +1,33 @@ -import { forwardRef } from "react"; +import { forwardRef, useCallback } from "react"; +// plane editor extensions +import { RichTextReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions/rich-text/read-only-extensions"; // types import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor } from "@/types"; +// local imports import { ReadOnlyEditorWrapper } from "../read-only-editor-wrapper"; -const RichTextReadOnlyEditorWithRef = forwardRef((props, ref) => ( - } /> -)); +const RichTextReadOnlyEditorWithRef = forwardRef((props, ref) => { + const { disabledExtensions, fileHandler } = props; + + const getExtensions = useCallback(() => { + const extensions = [ + ...RichTextReadOnlyEditorAdditionalExtensions({ + disabledExtensions, + fileHandler, + }), + ]; + + return extensions; + }, [disabledExtensions, fileHandler]); + + return ( + } + /> + ); +}); RichTextReadOnlyEditorWithRef.displayName = "RichReadOnlyEditorWithRef"; diff --git a/packages/editor/src/core/extensions/extensions.ts b/packages/editor/src/core/extensions/extensions.ts index 51969cd5cfa..eb6edd0848c 100644 --- a/packages/editor/src/core/extensions/extensions.ts +++ b/packages/editor/src/core/extensions/extensions.ts @@ -170,8 +170,9 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { CustomTextAlignExtension, CustomCalloutExtension, UtilityExtension({ - isEditable: editable, + disabledExtensions, fileHandler, + isEditable: editable, }), CustomColorExtension, ...CoreEditorAdditionalExtensions({ diff --git a/packages/editor/src/core/extensions/read-only-extensions.ts b/packages/editor/src/core/extensions/read-only-extensions.ts index bcfc7641159..b28e8a67aba 100644 --- a/packages/editor/src/core/extensions/read-only-extensions.ts +++ b/packages/editor/src/core/extensions/read-only-extensions.ts @@ -127,8 +127,9 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => { CustomTextAlignExtension, CustomCalloutReadOnlyExtension, UtilityExtension({ - isEditable: false, + disabledExtensions, fileHandler, + isEditable: false, }), ...CoreReadOnlyEditorAdditionalExtensions({ disabledExtensions, diff --git a/packages/editor/src/core/extensions/utility.ts b/packages/editor/src/core/extensions/utility.ts index 1d656de5a8e..9252e300c7d 100644 --- a/packages/editor/src/core/extensions/utility.ts +++ b/packages/editor/src/core/extensions/utility.ts @@ -8,7 +8,7 @@ import { DropHandlerPlugin } from "@/plugins/drop"; import { FilePlugins } from "@/plugins/file/root"; import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard"; // types -import { TFileHandler, TReadOnlyFileHandler } from "@/types"; +import { TExtensions, TFileHandler, TReadOnlyFileHandler } from "@/types"; declare module "@tiptap/core" { interface Commands { @@ -24,13 +24,14 @@ export interface UtilityExtensionStorage { } type Props = { + disabledExtensions: TExtensions[]; fileHandler: TFileHandler | TReadOnlyFileHandler; isEditable: boolean; }; export const UtilityExtension = (props: Props) => { - const { fileHandler, isEditable } = props; - const { restore: restoreImageFn } = fileHandler; + const { disabledExtensions, fileHandler, isEditable } = props; + const { restore } = fileHandler; return Extension.create, UtilityExtensionStorage>({ name: "utility", @@ -45,12 +46,15 @@ export const UtilityExtension = (props: Props) => { }), ...codemark({ markType: this.editor.schema.marks.code }), MarkdownClipboardPlugin(this.editor), - DropHandlerPlugin(this.editor), + DropHandlerPlugin({ + disabledExtensions, + editor: this.editor, + }), ]; }, onCreate() { - restorePublicImages(this.editor, restoreImageFn); + restorePublicImages(this.editor, restore); }, addStorage() { diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index 8677b29edb9..11e7ba3208e 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -92,7 +92,8 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { ...(extensions ?? []), ...DocumentEditorAdditionalExtensions({ disabledExtensions, - issueEmbedConfig: embedHandler?.issue, + embedConfig: embedHandler, + fileHandler, provider, userDetails: user, }), diff --git a/packages/editor/src/core/plugins/drop.ts b/packages/editor/src/core/plugins/drop.ts index a0bb65779fd..fbbfbf54db9 100644 --- a/packages/editor/src/core/plugins/drop.ts +++ b/packages/editor/src/core/plugins/drop.ts @@ -3,10 +3,17 @@ import { Plugin, PluginKey } from "@tiptap/pm/state"; // constants import { ACCEPTED_ATTACHMENT_MIME_TYPES, ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; // types -import { TEditorCommands } from "@/types"; +import { TEditorCommands, TExtensions } from "@/types"; -export const DropHandlerPlugin = (editor: Editor): Plugin => - new Plugin({ +type Props = { + disabledExtensions: TExtensions[]; + editor: Editor; +}; + +export const DropHandlerPlugin = (props: Props): Plugin => { + const { disabledExtensions, editor } = props; + + return new Plugin({ key: new PluginKey("drop-handler-plugin"), props: { handlePaste: (view, event) => { @@ -25,6 +32,7 @@ export const DropHandlerPlugin = (editor: Editor): Plugin => if (acceptedFiles.length) { const pos = view.state.selection.from; insertFilesSafely({ + disabledExtensions, editor, files: acceptedFiles, initialPos: pos, @@ -58,6 +66,7 @@ export const DropHandlerPlugin = (editor: Editor): Plugin => if (coordinates) { const pos = coordinates.pos; insertFilesSafely({ + disabledExtensions, editor, files: acceptedFiles, initialPos: pos, @@ -71,8 +80,10 @@ export const DropHandlerPlugin = (editor: Editor): Plugin => }, }, }); +}; type InsertFilesSafelyArgs = { + disabledExtensions: TExtensions[]; editor: Editor; event: "insert" | "drop"; files: File[]; @@ -81,7 +92,7 @@ type InsertFilesSafelyArgs = { }; export const insertFilesSafely = async (args: InsertFilesSafelyArgs) => { - const { editor, event, files, initialPos, type } = args; + const { disabledExtensions, editor, event, files, initialPos, type } = args; let pos = initialPos; for (const file of files) { @@ -100,7 +111,7 @@ export const insertFilesSafely = async (args: InsertFilesSafelyArgs) => { else if (ACCEPTED_ATTACHMENT_MIME_TYPES.includes(file.type)) fileType = "attachment"; } // insert file depending on the type at the current position - if (fileType === "image") { + if (fileType === "image" && !disabledExtensions.includes("image")) { editor.commands.insertImageComponent({ file, pos, diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index 647f52e7d43..ace1048ce8c 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -160,6 +160,7 @@ export interface IReadOnlyEditorProps { disabledExtensions: TExtensions[]; displayConfig?: TDisplayConfig; editorClassName?: string; + extensions?: Extensions; fileHandler: TReadOnlyFileHandler; forwardedRef?: React.MutableRefObject; id: string;