feat(#248): switch layout load to GET /api/tags/tree, expose tree + flat tags

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-16 22:40:23 +02:00
parent e01733eaf2
commit 9b5af67780
2 changed files with 89 additions and 16 deletions

View File

@@ -1,8 +1,34 @@
import { createApiClient } from '$lib/api.server'; import { createApiClient } from '$lib/api.server';
import type { components } from '$lib/generated/api';
import type { LayoutServerLoad } from './$types'; 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 }) => { export const load: LayoutServerLoad = async ({ fetch }) => {
const api = createApiClient(fetch); const api = createApiClient(fetch);
const result = await api.GET('/api/tags'); const result = await api.GET('/api/tags/tree');
return { tags: result.data ?? [] }; const tree = result.data ?? [];
return { tree, tags: flattenTree(tree) };
}; };

View File

@@ -5,37 +5,84 @@ vi.mock('$lib/api.server', () => ({ createApiClient: vi.fn() }));
import { createApiClient } from '$lib/api.server'; import { createApiClient } from '$lib/api.server';
function mockApi(tags: unknown[]) { function mockTreeApi(tree: unknown[]) {
vi.mocked(createApiClient).mockReturnValue({ vi.mocked(createApiClient).mockReturnValue({
GET: vi.fn().mockResolvedValueOnce({ response: { ok: true }, data: tags }) GET: vi.fn().mockResolvedValueOnce({ response: { ok: true }, data: tree })
} as ReturnType<typeof createApiClient>); } as ReturnType<typeof createApiClient>);
} }
beforeEach(() => vi.clearAllMocks()); 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', () => { describe('admin/tags layout load', () => {
it('returns the tags list', async () => { it('returns the tree list', async () => {
mockApi([ mockTreeApi(sampleTree);
{ id: 't1', name: 'Familie' },
{ id: 't2', name: 'Urlaub' }
]);
const result = await load({ fetch: vi.fn() as unknown as typeof fetch }); const result = await load({ fetch: vi.fn() as unknown as typeof fetch });
expect(result.tags).toHaveLength(2); expect(result.tree).toHaveLength(2);
expect(result.tags[0].name).toBe('Familie'); expect(result.tree[0].name).toBe('Familie');
}); });
it('returns an empty array when the API returns nothing', async () => { it('returns an empty tree when the API returns nothing', async () => {
mockApi([]); mockTreeApi([]);
const result = await load({ fetch: vi.fn() as unknown as typeof fetch }); 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: [] }); const mockGet = vi.fn().mockResolvedValue({ response: { ok: true }, data: [] });
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
typeof createApiClient typeof createApiClient
>); >);
await load({ fetch: vi.fn() as unknown as typeof fetch }); 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();
}); });
}); });