From 769937e03d06ca1a228ed1096be9636151879cf6 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 6 Apr 2026 13:53:54 +0200 Subject: [PATCH] feat(search): read sort/dir/tagQ from URL and unwrap DocumentSearchResult envelope Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/+page.server.ts | 18 +++++++-- frontend/src/routes/page.server.spec.ts | 54 ++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts index 3646840a..7aa08827 100644 --- a/frontend/src/routes/+page.server.ts +++ b/frontend/src/routes/+page.server.ts @@ -13,6 +13,9 @@ export async function load({ url, fetch }) { const senderId = url.searchParams.get('senderId') || ''; const receiverId = url.searchParams.get('receiverId') || ''; const tags = url.searchParams.getAll('tag'); + const sort = url.searchParams.get('sort') || 'DATE'; + const dir = url.searchParams.get('dir') || 'desc'; + const tagQ = url.searchParams.get('tagQ') || ''; const isDashboard = !q && !from && !to && !senderId && !receiverId && !tags.length; @@ -30,7 +33,10 @@ export async function load({ url, fetch }) { to: to || undefined, senderId: senderId || undefined, receiverId: receiverId || undefined, - tag: tags.length ? tags : undefined + tag: tags.length ? tags : undefined, + tagQ: tagQ || undefined, + sort: sort as 'DATE' | 'TITLE' | 'SENDER' | 'RECEIVER' | 'UPLOAD_DATE', + dir: dir || undefined } } }), @@ -44,7 +50,9 @@ export async function load({ url, fetch }) { throw redirect(302, '/login'); } - const documents: Document[] = docsResult?.data ?? []; + const searchResult = docsResult?.data as { documents?: Document[]; total?: number } | null; + const documents: Document[] = searchResult?.documents ?? []; + const total: number = searchResult?.total ?? 0; const allPersons = (personsResult.data ?? []) as { id: string; firstName: string; @@ -80,6 +88,7 @@ export async function load({ url, fetch }) { return { isDashboard, documents, + total, stats, incompleteDocs, recentDocs, @@ -87,7 +96,7 @@ export async function load({ url, fetch }) { senderName: senderObj ? `${senderObj.firstName} ${senderObj.lastName}` : '', receiverName: receiverObj ? `${receiverObj.firstName} ${receiverObj.lastName}` : '' }, - filters: { q, from, to, senderId, receiverId, tags }, + filters: { q, from, to, senderId, receiverId, tags, sort, dir, tagQ }, error: null as string | null }; } catch (e) { @@ -96,11 +105,12 @@ export async function load({ url, fetch }) { return { isDashboard, documents: [], + total: 0, stats: null, incompleteDocs: [], recentDocs: [], initialValues: { senderName: '', receiverName: '' }, - filters: { q, from, to, senderId, receiverId, tags }, + filters: { q, from, to, senderId, receiverId, tags, sort, dir, tagQ }, error: 'Daten konnten nicht geladen werden.' as string | null }; } diff --git a/frontend/src/routes/page.server.spec.ts b/frontend/src/routes/page.server.spec.ts index 24b2e0ad..f8af2b92 100644 --- a/frontend/src/routes/page.server.spec.ts +++ b/frontend/src/routes/page.server.spec.ts @@ -123,7 +123,10 @@ describe('home page load — search mode', () => { it('sets isDashboard false and skips widget APIs when q is set', async () => { const mockGet = vi .fn() - .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [{ id: 'd1' }] }) // search docs + .mockResolvedValueOnce({ + response: { ok: true, status: 200 }, + data: { documents: [{ id: 'd1' }], total: 1 } + }) // search docs .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }); // persons vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient @@ -146,7 +149,10 @@ describe('home page load — search mode', () => { it('passes search params from the URL to the documents API', async () => { const mockGet = vi .fn() - .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }) + .mockResolvedValueOnce({ + response: { ok: true, status: 200 }, + data: { documents: [], total: 0 } + }) .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }); vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient @@ -163,6 +169,50 @@ describe('home page load — search mode', () => { }); }); +it('passes sort, dir, and tagQ params to the documents API', async () => { + const mockGet = vi + .fn() + .mockResolvedValueOnce({ + response: { ok: true, status: 200 }, + data: { documents: [], total: 0 } + }) + .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }); + vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< + typeof createApiClient + >); + + await load({ + url: makeUrl({ q: 'test', sort: 'TITLE', dir: 'asc', tagQ: 'fam' }), + fetch: vi.fn() as unknown as typeof fetch + }); + + const firstCall = mockGet.mock.calls[0]; + expect(firstCall[1].params.query.sort).toBe('TITLE'); + expect(firstCall[1].params.query.dir).toBe('asc'); + expect(firstCall[1].params.query.tagQ).toBe('fam'); +}); + +it('returns total from the DocumentSearchResult envelope', async () => { + const mockGet = vi + .fn() + .mockResolvedValueOnce({ + response: { ok: true, status: 200 }, + data: { documents: [{ id: 'd1' }], total: 42 } + }) + .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }); + vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< + typeof createApiClient + >); + + const result = await load({ + url: makeUrl({ q: 'test' }), + fetch: vi.fn() as unknown as typeof fetch + }); + + expect(result.documents).toHaveLength(1); + expect(result.total).toBe(42); +}); + // ─── 401 redirect ───────────────────────────────────────────────────────────── describe('home page load — auth redirect', () => {