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:
@@ -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) };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user