diff --git a/frontend/src/routes/SearchFilterBar.svelte b/frontend/src/routes/SearchFilterBar.svelte
index 9ad11f39..344d6f26 100644
--- a/frontend/src/routes/SearchFilterBar.svelte
+++ b/frontend/src/routes/SearchFilterBar.svelte
@@ -15,6 +15,7 @@ let {
tagNames = $bindable<{ name: string; id?: string; color?: string; parentId?: string }[]>([]),
tagQ = $bindable(''),
tagOperator = $bindable<'AND' | 'OR'>('AND'),
+ undated = $bindable(false),
sort = $bindable('DATE'),
dir = $bindable('desc'),
showAdvanced = $bindable(false),
@@ -35,6 +36,7 @@ let {
tagNames?: { name: string; id?: string; color?: string; parentId?: string }[];
tagQ?: string;
tagOperator?: 'AND' | 'OR';
+ undated?: boolean;
sort?: string;
dir?: string;
showAdvanced?: boolean;
@@ -248,6 +250,33 @@ $effect(() => {
/>
+
+
+
+
+
{/if}
diff --git a/frontend/src/routes/SearchFilterBar.svelte.spec.ts b/frontend/src/routes/SearchFilterBar.svelte.spec.ts
index 26d1d333..11f4c32d 100644
--- a/frontend/src/routes/SearchFilterBar.svelte.spec.ts
+++ b/frontend/src/routes/SearchFilterBar.svelte.spec.ts
@@ -128,6 +128,42 @@ describe('SearchFilterBar – AND/OR tag operator toggle', () => {
});
});
+describe('SearchFilterBar – undated-only toggle (#668)', () => {
+ async function openAdvanced() {
+ const filterBtn = page.getByRole('button', { name: 'Filter', exact: true });
+ await filterBtn.click();
+ }
+
+ it('renders the "Nur undatierte" toggle in the advanced row', async () => {
+ render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'desc' });
+ await openAdvanced();
+ await expect.element(page.getByTestId('undated-only-toggle')).toBeInTheDocument();
+ });
+
+ it('reflects the active undated state via aria-pressed', async () => {
+ render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'desc', undated: true });
+ await openAdvanced();
+ await expect
+ .element(page.getByTestId('undated-only-toggle'))
+ .toHaveAttribute('aria-pressed', 'true');
+ });
+
+ it('calls onSearchImmediate when the undated toggle is clicked', async () => {
+ const onSearch = vi.fn();
+ const onSearchImmediate = vi.fn();
+ render(SearchFilterBar, {
+ ...defaultProps,
+ onSearch,
+ onSearchImmediate,
+ sort: 'DATE',
+ dir: 'desc'
+ });
+ await openAdvanced();
+ await page.getByTestId('undated-only-toggle').click();
+ await expect.poll(() => onSearchImmediate.mock.calls.length).toBeGreaterThan(0);
+ });
+});
+
describe('SearchFilterBar – tagQ live filter', () => {
it('calls onSearch when tag text changes in TagInput', async () => {
vi.stubGlobal(
diff --git a/frontend/src/routes/documents/+page.svelte b/frontend/src/routes/documents/+page.svelte
index f938334a..eabfea8c 100644
--- a/frontend/src/routes/documents/+page.svelte
+++ b/frontend/src/routes/documents/+page.svelte
@@ -32,10 +32,16 @@ let sort = $state(untrack(() => data.sort || 'DATE'));
let dir = $state(untrack(() => data.dir || 'desc'));
let tagQ = $state(untrack(() => data.tagQ || ''));
let tagOperator = $state<'AND' | 'OR'>(untrack(() => (data.tagOp as 'AND' | 'OR') || 'AND'));
+let undated = $state(untrack(() => data.undated ?? false));
function hasAdvancedFilters() {
return (
- (data.tags?.length ?? 0) > 0 || !!data.senderId || !!data.receiverId || !!data.from || !!data.to
+ (data.tags?.length ?? 0) > 0 ||
+ !!data.senderId ||
+ !!data.receiverId ||
+ !!data.from ||
+ !!data.to ||
+ !!data.undated
);
}
@@ -54,6 +60,7 @@ type FilterSnapshot = {
dir: string;
tagQ: string;
tagOp: 'AND' | 'OR';
+ undated: boolean;
zoomFrom?: string | null;
zoomTo?: string | null;
};
@@ -77,6 +84,7 @@ function buildSearchParams(filters: FilterSnapshot, targetPage?: number): Svelte
if (filters.dir) params.set('dir', filters.dir);
if (filters.tagQ) params.set('tagQ', filters.tagQ);
if (filters.tagOp === 'OR') params.set('tagOp', 'OR');
+ if (filters.undated) params.set('undated', 'true');
if (filters.zoomFrom) params.set('zoomFrom', filters.zoomFrom);
if (filters.zoomTo) params.set('zoomTo', filters.zoomTo);
if (targetPage !== undefined && targetPage > 0) params.set('page', String(targetPage));
@@ -112,6 +120,7 @@ function navigateWithZoom(zoomFrom: string | null, zoomTo: string | null) {
dir,
tagQ,
tagOp: tagOperator,
+ undated,
zoomFrom,
zoomTo
});
@@ -136,7 +145,8 @@ function buildPageHref(targetPage: number): string {
sort: data.sort || '',
dir: data.dir || '',
tagQ: data.tagQ || '',
- tagOp: (data.tagOp as 'AND' | 'OR') || 'AND'
+ tagOp: (data.tagOp as 'AND' | 'OR') || 'AND',
+ undated: data.undated ?? false
},
targetPage
);
@@ -188,7 +198,8 @@ async function editAllMatching() {
sort: '',
dir: '',
tagQ: data.tagQ || '',
- tagOp: (data.tagOp as 'AND' | 'OR') || 'AND'
+ tagOp: (data.tagOp as 'AND' | 'OR') || 'AND',
+ undated: data.undated ?? false
});
params.delete('sort');
params.delete('dir');
@@ -226,6 +237,7 @@ $effect(() => {
dir = data.dir || 'desc';
tagQ = data.tagQ || '';
tagOperator = (data.tagOp as 'AND' | 'OR') || 'AND';
+ undated = data.undated ?? false;
if (hasAdvancedFilters()) showAdvanced = true;
});
@@ -255,6 +267,7 @@ $effect(() => {
bind:dir={dir}
bind:tagQ={tagQ}
bind:tagOperator={tagOperator}
+ bind:undated={undated}
initialSenderName={initialSenderName}
initialReceiverName={initialReceiverName}
navKey={navKey}