feat(search): add sort/dir/tagQ props to SearchFilterBar with SortDropdown
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
||||||
import TagInput from '$lib/components/TagInput.svelte';
|
import TagInput from '$lib/components/TagInput.svelte';
|
||||||
|
import SortDropdown from '$lib/components/SortDropdown.svelte';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
|
|
||||||
@@ -11,6 +12,9 @@ let {
|
|||||||
senderId = $bindable(''),
|
senderId = $bindable(''),
|
||||||
receiverId = $bindable(''),
|
receiverId = $bindable(''),
|
||||||
tagNames = $bindable<string[]>([]),
|
tagNames = $bindable<string[]>([]),
|
||||||
|
tagQ = $bindable(''),
|
||||||
|
sort = $bindable('DATE'),
|
||||||
|
dir = $bindable('desc'),
|
||||||
showAdvanced = $bindable(false),
|
showAdvanced = $bindable(false),
|
||||||
initialSenderName = '',
|
initialSenderName = '',
|
||||||
initialReceiverName = '',
|
initialReceiverName = '',
|
||||||
@@ -24,6 +28,9 @@ let {
|
|||||||
senderId?: string;
|
senderId?: string;
|
||||||
receiverId?: string;
|
receiverId?: string;
|
||||||
tagNames?: string[];
|
tagNames?: string[];
|
||||||
|
tagQ?: string;
|
||||||
|
sort?: string;
|
||||||
|
dir?: string;
|
||||||
showAdvanced?: boolean;
|
showAdvanced?: boolean;
|
||||||
initialSenderName?: string;
|
initialSenderName?: string;
|
||||||
initialReceiverName?: string;
|
initialReceiverName?: string;
|
||||||
@@ -31,6 +38,20 @@ let {
|
|||||||
onfocus?: () => void;
|
onfocus?: () => void;
|
||||||
onblur?: () => void;
|
onblur?: () => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
// Plain (non-reactive) flag — not $state, so no reactive assignment inside $effect
|
||||||
|
let sortDirMounted = false;
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
// Track sort and dir so this effect re-runs when either changes
|
||||||
|
void sort;
|
||||||
|
void dir;
|
||||||
|
if (!sortDirMounted) {
|
||||||
|
sortDirMounted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSearch();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-8 rounded-sm border border-line bg-surface p-6 shadow-sm">
|
<div class="mb-8 rounded-sm border border-line bg-surface p-6 shadow-sm">
|
||||||
@@ -58,6 +79,9 @@ let {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Sort Dropdown -->
|
||||||
|
<SortDropdown bind:sort={sort} bind:dir={dir} />
|
||||||
|
|
||||||
<!-- Toggle Advanced Button -->
|
<!-- Toggle Advanced Button -->
|
||||||
<button
|
<button
|
||||||
onclick={() => (showAdvanced = !showAdvanced)}
|
onclick={() => (showAdvanced = !showAdvanced)}
|
||||||
@@ -98,7 +122,14 @@ let {
|
|||||||
<p class="mb-2 block text-xs font-bold tracking-widest text-ink-2 uppercase">
|
<p class="mb-2 block text-xs font-bold tracking-widest text-ink-2 uppercase">
|
||||||
{m.docs_filter_label_tags()}
|
{m.docs_filter_label_tags()}
|
||||||
</p>
|
</p>
|
||||||
<TagInput bind:tags={tagNames} allowCreation={false} />
|
<TagInput
|
||||||
|
bind:tags={tagNames}
|
||||||
|
allowCreation={false}
|
||||||
|
onTextInput={(text) => {
|
||||||
|
tagQ = text;
|
||||||
|
onSearch();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sender -->
|
<!-- Sender -->
|
||||||
|
|||||||
46
frontend/src/routes/SearchFilterBar.svelte.spec.ts
Normal file
46
frontend/src/routes/SearchFilterBar.svelte.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { render } from 'vitest-browser-svelte';
|
||||||
|
import { page } from 'vitest/browser';
|
||||||
|
import SearchFilterBar from './SearchFilterBar.svelte';
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
onSearch: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('SearchFilterBar – sort controls', () => {
|
||||||
|
it('renders a sort select when sort and dir are provided', async () => {
|
||||||
|
render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'desc' });
|
||||||
|
const select = page.getByRole('combobox');
|
||||||
|
await expect.element(select).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reflects the active sort value in the select', async () => {
|
||||||
|
render(SearchFilterBar, { ...defaultProps, sort: 'TITLE', dir: 'asc' });
|
||||||
|
const select = page.getByRole('combobox');
|
||||||
|
await expect.element(select).toHaveValue('TITLE');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders direction toggle button', async () => {
|
||||||
|
render(SearchFilterBar, { ...defaultProps, sort: 'DATE', dir: 'asc' });
|
||||||
|
const btn = page.getByRole('button', { name: /sortieren/i });
|
||||||
|
await expect.element(btn).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SearchFilterBar – tagQ live filter', () => {
|
||||||
|
it('calls onSearch when tag text changes in TagInput', async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([]) })
|
||||||
|
);
|
||||||
|
const onSearch = vi.fn();
|
||||||
|
render(SearchFilterBar, { ...defaultProps, onSearch, sort: 'DATE', dir: 'desc' });
|
||||||
|
// TagInput is only visible in advanced panel — open it first
|
||||||
|
const filterBtn = page.getByRole('button', { name: 'Filter', exact: true });
|
||||||
|
await filterBtn.click();
|
||||||
|
const tagTextbox = page.getByPlaceholder('Nach Schlagworten filtern...');
|
||||||
|
await tagTextbox.fill('fam');
|
||||||
|
await expect.poll(() => onSearch.mock.calls.length).toBeGreaterThan(0);
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user