From 1fd8c2b2d21213f0769bf556f0de1ec5e947b7a2 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 31 May 2026 12:25:52 +0200 Subject: [PATCH] feat(themen): show the subtree rollup count on reader surfaces (#698) The /themen page (box header, child rows, aria-labels) and the dashboard ThemenWidget now display subtreeDocumentCount instead of the direct documentCount, so a topic's number reflects its whole sub-topic tree and matches what /documents?tag=X actually returns. A parent with 0 direct documents but documents under its children now shows a non-zero total. Co-Authored-By: Claude Opus 4.8 --- .../src/lib/shared/dashboard/ThemenWidget.svelte | 8 ++++---- .../shared/dashboard/ThemenWidget.svelte.spec.ts | 13 +++++++++++-- frontend/src/routes/themen/+page.svelte | 8 ++++---- frontend/src/routes/themen/page.svelte.spec.ts | 13 +++++++++++-- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/frontend/src/lib/shared/dashboard/ThemenWidget.svelte b/frontend/src/lib/shared/dashboard/ThemenWidget.svelte index cef35418..1e8cbd20 100644 --- a/frontend/src/lib/shared/dashboard/ThemenWidget.svelte +++ b/frontend/src/lib/shared/dashboard/ThemenWidget.svelte @@ -41,8 +41,8 @@ const shownTags = $derived(visibleTags.slice(0, MAX_VISIBLE_TAGS)); {#each shownTags as tag (tag.id)} 0 + ? ', ' + m.themen_dokumente({ count: tag.subtreeDocumentCount }) : ''}" class="flex cursor-pointer items-stretch overflow-hidden rounded-sm border border-line bg-canvas hover:bg-surface focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:outline-none" style="min-height: 56px" @@ -54,9 +54,9 @@ const shownTags = $derived(visibleTags.slice(0, MAX_VISIBLE_TAGS)); > {tag.name} - {#if tag.documentCount > 0} + {#if tag.subtreeDocumentCount > 0} - {m.themen_dokumente({ count: tag.documentCount })} + {m.themen_dokumente({ count: tag.subtreeDocumentCount })} {/if} diff --git a/frontend/src/lib/shared/dashboard/ThemenWidget.svelte.spec.ts b/frontend/src/lib/shared/dashboard/ThemenWidget.svelte.spec.ts index 521ddeba..a8e1e552 100644 --- a/frontend/src/lib/shared/dashboard/ThemenWidget.svelte.spec.ts +++ b/frontend/src/lib/shared/dashboard/ThemenWidget.svelte.spec.ts @@ -12,9 +12,10 @@ type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO']; function makeTag( name: string, documentCount: number, - children: TagTreeNodeDTO[] = [] + children: TagTreeNodeDTO[] = [], + subtreeDocumentCount: number = documentCount ): TagTreeNodeDTO { - return { id: 'id-' + name, name, documentCount, children }; + return { id: 'id-' + name, name, documentCount, subtreeDocumentCount, children }; } describe('ThemenWidget', () => { @@ -32,6 +33,14 @@ describe('ThemenWidget', () => { expect(document.body.textContent).not.toContain('Leer'); }); + it('displays the subtree rollup count for a tag with 0 direct documents', async () => { + // 0 direct documents, but the subtree rolls up to 8 — the widget shows the rollup. + const tags = [makeTag('Reisen', 0, [], 8)]; + render(ThemenWidget, { tags }); + expect(document.body.textContent).toContain('Reisen'); + expect(document.body.textContent).toContain('8'); + }); + it('shows the empty state text when all tags are filtered out', async () => { render(ThemenWidget, { tags: [makeTag('Leer', 0)] }); expect(document.body.textContent).toMatch(/Noch keine Themen/); diff --git a/frontend/src/routes/themen/+page.svelte b/frontend/src/routes/themen/+page.svelte index 40daf0cf..c0a50b6c 100644 --- a/frontend/src/routes/themen/+page.svelte +++ b/frontend/src/routes/themen/+page.svelte @@ -41,14 +41,14 @@ const visibleTree = $derived.by(() => data.tree.filter(hasAnyDocuments)); 0 + ? ', ' + m.themen_dokumente({ count: tag.subtreeDocumentCount }) : ''}" class="flex min-h-[56px] items-center justify-between px-4 pt-4 pb-3 hover:bg-canvas focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:outline-none focus-visible:ring-inset" > {tag.name} - {#if tag.documentCount > 0}{tag.documentCount}{/if} + {#if tag.subtreeDocumentCount > 0}{tag.subtreeDocumentCount}{/if} @@ -63,7 +63,7 @@ const visibleTree = $derived.by(() => data.tree.filter(hasAnyDocuments)); > {child.name} - {#if child.documentCount > 0}{child.documentCount}{/if} + {#if child.subtreeDocumentCount > 0}{child.subtreeDocumentCount}{/if} diff --git a/frontend/src/routes/themen/page.svelte.spec.ts b/frontend/src/routes/themen/page.svelte.spec.ts index d9d9634a..4e0b9b55 100644 --- a/frontend/src/routes/themen/page.svelte.spec.ts +++ b/frontend/src/routes/themen/page.svelte.spec.ts @@ -12,9 +12,10 @@ type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO']; function makeTag( name: string, documentCount: number, - children: TagTreeNodeDTO[] = [] + children: TagTreeNodeDTO[] = [], + subtreeDocumentCount: number = documentCount ): TagTreeNodeDTO { - return { id: 'id-' + name, name, documentCount, children }; + return { id: 'id-' + name, name, documentCount, subtreeDocumentCount, children }; } describe('/themen +page', () => { @@ -48,6 +49,14 @@ describe('/themen +page', () => { expect(document.body.textContent).toContain('Kriegsbriefe'); }); + it('shows the subtree rollup in the header for a parent with 0 direct documents', async () => { + // AC#5: 0 direct docs, but the subtree rolls up to 7 (child contributes 3). + const tree = [makeTag('Reisen', 0, [makeTag('Italien', 3)], 7)]; + render(ThemenPage, { data: { tree } }); + expect(document.body.textContent).toContain('Reisen'); + expect(document.body.textContent).toContain('7'); + }); + it('shows "+ N weitere" when a root tag has more than 5 children', async () => { const children = Array.from({ length: 7 }, (_, i) => makeTag(`Kind${i}`, i + 1)); const tree = [makeTag('Briefe', 10, children)];