diff --git a/frontend/src/routes/admin/tags/+layout.server.ts b/frontend/src/routes/admin/tags/+layout.server.ts index 01255d35..f348a953 100644 --- a/frontend/src/routes/admin/tags/+layout.server.ts +++ b/frontend/src/routes/admin/tags/+layout.server.ts @@ -1,8 +1,34 @@ import { createApiClient } from '$lib/api.server'; +import type { components } from '$lib/generated/api'; import type { LayoutServerLoad } from './$types'; +type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO']; + +export type FlatTag = { + id: string; + name: string; + color?: string; + parentId?: string; + documentCount: number; +}; + +function flattenTree(nodes: TagTreeNodeDTO[], result: FlatTag[] = []): FlatTag[] { + for (const node of nodes) { + result.push({ + id: node.id!, + name: node.name!, + color: node.color ?? undefined, + parentId: node.parentId ?? undefined, + documentCount: node.documentCount ?? 0 + }); + if (node.children?.length) flattenTree(node.children, result); + } + return result; +} + export const load: LayoutServerLoad = async ({ fetch }) => { const api = createApiClient(fetch); - const result = await api.GET('/api/tags'); - return { tags: result.data ?? [] }; + const result = await api.GET('/api/tags/tree'); + const tree = result.data ?? []; + return { tree, tags: flattenTree(tree) }; }; diff --git a/frontend/src/routes/admin/tags/layout.server.spec.ts b/frontend/src/routes/admin/tags/layout.server.spec.ts index 466a970b..3a4bc3de 100644 --- a/frontend/src/routes/admin/tags/layout.server.spec.ts +++ b/frontend/src/routes/admin/tags/layout.server.spec.ts @@ -5,37 +5,84 @@ vi.mock('$lib/api.server', () => ({ createApiClient: vi.fn() })); import { createApiClient } from '$lib/api.server'; -function mockApi(tags: unknown[]) { +function mockTreeApi(tree: unknown[]) { vi.mocked(createApiClient).mockReturnValue({ - GET: vi.fn().mockResolvedValueOnce({ response: { ok: true }, data: tags }) + GET: vi.fn().mockResolvedValueOnce({ response: { ok: true }, data: tree }) } as ReturnType); } beforeEach(() => vi.clearAllMocks()); +const sampleTree = [ + { + id: 'parent1', + name: 'Familie', + color: 'teal', + documentCount: 3, + parentId: null, + children: [ + { + id: 'child1', + name: 'Eltern', + color: null, + documentCount: 2, + parentId: 'parent1', + children: [] + } + ] + }, + { + id: 'root2', + name: 'Urlaub', + color: null, + documentCount: 1, + parentId: null, + children: [] + } +]; + describe('admin/tags layout load', () => { - it('returns the tags list', async () => { - mockApi([ - { id: 't1', name: 'Familie' }, - { id: 't2', name: 'Urlaub' } - ]); + it('returns the tree list', async () => { + mockTreeApi(sampleTree); const result = await load({ fetch: vi.fn() as unknown as typeof fetch }); - expect(result.tags).toHaveLength(2); - expect(result.tags[0].name).toBe('Familie'); + expect(result.tree).toHaveLength(2); + expect(result.tree[0].name).toBe('Familie'); }); - it('returns an empty array when the API returns nothing', async () => { - mockApi([]); + it('returns an empty tree when the API returns nothing', async () => { + mockTreeApi([]); const result = await load({ fetch: vi.fn() as unknown as typeof fetch }); - expect(result.tags).toEqual([]); + expect(result.tree).toEqual([]); }); - it('calls GET /api/tags', async () => { + it('calls GET /api/tags/tree', async () => { const mockGet = vi.fn().mockResolvedValue({ response: { ok: true }, data: [] }); vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); await load({ fetch: vi.fn() as unknown as typeof fetch }); - expect(mockGet).toHaveBeenCalledWith('/api/tags'); + expect(mockGet).toHaveBeenCalledWith('/api/tags/tree'); + }); + + it('flattens the tree into a flat tags array', async () => { + mockTreeApi(sampleTree); + const result = await load({ fetch: vi.fn() as unknown as typeof fetch }); + // Both parent and child should be in the flat array + expect(result.tags).toHaveLength(3); + expect(result.tags.map((t) => t.name)).toContain('Eltern'); + }); + + it('preserves parentId on child tags in the flat array', async () => { + mockTreeApi(sampleTree); + const result = await load({ fetch: vi.fn() as unknown as typeof fetch }); + const child = result.tags.find((t) => t.name === 'Eltern'); + expect(child?.parentId).toBe('parent1'); + }); + + it('sets parentId to undefined on root tags in the flat array', async () => { + mockTreeApi(sampleTree); + const result = await load({ fetch: vi.fn() as unknown as typeof fetch }); + const root = result.tags.find((t) => t.name === 'Familie'); + expect(root?.parentId).toBeUndefined(); }); });