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
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:
@@ -2471,6 +2471,8 @@ export interface components {
|
|||||||
pageSize: number;
|
pageSize: number;
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
|
/** Format: int64 */
|
||||||
|
undatedCount: number;
|
||||||
};
|
};
|
||||||
MatchOffset: {
|
MatchOffset: {
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ let {
|
|||||||
tagQ = $bindable(''),
|
tagQ = $bindable(''),
|
||||||
tagOperator = $bindable<'AND' | 'OR'>('AND'),
|
tagOperator = $bindable<'AND' | 'OR'>('AND'),
|
||||||
undated = $bindable(false),
|
undated = $bindable(false),
|
||||||
|
undatedCount = 0,
|
||||||
sort = $bindable('DATE'),
|
sort = $bindable('DATE'),
|
||||||
dir = $bindable('desc'),
|
dir = $bindable('desc'),
|
||||||
showAdvanced = $bindable(false),
|
showAdvanced = $bindable(false),
|
||||||
@@ -37,6 +38,7 @@ let {
|
|||||||
tagQ?: string;
|
tagQ?: string;
|
||||||
tagOperator?: 'AND' | 'OR';
|
tagOperator?: 'AND' | 'OR';
|
||||||
undated?: boolean;
|
undated?: boolean;
|
||||||
|
undatedCount?: number;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
dir?: string;
|
dir?: string;
|
||||||
showAdvanced?: boolean;
|
showAdvanced?: boolean;
|
||||||
@@ -275,6 +277,18 @@ $effect(() => {
|
|||||||
{#if undated}✓{/if}
|
{#if undated}✓{/if}
|
||||||
</span>
|
</span>
|
||||||
{m.docs_filter_undated_only()}
|
{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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -162,6 +162,20 @@ describe('SearchFilterBar – undated-only toggle (#668)', () => {
|
|||||||
await page.getByTestId('undated-only-toggle').click();
|
await page.getByTestId('undated-only-toggle').click();
|
||||||
await expect.poll(() => onSearchImmediate.mock.calls.length).toBeGreaterThan(0);
|
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', () => {
|
describe('SearchFilterBar – tagQ live filter', () => {
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export async function load({ url, fetch }) {
|
|||||||
pageNumber: 0,
|
pageNumber: 0,
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
totalPages: 0,
|
totalPages: 0,
|
||||||
|
undatedCount: 0,
|
||||||
q,
|
q,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
@@ -116,6 +117,8 @@ export async function load({ url, fetch }) {
|
|||||||
pageNumber: result.data?.pageNumber ?? page,
|
pageNumber: result.data?.pageNumber ?? page,
|
||||||
pageSize: result.data?.pageSize ?? PAGE_SIZE,
|
pageSize: result.data?.pageSize ?? PAGE_SIZE,
|
||||||
totalPages: result.data?.totalPages ?? 0,
|
totalPages: result.data?.totalPages ?? 0,
|
||||||
|
// Global undated count for the active filter, across all pages (issue #668).
|
||||||
|
undatedCount: result.data?.undatedCount ?? 0,
|
||||||
q,
|
q,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ $effect(() => {
|
|||||||
bind:tagQ={tagQ}
|
bind:tagQ={tagQ}
|
||||||
bind:tagOperator={tagOperator}
|
bind:tagOperator={tagOperator}
|
||||||
bind:undated={undated}
|
bind:undated={undated}
|
||||||
|
undatedCount={data.undatedCount ?? 0}
|
||||||
initialSenderName={initialSenderName}
|
initialSenderName={initialSenderName}
|
||||||
initialReceiverName={initialReceiverName}
|
initialReceiverName={initialReceiverName}
|
||||||
navKey={navKey}
|
navKey={navKey}
|
||||||
|
|||||||
@@ -225,6 +225,51 @@ describe('documents page load — search params', () => {
|
|||||||
expect(result.totalElements).toBe(42);
|
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 () => {
|
it('returns filter values in the result for pre-filling the UI', async () => {
|
||||||
const mockGet = vi.fn().mockResolvedValue({
|
const mockGet = vi.fn().mockResolvedValue({
|
||||||
response: { ok: true, status: 200 },
|
response: { ok: true, status: 200 },
|
||||||
|
|||||||
Reference in New Issue
Block a user