From 6786c0112d85d5dae095007262b568f6376ea125 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 7 May 2026 22:16:05 +0200 Subject: [PATCH] feat(documents): wire TimelineDensityFilter into /documents/+page (#385) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mounts the timeline above the result count, hidden on mobile via \`hidden sm:block\` (defense-in-depth — +page.ts already gates the fetch). The component's onchange callback updates local from/to and triggers the existing search reload, so timeline selection composes with the SearchFilterBar's other filters via AND semantics for free. 3 new page-level integration tests cover: widget renders when density present, hides when null, and bar click navigates with correct from/to URL params. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/documents/+page.svelte | 16 +++++++ .../src/routes/documents/page.svelte.spec.ts | 42 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/frontend/src/routes/documents/+page.svelte b/frontend/src/routes/documents/+page.svelte index 4a5e0616..fadb201a 100644 --- a/frontend/src/routes/documents/+page.svelte +++ b/frontend/src/routes/documents/+page.svelte @@ -7,6 +7,7 @@ import SearchFilterBar from '../SearchFilterBar.svelte'; import DocumentList from '../DocumentList.svelte'; import Pagination from '$lib/shared/primitives/Pagination.svelte'; import BulkSelectionBar from '$lib/document/BulkSelectionBar.svelte'; +import TimelineDensityFilter from '$lib/document/TimelineDensityFilter.svelte'; import { bulkSelectionStore } from '$lib/document/bulkSelection.svelte'; import { getErrorMessage, parseBackendError } from '$lib/shared/errors'; import * as m from '$lib/paraglide/messages.js'; @@ -234,6 +235,21 @@ $effect(() => { onblur={() => (qFocused = false)} /> + +

{#if data.totalElements > 0}{m.docs_result_count({ count: data.totalElements })}{/if} diff --git a/frontend/src/routes/documents/page.svelte.spec.ts b/frontend/src/routes/documents/page.svelte.spec.ts index 1d8064c6..e0c0f117 100644 --- a/frontend/src/routes/documents/page.svelte.spec.ts +++ b/frontend/src/routes/documents/page.svelte.spec.ts @@ -135,3 +135,45 @@ describe('documents page — URL building', () => { expect(url).not.toContain('page='); }); }); + +// ─── Timeline density widget wiring (#385) ──────────────────────────────────── + +describe('documents page — timeline density widget', () => { + it('renders the timeline widget when density data is present', async () => { + render(Page, { + data: makeData({ + density: [{ month: '1915-08', count: 3 }], + minDate: '1915-08-01', + maxDate: '1915-08-31' + }) + }); + + await expect.element(page.getByTestId('timeline-density-filter')).toBeInTheDocument(); + }); + + it('hides the timeline widget when density is null (mobile / calendar view)', async () => { + render(Page, { data: makeData({ density: null, minDate: null, maxDate: null }) }); + expect(document.querySelector('[data-testid="timeline-density-filter"]')).toBeNull(); + }); + + it('clicking a timeline bar navigates with from/to set to that month boundary', async () => { + const { goto } = await import('$app/navigation'); + vi.mocked(goto).mockClear(); + + render(Page, { + data: makeData({ + density: [{ month: '1915-08', count: 3 }], + minDate: '1915-08-01', + maxDate: '1915-08-31' + }) + }); + + const bar = document.querySelector('[data-testid="timeline-bar"]') as HTMLButtonElement; + bar.dispatchEvent(new MouseEvent('click', { bubbles: true })); + + expect(goto).toHaveBeenCalledOnce(); + const [url] = vi.mocked(goto).mock.calls[0]; + expect(url).toContain('from=1915-08-01'); + expect(url).toContain('to=1915-08-31'); + }); +});