diff --git a/frontend/src/lib/generated/api.ts b/frontend/src/lib/generated/api.ts index 5b059dd5..8f9e303e 100644 --- a/frontend/src/lib/generated/api.ts +++ b/frontend/src/lib/generated/api.ts @@ -2230,6 +2230,11 @@ export interface components { color?: string; /** Format: int32 */ documentCount: number; + /** + * Format: int32 + * @description Distinct documents tagged with this tag or any descendant tag (subtree rollup) + */ + subtreeDocumentCount: number; children?: components["schemas"]["TagTreeNodeDTO"][]; /** * Format: uuid diff --git a/frontend/src/lib/shared/utils/tagUtils.test.ts b/frontend/src/lib/shared/utils/tagUtils.test.ts index 5fe4a874..87b33763 100644 --- a/frontend/src/lib/shared/utils/tagUtils.test.ts +++ b/frontend/src/lib/shared/utils/tagUtils.test.ts @@ -4,26 +4,29 @@ import type { components } from '$lib/generated/api'; type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO']; -function makeNode(documentCount: number, children: TagTreeNodeDTO[] = []): TagTreeNodeDTO { - return { id: 'id', name: 'name', documentCount, children }; +function makeNode( + documentCount: number, + subtreeDocumentCount: number, + children: TagTreeNodeDTO[] = [] +): TagTreeNodeDTO { + return { id: 'id', name: 'name', documentCount, subtreeDocumentCount, children }; } describe('hasAnyDocuments', () => { - it('returns false for a leaf node with documentCount=0', () => { - expect(hasAnyDocuments(makeNode(0))).toBe(false); + it('returns false for a node whose subtree holds no documents', () => { + expect(hasAnyDocuments(makeNode(0, 0))).toBe(false); }); - it('returns true for a leaf node with documentCount=3', () => { - expect(hasAnyDocuments(makeNode(3))).toBe(true); + it('returns true for a node whose subtree holds documents', () => { + expect(hasAnyDocuments(makeNode(3, 3))).toBe(true); }); - it('returns true for a root with documentCount=0 but a child with documentCount=5', () => { - const node = makeNode(0, [makeNode(5)]); - expect(hasAnyDocuments(node)).toBe(true); + it('keys on the subtree rollup, not direct documentCount: 0 direct but rollup 5 → true', () => { + // The rollup already includes descendants — a single field read, no recursion over children. + expect(hasAnyDocuments(makeNode(0, 5))).toBe(true); }); - it('returns false for a root with documentCount=0 and all children also 0', () => { - const node = makeNode(0, [makeNode(0), makeNode(0)]); - expect(hasAnyDocuments(node)).toBe(false); + it('keys on the subtree rollup, not direct documentCount: 5 direct but rollup 0 → false', () => { + expect(hasAnyDocuments(makeNode(5, 0))).toBe(false); }); }); diff --git a/frontend/src/lib/shared/utils/tagUtils.ts b/frontend/src/lib/shared/utils/tagUtils.ts index a7fe138d..89826e68 100644 --- a/frontend/src/lib/shared/utils/tagUtils.ts +++ b/frontend/src/lib/shared/utils/tagUtils.ts @@ -2,6 +2,12 @@ import type { components } from '$lib/generated/api'; type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO']; +/** + * Whether a tag's whole subtree holds any documents — keyed on the subtree rollup + * (`subtreeDocumentCount`), which the backend already computes across all descendants. + * Used by the reader surfaces (/themen page, dashboard ThemenWidget) to hide empty topics. + * A single field read: no recursion needed, the rollup is authoritative. + */ export function hasAnyDocuments(node: TagTreeNodeDTO): boolean { - return (node.documentCount ?? 0) > 0 || (node.children ?? []).some(hasAnyDocuments); + return (node.subtreeDocumentCount ?? 0) > 0; }