diff --git a/frontend/src/routes/geschichten/+page.server.ts b/frontend/src/routes/geschichten/+page.server.ts index ae4641e6..76828641 100644 --- a/frontend/src/routes/geschichten/+page.server.ts +++ b/frontend/src/routes/geschichten/+page.server.ts @@ -20,7 +20,7 @@ export const load: PageServerLoad = async ({ url, fetch }) => { query: { status: 'PUBLISHED', personId: personIds.length ? personIds : undefined, - documentId: rawDocumentId ?? undefined + documentId: documentId ?? undefined } } }), @@ -44,7 +44,7 @@ export const load: PageServerLoad = async ({ url, fetch }) => { const doc = docResult.data; documentFilter = { id: documentId, - title: doc.title || doc.originalFilename || null + title: doc.title ?? doc.originalFilename ?? null }; } else { documentFilter = { id: documentId, title: null }; diff --git a/frontend/src/routes/geschichten/+page.svelte b/frontend/src/routes/geschichten/+page.svelte index 35d67301..35ea31fa 100644 --- a/frontend/src/routes/geschichten/+page.svelte +++ b/frontend/src/routes/geschichten/+page.svelte @@ -28,13 +28,15 @@ const emptyMessage = $derived.by(() => { function rebuildUrl(personIds: string[]) { const url = new URL(window.location.href); url.searchParams.delete('personId'); - url.searchParams.delete('documentId'); for (const id of personIds) url.searchParams.append('personId', id); return url.pathname + url.search; } function clearAll() { - goto(rebuildUrl([]), { replaceState: true }); + const url = new URL(window.location.href); + url.searchParams.delete('personId'); + url.searchParams.delete('documentId'); + goto(url.pathname + url.search, { replaceState: true }); } function addPerson(personId: string) { @@ -51,7 +53,9 @@ function removePerson(personId: string) { } function removeDocument() { - goto(rebuildUrl(selectedPersonIds)); + const url = new URL(window.location.href); + url.searchParams.delete('documentId'); + goto(url.pathname + url.search); } diff --git a/frontend/src/routes/geschichten/page.server.test.ts b/frontend/src/routes/geschichten/page.server.test.ts index 6149c08f..c9ce8b94 100644 --- a/frontend/src/routes/geschichten/page.server.test.ts +++ b/frontend/src/routes/geschichten/page.server.test.ts @@ -81,6 +81,14 @@ describe('geschichten page load — documentFilter title resolution', () => { expect(result.documentFilter).toEqual({ id: VALID_UUID, title: 'scan_001.jpg' }); }); + it('preserves an empty-string title rather than falling back to filename', async () => { + mockApi({ docData: { id: VALID_UUID, title: '', originalFilename: 'scan_001.jpg' } }); + + const result = await callLoad(makeUrl({ documentId: VALID_UUID })); + + expect(result.documentFilter).toEqual({ id: VALID_UUID, title: '' }); + }); + it('degrades to {id, title: null} on 404 without throwing (resolves, never rejects)', async () => { // Explicit .resolves locks the no-throw guarantee — if error() were called, this would reject mockApi({ docOk: false }); @@ -159,13 +167,13 @@ describe('geschichten page load — documentFilter title resolution', () => { ); }); - it('passes invalid documentId to the list API without stripping (option B)', async () => { + it('omits documentId from the list API when the value is not a valid UUID', async () => { const mockGet = mockApi(); await callLoad(makeUrl({ documentId: 'not-a-uuid' })); const listCall = mockGet.mock.calls.find((c) => c[0] === '/api/geschichten'); - expect(listCall?.[1]?.params?.query?.documentId).toBe('not-a-uuid'); + expect(listCall?.[1]?.params?.query?.documentId).toBeUndefined(); }); it('keeps forwarding personId filters alongside documentId', async () => { diff --git a/frontend/src/routes/geschichten/page.svelte.spec.ts b/frontend/src/routes/geschichten/page.svelte.spec.ts index b2392185..410b1678 100644 --- a/frontend/src/routes/geschichten/page.svelte.spec.ts +++ b/frontend/src/routes/geschichten/page.svelte.spec.ts @@ -206,6 +206,64 @@ describe('geschichten page — multi-person filter chips', () => { }); }); + it('removing a person chip preserves an active document filter in the URL', async () => { + const { goto } = await import('$app/navigation'); + vi.mocked(goto).mockClear(); + + window.history.replaceState( + {}, + '', + '/geschichten?personId=a&documentId=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' + ); + + render(Page, { + data: makeData({ + personFilters: [person('a', 'Anna A')] as PageData['personFilters'], + documentFilter: makeDocumentFilter() as PageData['documentFilter'] + }) + }); + + const chipBtn = (await page + .getByRole('button', { name: /Anna A aus Filter entfernen/ }) + .element()) as HTMLElement; + chipBtn.click(); + + await vi.waitFor(() => expect(goto).toHaveBeenCalledOnce()); + const url = vi.mocked(goto).mock.calls[0][0] as string; + expect(url).not.toContain('personId=a'); + expect(url).toContain('documentId=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'); + + window.history.replaceState({}, '', '/'); + }); + + it('clearAll removes both person and document filters from the URL', async () => { + const { goto } = await import('$app/navigation'); + vi.mocked(goto).mockClear(); + + window.history.replaceState( + {}, + '', + '/geschichten?personId=a&documentId=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' + ); + + render(Page, { + data: makeData({ + personFilters: [person('a', 'Anna A')] as PageData['personFilters'], + documentFilter: makeDocumentFilter() as PageData['documentFilter'] + }) + }); + + const allBtn = (await page.getByRole('button', { name: 'Alle' }).element()) as HTMLElement; + allBtn.click(); + + await vi.waitFor(() => expect(goto).toHaveBeenCalledOnce()); + const url = vi.mocked(goto).mock.calls[0][0] as string; + expect(url).not.toContain('personId'); + expect(url).not.toContain('documentId'); + + window.history.replaceState({}, '', '/'); + }); + describe('empty state precedence', () => { it('shows geschichten_empty_for_document when only document filter is active', async () => { render(Page, {