feat(documents): add a "Nur undatierte" filter toggle wired to the URL
SearchFilterBar gains an aria-pressed "Nur undatierte" toggle in the advanced row (min-h-[44px] touch target, labels the state not the colour). The documents page threads `undated` through the filter snapshot so it is a shareable URL param picked up by both filter-change nav and pagination, and flows into the bulk-edit "select all" /ids request. Toggling resets to page 0 via the existing implicit page-drop. Refs #668
This commit is contained in:
@@ -15,6 +15,7 @@ let {
|
|||||||
tagNames = $bindable<{ name: string; id?: string; color?: string; parentId?: string }[]>([]),
|
tagNames = $bindable<{ name: string; id?: string; color?: string; parentId?: string }[]>([]),
|
||||||
tagQ = $bindable(''),
|
tagQ = $bindable(''),
|
||||||
tagOperator = $bindable<'AND' | 'OR'>('AND'),
|
tagOperator = $bindable<'AND' | 'OR'>('AND'),
|
||||||
|
undated = $bindable(false),
|
||||||
sort = $bindable('DATE'),
|
sort = $bindable('DATE'),
|
||||||
dir = $bindable('desc'),
|
dir = $bindable('desc'),
|
||||||
showAdvanced = $bindable(false),
|
showAdvanced = $bindable(false),
|
||||||
@@ -35,6 +36,7 @@ let {
|
|||||||
tagNames?: { name: string; id?: string; color?: string; parentId?: string }[];
|
tagNames?: { name: string; id?: string; color?: string; parentId?: string }[];
|
||||||
tagQ?: string;
|
tagQ?: string;
|
||||||
tagOperator?: 'AND' | 'OR';
|
tagOperator?: 'AND' | 'OR';
|
||||||
|
undated?: boolean;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
dir?: string;
|
dir?: string;
|
||||||
showAdvanced?: boolean;
|
showAdvanced?: boolean;
|
||||||
@@ -248,6 +250,33 @@ $effect(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Undated-only triage toggle (#668). aria-pressed states the toggle, not a
|
||||||
|
colour; min-h-[44px] meets the senior-audience touch target (WCAG 2.5.5). -->
|
||||||
|
<div class="md:col-span-12">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="undated-only-toggle"
|
||||||
|
aria-pressed={undated}
|
||||||
|
onclick={() => {
|
||||||
|
undated = !undated;
|
||||||
|
(onSearchImmediate ?? onSearch)();
|
||||||
|
}}
|
||||||
|
class="inline-flex min-h-[44px] items-center gap-2 rounded border px-3 text-xs font-bold tracking-widest uppercase transition-colors {undated
|
||||||
|
? 'border-primary bg-primary text-primary-fg'
|
||||||
|
: 'border-line bg-muted text-ink-2 hover:bg-line'}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="inline-flex h-4 w-4 items-center justify-center rounded-sm border {undated
|
||||||
|
? 'border-primary-fg bg-primary-fg/20'
|
||||||
|
: 'border-ink-3'}"
|
||||||
|
>
|
||||||
|
{#if undated}✓{/if}
|
||||||
|
</span>
|
||||||
|
{m.docs_filter_undated_only()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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', () => {
|
describe('SearchFilterBar – tagQ live filter', () => {
|
||||||
it('calls onSearch when tag text changes in TagInput', async () => {
|
it('calls onSearch when tag text changes in TagInput', async () => {
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
|
|||||||
@@ -32,10 +32,16 @@ let sort = $state(untrack(() => data.sort || 'DATE'));
|
|||||||
let dir = $state(untrack(() => data.dir || 'desc'));
|
let dir = $state(untrack(() => data.dir || 'desc'));
|
||||||
let tagQ = $state(untrack(() => data.tagQ || ''));
|
let tagQ = $state(untrack(() => data.tagQ || ''));
|
||||||
let tagOperator = $state<'AND' | 'OR'>(untrack(() => (data.tagOp as 'AND' | 'OR') || 'AND'));
|
let tagOperator = $state<'AND' | 'OR'>(untrack(() => (data.tagOp as 'AND' | 'OR') || 'AND'));
|
||||||
|
let undated = $state(untrack(() => data.undated ?? false));
|
||||||
|
|
||||||
function hasAdvancedFilters() {
|
function hasAdvancedFilters() {
|
||||||
return (
|
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;
|
dir: string;
|
||||||
tagQ: string;
|
tagQ: string;
|
||||||
tagOp: 'AND' | 'OR';
|
tagOp: 'AND' | 'OR';
|
||||||
|
undated: boolean;
|
||||||
zoomFrom?: string | null;
|
zoomFrom?: string | null;
|
||||||
zoomTo?: 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.dir) params.set('dir', filters.dir);
|
||||||
if (filters.tagQ) params.set('tagQ', filters.tagQ);
|
if (filters.tagQ) params.set('tagQ', filters.tagQ);
|
||||||
if (filters.tagOp === 'OR') params.set('tagOp', 'OR');
|
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.zoomFrom) params.set('zoomFrom', filters.zoomFrom);
|
||||||
if (filters.zoomTo) params.set('zoomTo', filters.zoomTo);
|
if (filters.zoomTo) params.set('zoomTo', filters.zoomTo);
|
||||||
if (targetPage !== undefined && targetPage > 0) params.set('page', String(targetPage));
|
if (targetPage !== undefined && targetPage > 0) params.set('page', String(targetPage));
|
||||||
@@ -112,6 +120,7 @@ function navigateWithZoom(zoomFrom: string | null, zoomTo: string | null) {
|
|||||||
dir,
|
dir,
|
||||||
tagQ,
|
tagQ,
|
||||||
tagOp: tagOperator,
|
tagOp: tagOperator,
|
||||||
|
undated,
|
||||||
zoomFrom,
|
zoomFrom,
|
||||||
zoomTo
|
zoomTo
|
||||||
});
|
});
|
||||||
@@ -136,7 +145,8 @@ function buildPageHref(targetPage: number): string {
|
|||||||
sort: data.sort || '',
|
sort: data.sort || '',
|
||||||
dir: data.dir || '',
|
dir: data.dir || '',
|
||||||
tagQ: data.tagQ || '',
|
tagQ: data.tagQ || '',
|
||||||
tagOp: (data.tagOp as 'AND' | 'OR') || 'AND'
|
tagOp: (data.tagOp as 'AND' | 'OR') || 'AND',
|
||||||
|
undated: data.undated ?? false
|
||||||
},
|
},
|
||||||
targetPage
|
targetPage
|
||||||
);
|
);
|
||||||
@@ -188,7 +198,8 @@ async function editAllMatching() {
|
|||||||
sort: '',
|
sort: '',
|
||||||
dir: '',
|
dir: '',
|
||||||
tagQ: data.tagQ || '',
|
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('sort');
|
||||||
params.delete('dir');
|
params.delete('dir');
|
||||||
@@ -226,6 +237,7 @@ $effect(() => {
|
|||||||
dir = data.dir || 'desc';
|
dir = data.dir || 'desc';
|
||||||
tagQ = data.tagQ || '';
|
tagQ = data.tagQ || '';
|
||||||
tagOperator = (data.tagOp as 'AND' | 'OR') || 'AND';
|
tagOperator = (data.tagOp as 'AND' | 'OR') || 'AND';
|
||||||
|
undated = data.undated ?? false;
|
||||||
if (hasAdvancedFilters()) showAdvanced = true;
|
if (hasAdvancedFilters()) showAdvanced = true;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -255,6 +267,7 @@ $effect(() => {
|
|||||||
bind:dir={dir}
|
bind:dir={dir}
|
||||||
bind:tagQ={tagQ}
|
bind:tagQ={tagQ}
|
||||||
bind:tagOperator={tagOperator}
|
bind:tagOperator={tagOperator}
|
||||||
|
bind:undated={undated}
|
||||||
initialSenderName={initialSenderName}
|
initialSenderName={initialSenderName}
|
||||||
initialReceiverName={initialReceiverName}
|
initialReceiverName={initialReceiverName}
|
||||||
navKey={navKey}
|
navKey={navKey}
|
||||||
|
|||||||
Reference in New Issue
Block a user