refactor(persons): update all callers for the paged /api/persons response

GET /api/persons now returns PersonSearchResult { items, … } instead of a bare
list. Update every caller: the dashboard top-persons path reads .items; the
unused full-list fetches in documents/new and documents/[id]/edit are dropped
(both pages use the self-fetching PersonTypeahead); the raw-fetch consumers
(PersonTypeahead, PersonMultiSelect, PersonMentionEditor) read body.items and
pass review=true so search still spans the whole directory. Specs updated to
the new envelope shape.

Refs #667

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-27 13:56:00 +02:00
parent 9d859dcb05
commit 6c3552dc6a
9 changed files with 56 additions and 39 deletions

View File

@@ -34,9 +34,10 @@ function handleInput() {
}
loading = true;
try {
const res = await fetch(`/api/persons?q=${encodeURIComponent(searchTerm)}`);
const res = await fetch(`/api/persons?review=true&q=${encodeURIComponent(searchTerm)}`);
if (res.ok) {
const all: Person[] = await res.json();
const body = await res.json();
const all: Person[] = body.items ?? [];
results = all.filter((p) => !selectedPersons.some((s) => s.id === p.id));
}
} catch {

View File

@@ -34,11 +34,12 @@ const PERSONS = [
];
function mockFetch(persons = PERSONS) {
// /api/persons now returns a paged { items } envelope.
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(persons)
json: vi.fn().mockResolvedValue({ items: persons })
})
);
}

View File

@@ -79,8 +79,12 @@ const typeahead = createTypeahead<Person>({
return res.ok ? filter(await res.json()) : [];
}
if (term.length < 1) return [];
const res = await fetch(`/api/persons?q=${encodeURIComponent(term)}`);
return res.ok ? filter(await res.json()) : [];
// review=true so the typeahead searches the whole directory (incl. provisional /
// zero-document persons), not just the clean reader subset.
const res = await fetch(`/api/persons?review=true&q=${encodeURIComponent(term)}`);
if (!res.ok) return [];
const body = await res.json();
return filter(body.items ?? []);
},
debounceMs: 300
});

View File

@@ -24,12 +24,18 @@ const PERSONS = [
];
function mockFetchWithPersons(persons = PERSONS) {
// The directory endpoint (/api/persons?…) now returns a paged { items } envelope; the
// correspondents endpoint still returns a bare array. Branch the mock on the URL.
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(persons)
})
vi.fn().mockImplementation((url: string) =>
Promise.resolve({
ok: true,
json: vi
.fn()
.mockResolvedValue(url.includes('/correspondents') ? persons : { items: persons })
})
)
);
}
@@ -266,7 +272,9 @@ describe('PersonTypeahead correspondent mode', () => {
await waitForDebounce();
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/persons?q=Anna'));
expect(fetchMock).toHaveBeenCalledWith(
expect.stringContaining('/api/persons?review=true&q=Anna')
);
});
});