feat(#248): admin tag page complete overhaul — tree panel, merge, subtree delete, new edit components #249
@@ -61,6 +61,11 @@ function handleTextSearch() {
|
|||||||
searchTimer = setTimeout(() => triggerSearch(), 500);
|
searchTimer = setTimeout(() => triggerSearch(), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleImmediateSearch() {
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
triggerSearch();
|
||||||
|
}
|
||||||
|
|
||||||
// Trigger search when tags change
|
// Trigger search when tags change
|
||||||
let prevTagStr = untrack(() => tagNames.map((t) => t.name).join(','));
|
let prevTagStr = untrack(() => tagNames.map((t) => t.name).join(','));
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -114,6 +119,7 @@ const showRightColumn = $derived(data.canWrite || (data.incompleteDocs?.length ?
|
|||||||
initialReceiverName={data.initialValues?.receiverName}
|
initialReceiverName={data.initialValues?.receiverName}
|
||||||
isLoading={navigating.to !== null}
|
isLoading={navigating.to !== null}
|
||||||
onSearch={handleTextSearch}
|
onSearch={handleTextSearch}
|
||||||
|
onSearchImmediate={handleImmediateSearch}
|
||||||
onfocus={() => (qFocused = true)}
|
onfocus={() => (qFocused = true)}
|
||||||
onblur={() => (qFocused = false)}
|
onblur={() => (qFocused = false)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ let {
|
|||||||
initialReceiverName = '',
|
initialReceiverName = '',
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
onSearch,
|
onSearch,
|
||||||
|
onSearchImmediate,
|
||||||
onfocus,
|
onfocus,
|
||||||
onblur
|
onblur
|
||||||
}: {
|
}: {
|
||||||
@@ -40,6 +41,7 @@ let {
|
|||||||
initialReceiverName?: string;
|
initialReceiverName?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
onSearch: () => void;
|
onSearch: () => void;
|
||||||
|
onSearchImmediate?: () => void;
|
||||||
onfocus?: () => void;
|
onfocus?: () => void;
|
||||||
onblur?: () => void;
|
onblur?: () => void;
|
||||||
} = $props();
|
} = $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'}"
|
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={() => {
|
onclick={() => {
|
||||||
tagOperator = 'AND';
|
tagOperator = 'AND';
|
||||||
onSearch();
|
(onSearchImmediate ?? onSearch)();
|
||||||
}}>AND</button
|
}}>AND</button
|
||||||
>
|
>
|
||||||
<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'}"
|
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={() => {
|
onclick={() => {
|
||||||
tagOperator = 'OR';
|
tagOperator = 'OR';
|
||||||
onSearch();
|
(onSearchImmediate ?? onSearch)();
|
||||||
}}>OR</button
|
}}>OR</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -103,6 +103,29 @@ describe('SearchFilterBar – AND/OR tag operator toggle', () => {
|
|||||||
await expect.poll(() => onSearch.mock.calls.length).toBeGreaterThan(0);
|
await expect.poll(() => onSearch.mock.calls.length).toBeGreaterThan(0);
|
||||||
vi.unstubAllGlobals();
|
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', () => {
|
describe('SearchFilterBar – tagQ live filter', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user