mentions loading state part done

This commit is contained in:
Palanikannan1437 2024-02-21 18:26:49 +05:30
parent 875eb054a2
commit a3033203af
14 changed files with 375 additions and 171 deletions

View File

@ -1,3 +1,4 @@
import { Editor, Range } from "@tiptap/react";
export type IMentionSuggestion = {
id: string;
type: string;
@ -7,4 +8,9 @@ export type IMentionSuggestion = {
redirect_uri: string;
};
export type CommandProps = {
editor: Editor;
range: Range;
};
export type IMentionHighlight = string;

View File

@ -35,7 +35,8 @@ export const CoreEditorExtensions = (
deleteFile: DeleteImage,
restoreFile: RestoreImage,
cancelUploadImage?: () => any
) => [
) => {
return [
StarterKit.configure({
bulletList: {
HTMLAttributes: {
@ -110,4 +111,5 @@ export const CoreEditorExtensions = (
TableCell,
TableRow,
Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, false),
];
];
};

View File

@ -32,6 +32,12 @@ export const CustomMention = Mention.extend<CustomMentionOptions>({
redirect_uri: {
default: "/",
},
entity_identifier: {
default: null,
},
entity_name: {
default: null,
},
};
},
@ -43,17 +49,6 @@ export const CustomMention = Mention.extend<CustomMentionOptions>({
return [
{
tag: "mention-component",
getAttrs: (node: string | HTMLElement) => {
if (typeof node === "string") {
return null;
}
return {
id: node.getAttribute("data-mention-id") || "",
target: node.getAttribute("data-mention-target") || "",
label: node.innerText.slice(1) || "",
redirect_uri: node.getAttribute("redirect_uri"),
};
},
},
];
},

View File

@ -1,15 +1,115 @@
// @ts-nocheck
import { Suggestion } from "src/ui/mentions/suggestion";
import { CustomMention } from "src/ui/mentions/custom";
import { IMentionHighlight } from "src/types/mention-suggestion";
import { IMentionHighlight, IMentionSuggestion } from "src/types/mention-suggestion";
import { ReactRenderer } from "@tiptap/react";
import { Editor } from "@tiptap/core";
import tippy from "tippy.js";
export const Mentions = (mentionSuggestions: IMentionSuggestion[], mentionHighlights: IMentionHighlight[], readonly) =>
import { v4 as uuidv4 } from "uuid";
import { MentionList } from "src/ui/mentions/mention-list";
export const getSuggestionItems =
(getSuggestions: () => Promise<IMentionSuggestion[]>) =>
async ({ query }: { query: string }) => {
console.log("yaa");
const suggestions = await getSuggestions();
const mappedSuggestions: IMentionSuggestion[] = suggestions.map((suggestion): IMentionSuggestion => {
const transactionId = uuidv4();
return {
...suggestion,
id: transactionId,
};
});
const filteredSuggestions = mappedSuggestions
.filter((suggestion) => suggestion.title.toLowerCase().startsWith(query.toLowerCase()))
.slice(0, 5);
console.log("yoo", filteredSuggestions);
return filteredSuggestions;
};
export const Mentions = (
mentionSuggestions: () => Promise<IMentionSuggestion[]>,
mentionHighlights: IMentionHighlight[],
readonly: boolean
) =>
CustomMention.configure({
HTMLAttributes: {
class: "mention",
},
readonly: readonly,
mentionHighlights: mentionHighlights,
suggestion: Suggestion(mentionSuggestions),
suggestion: {
items: ({ query }) => {
const suggestions = mentionSuggestions();
const mappedSuggestions: IMentionSuggestion[] = suggestions.map((suggestion): IMentionSuggestion => {
const transactionId = uuidv4();
return {
...suggestion,
id: transactionId,
};
});
const filteredSuggestions = mappedSuggestions
.filter((suggestion) => suggestion.title.toLowerCase().startsWith(query.toLowerCase()))
.slice(0, 5);
console.log("yoo", filteredSuggestions);
return filteredSuggestions;
},
// @ts-ignore
render: () => {
let reactRenderer: ReactRenderer | null = null;
let popup: any | null = null;
return {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
props.editor.storage.mentionsOpen = true;
reactRenderer = new ReactRenderer(MentionList, {
props,
editor: props.editor,
});
// @ts-ignore
popup = tippy("body", {
getReferenceClientRect: props.clientRect,
appendTo: () => document.querySelector("#editor-container"),
content: reactRenderer.element,
showOnCreate: true,
interactive: true,
trigger: "manual",
placement: "bottom-start",
});
},
onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
reactRenderer?.updateProps(props);
popup &&
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown: (props: { event: KeyboardEvent }) => {
if (props.event.key === "Escape") {
popup?.[0].hide();
return true;
}
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
if (navigationKeys.includes(props.event.key)) {
// @ts-ignore
reactRenderer?.ref?.onKeyDown(props);
event?.stopPropagation();
return true;
}
return false;
},
onExit: (props: { editor: Editor; event: KeyboardEvent }) => {
props.editor.storage.mentionsOpen = false;
popup?.[0].destroy();
reactRenderer?.destroy();
},
};
},
},
});

View File

@ -19,6 +19,8 @@ export const MentionList = forwardRef((props: MentionListProps, ref) => {
props.command({
id: item.id,
label: item.title,
entity_identifier: item.entity_identifier,
entity_name: item.entity_name,
target: "users",
redirect_uri: item.redirect_uri,
});

View File

@ -20,13 +20,13 @@ export const MentionNodeView = (props) => {
<NodeViewWrapper className="mention-component inline w-fit">
<span
className={cn("mention rounded bg-custom-primary-100/20 px-1 py-0.5 font-medium text-custom-primary-100", {
"bg-yellow-500/20 text-yellow-500": highlights ? highlights.includes(props.node.attrs.id) : false,
"bg-yellow-500/20 text-yellow-500": highlights
? highlights.includes(props.node.attrs.entity_identifier)
: false,
"cursor-pointer": !props.extension.options.readonly,
// "hover:bg-custom-primary-300" : !props.extension.options.readonly && !highlights.includes(props.node.attrs.id)
})}
onClick={handleClick}
data-mention-target={props.node.attrs.target}
data-mention-id={props.node.attrs.id}
>
@{props.node.attrs.label}
</span>

View File

@ -2,65 +2,80 @@ import { ReactRenderer } from "@tiptap/react";
import { Editor } from "@tiptap/core";
import tippy from "tippy.js";
import { MentionList } from "src/ui/mentions/mention-list";
import { v4 as uuidv4 } from "uuid";
import { IMentionSuggestion } from "src/types/mention-suggestion";
import { MentionList } from "src/ui/mentions/mention-list";
export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
items: ({ query }: { query: string }) =>
suggestions.filter((suggestion) => suggestion.title.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5),
render: () => {
let reactRenderer: ReactRenderer | null = null;
let popup: any | null = null;
export const getSuggestionItems = (suggestions: IMentionSuggestion[]) => {
return ({ query }: { query: string }) => {
const mappedSuggestions: IMentionSuggestion[] = suggestions.map((suggestion): IMentionSuggestion => {
const transactionId = uuidv4();
return {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
props.editor.storage.mentionsOpen = true;
reactRenderer = new ReactRenderer(MentionList, {
props,
editor: props.editor,
});
// @ts-ignore
popup = tippy("body", {
getReferenceClientRect: props.clientRect,
appendTo: () => document.querySelector("#editor-container"),
content: reactRenderer.element,
showOnCreate: true,
interactive: true,
trigger: "manual",
placement: "bottom-start",
});
},
onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
reactRenderer?.updateProps(props);
popup &&
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown: (props: { event: KeyboardEvent }) => {
if (props.event.key === "Escape") {
popup?.[0].hide();
return true;
}
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
if (navigationKeys.includes(props.event.key)) {
// @ts-ignore
reactRenderer?.ref?.onKeyDown(props);
event?.stopPropagation();
return true;
}
return false;
},
onExit: (props: { editor: Editor; event: KeyboardEvent }) => {
props.editor.storage.mentionsOpen = false;
popup?.[0].destroy();
reactRenderer?.destroy();
},
...suggestion,
id: transactionId,
};
},
});
});
return mappedSuggestions
.filter((suggestion) => suggestion.title.toLowerCase().startsWith(query.toLowerCase()))
.slice(0, 5);
};
};
// export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
// items: getSuggestionItems(suggestions),
// render: () => {
// let reactRenderer: ReactRenderer | null = null;
// let popup: any | null = null;
//
// return {
// onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
// props.editor.storage.mentionsOpen = true;
// reactRenderer = new ReactRenderer(MentionList, {
// props,
// editor: props.editor,
// });
// // @ts-ignore
// popup = tippy("body", {
// getReferenceClientRect: props.clientRect,
// appendTo: () => document.querySelector("#editor-container"),
// content: reactRenderer.element,
// showOnCreate: true,
// interactive: true,
// trigger: "manual",
// placement: "bottom-start",
// });
// },
//
// onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
// reactRenderer?.updateProps(props);
//
// popup &&
// popup[0].setProps({
// getReferenceClientRect: props.clientRect,
// });
// },
// onKeyDown: (props: { event: KeyboardEvent }) => {
// if (props.event.key === "Escape") {
// popup?.[0].hide();
//
// return true;
// }
//
// const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
//
// if (navigationKeys.includes(props.event.key)) {
// // @ts-ignore
// reactRenderer?.ref?.onKeyDown(props);
// event?.stopPropagation();
// return true;
// }
// return false;
// },
// onExit: (props: { editor: Editor; event: KeyboardEvent }) => {
// props.editor.storage.mentionsOpen = false;
// popup?.[0].destroy();
// reactRenderer?.destroy();
// },
// };
// },
// });

View File

@ -1,6 +1,13 @@
"use client";
import React, { useState } from "react";
import { UploadImage, DeleteImage, RestoreImage, getEditorClassNames, useEditor } from "@plane/editor-core";
import {
UploadImage,
DeleteImage,
RestoreImage,
getEditorClassNames,
useEditor,
IMentionSuggestion,
} from "@plane/editor-core";
import { DocumentEditorExtensions } from "src/ui/extensions";
import { IDuplicationConfig, IPageArchiveConfig, IPageLockConfig } from "src/types/menu-actions";
import { EditorHeader } from "src/ui/components/editor-header";
@ -43,6 +50,9 @@ interface IDocumentEditor {
debouncedUpdatesEnabled?: boolean;
isSubmitting: "submitting" | "submitted" | "saved";
mentionHighlights?: string[];
mentionSuggestions?: IMentionSuggestion[];
// embed configuration
duplicationConfig?: IDuplicationConfig;
pageLockConfig?: IPageLockConfig;
@ -66,6 +76,8 @@ const DocumentEditor = ({
editorContentCustomClassNames,
value,
uploadFile,
mentionHighlights,
mentionSuggestions,
deleteFile,
restoreFile,
isSubmitting,
@ -109,6 +121,8 @@ const DocumentEditor = ({
cancelUploadImage,
rerenderOnPropsChange,
forwardedRef,
mentionSuggestions,
mentionHighlights,
extensions: DocumentEditorExtensions(uploadFile, setHideDragHandleFunction, setIsSubmitting),
});

View File

@ -22,6 +22,8 @@ interface IDocumentReadOnlyEditor {
documentDetails: DocumentDetails;
pageLockConfig?: IPageLockConfig;
pageArchiveConfig?: IPageArchiveConfig;
mentionHighlights?: string[];
pageDuplicationConfig?: IDuplicationConfig;
onActionCompleteHandler: (action: {
title: string;
@ -44,6 +46,7 @@ const DocumentReadOnlyEditor = ({
borderOnFocus,
customClassName,
value,
mentionHighlights,
documentDetails,
forwardedRef,
pageDuplicationConfig,
@ -58,6 +61,7 @@ const DocumentReadOnlyEditor = ({
const editor = useReadOnlyEditor({
value,
mentionHighlights,
forwardedRef,
rerenderOnPropsChange,
extensions: [IssueWidgetPlaceholder()],

View File

@ -81,7 +81,6 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
render={({ field: { value, onChange } }) => (
<LiteTextEditorWithRef
onEnterKeyPress={(e) => {
console.log("yo");
handleSubmit(onSubmit)(e);
}}
cancelUploadImage={fileService.cancelUpload}

View File

@ -1,11 +1,50 @@
import { useContext } from "react";
// mobx store
import { StoreContext } from "contexts/store-context";
// types
import { IMentionStore } from "store/mention.store";
import useSWR from "swr";
export const useMention = (): IMentionStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useMention must be used within StoreProvider");
return context.mention;
import { ProjectMemberService } from "services/project";
import { IProjectMember } from "@plane/types";
import { UserService } from "services/user.service";
import { useRef, useEffect } from "react";
export const useMention = ({ workspaceSlug, projectId }: { workspaceSlug: string; projectId: string }) => {
const userService = new UserService();
const projectMemberService = new ProjectMemberService();
const { data: projectMembers } = useSWR(["projectMembers", workspaceSlug, projectId], async () => {
const members = await projectMemberService.fetchProjectMembers(workspaceSlug, projectId);
const detailedMembers = await Promise.all(
members.map(async (member) => projectMemberService.getProjectMember(workspaceSlug, projectId, member.id))
);
return detailedMembers;
});
const projectMembersRef = useRef<IProjectMember[] | undefined>();
useEffect(() => {
if (projectMembers) {
projectMembersRef.current = projectMembers;
}
}, [projectMembers]);
const { data: user } = useSWR("currentUser", async () => userService.currentUser());
const mentionHighlights = user ? [user.id] : [];
const getMentionSuggestions = () => () => {
const mentionSuggestions =
projectMembersRef.current?.map((memberDetails) => ({
entity_name: "user_mention",
entity_identifier: `${memberDetails?.member?.id}`,
type: "User",
title: `${memberDetails?.member?.display_name}`,
subtitle: memberDetails?.member?.email ?? "",
avatar: `${memberDetails?.member?.avatar}`,
redirect_uri: `/${workspaceSlug}/profile/${memberDetails?.member?.id}`,
})) || [];
return mentionSuggestions;
};
return {
getMentionSuggestions,
mentionHighlights,
};
};

View File

@ -6,7 +6,7 @@ import { ReactElement, useEffect, useRef, useState } from "react";
import { Controller, useForm } from "react-hook-form";
// hooks
import { useApplication, usePage, useUser, useWorkspace } from "hooks/store";
import { useApplication, useMention, usePage, useUser, useWorkspace } from "hooks/store";
import useReloadConfirmations from "hooks/use-reload-confirmation";
import useToast from "hooks/use-toast";
// services
@ -29,6 +29,8 @@ import { NextPageWithLayout } from "lib/types";
import { EUserProjectRoles } from "constants/project";
import { useProjectPages } from "hooks/store/use-project-specific-pages";
import { IssuePeekOverview } from "components/issues";
import { ProjectMemberService } from "services/project";
import { UserService } from "services/user.service";
// services
const fileService = new FileService();
@ -84,8 +86,22 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
: null
);
const projectMemberService = new ProjectMemberService();
const { data: projectMembers } = useSWR(["projectMembers", workspaceSlug, projectId], async () => {
const members = await projectMemberService.fetchProjectMembers(workspaceSlug, projectId);
const detailedMembers = await Promise.all(
members.map(async (member) => projectMemberService.getProjectMember(workspaceSlug, projectId, member.id))
);
console.log("ye toh chal", detailedMembers);
return detailedMembers;
});
const pageStore = usePage(pageId as string);
// store hooks
const { getMentionSuggestions, mentionHighlights, mentionSuggestions } = useMention({ workspaceSlug, projectId });
const { setShowAlert } = useReloadConfirmations(pageStore?.isSubmitting === "submitting");
useEffect(
@ -273,6 +289,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
last_updated_at: updated_at,
last_updated_by: updated_by,
}}
mentionHighlights={mentionHighlights}
pageLockConfig={userCanLock && !archived_at ? { action: unlockPage, is_locked: is_locked } : undefined}
pageDuplicationConfig={userCanDuplicate && !archived_at ? { action: duplicate_page } : undefined}
pageArchiveConfig={
@ -300,6 +317,8 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
last_updated_at: updated_at,
last_updated_by: updated_by,
}}
mentionSuggestions={getMentionSuggestions(projectMembers)}
mentionHighlights={mentionHighlights}
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
restoreFile={fileService.getRestoreImageFunction(workspaceId)}

View File

@ -33,9 +33,12 @@ export class MentionStore implements IMentionStore {
const suggestions = (projectMemberIds ?? [])?.map((memberId) => {
const memberDetails = this.rootStore.memberRoot.project.getProjectMemberDetails(memberId);
// __AUTO_GENERATED_PRINT_VAR_START__
console.log("MentionStore#mentionSuggestions#(anon) memberDetails are: %s", memberDetails?.member.id); // __AUTO_GENERATED_PRINT_VAR_END__
return {
id: `${memberDetails?.member?.id}`,
entity_name: "user_mention",
entity_identifier: `${memberDetails?.member?.id}`,
type: "User",
title: `${memberDetails?.member?.display_name}`,
subtitle: memberDetails?.member?.email ?? "",

View File

@ -5012,7 +5012,7 @@ fault@^2.0.0:
dependencies:
format "^0.2.0"
fflate@^0.4.1:
fflate@^0.4.8:
version "0.4.8"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
@ -7171,12 +7171,18 @@ postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.29:
picocolors "^1.0.0"
source-map-js "^1.0.2"
posthog-js@^1.88.4:
version "1.96.1"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.96.1.tgz#4f9719a24e4e14037b0e72d430194d7cdb576447"
integrity sha512-kv1vQqYMt2BV3YHS+wxsbGuP+tz+M3y1AzNhz8TfkpY1HT8W/ONT0i0eQpeRr9Y+d4x/fZ6M4cXG5GMvi9lRCA==
posthog-js@^1.105.0:
version "1.108.3"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.108.3.tgz#774353d7ad594b68e6f5e6cce0fe8b583562f455"
integrity sha512-Vi9lX/MhovsKIEdj2aJ5ioku9U/eMGY8/DzKf4EpyrElxPPdabAdCDRUa81eAqxC6npkOpkHskawUPLg20le4Q==
dependencies:
fflate "^0.4.1"
fflate "^0.4.8"
preact "^10.19.3"
preact@^10.19.3:
version "10.19.6"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.6.tgz#66007b67aad4d11899f583df1b0116d94a89b8f5"
integrity sha512-gympg+T2Z1fG1unB8NH29yHJwnEaCH37Z32diPDku316OTnRPeMbiRV9kTrfZpocXjdfnWuFUl/Mj4BHaf6gnw==
prebuild-install@^7.1.1:
version "7.1.1"