[WIKI-773] refactor: editor mention components and hooks (#8111)

This commit is contained in:
Aaryan Khandelwal 2025-11-17 14:07:37 +05:30 committed by GitHub
parent c04ae51d20
commit c65e2c4aab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 84 additions and 96 deletions

View file

@ -1,4 +1,4 @@
// plane editor // plane editor
import type { TMentionComponentProps } from "@plane/editor"; import type { TCallbackMentionComponentProps } from "@plane/editor";
export const EditorAdditionalMentionsRoot: React.FC<TMentionComponentProps> = () => null; export const EditorAdditionalMentionsRoot: React.FC<TCallbackMentionComponentProps> = () => null;

View file

@ -1,11 +1,11 @@
// plane editor // plane editor
import type { TMentionComponentProps } from "@plane/editor"; import type { TCallbackMentionComponentProps } from "@plane/editor";
// plane web components // plane web components
import { EditorAdditionalMentionsRoot } from "@/plane-web/components/editor"; import { EditorAdditionalMentionsRoot } from "@/plane-web/components/editor";
// local components // local components
import { EditorUserMention } from "./user"; import { EditorUserMention } from "./user";
export const EditorMentionsRoot: React.FC<TMentionComponentProps> = (props) => { export const EditorMentionsRoot: React.FC<TCallbackMentionComponentProps> = (props) => {
const { entity_identifier, entity_name } = props; const { entity_identifier, entity_name } = props;
switch (entity_name) { switch (entity_name) {

View file

@ -1 +0,0 @@
export * from "./mentions";

View file

@ -1,4 +1,6 @@
// plane editor // plane imports
import type { TMentionComponentProps } from "@plane/editor"; import type { TCallbackMentionComponentProps } from "@plane/editor";
export const EditorAdditionalMentionsRoot: React.FC<TMentionComponentProps> = () => null; export type TEditorMentionComponentProps = TCallbackMentionComponentProps;
export const EditorAdditionalMentionsRoot: React.FC<TEditorMentionComponentProps> = () => null;

View file

@ -1 +0,0 @@
export * from "./embeds";

View file

@ -1,4 +0,0 @@
// plane types
import type { TSearchEntities } from "@plane/types";
export const EDITOR_MENTION_TYPES: TSearchEntities[] = ["user_mention"];

View file

@ -1,11 +1,18 @@
import { useCallback } from "react"; import { useCallback, useMemo } from "react";
// plane editor // plane editor
import type { TMentionSection } from "@plane/editor"; import type { TMentionSection } from "@plane/editor";
// plane types // plane types
import type { TSearchEntities, TSearchResponse } from "@plane/types"; import type { TSearchEntities, TSearchResponse } from "@plane/types";
export type TUseAdditionalEditorMentionArgs = {
enableAdvancedMentions: boolean;
};
export type TAdditionalEditorMentionHandlerArgs = { export type TAdditionalEditorMentionHandlerArgs = {
response: TSearchResponse; response: TSearchResponse;
};
export type TAdditionalEditorMentionHandlerReturnType = {
sections: TMentionSection[]; sections: TMentionSection[];
}; };
@ -21,21 +28,24 @@ export type TAdditionalParseEditorContentReturnType =
} }
| undefined; | undefined;
export const useAdditionalEditorMention = () => { export const useAdditionalEditorMention = (_args: TUseAdditionalEditorMentionArgs) => {
const updateAdditionalSections = useCallback((args: TAdditionalEditorMentionHandlerArgs) => { const updateAdditionalSections = useCallback(
const {} = args; (_args: TAdditionalEditorMentionHandlerArgs): TAdditionalEditorMentionHandlerReturnType => ({
}, []); sections: [],
}),
const parseAdditionalEditorContent = useCallback(
(args: TAdditionalParseEditorContentArgs): TAdditionalParseEditorContentReturnType => {
const {} = args;
return undefined;
},
[] []
); );
const parseAdditionalEditorContent = useCallback(
(_args: TAdditionalParseEditorContentArgs): TAdditionalParseEditorContentReturnType => undefined,
[]
);
const editorMentionTypes: TSearchEntities[] = useMemo(() => ["user_mention"], []);
return { return {
updateAdditionalSections, updateAdditionalSections,
parseAdditionalEditorContent, parseAdditionalEditorContent,
editorMentionTypes,
}; };
}; };

View file

@ -1,4 +1,4 @@
import React, { forwardRef } from "react"; import { forwardRef } from "react";
// plane imports // plane imports
import { DocumentEditorWithRef } from "@plane/editor"; import { DocumentEditorWithRef } from "@plane/editor";
import type { IEditorPropsExtended, EditorRefApi, IDocumentEditorProps, TFileHandler } from "@plane/editor"; import type { IEditorPropsExtended, EditorRefApi, IDocumentEditorProps, TFileHandler } from "@plane/editor";
@ -50,6 +50,7 @@ export const DocumentEditor = forwardRef<EditorRefApi, DocumentEditorWrapperProp
}); });
// use editor mention // use editor mention
const { fetchMentions } = useEditorMention({ const { fetchMentions } = useEditorMention({
enableAdvancedMentions: true,
searchEntity: editable ? async (payload) => await props.searchMentionCallback(payload) : async () => ({}), searchEntity: editable ? async (payload) => await props.searchMentionCallback(payload) : async () => ({}),
}); });
// editor config // editor config

View file

@ -1,11 +1,10 @@
// plane editor // plane web imports
import type { TMentionComponentProps } from "@plane/editor"; import type { TEditorMentionComponentProps } from "@/plane-web/components/editor/embeds/mentions";
// plane web components import { EditorAdditionalMentionsRoot } from "@/plane-web/components/editor/embeds/mentions";
import { EditorAdditionalMentionsRoot } from "@/plane-web/components/editor"; // local imports
// local components
import { EditorUserMention } from "./user"; import { EditorUserMention } from "./user";
export const EditorMentionsRoot: React.FC<TMentionComponentProps> = (props) => { export const EditorMentionsRoot: React.FC<TEditorMentionComponentProps> = (props) => {
const { entity_identifier, entity_name } = props; const { entity_identifier, entity_name } = props;
switch (entity_name) { switch (entity_name) {

View file

@ -1,15 +1,11 @@
import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { usePopper } from "react-popper"; import { Link } from "react-router";
// plane imports // plane imports
import { ROLE } from "@plane/constants"; import { ROLE } from "@plane/constants";
// plane ui import { Popover } from "@plane/propel/popover";
import { Avatar } from "@plane/ui"; import { Avatar } from "@plane/ui";
import { cn, getFileURL } from "@plane/utils"; import { cn, getFileURL } from "@plane/utils";
// constants
// helpers
// hooks // hooks
import { useMember } from "@/hooks/store/use-member"; import { useMember } from "@/hooks/store/use-member";
import { useUser } from "@/hooks/store/user"; import { useUser } from "@/hooks/store/user";
@ -22,9 +18,6 @@ export const EditorUserMention: React.FC<Props> = observer((props) => {
const { id } = props; const { id } = props;
// router // router
const { projectId } = useParams(); const { projectId } = useParams();
// states
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const [referenceElement, setReferenceElement] = useState<HTMLAnchorElement | null>(null);
// params // params
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
// store hooks // store hooks
@ -33,18 +26,6 @@ export const EditorUserMention: React.FC<Props> = observer((props) => {
getUserDetails, getUserDetails,
project: { getProjectMemberDetails }, project: { getProjectMemberDetails },
} = useMember(); } = useMember();
// popper-js refs
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
// derived values // derived values
const userDetails = getUserDetails(id); const userDetails = getUserDetails(id);
const roleDetails = projectId ? getProjectMemberDetails(id, projectId.toString())?.role : null; const roleDetails = projectId ? getProjectMemberDetails(id, projectId.toString())?.role : null;
@ -53,7 +34,7 @@ export const EditorUserMention: React.FC<Props> = observer((props) => {
if (!userDetails) { if (!userDetails) {
return ( return (
<div className="not-prose inline px-1 py-0.5 rounded bg-custom-background-80 text-custom-text-300 no-underline"> <div className="not-prose inline px-1 py-0.5 rounded bg-custom-background-80 text-custom-text-300 no-underline">
@deactivated user @suspended user
</div> </div>
); );
} }
@ -61,39 +42,38 @@ export const EditorUserMention: React.FC<Props> = observer((props) => {
return ( return (
<div <div
className={cn( className={cn(
"not-prose group/user-mention inline px-1 py-0.5 rounded bg-custom-primary-100/20 text-custom-primary-100 no-underline", "not-prose inline px-1 py-0.5 rounded bg-custom-primary-100/20 text-custom-primary-100 no-underline",
{ {
"bg-yellow-500/20 text-yellow-500": id === currentUser?.id, "bg-yellow-500/20 text-yellow-500": id === currentUser?.id,
} }
)} )}
> >
<Link href={profileLink} ref={setReferenceElement}> <Popover delay={100} openOnHover>
@{userDetails?.display_name} <Popover.Button>
</Link> <Link to={profileLink}>@{userDetails?.display_name}</Link>
<div </Popover.Button>
className="top-full left-0 z-10 min-w-60 bg-custom-background-90 shadow-custom-shadow-rg rounded-lg p-4 opacity-0 pointer-events-none group-hover/user-mention:opacity-100 group-hover/user-mention:pointer-events-auto transition-opacity" <Popover.Panel side="bottom" align="start">
ref={setPopperElement} <div className="w-60 bg-custom-background-100 shadow-custom-shadow-rg rounded-lg p-3 border-[0.5px] border-custom-border-300">
style={styles.popper} <div className="flex items-center gap-3">
{...attributes.popper} <div className="flex-shrink-0 size-10 grid place-items-center">
> <Avatar
<div className="flex items-center gap-3"> src={getFileURL(userDetails?.avatar_url ?? "")}
<div className="flex-shrink-0 size-10 grid place-items-center"> name={userDetails?.display_name}
<Avatar size={40}
src={getFileURL(userDetails?.avatar_url ?? "")} className="text-xl"
name={userDetails?.display_name} showTooltip={false}
size={40} />
className="text-xl" </div>
showTooltip={false} <div>
/> <Link to={profileLink} className="not-prose font-medium text-custom-text-100 text-sm hover:underline">
{userDetails?.first_name} {userDetails?.last_name}
</Link>
{roleDetails && <p className="text-custom-text-200 text-xs">{ROLE[roleDetails]}</p>}
</div>
</div>
</div> </div>
<div> </Popover.Panel>
<Link href={profileLink} className="not-prose font-medium text-custom-text-100 text-sm hover:underline"> </Popover>
{userDetails?.first_name} {userDetails?.last_name}
</Link>
{roleDetails && <p className="text-custom-text-200 text-xs">{ROLE[roleDetails]}</p>}
</div>
</div>
</div>
</div> </div>
); );
}); });

View file

@ -95,6 +95,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? ""; const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? "";
// use editor mention // use editor mention
const { fetchMentions } = useEditorMention({ const { fetchMentions } = useEditorMention({
enableAdvancedMentions: true,
searchEntity: handlers.fetchEntity, searchEntity: handlers.fetchEntity,
}); });
// editor flaggings // editor flaggings

View file

@ -7,26 +7,27 @@ import type { TSearchEntities, TSearchEntityRequestPayload, TSearchResponse, TUs
import { Avatar } from "@plane/ui"; import { Avatar } from "@plane/ui";
// helpers // helpers
import { getFileURL } from "@plane/utils"; import { getFileURL } from "@plane/utils";
// plane web constants
import { EDITOR_MENTION_TYPES } from "@/plane-web/constants/editor";
// plane web hooks // plane web hooks
import { useAdditionalEditorMention } from "@/plane-web/hooks/use-additional-editor-mention"; import { useAdditionalEditorMention } from "@/plane-web/hooks/use-additional-editor-mention";
type TArgs = { type TArgs = {
enableAdvancedMentions?: boolean;
searchEntity: (payload: TSearchEntityRequestPayload) => Promise<TSearchResponse>; searchEntity: (payload: TSearchEntityRequestPayload) => Promise<TSearchResponse>;
}; };
export const useEditorMention = (args: TArgs) => { export const useEditorMention = (args: TArgs) => {
const { searchEntity } = args; const { enableAdvancedMentions = false, searchEntity } = args;
// additional mentions // additional mentions
const { updateAdditionalSections } = useAdditionalEditorMention(); const { editorMentionTypes, updateAdditionalSections } = useAdditionalEditorMention({
enableAdvancedMentions,
});
// fetch mentions handler // fetch mentions handler
const fetchMentions = useCallback( const fetchMentions = useCallback(
async (query: string): Promise<TMentionSection[]> => { async (query: string): Promise<TMentionSection[]> => {
try { try {
const res = await searchEntity({ const res = await searchEntity({
count: 5, count: 5,
query_type: EDITOR_MENTION_TYPES, query_type: editorMentionTypes,
query, query,
}); });
const suggestionSections: TMentionSection[] = []; const suggestionSections: TMentionSection[] = [];
@ -57,17 +58,16 @@ export const useEditorMention = (args: TArgs) => {
}); });
} }
}); });
updateAdditionalSections({ const { sections } = updateAdditionalSections({
response: res, response: res,
sections: suggestionSections,
}); });
return suggestionSections; return [...suggestionSections, ...sections];
} catch (error) { } catch (error) {
console.error("Error in fetching mentions for project pages:", error); console.error("Error in fetching mentions:", error);
throw error; throw error;
} }
}, },
[searchEntity, updateAdditionalSections] [editorMentionTypes, searchEntity, updateAdditionalSections]
); );
return { return {

View file

@ -15,7 +15,9 @@ export const useParseEditorContent = () => {
// store hooks // store hooks
const { getUserDetails } = useMember(); const { getUserDetails } = useMember();
// parse additional content // parse additional content
const { parseAdditionalEditorContent } = useAdditionalEditorMention(); const { parseAdditionalEditorContent } = useAdditionalEditorMention({
enableAdvancedMentions: true,
});
/** /**
* @description function to replace all the custom components from the html component to make it pdf compatible * @description function to replace all the custom components from the html component to make it pdf compatible

View file

@ -1 +0,0 @@
export * from "ce/components/editor";

View file

@ -1 +0,0 @@
export * from "ce/constants/editor";

View file

@ -16,10 +16,10 @@ export type TMentionSection = {
items: TMentionSuggestion[]; items: TMentionSuggestion[];
}; };
export type TMentionComponentProps = Pick<TMentionSuggestion, "entity_identifier" | "entity_name">; export type TCallbackMentionComponentProps = Pick<TMentionSuggestion, "entity_identifier" | "entity_name">;
export type TMentionHandler = { export type TMentionHandler = {
getMentionedEntityDetails?: (entity_identifier: string) => { display_name: string } | undefined; getMentionedEntityDetails?: (entity_identifier: string) => { display_name: string } | undefined;
renderComponent: (props: TMentionComponentProps) => React.ReactNode; renderComponent: (props: TCallbackMentionComponentProps) => React.ReactNode;
searchCallback?: (query: string) => Promise<TMentionSection[]>; searchCallback?: (query: string) => Promise<TMentionSection[]>;
}; };

View file

@ -1,4 +1,5 @@
import type { TIssuePriorities } from "../issues"; import type { TIssuePriorities } from "../issues";
import type { TStateGroups } from "../state";
import type { TIssuePublicComment } from "./activity/issue_comment"; import type { TIssuePublicComment } from "./activity/issue_comment";
import type { TIssueAttachment } from "./issue_attachment"; import type { TIssueAttachment } from "./issue_attachment";
import type { TIssueLink } from "./issue_link"; import type { TIssueLink } from "./issue_link";
@ -93,7 +94,7 @@ export type TIssue = TBaseIssue & {
tempId?: string; tempId?: string;
// sourceIssueId is used to store the original issue id when creating a copy of an issue. Used in cloning property values. It is not a part of the API response. // sourceIssueId is used to store the original issue id when creating a copy of an issue. Used in cloning property values. It is not a part of the API response.
sourceIssueId?: string; sourceIssueId?: string;
state__group?: string | null; state__group?: TStateGroups | null;
}; };
export type TIssueMap = { export type TIssueMap = {