[WIKI-773] refactor: editor mention components and hooks (#8111)
This commit is contained in:
parent
c04ae51d20
commit
c65e2c4aab
17 changed files with 84 additions and 96 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "./mentions";
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "./embeds";
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
// plane types
|
|
||||||
import type { TSearchEntities } from "@plane/types";
|
|
||||||
|
|
||||||
export const EDITOR_MENTION_TYPES: TSearchEntities[] = ["user_mention"];
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "ce/components/editor";
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "ce/constants/editor";
|
|
||||||
|
|
@ -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[]>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue