feat(documents): show global undated count chip on the filter toggle
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m50s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Failing after 4m3s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m3s

Surface the backend's global undatedCount on the "Nur undatierte" toggle as
a count chip — the total undated documents matching the current filter
across all pages, not the page slice. The loader forwards undatedCount
straight through (defaulting to 0); the chip hides at 0 and stays visible
regardless of the toggle state so it advertises the triage backlog size.

generate:api was hand-edited (undatedCount added to DocumentSearchResult) —
CI must re-run npm run generate:api to confirm parity.

Refs #668

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-27 19:42:57 +02:00
parent a3c3f14aea
commit c6137a26a2
6 changed files with 79 additions and 0 deletions

View File

@@ -2471,6 +2471,8 @@ export interface components {
pageSize: number;
/** Format: int32 */
totalPages: number;
/** Format: int64 */
undatedCount: number;
};
MatchOffset: {
/** Format: int32 */

View File

@@ -16,6 +16,7 @@ let {
tagQ = $bindable(''),
tagOperator = $bindable<'AND' | 'OR'>('AND'),
undated = $bindable(false),
undatedCount = 0,
sort = $bindable('DATE'),
dir = $bindable('desc'),
showAdvanced = $bindable(false),
@@ -37,6 +38,7 @@ let {
tagQ?: string;
tagOperator?: 'AND' | 'OR';
undated?: boolean;
undatedCount?: number;
sort?: string;
dir?: string;
showAdvanced?: boolean;
@@ -275,6 +277,18 @@ $effect(() => {
{#if undated}{/if}
</span>
{m.docs_filter_undated_only()}
<!-- Global count of undated docs matching the current filter across ALL
pages (not the page slice). Stays visible regardless of the toggle
state so it advertises the triage backlog size (issue #668). -->
{#if undatedCount > 0}
<span
data-testid="undated-count"
class="inline-flex min-w-[1.5rem] items-center justify-center rounded-full px-1.5 py-0.5 text-[0.65rem] leading-none tabular-nums {undated
? 'bg-primary-fg/20 text-primary-fg'
: 'bg-line text-ink-2'}"
>{undatedCount}</span
>
{/if}
</button>
</div>
</div>

View File

@@ -162,6 +162,20 @@ describe('SearchFilterBar undated-only toggle (#668)', () => {
await page.getByTestId('undated-only-toggle').click();
await expect.poll(() => onSearchImmediate.mock.calls.length).toBeGreaterThan(0);
});
it('shows the global undated count chip when undatedCount > 0', async () => {
// The count is the backend's global filtered total (#668), passed straight
// through — the chip must render it verbatim, not a page-derived number.
render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'desc', undatedCount: 42 });
await openAdvanced();
await expect.element(page.getByTestId('undated-count')).toHaveTextContent('42');
});
it('hides the undated count chip when undatedCount is 0', async () => {
render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'desc', undatedCount: 0 });
await openAdvanced();
await expect.element(page.getByTestId('undated-count')).not.toBeInTheDocument();
});
});
describe('SearchFilterBar tagQ live filter', () => {

View File

@@ -85,6 +85,7 @@ export async function load({ url, fetch }) {
pageNumber: 0,
pageSize: PAGE_SIZE,
totalPages: 0,
undatedCount: 0,
q,
from,
to,
@@ -116,6 +117,8 @@ export async function load({ url, fetch }) {
pageNumber: result.data?.pageNumber ?? page,
pageSize: result.data?.pageSize ?? PAGE_SIZE,
totalPages: result.data?.totalPages ?? 0,
// Global undated count for the active filter, across all pages (issue #668).
undatedCount: result.data?.undatedCount ?? 0,
q,
from,
to,

View File

@@ -268,6 +268,7 @@ $effect(() => {
bind:tagQ={tagQ}
bind:tagOperator={tagOperator}
bind:undated={undated}
undatedCount={data.undatedCount ?? 0}
initialSenderName={initialSenderName}
initialReceiverName={initialReceiverName}
navKey={navKey}

View File

@@ -225,6 +225,51 @@ describe('documents page load — search params', () => {
expect(result.totalElements).toBe(42);
});
it('forwards the global undatedCount from the search result (#668)', async () => {
// The backend returns the global undated total for the active filter across
// ALL pages; the loader must pass it straight through, not recompute it locally.
const mockGet = vi.fn().mockResolvedValue({
response: { ok: true, status: 200 },
data: {
items: [],
totalElements: 200,
pageNumber: 0,
pageSize: 50,
totalPages: 4,
undatedCount: 73
}
});
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
typeof createApiClient
>);
const result = await load({
url: makeUrl({ q: 'test' }),
request: new Request('http://localhost/documents'),
fetch: vi.fn() as unknown as typeof fetch
});
expect(result.undatedCount).toBe(73);
});
it('defaults undatedCount to 0 when the search result omits it', async () => {
const mockGet = vi.fn().mockResolvedValue({
response: { ok: true, status: 200 },
data: { items: [], totalElements: 0, pageNumber: 0, pageSize: 50, totalPages: 0 }
});
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
typeof createApiClient
>);
const result = await load({
url: makeUrl(),
request: new Request('http://localhost/documents'),
fetch: vi.fn() as unknown as typeof fetch
});
expect(result.undatedCount).toBe(0);
});
it('returns filter values in the result for pre-filling the UI', async () => {
const mockGet = vi.fn().mockResolvedValue({
response: { ok: true, status: 200 },