Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: copying mentions as text #6583

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/editor/src/core/extensions/extensions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
CustomCodeInlineExtension,
Markdown.configure({
html: true,
transformCopiedText: true,
transformCopiedText: false,
transformPastedText: true,
breaks: true,
}),
Expand Down
24 changes: 22 additions & 2 deletions packages/editor/src/core/extensions/mentions/extension-config.ts
Original file line number Diff line number Diff line change
@@ -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<TMentionExtensionOptions>({
Expand Down Expand Up @@ -40,9 +43,26 @@ export const CustomMentionExtensionConfig = Mention.extend<TMentionExtensionOpti
class: "mention",
},

addStorage(this) {
renderText({ node }) {
return getMentionDisplayText(this.options, node);
},

addStorage() {
const options = this.options;
return {
mentionsOpen: false,
markdown: {
serialize(state: MarkdownSerializerState, node: NodeType) {
state.write(getMentionDisplayText(options, node));
},
},
};
},
});

function getMentionDisplayText(options: TMentionExtensionOptions, node: NodeType): string {
const attrs = node.attrs as TMentionComponentAttributes;
const mentionEntityId = attrs[EMentionComponentAttributeNames.ENTITY_IDENTIFIER];
const mentionEntityDetails = options.getMentionedEntityDetails?.(mentionEntityId);
return `@${mentionEntityDetails?.display_name ?? attrs[EMentionComponentAttributeNames.ID] ?? mentionEntityId}`;
}
3 changes: 2 additions & 1 deletion packages/editor/src/core/extensions/mentions/extension.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import { MentionNodeView } from "./mention-node-view";
import { renderMentionsDropdown } from "./utils";

export const CustomMentionExtension = (props: TMentionHandler) => {
const { searchCallback, renderComponent } = props;
const { searchCallback, renderComponent, getMentionedEntityDetails } = props;
return CustomMentionExtensionConfig.extend({
addOptions(this) {
return {
...this.parent?.(),
renderComponent,
getMentionedEntityDetails,
};
},

Expand Down
3 changes: 2 additions & 1 deletion packages/editor/src/core/types/mention.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// plane types
import { TSearchEntities } from "@plane/types";
import { IUserLite, TSearchEntities } from "@plane/types";

export type TMentionSuggestion = {
entity_identifier: string;
Expand All @@ -20,6 +20,7 @@ export type TMentionComponentProps = Pick<TMentionSuggestion, "entity_identifier

export type TReadOnlyMentionHandler = {
renderComponent: (props: TMentionComponentProps) => React.ReactNode;
getMentionedEntityDetails?: (entity_identifier: string) => { display_name: string } | undefined;
};

export type TMentionHandler = TReadOnlyMentionHandler & {
Expand Down
38 changes: 23 additions & 15 deletions space/core/components/editor/lite-text-read-only-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,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 = Omit<
ILiteTextReadOnlyEditor,
Expand All @@ -15,21 +17,27 @@ type LiteTextReadOnlyEditorWrapperProps = Omit<
};

export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>(
({ anchor, ...props }, ref) => (
<LiteTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={[]}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
}}
{...props}
// overriding the customClassName to add relative class passed
containerClassName={cn(props.containerClassName, "relative p-2")}
/>
)
({ anchor, ...props }, ref) => {
const { getMemberById } = useMember();
return (
<LiteTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={[]}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
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";
7 changes: 7 additions & 0 deletions space/core/components/editor/rich-text-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,26 @@ import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef } from "@plane/edi
import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { getEditorFileHandlers } from "@/helpers/editor.helper";
// store hooks
import { useMember } from "@/hooks/store";

interface RichTextEditorWrapperProps
extends Omit<IRichTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
uploadFile: (file: File) => Promise<string>;
}

export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => {
const { getMemberById } = useMember();

const { containerClassName, uploadFile, ...rest } = props;

return (
<RichTextEditorWithRef
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
getMentionedEntityDetails: (id: string) => ({
display_name: getMemberById(id)?.member__display_name ?? "",
}),
Comment on lines +25 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Fixed mention text copying issue with inconsistent property naming.

This change fixes the core issue by returning only the user's display name when a mention is processed, but uses a different property name (member__display_name) compared to other components (display_name).

-        getMentionedEntityDetails: (id: string) => ({
-          display_name: getMemberById(id)?.member__display_name ?? "",
-        }),
+        getMentionedEntityDetails: (id: string) => ({ 
+          display_name: getUserDetails(id)?.display_name ?? "" 
+        }),

🏁 Script executed:

#!/bin/bash
# Check both implementations to confirm the correct property access
rg -t typescript "getUserDetails.*\.display_name"
rg -t typescript "getMemberById.*\.member__display_name"

Length of output: 180


Additional Verification Required: Recheck Property Access in Mentioned Entity Details

The initial rg -t typescript commands resulted in an error due to an unrecognized file type. To verify that the change correctly replaces the access from
getMemberById(id)?.member__display_name
to
getUserDetails(id)?.display_name
across the codebase, please run the following shell script that targets .tsx files directly:

Once verified, confirm that there are no unintended usages of the deprecated property access.


🏁 Script executed:

#!/bin/bash
# Re-run search queries with an explicit glob for .tsx files
echo "Searching for getUserDetails with .display_name in .tsx files..."
rg "getUserDetails.*\.display_name" --glob "*.tsx"
echo "Searching for getMemberById with .member__display_name in .tsx files..."
rg "getMemberById.*\.member__display_name" --glob "*.tsx"

Length of output: 5326


Fix Inconsistent Mentioned Entity Property Across Space Modules

The recent update correctly changes the implementation in several components (e.g., in the web module) to use getUserDetails(id)?.display_name instead of getMemberById(id)?.member__display_name. However, our recent search reveals that in the space modules the old accessor is still in use. In particular:

  • space/core/components/editor/rich-text-editor.tsx still uses:
    getMentionedEntityDetails: (id: string) => ({
      display_name: getMemberById(id)?.member__display_name ?? "",
    }),
  • space/core/components/editor/rich-text-read-only-editor.tsx and
    space/core/components/editor/lite-text-read-only-editor.tsx exhibit the same outdated approach.

Please update these files to use:

- getMentionedEntityDetails: (id: string) => ({
-   display_name: getMemberById(id)?.member__display_name ?? "",
- }),
+ getMentionedEntityDetails: (id: string) => ({
+   display_name: getUserDetails(id)?.display_name ?? "",
+ }),

This change will ensure consistent property naming for mention processing across all modules.

}}
ref={ref}
disabledExtensions={[]}
Expand Down
38 changes: 23 additions & 15 deletions space/core/components/editor/rich-text-read-only-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,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 = Omit<
IRichTextReadOnlyEditor,
Expand All @@ -15,21 +17,27 @@ type RichTextReadOnlyEditorWrapperProps = Omit<
};

export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>(
({ anchor, ...props }, ref) => (
<RichTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={[]}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
}}
{...props}
// overriding the customClassName to add relative class passed
containerClassName={cn("relative p-0 border-none", props.containerClassName)}
/>
)
({ anchor, ...props }, ref) => {
const { getMemberById } = useMember();
return (
<RichTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={[]}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
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";
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { getEditorFileHandlers } from "@/helpers/editor.helper";
import { isCommentEmpty } from "@/helpers/string.helper";
// hooks
import { useEditorMention } from "@/hooks/use-editor-mention";
// store hooks
import { useMember } from "@/hooks/store";
// plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
import { useFileSize } from "@/plane-web/hooks/use-file-size";
Expand Down Expand Up @@ -57,6 +59,8 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
const [isFocused, setIsFocused] = useState(showToolbarInitially);
// editor flaggings
const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
// store hooks
const { getUserDetails } = useMember();
// use editor mention
const { fetchMentions } = useEditorMention({
searchEntity: async (payload) =>
Expand Down Expand Up @@ -98,6 +102,7 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
return res;
},
renderComponent: (props) => <EditorMentionsRoot {...props} />,
getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }),
}}
placeholder={placeholder}
containerClassName={cn(containerClassName, "relative")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { cn } from "@/helpers/common.helper";
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
// plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
// store hooks
import { useMember } from "@/hooks/store";

type LiteTextReadOnlyEditorWrapperProps = Omit<
ILiteTextReadOnlyEditor,
Expand All @@ -19,6 +21,9 @@ type LiteTextReadOnlyEditorWrapperProps = Omit<

export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>(
({ workspaceSlug, projectId, ...props }, ref) => {
// store hooks
const { getUserDetails } = useMember();

// editor flaggings
const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());

Expand All @@ -32,6 +37,7 @@ export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Lit
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }),
}}
{...props}
// overriding the containerClassName to add relative class passed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { useEditorMention } from "@/hooks/use-editor-mention";
// plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
import { useFileSize } from "@/plane-web/hooks/use-file-size";
// store hooks
import { useMember } from "@/hooks/store";

interface RichTextEditorWrapperProps
extends Omit<IRichTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
Expand All @@ -26,6 +28,8 @@ interface RichTextEditorWrapperProps
export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => {
const { containerClassName, workspaceSlug, workspaceId, projectId, searchMentionCallback, uploadFile, ...rest } =
props;
// store hooks
const { getUserDetails } = useMember();
// editor flaggings
const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
// use editor mention
Expand Down Expand Up @@ -53,6 +57,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
return res;
},
renderComponent: (props) => <EditorMentionsRoot {...props} />,
getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }),
}}
{...rest}
containerClassName={cn("relative pl-3", containerClassName)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,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";
// plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";

Expand All @@ -19,6 +21,9 @@ type RichTextReadOnlyEditorWrapperProps = Omit<

export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>(
({ workspaceSlug, projectId, ...props }, ref) => {
// store hooks
const { getUserDetails } = useMember();

// editor flaggings
const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());

Expand All @@ -32,6 +37,7 @@ export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Ric
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }),
}}
{...props}
// overriding the containerClassName to add relative class passed
Expand Down
5 changes: 4 additions & 1 deletion web/core/components/pages/editor/editor-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { PageContentBrowser, PageContentLoader, PageEditorTitle } from "@/compon
import { cn, LIVE_BASE_PATH, LIVE_BASE_URL } from "@/helpers/common.helper";
import { generateRandomColor } from "@/helpers/string.helper";
// hooks
import { useUser } from "@/hooks/store";
import { useMember, useUser } from "@/hooks/store";
import { useEditorMention } from "@/hooks/use-editor-mention";
import { usePageFilters } from "@/hooks/use-page-filters";
// plane web components
Expand Down Expand Up @@ -66,6 +66,8 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
} = props;
// store hooks
const { data: currentUser } = useUser();
const { getUserDetails } = useMember();

// derived values
const { id: pageId, name: pageTitle, isContentEditable, updateTitle } = page;
// issue-embed
Expand Down Expand Up @@ -188,6 +190,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
return res;
},
renderComponent: (props) => <EditorMentionsRoot {...props} />,
getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }),
}}
embedHandler={{
issue: issueEmbedProps,
Expand Down
5 changes: 5 additions & 0 deletions web/core/components/pages/version/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { EditorMentionsRoot } from "@/components/editor";
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
// hooks
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";
Expand All @@ -25,6 +27,8 @@ export type TVersionEditorProps = {

export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props) => {
const { activeVersion, currentVersionDescription, isCurrentVersionActive, versionDetails } = props;
// store hooks
const { getUserDetails } = useMember();
// params
const { workspaceSlug, projectId } = useParams();
// editor flaggings
Expand Down Expand Up @@ -101,6 +105,7 @@ export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }),
}}
embedHandler={{
issue: {
Expand Down