Sync: Community Changes #3324

This commit is contained in:
sriram veeraghanta
2025-06-04 16:00:00 +05:30
committed by GitHub
16 changed files with 195 additions and 98 deletions

View File

@@ -1,36 +1,38 @@
import { HocuspocusProvider } from "@hocuspocus/provider";
import { AnyExtension } from "@tiptap/core";
import { AnyExtension, Extensions } 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 | undefined;
};
const extensionRegistry: ExtensionConfig[] = [
const extensionRegistry: TDocumentEditorAdditionalExtensionsRegistry[] = [
{
isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"),
getExtension: () => SlashCommands({}),
getExtension: ({ disabledExtensions }) => SlashCommands({ disabledExtensions }),
},
];
export const DocumentEditorAdditionalExtensions = (_props: Props) => {
const { disabledExtensions = [] } = _props;
export const DocumentEditorAdditionalExtensions = (props: TDocumentEditorAdditionalExtensionsProps) => {
const { disabledExtensions = [] } = props;
const documentExtensions = extensionRegistry
const documentExtensions: Extensions = extensionRegistry
.filter((config) => config.isEnabled(disabledExtensions))
.map((config) => config.getExtension(_props));
.map((config) => config.getExtension(props))
.filter((extension): extension is AnyExtension => extension !== undefined);
return documentExtensions;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -50,6 +50,7 @@ const DocumentEditor = (props: IDocumentEditor) => {
...DocumentEditorAdditionalExtensions({
disabledExtensions,
embedConfig: embedHandler,
fileHandler,
userDetails: user,
})
);

View File

@@ -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,

View File

@@ -3,7 +3,9 @@ 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";
@@ -11,6 +13,7 @@ const RichTextEditor = (props: IRichTextEditor) => {
const {
disabledExtensions,
dragDropEnabled,
fileHandler,
bubbleMenuEnabled = true,
extensions: externalExtensions = [],
isSmoothCursorEnabled,
@@ -23,17 +26,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 (
<EditorWrapper {...props} extensions={getExtensions()} isSmoothCursorEnabled={isSmoothCursorEnabled}>

View File

@@ -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<EditorReadOnlyRefApi, IRichTextReadOnlyEditor>((props, ref) => (
<ReadOnlyEditorWrapper {...props} forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>} />
));
const RichTextReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, IRichTextReadOnlyEditor>((props, ref) => {
const { disabledExtensions, fileHandler } = props;
const getExtensions = useCallback(() => {
const extensions = [
...RichTextReadOnlyEditorAdditionalExtensions({
disabledExtensions,
fileHandler,
}),
];
return extensions;
}, [disabledExtensions, fileHandler]);
return (
<ReadOnlyEditorWrapper
{...props}
extensions={getExtensions()}
forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>}
/>
);
});
RichTextReadOnlyEditorWithRef.displayName = "RichReadOnlyEditorWithRef";

View File

@@ -172,8 +172,9 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
CustomTextAlignExtension,
CustomCalloutExtension,
UtilityExtension({
isEditable: editable,
disabledExtensions,
fileHandler,
isEditable: editable,
}),
CustomColorExtension,
...CoreEditorAdditionalExtensions({

View File

@@ -127,8 +127,9 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
CustomTextAlignExtension,
CustomCalloutReadOnlyExtension,
UtilityExtension({
isEditable: false,
disabledExtensions,
fileHandler,
isEditable: false,
}),
...CoreReadOnlyEditorAdditionalExtensions({
disabledExtensions,

View File

@@ -6,7 +6,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";
// prosemirror plugins
import { codemark } from "./code-mark";
@@ -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<Record<string, unknown>, 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() {

View File

@@ -153,6 +153,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
...DocumentEditorAdditionalExtensions({
disabledExtensions,
embedConfig: embedHandler,
fileHandler,
provider,
userDetails: user,
}),

View File

@@ -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,

View File

@@ -196,6 +196,7 @@ export interface IReadOnlyEditorProps {
disabledExtensions: TExtensions[];
displayConfig?: TDisplayConfig;
editorClassName?: string;
extensions?: Extensions;
fileHandler: TReadOnlyFileHandler;
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
id: string;

View File

@@ -1,7 +1,11 @@
import { HocuspocusProvider } from "@hocuspocus/provider";
import { Extension, Node } from "@tiptap/core";
import { Extension } from "@tiptap/core";
import { FileText } from "lucide-react";
// ui
// core imports
import {
TDocumentEditorAdditionalExtensionsProps,
TDocumentEditorAdditionalExtensionsRegistry,
} from "src/ce/extensions";
// plane imports
import { LayersIcon } from "@plane/ui";
// extensions
import { SlashCommands, TSlashCommandAdditionalOption } from "@/extensions";
@@ -9,30 +13,11 @@ import { SlashCommands, TSlashCommandAdditionalOption } from "@/extensions";
import { insertPageEmbed } from "@/helpers/editor-commands";
// plane editor extensions
import { IssueEmbedSuggestions, IssueListRenderer, PageEmbedExtension } from "@/plane-editor/extensions";
// plane editor types
import { TEmbedConfig } from "@/plane-editor/types";
// types
import { TExtensions, TUserDetails } from "@/types";
// local extensions
import { TExtensions } from "@/types";
// local imports
import { CustomCollaborationCursor } from "./collaboration-cursor";
type Props = {
disabledExtensions?: TExtensions[];
embedConfig: TEmbedConfig | undefined;
provider?: HocuspocusProvider;
userDetails: TUserDetails;
};
/**
* Registry entry configuration for extensions
*/
type ExtensionConfig = {
/** Determines if the extension should be enabled based on disabled extensions */
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
/** Returns the extension instance(s) when enabled */
getExtension: (props: Props) => Extension[] | Node[] | undefined;
};
/**
* Registry for slash commands
* Each entry defines a single slash command option with its own enabling logic
@@ -58,7 +43,7 @@ const slashCommandRegistry = [
{
// Page embed slash command
isEnabled: (disabledExtensions: TExtensions[]) => !disabledExtensions.includes("nested-pages"),
getOption: ({ embedConfig }: Props): TSlashCommandAdditionalOption | null => {
getOption: ({ embedConfig }: TDocumentEditorAdditionalExtensionsProps): TSlashCommandAdditionalOption | null => {
// Only enable if page config with createCallback exists
const pageConfig = embedConfig?.page;
if (!pageConfig?.createCallback) return null;
@@ -94,7 +79,7 @@ const slashCommandRegistry = [
* Main extension registry
* Each entry defines a TipTap extension with its own enabling logic
*/
const extensionRegistry: ExtensionConfig[] = [
const extensionRegistry: TDocumentEditorAdditionalExtensionsRegistry[] = [
{
// Slash commands extension
isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"),
@@ -105,12 +90,10 @@ const extensionRegistry: ExtensionConfig[] = [
.map((command) => command.getOption(props))
.filter((option): option is TSlashCommandAdditionalOption => option !== null);
return [
SlashCommands({
additionalOptions: slashCommandOptions,
disabledExtensions: props.disabledExtensions,
}),
];
return SlashCommands({
additionalOptions: slashCommandOptions,
disabledExtensions: props.disabledExtensions,
});
},
},
{
@@ -121,27 +104,24 @@ const extensionRegistry: ExtensionConfig[] = [
const searchCallback = issueConfig?.searchCallback;
// Only enable if search callback exists
if (!searchCallback) return [];
if (!searchCallback) return undefined;
return [
IssueEmbedSuggestions.configure({
suggestion: {
render: () => IssueListRenderer(searchCallback),
},
}),
];
return IssueEmbedSuggestions.configure({
suggestion: {
render: () => IssueListRenderer(searchCallback),
},
});
},
},
{
// Collaboration cursor extension
isEnabled: (disabledExtensions) => !disabledExtensions.includes("collaboration-cursor"),
getExtension: ({ provider, userDetails }) =>
provider && [
CustomCollaborationCursor({
provider,
userDetails,
}),
],
provider &&
CustomCollaborationCursor({
provider,
userDetails,
}),
},
{
// Page embed extension
@@ -150,17 +130,15 @@ const extensionRegistry: ExtensionConfig[] = [
const pageConfig = embedConfig?.page;
// Only enable if widget callback exists
if (!pageConfig) return [];
if (!pageConfig) return undefined;
return [
PageEmbedExtension({
widgetCallback: pageConfig.widgetCallback,
archivePage: pageConfig.archivePage,
unarchivePage: pageConfig.unarchivePage,
deletePage: pageConfig.deletePage,
getPageDetailsCallback: pageConfig.getPageDetailsCallback,
}),
];
return PageEmbedExtension({
widgetCallback: pageConfig.widgetCallback,
archivePage: pageConfig.archivePage,
unarchivePage: pageConfig.unarchivePage,
deletePage: pageConfig.deletePage,
getPageDetailsCallback: pageConfig.getPageDetailsCallback,
});
},
},
];
@@ -168,14 +146,13 @@ const extensionRegistry: ExtensionConfig[] = [
/**
* Returns all enabled extensions for the document editor
*/
export const DocumentEditorAdditionalExtensions = (props: Props) => {
export const DocumentEditorAdditionalExtensions = (props: TDocumentEditorAdditionalExtensionsProps) => {
const { disabledExtensions = [] } = props;
// Filter enabled extensions and flatten the result
const extensions: Extension[] = extensionRegistry
.filter((config) => config.isEnabled(disabledExtensions))
.map((config) => config.getExtension(props))
.flat()
.filter((extension): extension is Extension => extension !== undefined);
return extensions;

View File

@@ -0,0 +1 @@
export * from "src/ce/extensions/rich-text/extensions";

View File

@@ -0,0 +1 @@
export * from "src/ce/extensions/rich-text/read-only-extensions";