From 2898fdf5d3a53eeb27de4910bffd5e44761397f5 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Thu, 2 Jul 2026 21:37:32 +0000 Subject: [PATCH] Write the URL immediately when a search filter is cleared The search box clear (X) button cleared the filter live but only wrote the URL when the dropdown closed. commitFilter now accepts an explicit value so the clear handlers can commit right away (avoiding the stale-state race of reading the live override in the same event), for both useColumnSearch and the Users filter. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../web_ui/src/components/UsersFilters.tsx | 13 ++++++++----- packages/web_ui/src/util/useColumnSearch.tsx | 2 ++ .../web_ui/src/util/useTableQueryState.ts | 19 ++++++++++++------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/web_ui/src/components/UsersFilters.tsx b/packages/web_ui/src/components/UsersFilters.tsx index ca54014c..3aeae466 100644 --- a/packages/web_ui/src/components/UsersFilters.tsx +++ b/packages/web_ui/src/components/UsersFilters.tsx @@ -1,5 +1,5 @@ import React, { useRef } from "react"; -import { InputRef, Input, Space, Segmented, Button, Typography, Tag } from "antd"; +import { InputRef, Input, Space, Segmented, Typography, Tag } from "antd"; import type { FilterDropdownProps } from "antd/es/table/interface"; import { CheckOutlined, CloseOutlined, MinusOutlined } from "@ant-design/icons"; import { UserDetails } from "@clusterio/lib"; @@ -135,13 +135,16 @@ export function useUserFilter( ? decodeFilter(String(selectedKeys[0])) : {}; - // Update the live filter as the user types/toggles; the URL is only written - // when the dropdown closes (see filterDropdownProps.onOpenChange below). - function update(next: Partial) { + // Update the live filter as the user types/toggles; the URL is only written when the + // dropdown closes (see onOpenChange), or immediately when `commit` is set (e.g. clear). + function update(next: Partial, commit = false) { const encoded = encodeFilter({ ...filter, ...next }); const values = encoded === "{}" ? null : [encoded]; setSelectedKeys(values ?? []); tableState.setFilter(columnKey, values); + if (commit) { + tableState.commitFilter(columnKey, values); + } } return ( @@ -154,7 +157,7 @@ export function useUserFilter( allowClear onSearch={() => close()} onChange={(e) => update({ search: e.target.value !== "" ? e.target.value : undefined })} - onClear={() => update({ search: 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 4fe3451d..37c4833c 100644 --- a/packages/web_ui/src/util/useColumnSearch.tsx +++ b/packages/web_ui/src/util/useColumnSearch.tsx @@ -47,6 +47,8 @@ export default function useColumnSearch( onClear={() => { setSelectedKeys([]); tableState.setFilter(columnKey, null); + // Clearing is a deliberate action, so write it to the URL right away. + tableState.commitFilter(columnKey, null); }} /> diff --git a/packages/web_ui/src/util/useTableQueryState.ts b/packages/web_ui/src/util/useTableQueryState.ts index b7e43e3e..8ee1c1a5 100644 --- a/packages/web_ui/src/util/useTableQueryState.ts +++ b/packages/web_ui/src/util/useTableQueryState.ts @@ -35,8 +35,8 @@ export interface TableQueryState { filteredValue: (columnKey: string) => string[] | null; /** Set a column's live filter — filters the table immediately without writing the URL. */ setFilter: (columnKey: string, values: string[] | null) => void; - /** Commit a column's live filter to the URL; call when its filter dropdown closes. */ - commitFilter: (columnKey: string) => void; + /** Commit a column's filter to the URL: pass `values` to write now (e.g. clear), omit to commit the override. */ + commitFilter: (columnKey: string, values?: string[] | null) => void; /** Controlled pagination ({@link current}/{@link pageSize}), or `false` when disabled. */ pagination: false | TablePaginationConfig; } @@ -114,17 +114,22 @@ export default function useTableQueryState(options: TableQueryStateOptions): setLiveFilters(prev => ({ ...prev, [columnKey]: values })); }, - commitFilter(columnKey) { - // Only write the URL when a live edit is pending for this column. - if (!(columnKey in liveFilters)) { + 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) { return; } const next = new URLSearchParams(params); const codec = codecs[columnKey]; if (codec) { - codec.encode(next, liveFilters[columnKey], prefix); + codec.encode(next, effective, prefix); } else { - applyFilters(next, prefix, { [columnKey]: liveFilters[columnKey] }); + applyFilters(next, prefix, { [columnKey]: effective }); } // Avoid pushing a duplicate history entry when nothing actually changed. if (next.toString() !== paramsString) {