fix(#221): bypass debounce on AND/OR operator toggle to prevent race condition
The tag-change $effect called triggerSearch() immediately (no debounce). When the user toggled AND/OR within the 500 ms debounce window, the prior navigation would complete and reset tagOperator back to AND before the debounced search fired. The toggle now calls onSearchImmediate, which clears any pending timer and fires triggerSearch() synchronously. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user