Import normalizer: offline tool to normalize the raw archive spreadsheets #663

Merged
marcel merged 172 commits from docs/import-migration into main 2026-05-28 15:05:51 +02:00
6 changed files with 79 additions and 0 deletions
Showing only changes of commit c6137a26a2 - Show all commits

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 },