diff --git a/apps/api/plane/api/serializers/issue.py b/apps/api/plane/api/serializers/issue.py index 5a771f2ad5c..b094a8cfef3 100644 --- a/apps/api/plane/api/serializers/issue.py +++ b/apps/api/plane/api/serializers/issue.py @@ -31,6 +31,7 @@ validate_html_content, validate_binary_data, ) +from plane.utils.entity_mention_parser import apply_entity_mention_transformation from .base import BaseSerializer from .cycle import CycleLiteSerializer, CycleSerializer @@ -89,6 +90,10 @@ def validate(self, data): except Exception: raise serializers.ValidationError("Invalid HTML passed") + # Transform @issue/PROJ-123 style mentions before sanitization + if data.get("description_html"): + apply_entity_mention_transformation(data, self.context, "description_html") + # Validate description content for security if data.get("description_html"): is_valid, error_msg, sanitized_html = validate_html_content(data["description_html"]) @@ -719,6 +724,18 @@ class Meta: "edited_at", ] + def validate(self, data): + if "comment_html" in data and data["comment_html"]: + apply_entity_mention_transformation(data, self.context, "comment_html") + + if "comment_html" in data and data["comment_html"]: + is_valid, error_msg, sanitized_html = validate_html_content(data["comment_html"]) + if not is_valid: + raise serializers.ValidationError({"comment_html": "HTML content is not valid"}) + if sanitized_html is not None: + data["comment_html"] = sanitized_html + return data + class IssueCommentSerializer(BaseSerializer): """ @@ -745,6 +762,9 @@ class Meta: exclude = ["comment_stripped", "comment_json"] def validate(self, data): + if "comment_html" in data and data["comment_html"]: + apply_entity_mention_transformation(data, self.context, "comment_html") + if "comment_html" in data and data["comment_html"]: is_valid, error_msg, sanitized_html = validate_html_content(data["comment_html"]) if not is_valid: diff --git a/apps/api/plane/api/views/issue.py b/apps/api/plane/api/views/issue.py index da9edc66d66..99308bf4e7a 100644 --- a/apps/api/plane/api/views/issue.py +++ b/apps/api/plane/api/views/issue.py @@ -1477,7 +1477,10 @@ def post(self, request, slug, project_id, issue_id): status=status.HTTP_409_CONFLICT, ) - serializer = IssueCommentCreateSerializer(data=request.data) + serializer = IssueCommentCreateSerializer( + data=request.data, + context={"project_id": project_id}, + ) if serializer.is_valid(): serializer.save(project_id=project_id, issue_id=issue_id, actor=request.user) issue_comment = IssueComment.objects.get(pk=serializer.instance.id) @@ -1624,7 +1627,12 @@ def patch(self, request, slug, project_id, issue_id, pk): status=status.HTTP_409_CONFLICT, ) - serializer = IssueCommentCreateSerializer(issue_comment, data=request.data, partial=True) + serializer = IssueCommentCreateSerializer( + issue_comment, + data=request.data, + partial=True, + context={"project_id": project_id}, + ) if serializer.is_valid(): serializer.save() issue_activity.delay( diff --git a/apps/api/plane/app/serializers/issue.py b/apps/api/plane/app/serializers/issue.py index 2e116cd6613..bed9256c862 100644 --- a/apps/api/plane/app/serializers/issue.py +++ b/apps/api/plane/app/serializers/issue.py @@ -47,6 +47,7 @@ validate_html_content, validate_binary_data, ) +from plane.utils.entity_mention_parser import apply_entity_mention_transformation class IssueFlatSerializer(BaseSerializer): @@ -132,6 +133,10 @@ def validate(self, attrs): ): raise serializers.ValidationError("Start date cannot exceed target date") + # Transform @issue/PROJ-123 style mentions before sanitization + if "description_html" in attrs and attrs["description_html"]: + apply_entity_mention_transformation(attrs, self.context, "description_html") + # Validate description content for security if "description_html" in attrs and attrs["description_html"]: is_valid, error_msg, sanitized_html = validate_html_content(attrs["description_html"]) @@ -716,6 +721,9 @@ class Meta: ] def validate(self, attrs): + if "comment_html" in attrs and attrs["comment_html"]: + apply_entity_mention_transformation(attrs, self.context, "comment_html") + if "comment_html" in attrs and attrs["comment_html"]: is_valid, error_msg, sanitized_html = validate_html_content(attrs["comment_html"]) if not is_valid: diff --git a/apps/api/plane/app/views/issue/comment.py b/apps/api/plane/app/views/issue/comment.py index 34fe0f9e4b9..4e45c841d00 100644 --- a/apps/api/plane/app/views/issue/comment.py +++ b/apps/api/plane/app/views/issue/comment.py @@ -79,7 +79,10 @@ def create(self, request, slug, project_id, issue_id): {"error": "You are not allowed to comment on the issue"}, status=status.HTTP_400_BAD_REQUEST, ) - serializer = IssueCommentSerializer(data=request.data) + serializer = IssueCommentSerializer( + data=request.data, + context={"project_id": project_id}, + ) if serializer.is_valid(): serializer.save(project_id=project_id, issue_id=issue_id, actor=request.user) issue_activity.delay( @@ -111,7 +114,12 @@ def partial_update(self, request, slug, project_id, issue_id, pk): issue_comment = IssueComment.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk) requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) current_instance = json.dumps(IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder) - serializer = IssueCommentSerializer(issue_comment, data=request.data, partial=True) + serializer = IssueCommentSerializer( + issue_comment, + data=request.data, + partial=True, + context={"project_id": project_id}, + ) if serializer.is_valid(): if "comment_html" in request.data and request.data["comment_html"] != issue_comment.comment_html: serializer.save(edited_at=timezone.now()) diff --git a/apps/api/plane/tests/unit/utils/test_entity_mention_parser.py b/apps/api/plane/tests/unit/utils/test_entity_mention_parser.py new file mode 100644 index 00000000000..bf348f38646 --- /dev/null +++ b/apps/api/plane/tests/unit/utils/test_entity_mention_parser.py @@ -0,0 +1,110 @@ +# Copyright (c) 2023-present Plane Software, Inc. and contributors +# SPDX-License-Identifier: AGPL-3.0-only +# See the LICENSE file for details. + +import pytest + +from plane.db.models import Issue, Project, State, Workspace +from plane.utils.entity_mention_parser import ( + build_mention_component, + transform_entity_mentions_in_html, + transform_entity_mentions_in_text, +) + + +@pytest.fixture +def workspace(create_user): + return Workspace.objects.create( + name="Test Workspace", + slug="test-workspace", + owner=create_user, + ) + + +@pytest.fixture +def project(workspace, create_user): + return Project.objects.create( + name="Test Project", + identifier="ENG", + workspace=workspace, + created_by=create_user, + ) + + +@pytest.fixture +def state(project): + return State.objects.create( + name="Todo", + project=project, + group="unstarted", + color="#60646C", + ) + + +@pytest.fixture +def issue(workspace, project, state, create_user): + return Issue.objects.create( + name="Referenced Issue", + workspace=workspace, + project=project, + state=state, + created_by=create_user, + ) + + +@pytest.mark.unit +class TestEntityMentionParser: + @pytest.mark.django_db + def test_transform_issue_mention_with_type_prefix(self, workspace, project, issue): + identifier = f"{project.identifier}-{issue.sequence_id}" + html = f"

Blocked by @issue/{identifier}

" + result = transform_entity_mentions_in_html(html, workspace_slug=workspace.slug) + + assert "mention-component" in result + assert f'entity_identifier="{issue.id}"' in result + assert 'entity_name="issue"' in result + assert f'entity_display_name="{identifier}"' in result + + @pytest.mark.django_db + def test_transform_issue_mention_without_type_prefix(self, workspace, project, issue): + identifier = f"{project.identifier}-{issue.sequence_id}" + text = f"See @{identifier} for details" + result = transform_entity_mentions_in_text(text, workspace_slug=workspace.slug) + + assert "mention-component" in result + assert f'entity_identifier="{issue.id}"' in result + + @pytest.mark.django_db + def test_transform_project_mention(self, workspace, project): + text = "Track in @project/ENG" + result = transform_entity_mentions_in_text(text, workspace_slug=workspace.slug) + + assert "mention-component" in result + assert f'entity_identifier="{project.id}"' in result + assert 'entity_name="project"' in result + assert 'entity_display_name="ENG"' in result + + @pytest.mark.django_db + def test_unknown_mention_is_left_unchanged(self, workspace): + text = "Unknown @issue/ZZZ-999 reference" + result = transform_entity_mentions_in_text(text, workspace_slug=workspace.slug) + assert result == text + + @pytest.mark.django_db + def test_existing_mention_component_is_not_replaced(self, workspace, project, issue): + existing = build_mention_component( + entity_name="issue", + entity_identifier=str(issue.id), + entity_display_name=f"{project.identifier}-{issue.sequence_id}", + ) + html = f"

Already linked {existing}

" + result = transform_entity_mentions_in_html(html, workspace_slug=workspace.slug) + assert result.count("mention-component") == 1 + + def test_build_mention_component_includes_display_name(self): + component = build_mention_component( + entity_name="issue", + entity_identifier="test-id", + entity_display_name="ENG-42", + ) + assert 'entity_display_name="ENG-42"' in component diff --git a/apps/api/plane/utils/content_validator.py b/apps/api/plane/utils/content_validator.py index 711d560fac0..dd3a72e6d0e 100644 --- a/apps/api/plane/utils/content_validator.py +++ b/apps/api/plane/utils/content_validator.py @@ -133,7 +133,7 @@ def validate_binary_data(data): "alt", "title", }, - "mention-component": {"id", "entity_identifier", "entity_name"}, + "mention-component": {"id", "entity_identifier", "entity_name", "entity_display_name"}, "th": { "colspan", "rowspan", diff --git a/apps/api/plane/utils/entity_mention_parser.py b/apps/api/plane/utils/entity_mention_parser.py new file mode 100644 index 00000000000..9e259890b19 --- /dev/null +++ b/apps/api/plane/utils/entity_mention_parser.py @@ -0,0 +1,176 @@ +# Copyright (c) 2023-present Plane Software, Inc. and contributors +# SPDX-License-Identifier: AGPL-3.0-only +# See the LICENSE file for details. + +import re +import uuid + +from bs4 import BeautifulSoup, NavigableString, Tag + +from plane.db.models import Issue, Project, Workspace + +ISSUE_MENTION_WITH_TYPE_PATTERN = re.compile( + r"@issue/(?P[A-Za-z0-9_-]+)-(?P\d+)\b", + re.IGNORECASE, +) +ISSUE_MENTION_PATTERN = re.compile( + r"@(?P[A-Za-z][A-Za-z0-9_-]*)-(?P\d+)\b", + re.IGNORECASE, +) +PROJECT_MENTION_PATTERN = re.compile( + r"@project/(?P[A-Za-z0-9_-]+)\b", + re.IGNORECASE, +) + + +def build_mention_component(*, entity_name: str, entity_identifier: str, entity_display_name: str) -> str: + mention_id = str(uuid.uuid4()) + return ( + f'' + ) + + +def _resolve_workspace_slug(workspace_id=None, workspace_slug=None, project_id=None): + if workspace_slug: + return workspace_slug + if workspace_id: + slug = Workspace.objects.filter(pk=workspace_id).values_list("slug", flat=True).first() + if slug: + return slug + if project_id: + return Project.objects.filter(pk=project_id).values_list("workspace__slug", flat=True).first() + return None + + +def _resolve_issue(workspace_slug, project_identifier, sequence_id): + try: + sequence_id = int(sequence_id) + except (TypeError, ValueError): + return None + + return ( + Issue.issue_objects.filter( + workspace__slug=workspace_slug, + project__identifier__iexact=project_identifier, + sequence_id=sequence_id, + ) + .select_related("project") + .first() + ) + + +def _resolve_project(workspace_slug, project_identifier): + return Project.objects.filter( + workspace__slug=workspace_slug, + identifier__iexact=project_identifier, + archived_at__isnull=True, + ).first() + + +def _replace_issue_mention(match, workspace_slug): + project_identifier = match.group("project") + sequence_id = match.group("sequence") + issue = _resolve_issue(workspace_slug, project_identifier, sequence_id) + if not issue: + return match.group(0) + + display_name = f"{issue.project.identifier}-{issue.sequence_id}" + return build_mention_component( + entity_name="issue", + entity_identifier=str(issue.id), + entity_display_name=display_name, + ) + + +def _replace_project_mention(match, workspace_slug): + project_identifier = match.group("identifier") + project = _resolve_project(workspace_slug, project_identifier) + if not project: + return match.group(0) + + return build_mention_component( + entity_name="project", + entity_identifier=str(project.id), + entity_display_name=project.identifier, + ) + + +def transform_entity_mentions_in_text(text: str, *, workspace_slug: str) -> str: + if not text or not workspace_slug: + return text + + transformed = ISSUE_MENTION_WITH_TYPE_PATTERN.sub( + lambda match: _replace_issue_mention(match, workspace_slug), + text, + ) + transformed = PROJECT_MENTION_PATTERN.sub( + lambda match: _replace_project_mention(match, workspace_slug), + transformed, + ) + transformed = ISSUE_MENTION_PATTERN.sub( + lambda match: _replace_issue_mention(match, workspace_slug), + transformed, + ) + return transformed + + +def _is_inside_mention_component(node) -> bool: + parent = node.parent if isinstance(node, NavigableString) else node + while parent: + if isinstance(parent, Tag) and parent.name == "mention-component": + return True + parent = parent.parent + return False + + +def transform_entity_mentions_in_html( + html_content: str, + *, + workspace_id=None, + workspace_slug=None, + project_id=None, +) -> str: + del project_id # reserved for future project-scoped resolution defaults + + if not html_content: + return html_content + + workspace_slug = _resolve_workspace_slug(workspace_id, workspace_slug, project_id) + if not workspace_slug: + return html_content + + soup = BeautifulSoup(html_content, "html.parser") + + for text_node in list(soup.find_all(string=True)): + if _is_inside_mention_component(text_node): + continue + + parent = text_node.parent + if not parent or parent.name in {"script", "style"}: + continue + + original_text = str(text_node) + transformed_text = transform_entity_mentions_in_text(original_text, workspace_slug=workspace_slug) + if transformed_text == original_text: + continue + + fragment = BeautifulSoup(transformed_text, "html.parser") + text_node.replace_with(*fragment.contents) + + return str(soup) + + +def apply_entity_mention_transformation(data: dict, context: dict, html_field: str) -> None: + html_content = data.get(html_field) + if not html_content: + return + + data[html_field] = transform_entity_mentions_in_html( + html_content, + workspace_id=context.get("workspace_id"), + workspace_slug=context.get("workspace_slug"), + project_id=context.get("project_id"), + ) diff --git a/apps/web/core/components/editor/embeds/mentions/issue.tsx b/apps/web/core/components/editor/embeds/mentions/issue.tsx new file mode 100644 index 00000000000..c8d5124afeb --- /dev/null +++ b/apps/web/core/components/editor/embeds/mentions/issue.tsx @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { Link } from "react-router"; +import { generateWorkItemLink } from "@plane/utils"; +import { useIssueDetail } from "@/hooks/store/use-issue-detail"; + +type Props = { + id: string; + entityDisplayName?: string | null; +}; + +export const EditorIssueMention = observer(function EditorIssueMention(props: Props) { + const { id, entityDisplayName } = props; + const { workspaceSlug } = useParams(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + const issue = getIssueById(id); + + const [projectIdentifier, sequenceId] = entityDisplayName?.split("-") ?? []; + const href = + workspaceSlug && projectIdentifier && sequenceId + ? generateWorkItemLink({ + workspaceSlug: workspaceSlug.toString(), + projectIdentifier, + sequenceId, + issueId: id, + projectId: issue?.project_id, + }) + : "#"; + + const label = entityDisplayName ?? issue?.name ?? "work item"; + + return ( + + {label} + + ); +}); diff --git a/apps/web/core/components/editor/embeds/mentions/project.tsx b/apps/web/core/components/editor/embeds/mentions/project.tsx new file mode 100644 index 00000000000..66ece377072 --- /dev/null +++ b/apps/web/core/components/editor/embeds/mentions/project.tsx @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { Link } from "react-router"; + +type Props = { + id: string; + entityDisplayName?: string | null; +}; + +export const EditorProjectMention = observer(function EditorProjectMention(props: Props) { + const { id, entityDisplayName } = props; + const { workspaceSlug } = useParams(); + const href = workspaceSlug ? `/${workspaceSlug.toString()}/projects/${id}/` : "#"; + const label = entityDisplayName ?? "project"; + + return ( + + @{label} + + ); +}); diff --git a/apps/web/core/components/editor/embeds/mentions/root.tsx b/apps/web/core/components/editor/embeds/mentions/root.tsx index cd6338d9100..926b9d65f64 100644 --- a/apps/web/core/components/editor/embeds/mentions/root.tsx +++ b/apps/web/core/components/editor/embeds/mentions/root.tsx @@ -5,15 +5,21 @@ */ // local imports -import { EditorUserMention } from "./user"; import type { TCallbackMentionComponentProps } from "@plane/editor"; +import { EditorIssueMention } from "./issue"; +import { EditorProjectMention } from "./project"; +import { EditorUserMention } from "./user"; export function EditorMentionsRoot(props: TCallbackMentionComponentProps) { - const { entity_identifier, entity_name } = props; + const { entity_identifier, entity_name, entity_display_name } = props; switch (entity_name) { case "user_mention": return ; + case "issue": + return ; + case "project": + return ; default: return null; } diff --git a/apps/web/core/hooks/use-additional-editor-mention.tsx b/apps/web/core/hooks/use-additional-editor-mention.tsx index 3f936ebd260..fb322bae5e2 100644 --- a/apps/web/core/hooks/use-additional-editor-mention.tsx +++ b/apps/web/core/hooks/use-additional-editor-mention.tsx @@ -6,9 +6,14 @@ import { useCallback, useMemo } from "react"; // plane editor -import type { TMentionSection } from "@plane/editor"; +import type { TMentionSection, TMentionSuggestion } from "@plane/editor"; // plane types -import type { TSearchEntities, TSearchResponse } from "@plane/types"; +import type { TIssueSearchResponse, TProjectSearchResponse, TSearchEntities, TSearchResponse } from "@plane/types"; +import { generateWorkItemLink } from "@plane/utils"; +// hooks +import { useIssueDetail } from "@/hooks/store/use-issue-detail"; +import { useProject } from "@/hooks/store/use-project"; +import { useParams } from "next/navigation"; export type TUseAdditionalEditorMentionArgs = { enableAdvancedMentions: boolean; @@ -34,20 +39,104 @@ export type TAdditionalParseEditorContentReturnType = } | undefined; -export const useAdditionalEditorMention = (_args: TUseAdditionalEditorMentionArgs) => { +export const useAdditionalEditorMention = (args: TUseAdditionalEditorMentionArgs) => { + const { enableAdvancedMentions } = args; + const { workspaceSlug } = useParams(); + const { getProjectById } = useProject(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + const updateAdditionalSections = useCallback( - (_args: TAdditionalEditorMentionHandlerArgs): TAdditionalEditorMentionHandlerReturnType => ({ - sections: [], - }), - [] + ({ response }: TAdditionalEditorMentionHandlerArgs): TAdditionalEditorMentionHandlerReturnType => { + if (!enableAdvancedMentions) { + return { sections: [] }; + } + + const sections: TMentionSection[] = []; + + if (response.issue?.length) { + const items: TMentionSuggestion[] = (response.issue as TIssueSearchResponse[]).map((issue) => ({ + id: issue.id, + entity_identifier: issue.id, + entity_name: "issue", + entity_display_name: `${issue.project__identifier}-${issue.sequence_id}`, + title: `${issue.project__identifier}-${issue.sequence_id}`, + subTitle: issue.name, + icon: null, + })); + sections.push({ + key: "issues", + title: "Work items", + items, + }); + } + + if (response.project?.length) { + const items: TMentionSuggestion[] = (response.project as TProjectSearchResponse[]).map((project) => ({ + id: project.id, + entity_identifier: project.id, + entity_name: "project", + entity_display_name: project.identifier, + title: project.identifier, + subTitle: project.name, + icon: null, + })); + sections.push({ + key: "projects", + title: "Projects", + items, + }); + } + + return { sections }; + }, + [enableAdvancedMentions] ); const parseAdditionalEditorContent = useCallback( - (_args: TAdditionalParseEditorContentArgs): TAdditionalParseEditorContentReturnType => undefined, - [] + ({ id, entityType }: TAdditionalParseEditorContentArgs): TAdditionalParseEditorContentReturnType => { + if (!enableAdvancedMentions || !workspaceSlug) return undefined; + + if (entityType === "issue") { + const issue = getIssueById(id); + const project = issue?.project_id ? getProjectById(issue.project_id) : undefined; + const identifier = project?.identifier; + const sequenceId = issue?.sequence_id; + + if (!identifier || sequenceId === undefined) return undefined; + + return { + textContent: `${identifier}-${sequenceId}`, + redirectionPath: generateWorkItemLink({ + workspaceSlug: workspaceSlug.toString(), + projectIdentifier: identifier, + sequenceId, + issueId: id, + projectId: issue?.project_id, + }), + }; + } + + if (entityType === "project") { + const project = getProjectById(id); + if (!project) return undefined; + + return { + textContent: project.identifier, + redirectionPath: `/${workspaceSlug.toString()}/projects/${id}/`, + }; + } + + return undefined; + }, + [enableAdvancedMentions, getIssueById, getProjectById, workspaceSlug] ); - const editorMentionTypes: TSearchEntities[] = useMemo(() => ["user_mention"], []); + const editorMentionTypes: TSearchEntities[] = useMemo( + () => (enableAdvancedMentions ? ["user_mention", "issue", "project"] : ["user_mention"]), + [enableAdvancedMentions] + ); return { updateAdditionalSections, diff --git a/packages/editor/src/core/extensions/mentions/extension-config.ts b/packages/editor/src/core/extensions/mentions/extension-config.ts index f3c24511faa..6ab20472124 100644 --- a/packages/editor/src/core/extensions/mentions/extension-config.ts +++ b/packages/editor/src/core/extensions/mentions/extension-config.ts @@ -6,7 +6,7 @@ import { mergeAttributes } from "@tiptap/core"; import type { MentionOptions } from "@tiptap/extension-mention"; -import Mention from "@tiptap/extension-mention"; +import MentionExtension from "@tiptap/extension-mention"; import type { MarkdownSerializerState } from "@tiptap/pm/markdown"; import type { Node as NodeType } from "@tiptap/pm/model"; // types @@ -20,7 +20,7 @@ export type TMentionExtensionOptions = MentionOptions & { getMentionedEntityDetails: TMentionHandler["getMentionedEntityDetails"]; }; -export const CustomMentionExtensionConfig = Mention.extend({ +export const CustomMentionExtensionConfig = MentionExtension.extend({ addAttributes() { return { [EMentionComponentAttributeNames.ID]: { @@ -32,6 +32,9 @@ export const CustomMentionExtensionConfig = Mention.extend ); diff --git a/packages/editor/src/core/extensions/mentions/types.ts b/packages/editor/src/core/extensions/mentions/types.ts index 9d175bc5f3a..a90805ba950 100644 --- a/packages/editor/src/core/extensions/mentions/types.ts +++ b/packages/editor/src/core/extensions/mentions/types.ts @@ -11,10 +11,12 @@ export enum EMentionComponentAttributeNames { ID = "id", ENTITY_IDENTIFIER = "entity_identifier", ENTITY_NAME = "entity_name", + ENTITY_DISPLAY_NAME = "entity_display_name", } export type TMentionComponentAttributes = { [EMentionComponentAttributeNames.ID]: string | null; [EMentionComponentAttributeNames.ENTITY_IDENTIFIER]: string | null; [EMentionComponentAttributeNames.ENTITY_NAME]: TSearchEntities | null; + [EMentionComponentAttributeNames.ENTITY_DISPLAY_NAME]: string | null; }; diff --git a/packages/editor/src/core/types/mention.ts b/packages/editor/src/core/types/mention.ts index 6481b165940..991d8a08670 100644 --- a/packages/editor/src/core/types/mention.ts +++ b/packages/editor/src/core/types/mention.ts @@ -10,6 +10,7 @@ import type { TSearchEntities } from "@plane/types"; export type TMentionSuggestion = { entity_identifier: string; entity_name: TSearchEntities; + entity_display_name?: string; icon: React.ReactNode; id: string; subTitle?: string; @@ -22,7 +23,9 @@ export type TMentionSection = { items: TMentionSuggestion[]; }; -export type TCallbackMentionComponentProps = Pick; +export type TCallbackMentionComponentProps = Pick & { + entity_display_name?: string | null; +}; export type TMentionHandler = { getMentionedEntityDetails?: (entity_identifier: string) => { display_name: string } | undefined;