From d4c1b1342e68c61e9728a100e24d114edd7722ed Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Sat, 16 May 2026 15:15:11 +0700 Subject: [PATCH] fix(frontend): refilter DataTable when search columns change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first implementation routed the active-column set through TanStack's `getColumnCanGlobalFilter` predicate and tried to nudge the pipeline with `table.setGlobalFilter((current) => current)`. TanStack treats that as a no-op because the resolved value equals the current state, so the filter pipeline never re-ran after the user narrowed the picker — the table kept matching against the previous column set. Bake the active set into per-column `enableGlobalFilter` via a memoised `tanstackColumns` instead. TanStack sees a new columns reference whenever the user's selection changes and refilters automatically, which is the standard mechanism for this kind of dynamic filter and verified end-to-end in the browser on the Flows list. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/components/ui/data-table.tsx | 45 +++++++++-------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/ui/data-table.tsx b/frontend/src/components/ui/data-table.tsx index 22fc7f57..c3e1beee 100644 --- a/frontend/src/components/ui/data-table.tsx +++ b/frontend/src/components/ui/data-table.tsx @@ -396,24 +396,29 @@ function DataTable({ // Active column set for the global filter: when the user has narrowed via // the picker, honour the explicit list; otherwise fall back to all - // candidates ("empty selection = search everywhere"). Recomputed on each - // render — cheap because it's just a couple of arrays — and consumed by - // the closure below. - const getColumnCanGlobalFilter = useCallback( - (column: Column) => { - const active = searchColumns.length > 0 ? searchColumns : searchCandidateIds; + // candidates ("empty selection = search everywhere"). Bake the predicate + // into per-column `enableGlobalFilter` so TanStack re-evaluates the row + // model whenever the set changes — a `getColumnCanGlobalFilter` closure + // alone would update silently because `state.globalFilter` stays equal. + const tanstackColumns = useMemo[]>(() => { + const active = searchColumns.length > 0 ? searchColumns : searchCandidateIds; - return active.includes(column.id); - }, - [searchCandidateIds, searchColumns], - ); + return columns.map((column) => { + const withId = column as { id?: string }; + const withAccessor = column as { accessorKey?: string }; + const columnId = + withId.id ?? (typeof withAccessor.accessorKey === 'string' ? withAccessor.accessorKey : undefined); + const enableGlobalFilter = typeof columnId === 'string' && active.includes(columnId); + + return { ...column, enableGlobalFilter }; + }); + }, [columns, searchCandidateIds, searchColumns]); const table = useReactTable({ autoResetPageIndex: false, - columns, + columns: tanstackColumns, data, enableSortingRemoval: true, - getColumnCanGlobalFilter, getCoreRowModel: getCoreRowModel(), getExpandedRowModel: getExpandedRowModel(), getFilteredRowModel: getFilteredRowModel(), @@ -436,22 +441,6 @@ function DataTable({ }, }); - // TanStack doesn't re-run the filter pipeline when only the - // `getColumnCanGlobalFilter` predicate's closure changes — it watches - // `state.globalFilter`. Re-set the same value through TanStack's API on - // every `searchColumns` change so the pipeline picks up the new predicate. - // Skip the very first render to avoid a redundant cycle on mount. - const isFirstRefilterRender = useRef(true); - useEffect(() => { - if (isFirstRefilterRender.current) { - isFirstRefilterRender.current = false; - - return; - } - - table.setGlobalFilter((current: string) => current); - }, [searchColumns, table]); - const handleRowClick = useCallback( (row: Row) => { if (onRowClick) {