feat(ui): collapsible date filter with sort + filter toggle on person row
Move sort button and filter toggle to the person row, matching the document search page pattern (sort + filter + count inline). Date range inputs are now a collapsible section behind the filter toggle, using slide transition and the same grid layout as the document search advanced filters. Fix date input padding (add px-3). Refs: #179 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ $effect(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isSinglePerson = $derived(!!senderId && !receiverId);
|
const isSinglePerson = $derived(!!senderId && !receiverId);
|
||||||
|
let showAdvanced = $state(false);
|
||||||
|
|
||||||
const RECENT_STORAGE_KEY = 'korrespondenz_recent_persons';
|
const RECENT_STORAGE_KEY = 'korrespondenz_recent_persons';
|
||||||
const MAX_RECENT = 5;
|
const MAX_RECENT = 5;
|
||||||
@@ -107,20 +108,23 @@ function selectPerson(id: string) {
|
|||||||
bind:receiverId={receiverId}
|
bind:receiverId={receiverId}
|
||||||
initialSenderName={data.initialValues.senderName}
|
initialSenderName={data.initialValues.senderName}
|
||||||
initialReceiverName={data.initialValues.receiverName}
|
initialReceiverName={data.initialValues.receiverName}
|
||||||
onapplyFilters={applyFilters}
|
sortDir={sortDir}
|
||||||
onswapPersons={swapPersons}
|
showAdvanced={showAdvanced}
|
||||||
/>
|
|
||||||
|
|
||||||
<CorrespondenzFilterControls
|
|
||||||
senderId={senderId}
|
|
||||||
bind:fromDate={fromDate}
|
|
||||||
bind:toDate={toDate}
|
|
||||||
bind:sortDir={sortDir}
|
|
||||||
documentCount={data.documents.length}
|
documentCount={data.documents.length}
|
||||||
onapplyFilters={applyFilters}
|
onapplyFilters={applyFilters}
|
||||||
|
onswapPersons={swapPersons}
|
||||||
ontoggleSort={toggleSort}
|
ontoggleSort={toggleSort}
|
||||||
|
ontoggleAdvanced={() => (showAdvanced = !showAdvanced)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{#if showAdvanced}
|
||||||
|
<CorrespondenzFilterControls
|
||||||
|
bind:fromDate={fromDate}
|
||||||
|
bind:toDate={toDate}
|
||||||
|
onapplyFilters={applyFilters}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if isSinglePerson}
|
{#if isSinglePerson}
|
||||||
<SinglePersonHintBar
|
<SinglePersonHintBar
|
||||||
senderName={senderName}
|
senderName={senderName}
|
||||||
|
|||||||
@@ -1,116 +1,50 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { slide } from 'svelte/transition';
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import DateInput from '$lib/components/DateInput.svelte';
|
import DateInput from '$lib/components/DateInput.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
senderId: string;
|
|
||||||
fromDate?: string;
|
fromDate?: string;
|
||||||
toDate?: string;
|
toDate?: string;
|
||||||
sortDir?: string;
|
|
||||||
documentCount?: number;
|
|
||||||
onapplyFilters: () => void;
|
onapplyFilters: () => void;
|
||||||
ontoggleSort: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { fromDate = $bindable(''), toDate = $bindable(''), onapplyFilters }: Props = $props();
|
||||||
senderId,
|
|
||||||
fromDate = $bindable(''),
|
|
||||||
toDate = $bindable(''),
|
|
||||||
sortDir = $bindable('DESC'),
|
|
||||||
documentCount,
|
|
||||||
onapplyFilters,
|
|
||||||
ontoggleSort
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
let hasDateFilter = $derived(!!(fromDate || toDate));
|
|
||||||
let isActive = $derived(!!(fromDate || toDate || sortDir !== 'DESC'));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
data-testid="conv-filter-controls"
|
data-testid="conv-filter-controls"
|
||||||
class="mt-6 flex items-center gap-4 border-t border-line-2 pt-6 transition-opacity"
|
transition:slide
|
||||||
class:opacity-40={!senderId}
|
class="mt-6 grid grid-cols-1 gap-6 border-t border-line-2 pt-6 md:grid-cols-12"
|
||||||
class:pointer-events-none={!senderId}
|
|
||||||
aria-disabled={!senderId}
|
|
||||||
>
|
>
|
||||||
<!-- Period label -->
|
|
||||||
<span class="text-xs font-bold tracking-widest text-ink-2 uppercase">
|
|
||||||
{m.conv_strip_period()}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- From date -->
|
<!-- From date -->
|
||||||
<DateInput
|
<div class="md:col-span-3">
|
||||||
bind:value={fromDate}
|
<label
|
||||||
onchange={() => onapplyFilters()}
|
for="conv-from"
|
||||||
placeholder={m.conv_strip_from_placeholder()}
|
class="mb-2 block text-xs font-bold tracking-widest text-ink-2 uppercase"
|
||||||
class="w-[120px] border-line py-2.5 text-sm text-ink shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring {fromDate ? 'border-primary' : 'border-line'}"
|
>
|
||||||
/>
|
{m.conv_label_from()}
|
||||||
|
</label>
|
||||||
<span class="text-sm text-ink-3">–</span>
|
<DateInput
|
||||||
|
id="conv-from"
|
||||||
|
bind:value={fromDate}
|
||||||
|
onchange={() => onapplyFilters()}
|
||||||
|
placeholder={m.conv_strip_from_placeholder()}
|
||||||
|
class="block w-full border-line px-3 py-2.5 text-sm text-ink shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- To date -->
|
<!-- To date -->
|
||||||
<DateInput
|
<div class="md:col-span-3">
|
||||||
bind:value={toDate}
|
<label for="conv-to" class="mb-2 block text-xs font-bold tracking-widest text-ink-2 uppercase">
|
||||||
onchange={() => onapplyFilters()}
|
{m.conv_label_to()}
|
||||||
placeholder={m.conv_strip_to_placeholder()}
|
</label>
|
||||||
class="w-[120px] border-line py-2.5 text-sm text-ink shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring {toDate ? 'border-primary' : 'border-line'}"
|
<DateInput
|
||||||
/>
|
id="conv-to"
|
||||||
|
bind:value={toDate}
|
||||||
<!-- Document count -->
|
onchange={() => onapplyFilters()}
|
||||||
<span
|
placeholder={m.conv_strip_to_placeholder()}
|
||||||
data-testid="conv-strip-count"
|
class="block w-full border-line px-3 py-2.5 text-sm text-ink shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||||
class="ml-auto text-sm font-bold"
|
/>
|
||||||
class:text-primary={hasDateFilter}
|
</div>
|
||||||
class:text-ink-3={!hasDateFilter}
|
|
||||||
>
|
|
||||||
{m.conv_letters_count({ count: documentCount ?? 0 })}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- Sort button -->
|
|
||||||
<button
|
|
||||||
data-testid="conv-sort-btn"
|
|
||||||
type="button"
|
|
||||||
aria-label="Sortierung umkehren"
|
|
||||||
aria-pressed={sortDir === 'ASC'}
|
|
||||||
onclick={ontoggleSort}
|
|
||||||
class="flex items-center gap-2 border px-4 py-2.5 text-sm font-bold tracking-wide uppercase transition hover:bg-muted hover:text-ink"
|
|
||||||
class:border-primary={isActive}
|
|
||||||
class:text-primary={isActive}
|
|
||||||
class:border-line={!isActive}
|
|
||||||
class:text-ink-2={!isActive}
|
|
||||||
>
|
|
||||||
{#if sortDir === 'ASC'}
|
|
||||||
{m.conv_strip_sort_oldest()}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="12"
|
|
||||||
height="12"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2.5"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<polyline points="18 15 12 9 6 15" />
|
|
||||||
</svg>
|
|
||||||
{:else}
|
|
||||||
{m.conv_strip_sort_newest()}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="12"
|
|
||||||
height="12"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2.5"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<polyline points="6 9 12 15 18 9" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
||||||
import CorrespondentSuggestionsDropdown from './CorrespondentSuggestionsDropdown.svelte';
|
import CorrespondentSuggestionsDropdown from './CorrespondentSuggestionsDropdown.svelte';
|
||||||
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
senderId?: string;
|
senderId?: string;
|
||||||
receiverId?: string;
|
receiverId?: string;
|
||||||
initialSenderName?: string;
|
initialSenderName?: string;
|
||||||
initialReceiverName?: string;
|
initialReceiverName?: string;
|
||||||
|
sortDir?: string;
|
||||||
|
showAdvanced?: boolean;
|
||||||
|
documentCount?: number;
|
||||||
onapplyFilters: () => void;
|
onapplyFilters: () => void;
|
||||||
onswapPersons: () => void;
|
onswapPersons: () => void;
|
||||||
|
ontoggleSort: () => void;
|
||||||
|
ontoggleAdvanced: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -16,8 +22,13 @@ let {
|
|||||||
receiverId = $bindable(''),
|
receiverId = $bindable(''),
|
||||||
initialSenderName = '',
|
initialSenderName = '',
|
||||||
initialReceiverName = '',
|
initialReceiverName = '',
|
||||||
|
sortDir = 'DESC',
|
||||||
|
showAdvanced = false,
|
||||||
|
documentCount = 0,
|
||||||
onapplyFilters,
|
onapplyFilters,
|
||||||
onswapPersons
|
onswapPersons,
|
||||||
|
ontoggleSort,
|
||||||
|
ontoggleAdvanced
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
interface Correspondent {
|
interface Correspondent {
|
||||||
@@ -53,6 +64,7 @@ function handleSuggestionSelect(id: string) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Row 1: Person inputs -->
|
||||||
<div data-testid="conv-person-bar" class="flex items-end gap-4">
|
<div data-testid="conv-person-bar" class="flex items-end gap-4">
|
||||||
<!-- Person A -->
|
<!-- Person A -->
|
||||||
<div
|
<div
|
||||||
@@ -127,3 +139,67 @@ function handleSuggestionSelect(id: string) {
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 2: Sort + Filter toggle + Count (mirrors document search bar pattern) -->
|
||||||
|
<div class="mt-4 flex items-center gap-4">
|
||||||
|
<!-- Sort button -->
|
||||||
|
<button
|
||||||
|
data-testid="conv-sort-btn"
|
||||||
|
type="button"
|
||||||
|
aria-label="Sortierung umkehren"
|
||||||
|
aria-pressed={sortDir === 'ASC'}
|
||||||
|
onclick={ontoggleSort}
|
||||||
|
class="flex items-center gap-2 border border-line bg-muted px-4 py-2.5 text-sm font-bold tracking-wide text-ink-2 uppercase transition hover:bg-muted hover:text-ink"
|
||||||
|
>
|
||||||
|
{#if sortDir === 'ASC'}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="h-4 w-4"><polyline points="18 15 12 9 6 15" /></svg
|
||||||
|
>
|
||||||
|
{m.conv_strip_sort_oldest()}
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="h-4 w-4"><polyline points="6 9 12 15 18 9" /></svg
|
||||||
|
>
|
||||||
|
{m.conv_strip_sort_newest()}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Filter toggle button -->
|
||||||
|
<button
|
||||||
|
onclick={ontoggleAdvanced}
|
||||||
|
class="flex items-center gap-2 border border-line bg-muted px-4 py-2.5 text-sm font-bold tracking-wide text-ink-2 uppercase transition hover:bg-muted hover:text-ink"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/degruyter-icons/Simple/Small-16px/SVG/Action/Chevron/Chevron-Down-SM.svg"
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
class="h-4 w-4 transform transition-transform duration-200 {showAdvanced ? 'rotate-180' : ''}"
|
||||||
|
/>
|
||||||
|
{m.docs_btn_filter()}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Document count -->
|
||||||
|
<span data-testid="conv-strip-count" class="ml-auto text-sm font-bold text-ink-3">
|
||||||
|
{m.conv_letters_count({ count: documentCount })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -103,9 +103,10 @@ describe('Briefwechsel page – results state', () => {
|
|||||||
await expect.element(page.getByTestId('conv-person-bar')).toBeInTheDocument();
|
await expect.element(page.getByTestId('conv-person-bar')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows filter controls when senderId is set', async () => {
|
it('hides filter controls by default (collapsible)', async () => {
|
||||||
render(Page, { data: withSender });
|
render(Page, { data: withSender });
|
||||||
await expect.element(page.getByTestId('conv-filter-controls')).toBeInTheDocument();
|
await expect.element(page.getByTestId('conv-person-bar')).toBeInTheDocument();
|
||||||
|
await expect.element(page.getByTestId('conv-filter-controls')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -149,16 +150,6 @@ describe('Briefwechsel page – single-person hint bar', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Filter controls disabled state ──────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('Briefwechsel page – filter strip Row 2 disabled state', () => {
|
|
||||||
it('filter controls are not aria-disabled when senderId is set', async () => {
|
|
||||||
render(Page, { data: withSender });
|
|
||||||
const strip = document.querySelector('[aria-disabled="false"]');
|
|
||||||
expect(strip).not.toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Strip letter count ───────────────────────────────────────────────────────
|
// ─── Strip letter count ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('Briefwechsel page – strip letter count', () => {
|
describe('Briefwechsel page – strip letter count', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user