From edc103561fb486f15ad30a3f4ab55a11174a8485 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:56:08 +0100 Subject: [PATCH 1/4] Fix `useReferenceManyFieldController` does not debounce `setFilters` --- .../useReferenceManyFieldController.spec.tsx | 53 +++++++++++++++++-- .../field/useReferenceManyFieldController.ts | 24 +++++++-- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx b/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx index 3c12d36ab0e..e7719305864 100644 --- a/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx +++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import expect from 'expect'; import { testDataProvider } from '../../dataProvider/testDataProvider'; @@ -19,7 +19,7 @@ const ReferenceManyFieldController = props => { describe('useReferenceManyFieldController', () => { it('should set isLoading to true when related records are not yet fetched', async () => { const ComponentToTest = ({ isLoading }: { isLoading?: boolean }) => { - return
isLoading: {isLoading.toString()}
; + return
isLoading: {isLoading?.toString()}
; }; const dataProvider = testDataProvider({ getManyReference: () => Promise.resolve({ data: [], total: 0 }), @@ -47,7 +47,7 @@ describe('useReferenceManyFieldController', () => { it('should set isLoading to false when related records have been fetched and there are results', async () => { const ComponentToTest = ({ isLoading }: { isLoading?: boolean }) => { - return
isLoading: {isLoading.toString()}
; + return
isLoading: {isLoading?.toString()}
; }; const dataProvider = testDataProvider({ getManyReference: () => @@ -273,4 +273,51 @@ describe('useReferenceManyFieldController', () => { ); }); }); + + it('should take only last change in case of a burst of setFilters calls (case of inputs being currently edited)', async () => { + let childFunction = ({ setFilters, filterValues }) => ( + // TODO: we shouldn't import mui components in ra-core + { + setFilters({ q: event.target.value }); + }} + /> + ); + const dataProvider = testDataProvider(); + const getManyReference = jest.spyOn(dataProvider, 'getManyReference'); + render( + + + {childFunction} + + + ); + const searchInput = screen.getByLabelText('search'); + + fireEvent.change(searchInput, { target: { value: 'hel' } }); + fireEvent.change(searchInput, { target: { value: 'hell' } }); + fireEvent.change(searchInput, { target: { value: 'hello' } }); + + await waitFor(() => new Promise(resolve => setTimeout(resolve, 600))); + + // Called twice: on load and on filter changes + expect(getManyReference).toHaveBeenCalledTimes(2); + expect(getManyReference).toHaveBeenCalledWith('books', { + target: 'author_id', + id: 123, + filter: { q: 'hello' }, + pagination: { page: 1, perPage: 25 }, + sort: { field: 'id', order: 'DESC' }, + meta: undefined, + }); + }); }); diff --git a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts index 9c16fc56980..ad2978dec95 100644 --- a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts +++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef } from 'react'; import get from 'lodash/get'; import isEqual from 'lodash/isEqual'; +import lodashDebounce from 'lodash/debounce'; import { useSafeSetState, removeEmpty } from '../../util'; import { useGetManyReference } from '../../dataProvider'; @@ -15,6 +16,7 @@ import { useResourceContext } from '../../core'; export interface UseReferenceManyFieldControllerParams< RecordType extends RaRecord = RaRecord > { + debounce?: number; filter?: any; page?: number; perPage?: number; @@ -61,6 +63,7 @@ export const useReferenceManyFieldController = < props: UseReferenceManyFieldControllerParams ): ListControllerResult => { const { + debounce = 500, reference, record, target, @@ -128,14 +131,29 @@ export const useReferenceManyFieldController = < }, [setDisplayedFilters, setFilterValues] ); - const setFilters = useCallback( - (filters, displayedFilters) => { + + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedSetFilters = useCallback( + lodashDebounce((filters, displayedFilters) => { setFilterValues(removeEmpty(filters)); setDisplayedFilters(displayedFilters); setPage(1); - }, + }, debounce), [setDisplayedFilters, setFilterValues, setPage] ); + + const setFilters = useCallback( + (filters, displayedFilters, debounce = true) => { + if (debounce) { + debouncedSetFilters(filters, displayedFilters); + } else { + setFilterValues(removeEmpty(filters)); + setDisplayedFilters(displayedFilters); + setPage(1); + } + }, + [setDisplayedFilters, setFilterValues, setPage, debouncedSetFilters] + ); // handle filter prop change useEffect(() => { if (!isEqual(filter, filterRef.current)) { From e23f7b9a669feb465fd4f235b33ca6548645f69d Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 13 Dec 2023 16:28:44 +0100 Subject: [PATCH 2/4] Remove unnecessary comment --- .../controller/field/useReferenceManyFieldController.spec.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx b/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx index e7719305864..dbac5cff76f 100644 --- a/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx +++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx @@ -276,7 +276,6 @@ describe('useReferenceManyFieldController', () => { it('should take only last change in case of a burst of setFilters calls (case of inputs being currently edited)', async () => { let childFunction = ({ setFilters, filterValues }) => ( - // TODO: we shouldn't import mui components in ra-core Date: Thu, 14 Dec 2023 10:32:59 +0100 Subject: [PATCH 3/4] Update ReferenceManyField --- docs/ReferenceManyField.md | 16 ++++++++++++++++ .../src/field/ReferenceManyField.tsx | 3 +++ 2 files changed, 19 insertions(+) diff --git a/docs/ReferenceManyField.md b/docs/ReferenceManyField.md index c95bb1d465a..89604ac9545 100644 --- a/docs/ReferenceManyField.md +++ b/docs/ReferenceManyField.md @@ -111,6 +111,7 @@ This example leverages [``](./SingleFieldList.md) to display an | `pagination` | Optional | `Element` | - | Pagination element to display pagination controls. empty by default (no pagination) | | `perPage` | Optional | `number` | 25 | Maximum number of referenced records to fetch | | `sort` | Optional | `{ field, order }` | `{ field: 'id', order: 'DESC' }` | Sort order to use when fetching the related records, passed to `getManyReference()` | +| `debounce` | Optional | `number` | 500 | debounce time in ms for the `setFilters` callbacks | `` also accepts the [common field props](./Fields.md#common-field-props), except `emptyText` (use the child `empty` prop instead). @@ -172,6 +173,21 @@ export const AuthorShow = () => ( ); ``` +## `debounce` + +By default, `` does not refresh the data as soon as the user enters data in the filter form. Instead, it waits for half a second of user inactivity (via `lodash.debounce`) before calling the `dataProvider` on filter change. This is to prevent repeated (and useless) calls to the API. + +You can customize the debounce duration in milliseconds - or disable it completely - by passing a `debounce` prop to the `` component: + +```jsx +// wait 1 seconds instead of 500 milliseconds befoce calling the dataProvider +const PostCommentsField = () => ( + + ... + +); +``` + ## `filter` You can filter the query used to populate the possible values. Use the `filter` prop for that. diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx index a02ce9336dd..8f5291e3a15 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx @@ -67,6 +67,7 @@ export const ReferenceManyField = < ) => { const { children, + debounce, filter = defaultFilter, page = 1, pagination = null, @@ -83,6 +84,7 @@ export const ReferenceManyField = < RecordType, ReferenceRecordType >({ + debounce, filter, page, perPage, @@ -108,6 +110,7 @@ export interface ReferenceManyFieldProps< RecordType extends Record = Record > extends FieldProps { children: ReactNode; + debounce?: number; filter?: FilterPayload; page?: number; pagination?: ReactElement; From 979f3dcba064b9da8433de73233c6ad514c9dba8 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser Date: Thu, 14 Dec 2023 11:07:36 +0100 Subject: [PATCH 4/4] [no ci] Update docs/ReferenceManyField.md --- docs/ReferenceManyField.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ReferenceManyField.md b/docs/ReferenceManyField.md index 89604ac9545..124b38b09cf 100644 --- a/docs/ReferenceManyField.md +++ b/docs/ReferenceManyField.md @@ -180,7 +180,7 @@ By default, `` does not refresh the data as soon as the user You can customize the debounce duration in milliseconds - or disable it completely - by passing a `debounce` prop to the `` component: ```jsx -// wait 1 seconds instead of 500 milliseconds befoce calling the dataProvider +// wait 1 seconds instead of 500 milliseconds before calling the dataProvider const PostCommentsField = () => ( ...