feat(#248): admin tag page complete overhaul — tree panel, merge, subtree delete, new edit components #249

Merged
marcel merged 51 commits from feat/issue-221-tag-hierarchy into main 2026-04-17 10:24:10 +02:00
3 changed files with 33 additions and 2 deletions
Showing only changes of commit 532692e0fb - Show all commits

View File

@@ -61,6 +61,11 @@ function handleTextSearch() {
searchTimer = setTimeout(() => triggerSearch(), 500);
}
function handleImmediateSearch() {
clearTimeout(searchTimer);
triggerSearch();
}
// Trigger search when tags change
let prevTagStr = untrack(() => tagNames.map((t) => t.name).join(','));
$effect(() => {
@@ -114,6 +119,7 @@ const showRightColumn = $derived(data.canWrite || (data.incompleteDocs?.length ?
initialReceiverName={data.initialValues?.receiverName}
isLoading={navigating.to !== null}
onSearch={handleTextSearch}
onSearchImmediate={handleImmediateSearch}
onfocus={() => (qFocused = true)}
onblur={() => (qFocused = false)}
/>

View File

@@ -22,6 +22,7 @@ let {
initialReceiverName = '',
isLoading = false,
onSearch,
onSearchImmediate,
onfocus,
onblur
}: {
@@ -40,6 +41,7 @@ let {
initialReceiverName?: string;
isLoading?: boolean;
onSearch: () => void;
onSearchImmediate?: () => void;
onfocus?: () => void;
onblur?: () => void;
} = $props();
@@ -162,7 +164,7 @@ $effect(() => {
class="rounded px-2 py-0.5 text-xs font-bold tracking-widest uppercase transition-colors {tagOperator === 'AND' ? 'bg-primary text-primary-fg' : 'bg-muted text-ink-2 hover:bg-line'}"
onclick={() => {
tagOperator = 'AND';
onSearch();
(onSearchImmediate ?? onSearch)();
}}>AND</button
>
<button
@@ -170,7 +172,7 @@ $effect(() => {
class="rounded px-2 py-0.5 text-xs font-bold tracking-widest uppercase transition-colors {tagOperator === 'OR' ? 'bg-primary text-primary-fg' : 'bg-muted text-ink-2 hover:bg-line'}"
onclick={() => {
tagOperator = 'OR';
onSearch();
(onSearchImmediate ?? onSearch)();
}}>OR</button
>
</div>

View File

@@ -103,6 +103,29 @@ describe('SearchFilterBar AND/OR tag operator toggle', () => {
await expect.poll(() => onSearch.mock.calls.length).toBeGreaterThan(0);
vi.unstubAllGlobals();
});
it('calls onSearchImmediate instead of onSearch when operator is toggled and onSearchImmediate is provided', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([]) })
);
const onSearch = vi.fn();
const onSearchImmediate = vi.fn();
render(SearchFilterBar, {
...defaultProps,
onSearch,
onSearchImmediate,
sort: 'DATE',
dir: 'desc',
tagNames: [{ name: 'Tag1' }, { name: 'Tag2' }]
});
await openAdvanced();
const toggle = page.getByTestId('and-or-toggle');
await toggle.getByRole('button', { name: 'OR' }).click();
await expect.poll(() => onSearchImmediate.mock.calls.length).toBeGreaterThan(0);
expect(onSearch).not.toHaveBeenCalled();
vi.unstubAllGlobals();
});
});
describe('SearchFilterBar tagQ live filter', () => {