feat(#248): admin tag page complete overhaul — tree panel, merge, subtree delete, new edit components #249
@@ -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) };
|
||||
};
|
||||
|
||||
@@ -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<typeof createApiClient>);
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user