From 40407e52d609e52fccbda814334231215d8e3b84 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Thu, 2 Jul 2026 22:02:25 +0000 Subject: [PATCH] Fix clearing a live-but-uncommitted search re-committing the old value commitFilter now reads the live overrides from a ref that setFilter updates synchronously, so committing within the same event that changed a filter no longer sees a stale render closure. antd's Input.Search fires onSearch (before onChange) when the clear button is clicked, so onSearch now commits the cleared state instead of closing the dropdown, which had re-committed the still-live typed value. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/web_ui/src/components/UsersFilters.tsx | 10 ++++++++-- packages/web_ui/src/util/useColumnSearch.tsx | 17 +++++++++++------ packages/web_ui/src/util/useTableQueryState.ts | 14 ++++++++++---- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/packages/web_ui/src/components/UsersFilters.tsx b/packages/web_ui/src/components/UsersFilters.tsx index 3aeae466..4a8bac72 100644 --- a/packages/web_ui/src/components/UsersFilters.tsx +++ b/packages/web_ui/src/components/UsersFilters.tsx @@ -155,9 +155,15 @@ export function useUserFilter( placeholder="Search username" value={filter.search} allowClear - onSearch={() => close()} + onSearch={value => { + if (value) { + close(); + } else { + // Clear (X) fires onSearch with an empty value before onChange. + update({ search: undefined }, true); + } + }} onChange={(e) => update({ search: e.target.value !== "" ? e.target.value : undefined })} - onClear={() => update({ search: undefined }, true)} /> {withStatus &&<> diff --git a/packages/web_ui/src/util/useColumnSearch.tsx b/packages/web_ui/src/util/useColumnSearch.tsx index 37c4833c..c5fed6b9 100644 --- a/packages/web_ui/src/util/useColumnSearch.tsx +++ b/packages/web_ui/src/util/useColumnSearch.tsx @@ -43,12 +43,17 @@ export default function useColumnSearch( setSelectedKeys(values); tableState.setFilter(columnKey, values.length ? values : null); }} - onSearch={() => close()} - onClear={() => { - setSelectedKeys([]); - tableState.setFilter(columnKey, null); - // Clearing is a deliberate action, so write it to the URL right away. - tableState.commitFilter(columnKey, null); + onSearch={value => { + if (value) { + close(); + } else { + // antd fires onSearch with an empty value when the clear (X) is clicked + // (before onChange); commit the cleared state instead of closing, which + // would re-commit the still-live typed value. + setSelectedKeys([]); + tableState.setFilter(columnKey, null); + tableState.commitFilter(columnKey, null); + } }} /> diff --git a/packages/web_ui/src/util/useTableQueryState.ts b/packages/web_ui/src/util/useTableQueryState.ts index 8ee1c1a5..068f33fd 100644 --- a/packages/web_ui/src/util/useTableQueryState.ts +++ b/packages/web_ui/src/util/useTableQueryState.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useSearchParams } from "react-router-dom"; import type { TablePaginationConfig } from "antd"; import type { FilterValue, SorterResult } from "antd/es/table/interface"; @@ -61,8 +61,12 @@ export default function useTableQueryState(options: TableQueryStateOptions): // URL on every keystroke; they are committed to the URL when the filter dropdown // closes (see commitFilter) and cleared whenever the URL changes (commit, back/forward). const [liveFilters, setLiveFilters] = useState>({}); + // A ref mirrors the overrides so commitFilter always reads the latest values even within + // the same event that set them (e.g. clearing then closing), instead of a stale closure. + const liveFiltersRef = useRef>(liveFilters); const paramsString = params.toString(); useEffect(() => { + liveFiltersRef.current = {}; setLiveFilters({}); }, [paramsString]); @@ -111,15 +115,17 @@ export default function useTableQueryState(options: TableQueryStateOptions): }, setFilter(columnKey, values) { - setLiveFilters(prev => ({ ...prev, [columnKey]: values })); + const nextLive = { ...liveFiltersRef.current, [columnKey]: values }; + liveFiltersRef.current = nextLive; + setLiveFilters(nextLive); }, commitFilter(columnKey, values) { // Explicit values (e.g. from the clear button) are written immediately; otherwise // commit the current live override, if any (e.g. when the dropdown closes). let effective = values; - if (effective === undefined && columnKey in liveFilters) { - effective = liveFilters[columnKey]; + if (effective === undefined && columnKey in liveFiltersRef.current) { + effective = liveFiltersRef.current[columnKey]; } if (effective === undefined) { return;