diff --git a/packages/editor/src/core/extensions/clipboard.ts b/packages/editor/src/core/extensions/clipboard.ts new file mode 100644 index 000000000..252f0a113 --- /dev/null +++ b/packages/editor/src/core/extensions/clipboard.ts @@ -0,0 +1,89 @@ +import { Extension } from "@tiptap/core"; +import { Fragment, Node } from "@tiptap/pm/model"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; + +export const MarkdownClipboard = Extension.create({ + name: "markdownClipboard", + + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey("markdownClipboard"), + props: { + clipboardTextSerializer: (slice) => { + const markdownSerializer = this.editor.storage.markdown.serializer; + const isTableRow = slice.content.firstChild?.type?.name === "tableRow"; + const nodeSelect = slice.openStart === 0 && slice.openEnd === 0; + + if (nodeSelect) { + return markdownSerializer.serialize(slice.content); + } + + const processTableContent = (tableNode: Node | Fragment) => { + let result = ""; + tableNode.content?.forEach?.((tableRowNode: Node | Fragment) => { + tableRowNode.content?.forEach?.((cell: Node) => { + const cellContent = cell.content ? markdownSerializer.serialize(cell.content) : ""; + result += cellContent + "\n"; + }); + }); + return result; + }; + + if (isTableRow) { + const rowsCount = slice.content?.childCount || 0; + const cellsCount = slice.content?.firstChild?.content?.childCount || 0; + if (rowsCount === 1 || cellsCount === 1) { + return processTableContent(slice.content); + } else { + return markdownSerializer.serialize(slice.content); + } + } + + const traverseToParentOfLeaf = ( + node: Node | null, + parent: Fragment | Node, + depth: number + ): Node | Fragment => { + let currentNode = node; + let currentParent = parent; + let currentDepth = depth; + + while (currentNode && currentDepth > 1 && currentNode.content?.firstChild) { + if (currentNode.content?.childCount > 1) { + if (currentNode.content.firstChild?.type?.name === "listItem") { + return currentParent; + } else { + return currentNode.content; + } + } + + currentParent = currentNode; + currentNode = currentNode.content?.firstChild || null; + currentDepth--; + } + + return currentParent; + }; + + if (slice.content.childCount > 1) { + return markdownSerializer.serialize(slice.content); + } else { + const targetNode = traverseToParentOfLeaf(slice.content.firstChild, slice.content, slice.openStart); + + let currentNode = targetNode; + while (currentNode && currentNode.content && currentNode.childCount === 1 && currentNode.firstChild) { + currentNode = currentNode.firstChild; + } + if (currentNode instanceof Node && currentNode.isText) { + return currentNode.text; + } + + return markdownSerializer.serialize(targetNode); + } + }, + }, + }), + ]; + }, +}); diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index 8b9290d62..002dce945 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -29,6 +29,7 @@ import { TableCell, TableHeader, TableRow, + MarkdownClipboard, } from "@/extensions"; // helpers import { isValidHttpUrl } from "@/helpers/common"; @@ -130,10 +131,11 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { CustomCodeInlineExtension, Markdown.configure({ html: true, - transformCopiedText: true, + transformCopiedText: false, transformPastedText: true, breaks: true, }), + MarkdownClipboard, Table, TableHeader, TableCell, diff --git a/packages/editor/src/core/extensions/index.ts b/packages/editor/src/core/extensions/index.ts index d1fa0ce6d..e98607585 100644 --- a/packages/editor/src/core/extensions/index.ts +++ b/packages/editor/src/core/extensions/index.ts @@ -23,3 +23,4 @@ export * from "./quote"; export * from "./read-only-extensions"; export * from "./side-menu"; export * from "./text-align"; +export * from "./clipboard"; diff --git a/packages/editor/src/core/extensions/mentions/extension-config.ts b/packages/editor/src/core/extensions/mentions/extension-config.ts index 827137a1d..cf192507f 100644 --- a/packages/editor/src/core/extensions/mentions/extension-config.ts +++ b/packages/editor/src/core/extensions/mentions/extension-config.ts @@ -1,12 +1,15 @@ import { mergeAttributes } from "@tiptap/core"; import Mention, { MentionOptions } from "@tiptap/extension-mention"; +import { MarkdownSerializerState } from "@tiptap/pm/markdown"; +import { Node as NodeType } from "@tiptap/pm/model"; // types import { TMentionHandler } from "@/types"; // local types -import { EMentionComponentAttributeNames } from "./types"; +import { EMentionComponentAttributeNames, TMentionComponentAttributes } from "./types"; export type TMentionExtensionOptions = MentionOptions & { renderComponent: TMentionHandler["renderComponent"]; + getMentionedEntityDetails: TMentionHandler["getMentionedEntityDetails"]; }; export const CustomMentionExtensionConfig = Mention.extend({ @@ -40,9 +43,26 @@ export const CustomMentionExtensionConfig = Mention.extend { - const { searchCallback, renderComponent } = props; + const { searchCallback, renderComponent, getMentionedEntityDetails } = props; return CustomMentionExtensionConfig.extend({ addOptions(this) { return { ...this.parent?.(), renderComponent, + getMentionedEntityDetails, }; }, diff --git a/packages/editor/src/core/extensions/read-only-extensions.tsx b/packages/editor/src/core/extensions/read-only-extensions.tsx index b949fe6b7..6f09cb683 100644 --- a/packages/editor/src/core/extensions/read-only-extensions.tsx +++ b/packages/editor/src/core/extensions/read-only-extensions.tsx @@ -24,6 +24,7 @@ import { CustomTextAlignExtension, CustomCalloutReadOnlyExtension, CustomColorExtension, + MarkdownClipboard, } from "@/extensions"; // helpers import { isValidHttpUrl } from "@/helpers/common"; @@ -114,8 +115,9 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => { CustomCodeInlineExtension, Markdown.configure({ html: true, - transformCopiedText: true, + transformCopiedText: false, }), + MarkdownClipboard, Table, TableHeader, TableCell, diff --git a/packages/editor/src/core/extensions/table/table/table-controls.ts b/packages/editor/src/core/extensions/table/table/table-controls.ts index bd5f8f589..052922579 100644 --- a/packages/editor/src/core/extensions/table/table/table-controls.ts +++ b/packages/editor/src/core/extensions/table/table/table-controls.ts @@ -16,6 +16,22 @@ export function tableControls() { }, }, props: { + handleTripleClickOn(view, pos, node, nodePos, event, direct) { + if (node.type.name === 'tableCell') { + event.preventDefault(); + const $pos = view.state.doc.resolve(pos); + const line = $pos.parent; + const linePos = $pos.start(); + const start = linePos; + const end = linePos + line.nodeSize - 1; + const tr = view.state.tr.setSelection( + TextSelection.create(view.state.doc, start, end) + ); + view.dispatch(tr); + return true; + } + return false; + }, handleDOMEvents: { mousemove: (view, event) => { const pluginState = key.getState(view.state); diff --git a/packages/editor/src/core/types/mention.ts b/packages/editor/src/core/types/mention.ts index 20f1ec0dc..b7a65f8b4 100644 --- a/packages/editor/src/core/types/mention.ts +++ b/packages/editor/src/core/types/mention.ts @@ -1,5 +1,5 @@ // plane types -import { TSearchEntities } from "@plane/types"; +import { IUserLite, TSearchEntities } from "@plane/types"; export type TMentionSuggestion = { entity_identifier: string; @@ -20,6 +20,7 @@ export type TMentionComponentProps = Pick React.ReactNode; + getMentionedEntityDetails?: (entity_identifier: string) => { display_name: string } | undefined; }; export type TMentionHandler = TReadOnlyMentionHandler & { diff --git a/space/core/components/editor/lite-text-read-only-editor.tsx b/space/core/components/editor/lite-text-read-only-editor.tsx index 5c8785e90..acb5cf14d 100644 --- a/space/core/components/editor/lite-text-read-only-editor.tsx +++ b/space/core/components/editor/lite-text-read-only-editor.tsx @@ -7,6 +7,8 @@ import { EditorMentionsRoot } from "@/components/editor"; // helpers import { cn } from "@/helpers/common.helper"; import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper"; +// store hooks +import { useMember } from "@/hooks/store"; type LiteTextReadOnlyEditorWrapperProps = MakeOptional< Omit, @@ -17,22 +19,29 @@ type LiteTextReadOnlyEditorWrapperProps = MakeOptional< }; export const LiteTextReadOnlyEditor = React.forwardRef( - ({ anchor, workspaceId, disabledExtensions, ...props }, ref) => ( - , - }} - {...props} - // overriding the customClassName to add relative class passed - containerClassName={cn(props.containerClassName, "relative p-2")} - /> - ) + ({ anchor, workspaceId, disabledExtensions, ...props }, ref) => { + const { getMemberById } = useMember(); + + return ( + , + getMentionedEntityDetails: (id: string) => ({ + display_name: getMemberById(id)?.member__display_name ?? "", + }), + }} + {...props} + // overriding the customClassName to add relative class passed + containerClassName={cn(props.containerClassName, "relative p-2")} + /> + ); + } ); LiteTextReadOnlyEditor.displayName = "LiteTextReadOnlyEditor"; diff --git a/space/core/components/editor/rich-text-editor.tsx b/space/core/components/editor/rich-text-editor.tsx index 682036f2a..8bf98c230 100644 --- a/space/core/components/editor/rich-text-editor.tsx +++ b/space/core/components/editor/rich-text-editor.tsx @@ -6,6 +6,8 @@ import { MakeOptional } from "@plane/types"; import { EditorMentionsRoot } from "@/components/editor"; // helpers import { getEditorFileHandlers } from "@/helpers/editor.helper"; +// store hooks +import { useMember } from "@/hooks/store"; interface RichTextEditorWrapperProps extends MakeOptional, "disabledExtensions"> { @@ -16,11 +18,14 @@ interface RichTextEditorWrapperProps export const RichTextEditor = forwardRef((props, ref) => { const { anchor, containerClassName, uploadFile, workspaceId, disabledExtensions, ...rest } = props; - + const { getMemberById } = useMember(); return ( , + getMentionedEntityDetails: (id: string) => ({ + display_name: getMemberById(id)?.member__display_name ?? "", + }), }} ref={ref} disabledExtensions={disabledExtensions ?? []} diff --git a/space/core/components/editor/rich-text-read-only-editor.tsx b/space/core/components/editor/rich-text-read-only-editor.tsx index b989e1e41..f2d386629 100644 --- a/space/core/components/editor/rich-text-read-only-editor.tsx +++ b/space/core/components/editor/rich-text-read-only-editor.tsx @@ -7,6 +7,8 @@ import { EditorMentionsRoot } from "@/components/editor"; // helpers import { cn } from "@/helpers/common.helper"; import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper"; +// store hooks +import { useMember } from "@/hooks/store"; type RichTextReadOnlyEditorWrapperProps = MakeOptional< Omit, @@ -17,22 +19,29 @@ type RichTextReadOnlyEditorWrapperProps = MakeOptional< }; export const RichTextReadOnlyEditor = React.forwardRef( - ({ anchor, workspaceId, disabledExtensions, ...props }, ref) => ( - , - }} - {...props} - // overriding the customClassName to add relative class passed - containerClassName={cn("relative p-0 border-none", props.containerClassName)} - /> - ) + ({ anchor, workspaceId, disabledExtensions, ...props }, ref) => { + const { getMemberById } = useMember(); + + return ( + , + getMentionedEntityDetails: (id: string) => ({ + display_name: getMemberById(id)?.member__display_name ?? "", + }), + }} + {...props} + // overriding the customClassName to add relative class passed + containerClassName={cn("relative p-0 border-none", props.containerClassName)} + /> + ); + } ); RichTextReadOnlyEditor.displayName = "RichTextReadOnlyEditor"; diff --git a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx index b06d39c85..714b773a4 100644 --- a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx +++ b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx @@ -13,6 +13,8 @@ import { cn } from "@/helpers/common.helper"; import { isCommentEmpty } from "@/helpers/string.helper"; // hooks import { useEditorConfig, useEditorMention } from "@/hooks/editor"; +// store hooks +import { useMember } from "@/hooks/store"; // plane web hooks import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; // plane web services @@ -57,6 +59,8 @@ export const LiteTextEditor = React.forwardRef @@ -97,6 +101,7 @@ export const LiteTextEditor = React.forwardRef , + getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }), }} placeholder={placeholder} containerClassName={cn(containerClassName, "relative")} diff --git a/web/core/components/editor/lite-text-editor/lite-text-read-only-editor.tsx b/web/core/components/editor/lite-text-editor/lite-text-read-only-editor.tsx index 0763a49f9..a8747538e 100644 --- a/web/core/components/editor/lite-text-editor/lite-text-read-only-editor.tsx +++ b/web/core/components/editor/lite-text-editor/lite-text-read-only-editor.tsx @@ -8,6 +8,8 @@ import { EditorMentionsRoot } from "@/components/editor"; import { cn } from "@/helpers/common.helper"; // hooks import { useEditorConfig } from "@/hooks/editor"; +// store hooks +import { useMember } from "@/hooks/store"; // plane web hooks import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; @@ -22,6 +24,9 @@ type LiteTextReadOnlyEditorWrapperProps = MakeOptional< export const LiteTextReadOnlyEditor = React.forwardRef( ({ workspaceId, workspaceSlug, projectId, disabledExtensions: additionalDisabledExtensions, ...props }, ref) => { + // store hooks + const { getUserDetails } = useMember(); + // editor flaggings const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString()); // editor config @@ -38,6 +43,7 @@ export const LiteTextReadOnlyEditor = React.forwardRef , + getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }), }} {...props} // overriding the containerClassName to add relative class passed diff --git a/web/core/components/editor/rich-text-editor/rich-text-editor.tsx b/web/core/components/editor/rich-text-editor/rich-text-editor.tsx index b18fc1859..ea27ec45d 100644 --- a/web/core/components/editor/rich-text-editor/rich-text-editor.tsx +++ b/web/core/components/editor/rich-text-editor/rich-text-editor.tsx @@ -8,6 +8,8 @@ import { EditorMentionsRoot } from "@/components/editor"; import { cn } from "@/helpers/common.helper"; // hooks import { useEditorConfig, useEditorMention } from "@/hooks/editor"; +// store hooks +import { useMember } from "@/hooks/store"; // plane web hooks import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; @@ -31,6 +33,8 @@ export const RichTextEditor = forwardRef , + getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }), }} {...rest} containerClassName={cn("relative pl-3 pb-3", containerClassName)} diff --git a/web/core/components/editor/rich-text-editor/rich-text-read-only-editor.tsx b/web/core/components/editor/rich-text-editor/rich-text-read-only-editor.tsx index 31ce65159..50eb65e78 100644 --- a/web/core/components/editor/rich-text-editor/rich-text-read-only-editor.tsx +++ b/web/core/components/editor/rich-text-editor/rich-text-read-only-editor.tsx @@ -8,6 +8,8 @@ import { EditorMentionsRoot } from "@/components/editor"; import { cn } from "@/helpers/common.helper"; // hooks import { useEditorConfig } from "@/hooks/editor"; +// store hooks +import { useMember } from "@/hooks/store"; // plane web hooks import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; @@ -22,6 +24,9 @@ type RichTextReadOnlyEditorWrapperProps = MakeOptional< export const RichTextReadOnlyEditor = React.forwardRef( ({ workspaceId, workspaceSlug, projectId, disabledExtensions: additionalDisabledExtensions, ...props }, ref) => { + // store hooks + const { getUserDetails } = useMember(); + // editor flaggings const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString()); // editor config @@ -38,6 +43,7 @@ export const RichTextReadOnlyEditor = React.forwardRef , + getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }), }} {...props} // overriding the containerClassName to add relative class passed diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 72533c3dc..aba274aff 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -22,7 +22,7 @@ import { cn, LIVE_BASE_PATH, LIVE_BASE_URL } from "@/helpers/common.helper"; import { generateRandomColor } from "@/helpers/string.helper"; // hooks import { useEditorMention } from "@/hooks/editor"; -import { useUser, useWorkspace } from "@/hooks/store"; +import { useUser, useWorkspace, useMember } from "@/hooks/store"; import { usePageFilters } from "@/hooks/use-page-filters"; // plane web components import { EditorAIMenu } from "@/plane-web/components/pages"; @@ -68,6 +68,8 @@ export const PageEditorBody: React.FC = observer((props) => { // store hooks const { data: currentUser } = useUser(); const { getWorkspaceBySlug } = useWorkspace(); + const { getUserDetails } = useMember(); + // derived values const { id: pageId, name: pageTitle, isContentEditable, updateTitle } = page; const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? ""; @@ -192,6 +194,7 @@ export const PageEditorBody: React.FC = observer((props) => { return res; }, renderComponent: (props) => , + getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }), }} embedHandler={{ issue: issueEmbedProps, diff --git a/web/core/components/pages/version/editor.tsx b/web/core/components/pages/version/editor.tsx index d20123290..c067c5d9e 100644 --- a/web/core/components/pages/version/editor.tsx +++ b/web/core/components/pages/version/editor.tsx @@ -12,6 +12,8 @@ import { EditorMentionsRoot } from "@/components/editor"; import { useEditorConfig } from "@/hooks/editor"; import { useWorkspace } from "@/hooks/store"; import { usePageFilters } from "@/hooks/use-page-filters"; +// store hooks +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"; @@ -25,6 +27,8 @@ export type TVersionEditorProps = { export const PagesVersionEditor: React.FC = observer((props) => { const { activeVersion, currentVersionDescription, isCurrentVersionActive, versionDetails } = props; + // store hooks + const { getUserDetails } = useMember(); // params const { workspaceSlug, projectId } = useParams(); // store hooks @@ -108,6 +112,7 @@ export const PagesVersionEditor: React.FC = observer((props })} mentionHandler={{ renderComponent: (props) => , + getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }), }} embedHandler={{ issue: {