diff --git a/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte b/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte index a552c404..ed512c65 100644 --- a/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte +++ b/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte @@ -151,16 +151,13 @@ onMount(() => { // Nora #5618 #3 — separate issue tracks the GET /api/persons // response-shape audit (PersonSummaryDTO leaks `notes`). // ───────────────────────────────────────────────────────────── - items: async ({ query }: { query: string }) => { - if (!query) return []; - try { - const res = await fetch(`/api/persons?q=${encodeURIComponent(query)}`); - if (!res.ok) return []; - return ((await res.json()) as Person[]).slice(0, 5); - } catch { - return []; - } - }, + // Tiptap's suggestion plugin requires an `items()` callback to keep + // the dropdown alive, but the actual fetch is owned by `runSearch` + // below — routed through the dropdown's search input via the + // debounced `onSearch` channel. Returning `[]` here keeps Tiptap + // happy without firing a duplicate per-keystroke fetch. + // Markus #5616 / Felix / Nora / Sara on PR #629. + items: async () => [], // AC-1 fix: insert the typed query as displayName, not person.displayName. command({ editor: ed, range, props }) { const p = props as unknown as { personId: string; displayName: string }; diff --git a/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts b/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts index 7d580f09..ddbde152 100644 --- a/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts +++ b/frontend/src/lib/shared/discussion/PersonMentionEditor.svelte.spec.ts @@ -195,6 +195,24 @@ describe('PersonMentionEditor — AC-2/3: search input drives fetch', () => { expect(fetchesAfterSearch).toBe(1); }); + it('fires exactly one /api/persons fetch when the user types @Walter (debounced)', async () => { + const fetchMock = vi + .fn() + .mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([AUGUSTE]) }); + vi.stubGlobal('fetch', fetchMock); + renderHost(); + + await userEvent.type(page.getByRole('textbox'), '@Walter'); + + // Wait beyond the 150 ms debounce window so the trailing call has flushed. + await new Promise((r) => setTimeout(r, 300)); + + const personsFetches = fetchMock.mock.calls.filter( + ([url]) => typeof url === 'string' && url.startsWith('/api/persons') + ); + expect(personsFetches.length).toBe(1); + }); + it('clearing the search input clears the list without firing a fetch', async () => { const fetchMock = vi .fn()