(
},
}));
- if (items.length === 0) {
+ if (displayItems.length === 0) {
+ const isWaitingForServer =
+ normalizedQuery !== "" &&
+ (isSearchingIssues || searchedIssueQuery !== normalizedQuery);
+
return (
- No results
+ {isWaitingForServer ? "Searching..." : "No results"}
);
}
- const groups = groupItems(items);
+ const groups = groupItems(displayItems);
// Build a flat index mapping: globalIndex → item
let globalIndex = 0;
@@ -231,18 +333,13 @@ function issueToMention(i: Pick
};
}
-const MAX_ITEMS = 15;
-
export function createMentionSuggestion(qc: QueryClient): Omit<
SuggestionOptions,
"editor"
> {
- // Per-editor state lives in this closure so multiple ContentEditor instances
- // (e.g. comment input + reply box) don't abort each other's searches.
+ // Renderer/popup instances live in this closure so each ContentEditor owns
+ // its own TipTap suggestion popup lifecycle.
let renderer: ReactRenderer | null = null;
- let activeCommand: ((item: MentionItem) => void) | null = null;
- let searchSeq = 0;
- let searchAbort: AbortController | null = null;
let popup: HTMLDivElement | null = null;
function buildSyncItems(query: string): MentionItem[] {
@@ -276,8 +373,8 @@ export function createMentionSuggestion(qc: QueryClient): Omit<
.filter((a) => !a.archived_at && a.name.toLowerCase().includes(q))
.map((a) => ({ id: a.id, label: a.name, type: "agent" as const }));
- // Cached issues give an instant first paint; the server search below
- // adds done/cancelled and any other matches not in the local cache.
+ // Cached issues give an instant first paint; MentionList adds server
+ // matches for done/cancelled and any other issues not in this cache.
const issueItems: MentionItem[] = cachedIssues
.filter(
(i) =>
@@ -289,68 +386,23 @@ export function createMentionSuggestion(qc: QueryClient): Omit<
return [...allItem, ...memberItems, ...agentItems, ...issueItems];
}
- function startServerIssueSearch(query: string, syncItems: MentionItem[]) {
- // Supersede any in-flight search; the next-arrived response wins.
- if (searchAbort) searchAbort.abort();
- const mySeq = ++searchSeq;
- const wsId = getCurrentWsId();
- if (!wsId) return;
-
- void (async () => {
- // Debounce: skip the fetch if a newer keystroke arrives within 150ms.
- await new Promise((r) => setTimeout(r, 150));
- if (mySeq !== searchSeq) return;
-
- const controller = new AbortController();
- searchAbort = controller;
- try {
- const res = await api.searchIssues({
- q: query,
- limit: 10,
- include_closed: true,
- signal: controller.signal,
- });
- if (mySeq !== searchSeq) return;
- if (!renderer || !activeCommand) return;
-
- const existingIssueIds = new Set(
- syncItems.filter((i) => i.type === "issue").map((i) => i.id),
- );
- const extraIssueItems = res.issues
- .map(issueToMention)
- .filter((i) => !existingIssueIds.has(i.id));
- if (extraIssueItems.length === 0) return;
-
- const merged = [...syncItems, ...extraIssueItems].slice(0, MAX_ITEMS);
- renderer.updateProps({ items: merged, command: activeCommand });
- } catch {
- // Aborted or network error: nothing to do — sync items remain.
- }
- })();
- }
-
return {
items: ({ query }) => {
const syncItems = buildSyncItems(query);
- // Empty query has no server search — cached issues are enough, and
- // we still bump the seq to cancel any pending fetch from a prior key.
- if (query === "") {
- if (searchAbort) searchAbort.abort();
- ++searchSeq;
- } else {
- startServerIssueSearch(query, syncItems);
- }
- return syncItems.slice(0, MAX_ITEMS);
+ return syncItems;
},
render: () => {
return {
onStart: (props: SuggestionProps) => {
renderer = new ReactRenderer(MentionList, {
- props: { items: props.items, command: props.command },
+ props: {
+ items: props.items,
+ query: props.query,
+ command: props.command,
+ },
editor: props.editor,
});
- activeCommand = props.command;
popup = document.createElement("div");
popup.style.position = "fixed";
@@ -364,9 +416,9 @@ export function createMentionSuggestion(qc: QueryClient): Omit<
onUpdate: (props: SuggestionProps) => {
renderer?.updateProps({
items: props.items,
+ query: props.query,
command: props.command,
});
- activeCommand = props.command;
if (popup) updatePosition(popup, props.clientRect);
},
@@ -404,13 +456,8 @@ export function createMentionSuggestion(qc: QueryClient): Omit<
function cleanup() {
renderer?.destroy();
renderer = null;
- activeCommand = null;
popup?.remove();
popup = null;
- // Cancel any in-flight server search; its result would target a
- // destroyed renderer.
- if (searchAbort) searchAbort.abort();
- ++searchSeq;
}
},
};