diff --git a/.changeset/quiet-otters-listen.md b/.changeset/quiet-otters-listen.md new file mode 100644 index 0000000000000..8f9cba0f451e6 --- /dev/null +++ b/.changeset/quiet-otters-listen.md @@ -0,0 +1,26 @@ +--- +"@refinedev/mui": patch +--- + +feat: add `filters.syncFilterModel` option to `useDataGrid` + +`useDataGrid` controls `dataGridProps.filterModel` from an internal state that +was only updated by the DataGrid's own filter panel and by `search()`. As a +result, calls to the returned `setFilters` (or filter changes synced from the +URL via `syncWithLocation`) updated `filters` but left `filterModel` stale, so +external filter UIs could not drive the grid's visible filter state. + +Add an opt-in `filters.syncFilterModel` flag. When `true`, the hook reflects +`filters` changes in `dataGridProps.filterModel`. The default is `false` to +preserve compatibility with apps that mix column-header filtering and +external filter inputs — see #5860 for the original reasoning behind the +controlled-but-unsynced behavior. + +```tsx +const { dataGridProps } = useDataGrid({ + resource: "posts", + filters: { + syncFilterModel: true, + }, +}); +``` diff --git a/packages/mui/src/hooks/useDataGrid/index.spec.ts b/packages/mui/src/hooks/useDataGrid/index.spec.ts index e3eafa5f2831f..b9f99de1f255e 100644 --- a/packages/mui/src/hooks/useDataGrid/index.spec.ts +++ b/packages/mui/src/hooks/useDataGrid/index.spec.ts @@ -308,6 +308,78 @@ describe("useDataGrid Hook", () => { }); }); + it("when filters.syncFilterModel is true, external setFilters updates dataGridProps.filterModel", async () => { + const { result } = renderHook( + () => + useDataGrid({ + resource: "posts", + filters: { + syncFilterModel: true, + }, + }), + { + wrapper: TestWrapper({}), + }, + ); + + await waitFor(() => { + expect(!result.current.tableQuery?.isLoading).toBeTruthy(); + }); + + await act(async () => { + result.current.setFilters([ + { + field: "title", + operator: "contains", + value: "test", + }, + ]); + }); + + await waitFor(() => { + expect(result.current.dataGridProps.filterModel?.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + field: "title", + value: "test", + }), + ]), + ); + }); + }); + + it("by default, external setFilters does not update dataGridProps.filterModel", async () => { + const { result } = renderHook( + () => + useDataGrid({ + resource: "posts", + }), + { + wrapper: TestWrapper({}), + }, + ); + + await waitFor(() => { + expect(!result.current.tableQuery?.isLoading).toBeTruthy(); + }); + + const initialItems = result.current.dataGridProps.filterModel?.items; + + await act(async () => { + result.current.setFilters([ + { + field: "title", + operator: "contains", + value: "test", + }, + ]); + }); + + expect(result.current.dataGridProps.filterModel?.items).toEqual( + initialItems, + ); + }); + it("should not change sortModel when page changes", async () => { const { result } = renderHook( () => diff --git a/packages/mui/src/hooks/useDataGrid/index.ts b/packages/mui/src/hooks/useDataGrid/index.ts index 2a89c85468bee..5b0c06081d57d 100644 --- a/packages/mui/src/hooks/useDataGrid/index.ts +++ b/packages/mui/src/hooks/useDataGrid/index.ts @@ -90,6 +90,16 @@ export type UseDataGridProps< * @default "replace" */ defaultBehavior?: "replace" | "merge"; + /** + * When `true`, changes to `filters` made outside the DataGrid (via the + * returned `setFilters`, `search`, or `syncWithLocation`) are reflected + * in `dataGridProps.filterModel`. Defaults to `false` to preserve + * compatibility with apps that mix DataGrid column-header filtering and + * external filter inputs, where forcing a sync would clobber in-progress + * header edits. + * @default false + */ + syncFilterModel?: boolean; } >; editable?: boolean; @@ -200,6 +210,13 @@ export function useDataGrid< const [muiCrudFilters, setMuiCrudFilters] = useState(filters); + const syncFilterModel = filtersFromProp?.syncFilterModel ?? false; + + useEffect(() => { + if (!syncFilterModel) return; + setMuiCrudFilters((prev) => (isEqual(prev, filters) ? prev : filters)); + }, [filters, syncFilterModel]); + const { data, isFetched, isLoading } = tableQuery; const rowCountRef = useRef(data?.total || 0);