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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import React, { forwardRef } from "react";
import { forwardRef } from "react";
// plane imports
import { DocumentEditorWithRef } 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
const { fetchMentions } = useEditorMention({
enableAdvancedMentions: true,
searchEntity: editable ? async (payload) => await props.searchMentionCallback(payload) : async () => ({}),
});
// editor config

View file

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

View file

@ -1,15 +1,11 @@
import { useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { usePopper } from "react-popper";
import { Link } from "react-router";
// plane imports
import { ROLE } from "@plane/constants";
// plane ui
import { Popover } from "@plane/propel/popover";
import { Avatar } from "@plane/ui";
import { cn, getFileURL } from "@plane/utils";
// constants
// helpers
// hooks
import { useMember } from "@/hooks/store/use-member";
import { useUser } from "@/hooks/store/user";
@ -22,9 +18,6 @@ export const EditorUserMention: React.FC<Props> = observer((props) => {
const { id } = props;
// router
const { projectId } = useParams();
// states
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const [referenceElement, setReferenceElement] = useState<HTMLAnchorElement | null>(null);
// params
const { workspaceSlug } = useParams();
// store hooks
@ -33,18 +26,6 @@ export const EditorUserMention: React.FC<Props> = observer((props) => {
getUserDetails,
project: { getProjectMemberDetails },
} = useMember();
// popper-js refs
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
// derived values
const userDetails = getUserDetails(id);
const roleDetails = projectId ? getProjectMemberDetails(id, projectId.toString())?.role : null;
@ -53,7 +34,7 @@ export const EditorUserMention: React.FC<Props> = observer((props) => {
if (!userDetails) {
return (
<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>
);
}
@ -61,21 +42,18 @@ export const EditorUserMention: React.FC<Props> = observer((props) => {
return (
<div
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,
}
)}
>
<Link href={profileLink} ref={setReferenceElement}>
@{userDetails?.display_name}
</Link>
<div
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"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<Popover delay={100} openOnHover>
<Popover.Button>
<Link to={profileLink}>@{userDetails?.display_name}</Link>
</Popover.Button>
<Popover.Panel side="bottom" align="start">
<div className="w-60 bg-custom-background-100 shadow-custom-shadow-rg rounded-lg p-3 border-[0.5px] border-custom-border-300">
<div className="flex items-center gap-3">
<div className="flex-shrink-0 size-10 grid place-items-center">
<Avatar
@ -87,13 +65,15 @@ export const EditorUserMention: React.FC<Props> = observer((props) => {
/>
</div>
<div>
<Link href={profileLink} className="not-prose font-medium text-custom-text-100 text-sm hover:underline">
<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>
</Popover.Panel>
</Popover>
</div>
);
});

View file

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

View file

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

View file

@ -15,7 +15,9 @@ export const useParseEditorContent = () => {
// store hooks
const { getUserDetails } = useMember();
// 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

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

View file

@ -1,4 +1,5 @@
import type { TIssuePriorities } from "../issues";
import type { TStateGroups } from "../state";
import type { TIssuePublicComment } from "./activity/issue_comment";
import type { TIssueAttachment } from "./issue_attachment";
import type { TIssueLink } from "./issue_link";
@ -93,7 +94,7 @@ export type TIssue = TBaseIssue & {
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?: string;
state__group?: string | null;
state__group?: TStateGroups | null;
};
export type TIssueMap = {