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 <noreply@anthropic.com>
This commit is contained in:
@@ -41,8 +41,8 @@ const shownTags = $derived(visibleTags.slice(0, MAX_VISIBLE_TAGS));
|
|||||||
{#each shownTags as tag (tag.id)}
|
{#each shownTags as tag (tag.id)}
|
||||||
<a
|
<a
|
||||||
href="/documents?tag={encodeURIComponent(tag.name)}"
|
href="/documents?tag={encodeURIComponent(tag.name)}"
|
||||||
aria-label="{tag.name}{tag.documentCount > 0
|
aria-label="{tag.name}{tag.subtreeDocumentCount > 0
|
||||||
? ', ' + m.themen_dokumente({ count: tag.documentCount })
|
? ', ' + 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"
|
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"
|
style="min-height: 56px"
|
||||||
@@ -54,9 +54,9 @@ const shownTags = $derived(visibleTags.slice(0, MAX_VISIBLE_TAGS));
|
|||||||
></span>
|
></span>
|
||||||
<span class="flex min-w-0 flex-1 flex-col justify-center gap-0.5 px-3 py-3">
|
<span class="flex min-w-0 flex-1 flex-col justify-center gap-0.5 px-3 py-3">
|
||||||
<span class="truncate font-serif text-sm font-semibold text-ink">{tag.name}</span>
|
<span class="truncate font-serif text-sm font-semibold text-ink">{tag.name}</span>
|
||||||
{#if tag.documentCount > 0}
|
{#if tag.subtreeDocumentCount > 0}
|
||||||
<span class="font-sans text-xs text-ink-3 tabular-nums">
|
<span class="font-sans text-xs text-ink-3 tabular-nums">
|
||||||
{m.themen_dokumente({ count: tag.documentCount })}
|
{m.themen_dokumente({ count: tag.subtreeDocumentCount })}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO'];
|
|||||||
function makeTag(
|
function makeTag(
|
||||||
name: string,
|
name: string,
|
||||||
documentCount: number,
|
documentCount: number,
|
||||||
children: TagTreeNodeDTO[] = []
|
children: TagTreeNodeDTO[] = [],
|
||||||
|
subtreeDocumentCount: number = documentCount
|
||||||
): TagTreeNodeDTO {
|
): TagTreeNodeDTO {
|
||||||
return { id: 'id-' + name, name, documentCount, children };
|
return { id: 'id-' + name, name, documentCount, subtreeDocumentCount, children };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('ThemenWidget', () => {
|
describe('ThemenWidget', () => {
|
||||||
@@ -32,6 +33,14 @@ describe('ThemenWidget', () => {
|
|||||||
expect(document.body.textContent).not.toContain('Leer');
|
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 () => {
|
it('shows the empty state text when all tags are filtered out', async () => {
|
||||||
render(ThemenWidget, { tags: [makeTag('Leer', 0)] });
|
render(ThemenWidget, { tags: [makeTag('Leer', 0)] });
|
||||||
expect(document.body.textContent).toMatch(/Noch keine Themen/);
|
expect(document.body.textContent).toMatch(/Noch keine Themen/);
|
||||||
|
|||||||
@@ -41,14 +41,14 @@ const visibleTree = $derived.by(() => data.tree.filter(hasAnyDocuments));
|
|||||||
|
|
||||||
<a
|
<a
|
||||||
href="/documents?tag={encodeURIComponent(tag.name)}"
|
href="/documents?tag={encodeURIComponent(tag.name)}"
|
||||||
aria-label="{tag.name}{tag.documentCount > 0
|
aria-label="{tag.name}{tag.subtreeDocumentCount > 0
|
||||||
? ', ' + m.themen_dokumente({ count: tag.documentCount })
|
? ', ' + 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"
|
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"
|
||||||
>
|
>
|
||||||
<span class="font-serif text-base font-semibold text-ink">{tag.name}</span>
|
<span class="font-serif text-base font-semibold text-ink">{tag.name}</span>
|
||||||
<span class="mr-1 ml-auto font-sans text-sm text-ink-3 tabular-nums">
|
<span class="mr-1 ml-auto font-sans text-sm text-ink-3 tabular-nums">
|
||||||
{#if tag.documentCount > 0}{tag.documentCount}{/if}
|
{#if tag.subtreeDocumentCount > 0}{tag.subtreeDocumentCount}{/if}
|
||||||
</span>
|
</span>
|
||||||
<span aria-hidden="true" class="h-3.5 w-3.5 flex-shrink-0 text-brand-mint">›</span>
|
<span aria-hidden="true" class="h-3.5 w-3.5 flex-shrink-0 text-brand-mint">›</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -63,7 +63,7 @@ const visibleTree = $derived.by(() => data.tree.filter(hasAnyDocuments));
|
|||||||
>
|
>
|
||||||
<span class="font-sans text-sm text-ink">{child.name}</span>
|
<span class="font-sans text-sm text-ink">{child.name}</span>
|
||||||
<span class="mr-1 ml-auto font-sans text-xs text-ink-3 tabular-nums">
|
<span class="mr-1 ml-auto font-sans text-xs text-ink-3 tabular-nums">
|
||||||
{#if child.documentCount > 0}{child.documentCount}{/if}
|
{#if child.subtreeDocumentCount > 0}{child.subtreeDocumentCount}{/if}
|
||||||
</span>
|
</span>
|
||||||
<span aria-hidden="true" class="h-3 w-3 flex-shrink-0 text-brand-mint">›</span>
|
<span aria-hidden="true" class="h-3 w-3 flex-shrink-0 text-brand-mint">›</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO'];
|
|||||||
function makeTag(
|
function makeTag(
|
||||||
name: string,
|
name: string,
|
||||||
documentCount: number,
|
documentCount: number,
|
||||||
children: TagTreeNodeDTO[] = []
|
children: TagTreeNodeDTO[] = [],
|
||||||
|
subtreeDocumentCount: number = documentCount
|
||||||
): TagTreeNodeDTO {
|
): TagTreeNodeDTO {
|
||||||
return { id: 'id-' + name, name, documentCount, children };
|
return { id: 'id-' + name, name, documentCount, subtreeDocumentCount, children };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('/themen +page', () => {
|
describe('/themen +page', () => {
|
||||||
@@ -48,6 +49,14 @@ describe('/themen +page', () => {
|
|||||||
expect(document.body.textContent).toContain('Kriegsbriefe');
|
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 () => {
|
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 children = Array.from({ length: 7 }, (_, i) => makeTag(`Kind${i}`, i + 1));
|
||||||
const tree = [makeTag('Briefe', 10, children)];
|
const tree = [makeTag('Briefe', 10, children)];
|
||||||
|
|||||||
Reference in New Issue
Block a user