mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
feat: added loading indicator logic in mentions
This commit is contained in:
parent
2f4fc63858
commit
48f913fbe6
@ -23,25 +23,6 @@ export const Mentions = ({
|
|||||||
readonly: readonly,
|
readonly: readonly,
|
||||||
mentionHighlights: mentionHighlights,
|
mentionHighlights: mentionHighlights,
|
||||||
suggestion: {
|
suggestion: {
|
||||||
items: async ({ query }) => {
|
|
||||||
const suggestions = await mentionSuggestions?.();
|
|
||||||
if (!suggestions) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
|
|
||||||
return filteredSuggestions;
|
|
||||||
},
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
render: () => {
|
render: () => {
|
||||||
let component: ReactRenderer | null = null;
|
let component: ReactRenderer | null = null;
|
||||||
@ -56,11 +37,11 @@ export const Mentions = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
component = new ReactRenderer(MentionList, {
|
component = new ReactRenderer(MentionList, {
|
||||||
props,
|
props: { ...props, mentionSuggestions },
|
||||||
editor: props.editor,
|
editor: props.editor,
|
||||||
});
|
});
|
||||||
props.editor.storage.mentionsOpen = true;
|
props.editor.storage.mentionsOpen = true;
|
||||||
// @ts-ignore
|
// @ts-expect-error - Tippy types are incorrect
|
||||||
popup = tippy("body", {
|
popup = tippy("body", {
|
||||||
getReferenceClientRect: props.clientRect,
|
getReferenceClientRect: props.clientRect,
|
||||||
appendTo: () => document.body,
|
appendTo: () => document.body,
|
||||||
@ -70,7 +51,7 @@ export const Mentions = ({
|
|||||||
trigger: "manual",
|
trigger: "manual",
|
||||||
placement: "bottom-start",
|
placement: "bottom-start",
|
||||||
});
|
});
|
||||||
document.addEventListener("scroll", hidePopup, true);
|
// document.addEventListener("scroll", hidePopup, true);
|
||||||
},
|
},
|
||||||
onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
|
onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
|
||||||
component?.updateProps(props);
|
component?.updateProps(props);
|
||||||
@ -107,7 +88,7 @@ export const Mentions = ({
|
|||||||
popup?.[0].destroy();
|
popup?.[0].destroy();
|
||||||
component?.destroy();
|
component?.destroy();
|
||||||
|
|
||||||
document.removeEventListener("scroll", hidePopup, true);
|
// document.removeEventListener("scroll", hidePopup, true);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Editor } from "@tiptap/react";
|
import { Editor } from "@tiptap/react";
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
||||||
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
interface MentionListProps {
|
interface MentionListProps {
|
||||||
items: IMentionSuggestion[];
|
|
||||||
command: (item: {
|
command: (item: {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
@ -12,14 +12,42 @@ interface MentionListProps {
|
|||||||
target: string;
|
target: string;
|
||||||
redirect_uri: string;
|
redirect_uri: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
query: string;
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
|
mentionSuggestions: () => Promise<IMentionSuggestion[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MentionList = forwardRef((props: MentionListProps, ref) => {
|
export const MentionList = forwardRef((props: MentionListProps, ref) => {
|
||||||
|
const { query, mentionSuggestions } = props;
|
||||||
|
const [items, setItems] = useState<IMentionSuggestion[]>([]);
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(false); // New loading state
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSuggestions = async () => {
|
||||||
|
setIsLoading(true); // Start loading
|
||||||
|
const suggestions = await 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);
|
||||||
|
|
||||||
|
setItems(filteredSuggestions);
|
||||||
|
setIsLoading(false); // End loading
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchSuggestions();
|
||||||
|
}, [query, mentionSuggestions]);
|
||||||
|
|
||||||
const selectItem = (index: number) => {
|
const selectItem = (index: number) => {
|
||||||
const item = props.items[index];
|
const item = items[index];
|
||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
props.command({
|
props.command({
|
||||||
@ -33,12 +61,35 @@ export const MentionList = forwardRef((props: MentionListProps, ref) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const commandListContainer = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const container = commandListContainer?.current;
|
||||||
|
|
||||||
|
const item = container?.children[selectedIndex] as HTMLElement;
|
||||||
|
|
||||||
|
if (item && container) updateScrollView(container, item);
|
||||||
|
}, [selectedIndex]);
|
||||||
|
|
||||||
|
const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
|
||||||
|
const containerHeight = container.offsetHeight;
|
||||||
|
const itemHeight = item ? item.offsetHeight : 0;
|
||||||
|
|
||||||
|
const top = item.offsetTop;
|
||||||
|
const bottom = top + itemHeight;
|
||||||
|
|
||||||
|
if (top < container.scrollTop) {
|
||||||
|
container.scrollTop -= container.scrollTop - top + 5;
|
||||||
|
} else if (bottom > containerHeight + container.scrollTop) {
|
||||||
|
container.scrollTop += bottom - containerHeight - container.scrollTop + 5;
|
||||||
|
}
|
||||||
|
};
|
||||||
const upHandler = () => {
|
const upHandler = () => {
|
||||||
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
|
setSelectedIndex((selectedIndex + items.length - 1) % items.length);
|
||||||
};
|
};
|
||||||
|
|
||||||
const downHandler = () => {
|
const downHandler = () => {
|
||||||
setSelectedIndex((selectedIndex + 1) % props.items.length);
|
setSelectedIndex((selectedIndex + 1) % items.length);
|
||||||
};
|
};
|
||||||
|
|
||||||
const enterHandler = () => {
|
const enterHandler = () => {
|
||||||
@ -47,7 +98,7 @@ export const MentionList = forwardRef((props: MentionListProps, ref) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedIndex(0);
|
setSelectedIndex(0);
|
||||||
}, [props.items]);
|
}, [items]);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
onKeyDown: ({ event }: { event: KeyboardEvent }) => {
|
onKeyDown: ({ event }: { event: KeyboardEvent }) => {
|
||||||
@ -70,10 +121,15 @@ export const MentionList = forwardRef((props: MentionListProps, ref) => {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return props.items && props.items.length !== 0 ? (
|
return (
|
||||||
<div className="mentions absolute max-h-40 w-48 space-y-0.5 overflow-y-auto rounded-md bg-custom-background-100 p-1 text-sm text-custom-text-300 shadow-custom-shadow-sm">
|
<div
|
||||||
{props.items.length ? (
|
ref={commandListContainer}
|
||||||
props.items.map((item, index) => (
|
className="mentions absolute max-h-40 w-48 space-y-0.5 overflow-y-auto rounded-md bg-custom-background-100 p-1 text-sm text-custom-text-300 shadow-custom-shadow-sm"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex justify-center items-center h-full text-gray-500">Loading...</div>
|
||||||
|
) : items.length ? (
|
||||||
|
items.map((item, index) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className={`flex cursor-pointer items-center gap-2 rounded p-1 hover:bg-custom-background-80 ${
|
className={`flex cursor-pointer items-center gap-2 rounded p-1 hover:bg-custom-background-80 ${
|
||||||
@ -92,16 +148,13 @@ export const MentionList = forwardRef((props: MentionListProps, ref) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-grow space-y-1 truncate">
|
<div className="flex-grow space-y-1 truncate">
|
||||||
<p className="truncate text-sm font-medium">{item.title}</p>
|
<p className="truncate text-sm font-medium">{item.title}</p>
|
||||||
{/* <p className="text-xs text-gray-400">{item.subtitle}</p> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="item">No result</div>
|
<div className="flex justify-center items-center h-full">No results</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2722,7 +2722,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react@*", "@types/react@^18.2.42":
|
"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42":
|
||||||
version "18.2.42"
|
version "18.2.42"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
|
||||||
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==
|
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==
|
||||||
|
Loading…
Reference in New Issue
Block a user