feat(documents): timeline density refetches when other filters change (#385)
The +page.ts client-side load now forwards the active /documents URL
filters (q, senderId, receiverId, tag, tagQ, status, tagOp) to
/api/documents/density so the bars recompute when the user narrows the
search. Date bounds (from/to) are deliberately omitted — the chart is
the surface for picking those.
- New `DensityFilters` type and `buildDensityUrl(filters)` helper.
- `fetchDensity` accepts a filter snapshot (defaulting to {} for
back-compat in tests).
- 6 new unit tests cover URL building, multi-tag repetition, AND/OR
forwarding, the explicit-no-from/to invariant, and filter-aware fetch.
- Generated API types refreshed against the new backend signature.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
buildMonthSequence,
|
||||
fillDensityGaps,
|
||||
fetchDensity,
|
||||
buildDensityUrl,
|
||||
aggregateToYears,
|
||||
selectionBoundaryFrom,
|
||||
selectionBoundaryTo
|
||||
@@ -152,6 +153,39 @@ describe('selectionBoundaryFrom / To', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildDensityUrl', () => {
|
||||
it('returns the bare endpoint when no filters provided', () => {
|
||||
expect(buildDensityUrl()).toBe('/api/documents/density');
|
||||
});
|
||||
|
||||
it('forwards single-value filters as query params', () => {
|
||||
expect(buildDensityUrl({ q: 'Brief', senderId: 's-1' })).toBe(
|
||||
'/api/documents/density?q=Brief&senderId=s-1'
|
||||
);
|
||||
});
|
||||
|
||||
it('repeats the tag param for multi-value tag filters', () => {
|
||||
const url = buildDensityUrl({ tags: ['Familie', 'Urlaub'], tagOp: 'OR' });
|
||||
expect(url).toContain('tag=Familie');
|
||||
expect(url).toContain('tag=Urlaub');
|
||||
expect(url).toContain('tagOp=OR');
|
||||
});
|
||||
|
||||
it('omits tagOp when it is AND (default on backend)', () => {
|
||||
const url = buildDensityUrl({ tags: ['Familie'], tagOp: 'AND' });
|
||||
expect(url).not.toContain('tagOp=');
|
||||
});
|
||||
|
||||
it('does not forward from/to even if a caller mistakenly adds them', () => {
|
||||
// Intentional: density is the surface for picking from/to, so it must always
|
||||
// span the broader space the user is selecting within.
|
||||
// @ts-expect-error - from/to are explicitly absent from DensityFilters
|
||||
const url = buildDensityUrl({ q: 'Brief', from: '1915-01-01', to: '1916-12-31' });
|
||||
expect(url).not.toContain('from=');
|
||||
expect(url).not.toContain('to=');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchDensity', () => {
|
||||
it('skips fetch and returns null density on mobile', async () => {
|
||||
const fetch = vi.fn();
|
||||
@@ -189,6 +223,19 @@ describe('fetchDensity', () => {
|
||||
expect(result.maxDate).toBe('1916-12-31');
|
||||
});
|
||||
|
||||
it('forwards active filters as query params', async () => {
|
||||
const fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ buckets: [], minDate: null, maxDate: null })
|
||||
});
|
||||
|
||||
await fetchDensity(fetch, null, true, { senderId: 's-1', tags: ['Familie'] });
|
||||
|
||||
const calledWith = fetch.mock.calls[0][0] as string;
|
||||
expect(calledWith).toContain('senderId=s-1');
|
||||
expect(calledWith).toContain('tag=Familie');
|
||||
});
|
||||
|
||||
it('returns empty density and null bounds when the API responds non-ok', async () => {
|
||||
const fetch = vi.fn().mockResolvedValue({ ok: false, status: 500 });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user