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);
|
||||
let showAdvanced = $state(false);
|
||||
|
||||
const RECENT_STORAGE_KEY = 'korrespondenz_recent_persons';
|
||||
const MAX_RECENT = 5;
|
||||
@@ -107,20 +108,23 @@ function selectPerson(id: string) {
|
||||
bind:receiverId={receiverId}
|
||||
initialSenderName={data.initialValues.senderName}
|
||||
initialReceiverName={data.initialValues.receiverName}
|
||||
onapplyFilters={applyFilters}
|
||||
onswapPersons={swapPersons}
|
||||
/>
|
||||
|
||||
<CorrespondenzFilterControls
|
||||
senderId={senderId}
|
||||
bind:fromDate={fromDate}
|
||||
bind:toDate={toDate}
|
||||
bind:sortDir={sortDir}
|
||||
sortDir={sortDir}
|
||||
showAdvanced={showAdvanced}
|
||||
documentCount={data.documents.length}
|
||||
onapplyFilters={applyFilters}
|
||||
onswapPersons={swapPersons}
|
||||
ontoggleSort={toggleSort}
|
||||
ontoggleAdvanced={() => (showAdvanced = !showAdvanced)}
|
||||
/>
|
||||
|
||||
{#if showAdvanced}
|
||||
<CorrespondenzFilterControls
|
||||
bind:fromDate={fromDate}
|
||||
bind:toDate={toDate}
|
||||
onapplyFilters={applyFilters}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isSinglePerson}
|
||||
<SinglePersonHintBar
|
||||
senderName={senderName}
|
||||
|
||||
@@ -1,116 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import DateInput from '$lib/components/DateInput.svelte';
|
||||
|
||||
interface Props {
|
||||
senderId: string;
|
||||
fromDate?: string;
|
||||
toDate?: string;
|
||||
sortDir?: string;
|
||||
documentCount?: number;
|
||||
onapplyFilters: () => void;
|
||||
ontoggleSort: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
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'));
|
||||
let { fromDate = $bindable(''), toDate = $bindable(''), onapplyFilters }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
data-testid="conv-filter-controls"
|
||||
class="mt-6 flex items-center gap-4 border-t border-line-2 pt-6 transition-opacity"
|
||||
class:opacity-40={!senderId}
|
||||
class:pointer-events-none={!senderId}
|
||||
aria-disabled={!senderId}
|
||||
transition:slide
|
||||
class="mt-6 grid grid-cols-1 gap-6 border-t border-line-2 pt-6 md:grid-cols-12"
|
||||
>
|
||||
<!-- Period label -->
|
||||
<span class="text-xs font-bold tracking-widest text-ink-2 uppercase">
|
||||
{m.conv_strip_period()}
|
||||
</span>
|
||||
|
||||
<!-- From date -->
|
||||
<DateInput
|
||||
bind:value={fromDate}
|
||||
onchange={() => onapplyFilters()}
|
||||
placeholder={m.conv_strip_from_placeholder()}
|
||||
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'}"
|
||||
/>
|
||||
|
||||
<span class="text-sm text-ink-3">–</span>
|
||||
<div class="md:col-span-3">
|
||||
<label
|
||||
for="conv-from"
|
||||
class="mb-2 block text-xs font-bold tracking-widest text-ink-2 uppercase"
|
||||
>
|
||||
{m.conv_label_from()}
|
||||
</label>
|
||||
<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 -->
|
||||
<DateInput
|
||||
bind:value={toDate}
|
||||
onchange={() => onapplyFilters()}
|
||||
placeholder={m.conv_strip_to_placeholder()}
|
||||
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'}"
|
||||
/>
|
||||
|
||||
<!-- Document count -->
|
||||
<span
|
||||
data-testid="conv-strip-count"
|
||||
class="ml-auto text-sm font-bold"
|
||||
class:text-primary={hasDateFilter}
|
||||
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 class="md:col-span-3">
|
||||
<label for="conv-to" class="mb-2 block text-xs font-bold tracking-widest text-ink-2 uppercase">
|
||||
{m.conv_label_to()}
|
||||
</label>
|
||||
<DateInput
|
||||
id="conv-to"
|
||||
bind:value={toDate}
|
||||
onchange={() => onapplyFilters()}
|
||||
placeholder={m.conv_strip_to_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>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
<script lang="ts">
|
||||
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
||||
import CorrespondentSuggestionsDropdown from './CorrespondentSuggestionsDropdown.svelte';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
interface Props {
|
||||
senderId?: string;
|
||||
receiverId?: string;
|
||||
initialSenderName?: string;
|
||||
initialReceiverName?: string;
|
||||
sortDir?: string;
|
||||
showAdvanced?: boolean;
|
||||
documentCount?: number;
|
||||
onapplyFilters: () => void;
|
||||
onswapPersons: () => void;
|
||||
ontoggleSort: () => void;
|
||||
ontoggleAdvanced: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -16,8 +22,13 @@ let {
|
||||
receiverId = $bindable(''),
|
||||
initialSenderName = '',
|
||||
initialReceiverName = '',
|
||||
sortDir = 'DESC',
|
||||
showAdvanced = false,
|
||||
documentCount = 0,
|
||||
onapplyFilters,
|
||||
onswapPersons
|
||||
onswapPersons,
|
||||
ontoggleSort,
|
||||
ontoggleAdvanced
|
||||
}: Props = $props();
|
||||
|
||||
interface Correspondent {
|
||||
@@ -53,6 +64,7 @@ function handleSuggestionSelect(id: string) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Row 1: Person inputs -->
|
||||
<div data-testid="conv-person-bar" class="flex items-end gap-4">
|
||||
<!-- Person A -->
|
||||
<div
|
||||
@@ -127,3 +139,67 @@ function handleSuggestionSelect(id: string) {
|
||||
{/if}
|
||||
</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();
|
||||
});
|
||||
|
||||
it('shows filter controls when senderId is set', async () => {
|
||||
it('hides filter controls by default (collapsible)', async () => {
|
||||
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 ───────────────────────────────────────────────────────
|
||||
|
||||
describe('Briefwechsel page – strip letter count', () => {
|
||||
|
||||
Reference in New Issue
Block a user