feat(documents): thread undated filter through the search loader + i18n
Parses ?undated strictly (=== 'true', mirroring the tagOp clamp), forwards
it as undated || undefined so the absent case drops out of the query, and
returns the flag in page data for the control to reflect. Adds the
docs_filter_undated_only toggle label and the explanatory
docs_range_excludes_undated empty-state copy in de/en/es. The badge reuses
the existing date_precision_unknown ("Datum unbekannt") key from #677.
OpenAPI types hand-edited for the new undated query param on /search and
/ids — CI must run `npm run generate:api` to confirm parity with the spec.
Refs #668
This commit is contained in:
@@ -100,6 +100,8 @@
|
|||||||
"docs_list_summary": "Zusammenfassung",
|
"docs_list_summary": "Zusammenfassung",
|
||||||
"docs_list_unknown": "Unbekannt",
|
"docs_list_unknown": "Unbekannt",
|
||||||
"docs_group_undated": "Undatiert",
|
"docs_group_undated": "Undatiert",
|
||||||
|
"docs_filter_undated_only": "Nur undatierte",
|
||||||
|
"docs_range_excludes_undated": "Ein Datumsfilter schließt undatierte Dokumente aus, da sie keinem Zeitraum zugeordnet werden können.",
|
||||||
"docs_group_unknown": "Unbekannt",
|
"docs_group_unknown": "Unbekannt",
|
||||||
"doc_section_who_when": "Wer & Wann",
|
"doc_section_who_when": "Wer & Wann",
|
||||||
"doc_section_description": "Beschreibung",
|
"doc_section_description": "Beschreibung",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"docs_list_summary": "Summary",
|
"docs_list_summary": "Summary",
|
||||||
"docs_list_unknown": "Unknown",
|
"docs_list_unknown": "Unknown",
|
||||||
"docs_group_undated": "Undated",
|
"docs_group_undated": "Undated",
|
||||||
|
"docs_filter_undated_only": "Undated only",
|
||||||
|
"docs_range_excludes_undated": "A date range filter excludes undated documents, because they cannot belong to any time span.",
|
||||||
"docs_group_unknown": "Unknown",
|
"docs_group_unknown": "Unknown",
|
||||||
"doc_section_who_when": "Who & When",
|
"doc_section_who_when": "Who & When",
|
||||||
"doc_section_description": "Description",
|
"doc_section_description": "Description",
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"docs_list_summary": "Resumen",
|
"docs_list_summary": "Resumen",
|
||||||
"docs_list_unknown": "Desconocido",
|
"docs_list_unknown": "Desconocido",
|
||||||
"docs_group_undated": "Sin fecha",
|
"docs_group_undated": "Sin fecha",
|
||||||
|
"docs_filter_undated_only": "Solo sin fecha",
|
||||||
|
"docs_range_excludes_undated": "Un filtro de intervalo de fechas excluye los documentos sin fecha, ya que no pueden pertenecer a ningún periodo.",
|
||||||
"docs_group_unknown": "Desconocido",
|
"docs_group_unknown": "Desconocido",
|
||||||
"doc_section_who_when": "Quién & Cuándo",
|
"doc_section_who_when": "Quién & Cuándo",
|
||||||
"doc_section_description": "Descripción",
|
"doc_section_description": "Descripción",
|
||||||
|
|||||||
@@ -5083,6 +5083,8 @@ export interface operations {
|
|||||||
dir?: string;
|
dir?: string;
|
||||||
/** @description Tag operator: AND (default) or OR */
|
/** @description Tag operator: AND (default) or OR */
|
||||||
tagOp?: string;
|
tagOp?: string;
|
||||||
|
/** @description Restrict to undated documents (meta_date IS NULL) */
|
||||||
|
undated?: boolean;
|
||||||
/** @description Page number (0-indexed) */
|
/** @description Page number (0-indexed) */
|
||||||
page?: number;
|
page?: number;
|
||||||
/** @description Page size (max 100) */
|
/** @description Page size (max 100) */
|
||||||
@@ -5184,6 +5186,7 @@ export interface operations {
|
|||||||
tagQ?: string;
|
tagQ?: string;
|
||||||
status?: "PLACEHOLDER" | "UPLOADED" | "TRANSCRIBED" | "REVIEWED" | "ARCHIVED";
|
status?: "PLACEHOLDER" | "UPLOADED" | "TRANSCRIBED" | "REVIEWED" | "ARCHIVED";
|
||||||
tagOp?: string;
|
tagOp?: string;
|
||||||
|
undated?: boolean;
|
||||||
};
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export async function load({ url, fetch }) {
|
|||||||
: 'desc';
|
: 'desc';
|
||||||
const tagQ = url.searchParams.get('tagQ') || '';
|
const tagQ = url.searchParams.get('tagQ') || '';
|
||||||
const tagOp = url.searchParams.get('tagOp') === 'OR' ? 'OR' : 'AND';
|
const tagOp = url.searchParams.get('tagOp') === 'OR' ? 'OR' : 'AND';
|
||||||
|
// Narrow the accepted truthy surface to exactly "true" (mirrors the tagOp clamp).
|
||||||
|
const undated = url.searchParams.get('undated') === 'true';
|
||||||
const page = Math.max(0, Number(url.searchParams.get('page') ?? '0') || 0);
|
const page = Math.max(0, Number(url.searchParams.get('page') ?? '0') || 0);
|
||||||
|
|
||||||
const api = createApiClient(fetch);
|
const api = createApiClient(fetch);
|
||||||
@@ -66,6 +68,7 @@ export async function load({ url, fetch }) {
|
|||||||
tag: tags.length ? tags : undefined,
|
tag: tags.length ? tags : undefined,
|
||||||
tagQ: tagQ && !tags.length ? tagQ : undefined,
|
tagQ: tagQ && !tags.length ? tagQ : undefined,
|
||||||
tagOp: tagOp === 'OR' ? 'OR' : undefined,
|
tagOp: tagOp === 'OR' ? 'OR' : undefined,
|
||||||
|
undated: undated || undefined,
|
||||||
sort,
|
sort,
|
||||||
dir: dir || undefined,
|
dir: dir || undefined,
|
||||||
page,
|
page,
|
||||||
@@ -94,6 +97,7 @@ export async function load({ url, fetch }) {
|
|||||||
dir,
|
dir,
|
||||||
tagQ,
|
tagQ,
|
||||||
tagOp,
|
tagOp,
|
||||||
|
undated,
|
||||||
error: 'Daten konnten nicht geladen werden.' as string | null
|
error: 'Daten konnten nicht geladen werden.' as string | null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -124,6 +128,7 @@ export async function load({ url, fetch }) {
|
|||||||
dir,
|
dir,
|
||||||
tagQ,
|
tagQ,
|
||||||
tagOp,
|
tagOp,
|
||||||
|
undated,
|
||||||
error: errorMessage
|
error: errorMessage
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,106 @@ describe('documents page load — search params', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('forwards undated=true to the search API as a boolean true', 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
|
||||||
|
>);
|
||||||
|
|
||||||
|
await load({
|
||||||
|
url: makeUrl({ undated: 'true' }),
|
||||||
|
request: new Request('http://localhost/documents'),
|
||||||
|
fetch: vi.fn() as unknown as typeof fetch
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGet).toHaveBeenCalledWith(
|
||||||
|
'/api/documents/search',
|
||||||
|
expect.objectContaining({
|
||||||
|
params: expect.objectContaining({
|
||||||
|
query: expect.objectContaining({ undated: true })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('omits undated from the query when the param is absent', 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
|
||||||
|
>);
|
||||||
|
|
||||||
|
await load({
|
||||||
|
url: makeUrl({}),
|
||||||
|
request: new Request('http://localhost/documents'),
|
||||||
|
fetch: vi.fn() as unknown as typeof fetch
|
||||||
|
});
|
||||||
|
|
||||||
|
const query = mockGet.mock.calls[0][1].params.query;
|
||||||
|
expect(query.undated).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats any undated value other than the literal "true" as not-undated', 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({ undated: '1' }),
|
||||||
|
request: new Request('http://localhost/documents'),
|
||||||
|
fetch: vi.fn() as unknown as typeof fetch
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.undated).toBe(false);
|
||||||
|
expect(mockGet.mock.calls[0][1].params.query.undated).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the undated flag in page data when enabled', 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({ undated: 'true' }),
|
||||||
|
request: new Request('http://localhost/documents'),
|
||||||
|
fetch: vi.fn() as unknown as typeof fetch
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.undated).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not carry page when toggling undated (page reset)', 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
|
||||||
|
>);
|
||||||
|
|
||||||
|
// A bare undated toggle URL carries no page param → loader requests page 0.
|
||||||
|
await load({
|
||||||
|
url: makeUrl({ undated: 'true' }),
|
||||||
|
request: new Request('http://localhost/documents'),
|
||||||
|
fetch: vi.fn() as unknown as typeof fetch
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGet.mock.calls[0][1].params.query.page).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns items and total from the search result', async () => {
|
it('returns items and total from the search result', async () => {
|
||||||
const item = {
|
const item = {
|
||||||
document: { id: 'd1' },
|
document: { id: 'd1' },
|
||||||
|
|||||||
Reference in New Issue
Block a user