feat(themen): key reader tag visibility on the subtree rollup (#698)

Regenerate the TagTreeNodeDTO type with subtreeDocumentCount and switch
hasAnyDocuments to read it directly — the backend rollup already includes all
descendants, so the recursive children walk is no longer needed. Reader
surfaces now hide a topic only when its whole subtree is empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-31 12:17:40 +02:00
parent 4e8962a06d
commit e8377b579e
3 changed files with 27 additions and 13 deletions

View File

@@ -2230,6 +2230,11 @@ export interface components {
color?: string; color?: string;
/** Format: int32 */ /** Format: int32 */
documentCount: number; documentCount: number;
/**
* Format: int32
* @description Distinct documents tagged with this tag or any descendant tag (subtree rollup)
*/
subtreeDocumentCount: number;
children?: components["schemas"]["TagTreeNodeDTO"][]; children?: components["schemas"]["TagTreeNodeDTO"][];
/** /**
* Format: uuid * Format: uuid

View File

@@ -4,26 +4,29 @@ import type { components } from '$lib/generated/api';
type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO']; type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO'];
function makeNode(documentCount: number, children: TagTreeNodeDTO[] = []): TagTreeNodeDTO { function makeNode(
return { id: 'id', name: 'name', documentCount, children }; documentCount: number,
subtreeDocumentCount: number,
children: TagTreeNodeDTO[] = []
): TagTreeNodeDTO {
return { id: 'id', name: 'name', documentCount, subtreeDocumentCount, children };
} }
describe('hasAnyDocuments', () => { describe('hasAnyDocuments', () => {
it('returns false for a leaf node with documentCount=0', () => { it('returns false for a node whose subtree holds no documents', () => {
expect(hasAnyDocuments(makeNode(0))).toBe(false); expect(hasAnyDocuments(makeNode(0, 0))).toBe(false);
}); });
it('returns true for a leaf node with documentCount=3', () => { it('returns true for a node whose subtree holds documents', () => {
expect(hasAnyDocuments(makeNode(3))).toBe(true); expect(hasAnyDocuments(makeNode(3, 3))).toBe(true);
}); });
it('returns true for a root with documentCount=0 but a child with documentCount=5', () => { it('keys on the subtree rollup, not direct documentCount: 0 direct but rollup 5 → true', () => {
const node = makeNode(0, [makeNode(5)]); // The rollup already includes descendants — a single field read, no recursion over children.
expect(hasAnyDocuments(node)).toBe(true); expect(hasAnyDocuments(makeNode(0, 5))).toBe(true);
}); });
it('returns false for a root with documentCount=0 and all children also 0', () => { it('keys on the subtree rollup, not direct documentCount: 5 direct but rollup 0 → false', () => {
const node = makeNode(0, [makeNode(0), makeNode(0)]); expect(hasAnyDocuments(makeNode(5, 0))).toBe(false);
expect(hasAnyDocuments(node)).toBe(false);
}); });
}); });

View File

@@ -2,6 +2,12 @@ import type { components } from '$lib/generated/api';
type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO']; 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 { export function hasAnyDocuments(node: TagTreeNodeDTO): boolean {
return (node.documentCount ?? 0) > 0 || (node.children ?? []).some(hasAnyDocuments); return (node.subtreeDocumentCount ?? 0) > 0;
} }