diff --git a/frontend/src/routes/chronik/+page.server.ts b/frontend/src/routes/chronik/+page.server.ts index 71a03f00..89addea6 100644 --- a/frontend/src/routes/chronik/+page.server.ts +++ b/frontend/src/routes/chronik/+page.server.ts @@ -1,8 +1,13 @@ import { createApiClient } from '$lib/api.server'; -import type { components } from '$lib/generated/api'; +import type { components, operations } from '$lib/generated/api'; type ActivityFeedItemDTO = components['schemas']['ActivityFeedItemDTO']; type NotificationDTO = components['schemas']['NotificationDTO']; +type AuditKind = NonNullable['kinds'] extends + | (infer K)[] + | undefined + ? K + : never; export type FilterValue = 'alle' | 'fuer-dich' | 'hochgeladen' | 'transkription' | 'kommentare'; @@ -14,6 +19,13 @@ const VALID_FILTERS: FilterValue[] = [ 'kommentare' ]; +// fuer-dich stays client-side: youMentioned || youParticipated cannot be expressed as a kinds filter +const KINDS_FOR_FILTER: Partial> = { + hochgeladen: ['FILE_UPLOADED'], + transkription: ['TEXT_SAVED', 'BLOCK_REVIEWED', 'ANNOTATION_CREATED'], + kommentare: ['COMMENT_ADDED', 'MENTION_CREATED'] +}; + function parseFilter(raw: string | null): FilterValue { if (raw && (VALID_FILTERS as string[]).includes(raw)) return raw as FilterValue; return 'alle'; @@ -23,9 +35,10 @@ export async function load({ fetch, url }) { const api = createApiClient(fetch); const filter = parseFilter(url.searchParams.get('filter')); const limit = Math.min(Number(url.searchParams.get('limit')) || 40, 40); + const kinds = KINDS_FOR_FILTER[filter]; const [activityResult, unreadResult] = await Promise.allSettled([ - api.GET('/api/dashboard/activity', { params: { query: { limit } } }), + api.GET('/api/dashboard/activity', { params: { query: { limit, ...(kinds && { kinds }) } } }), api.GET('/api/notifications', { params: { query: { read: false, page: 0, size: 20 } } }) diff --git a/frontend/src/routes/chronik/page.server.spec.ts b/frontend/src/routes/chronik/page.server.spec.ts index 7a8dc135..08c3694d 100644 --- a/frontend/src/routes/chronik/page.server.spec.ts +++ b/frontend/src/routes/chronik/page.server.spec.ts @@ -13,36 +13,23 @@ function buildUrl(search = ''): URL { return new URL(`http://localhost/chronik${search}`); } +function mockSuccess() { + mockApi.GET.mockImplementation((path: string) => { + if (path === '/api/dashboard/activity') { + return Promise.resolve({ response: { ok: true }, data: [] }); + } + return Promise.resolve({ response: { ok: true }, data: { content: [] } }); + }); +} + beforeEach(() => { vi.clearAllMocks(); }); -describe('chronik/load', () => { - it('requests the activity feed with a 40-item limit', async () => { - mockApi.GET.mockImplementation((path: string) => { - if (path === '/api/dashboard/activity') { - return Promise.resolve({ response: { ok: true }, data: [] }); - } - return Promise.resolve({ response: { ok: true }, data: { content: [] } }); - }); - - await load({ fetch, url: buildUrl() } as never); - - expect(mockApi.GET).toHaveBeenCalledWith('/api/dashboard/activity', { - params: { query: { limit: 40 } } - }); - }); - +describe('chronik/load — core', () => { it('requests only unread notifications for Für-dich', async () => { - mockApi.GET.mockImplementation((path: string) => { - if (path === '/api/dashboard/activity') { - return Promise.resolve({ response: { ok: true }, data: [] }); - } - return Promise.resolve({ response: { ok: true }, data: { content: [] } }); - }); - + mockSuccess(); await load({ fetch, url: buildUrl() } as never); - expect(mockApi.GET).toHaveBeenCalledWith('/api/notifications', { params: { query: { read: false, page: 0, size: 20 } } }); @@ -82,7 +69,6 @@ describe('chronik/load', () => { it('parses the filter query param, falling back to "alle" for invalid values', async () => { mockApi.GET.mockResolvedValue({ response: { ok: true }, data: [] }); - const validResult = await load({ fetch, url: buildUrl('?filter=fuer-dich') } as never); expect(validResult.filter).toBe('fuer-dich'); @@ -91,3 +77,59 @@ describe('chronik/load', () => { expect(invalidResult.filter).toBe('alle'); }); }); + +describe('chronik/load — kinds param per filter', () => { + it('omits kinds for filter=alle (server defaults to ROLLUP_ELIGIBLE)', async () => { + mockSuccess(); + await load({ fetch, url: buildUrl() } as never); + expect(mockApi.GET).toHaveBeenCalledWith('/api/dashboard/activity', { + params: { query: { limit: 40 } } + }); + }); + + it('omits kinds for filter=fuer-dich (client-side filtering on youMentioned/youParticipated)', async () => { + mockSuccess(); + await load({ fetch, url: buildUrl('?filter=fuer-dich') } as never); + expect(mockApi.GET).toHaveBeenCalledWith('/api/dashboard/activity', { + params: { query: { limit: 40 } } + }); + }); + + it('sends kinds=FILE_UPLOADED for filter=hochgeladen', async () => { + mockSuccess(); + await load({ fetch, url: buildUrl('?filter=hochgeladen') } as never); + expect(mockApi.GET).toHaveBeenCalledWith('/api/dashboard/activity', { + params: { query: { limit: 40, kinds: ['FILE_UPLOADED'] } } + }); + }); + + it('sends TEXT_SAVED, BLOCK_REVIEWED, ANNOTATION_CREATED for filter=transkription', async () => { + mockSuccess(); + await load({ fetch, url: buildUrl('?filter=transkription') } as never); + expect(mockApi.GET).toHaveBeenCalledWith('/api/dashboard/activity', { + params: { + query: { + limit: 40, + kinds: expect.arrayContaining(['TEXT_SAVED', 'BLOCK_REVIEWED', 'ANNOTATION_CREATED']) + } + } + }); + const call = mockApi.GET.mock.calls.find(([p]) => p === '/api/dashboard/activity'); + expect(call[1].params.query.kinds).toHaveLength(3); + }); + + it('sends COMMENT_ADDED, MENTION_CREATED for filter=kommentare', async () => { + mockSuccess(); + await load({ fetch, url: buildUrl('?filter=kommentare') } as never); + expect(mockApi.GET).toHaveBeenCalledWith('/api/dashboard/activity', { + params: { + query: { + limit: 40, + kinds: expect.arrayContaining(['COMMENT_ADDED', 'MENTION_CREATED']) + } + } + }); + const call = mockApi.GET.mock.calls.find(([p]) => p === '/api/dashboard/activity'); + expect(call[1].params.query.kinds).toHaveLength(2); + }); +});