From 13eb80da150d161924eb4e3ec216fda1f4a6fb44 Mon Sep 17 00:00:00 2001 From: Antoine Fricker Date: Sat, 13 Aug 2022 01:33:46 +0200 Subject: [PATCH 1/8] Add storeKey property to useListController --- docs/useListController.md | 31 ++++- .../list/useListController.spec.tsx | 3 + .../list/useListController.storeKey.spec.tsx | 47 +++++++ .../useListController.storeKey.stories.tsx | 129 ++++++++++++++++++ .../src/controller/list/useListController.ts | 19 +-- .../src/controller/list/useListParams.ts | 24 ++-- 6 files changed, 232 insertions(+), 21 deletions(-) create mode 100644 packages/ra-core/src/controller/list/useListController.storeKey.spec.tsx create mode 100644 packages/ra-core/src/controller/list/useListController.storeKey.stories.tsx diff --git a/docs/useListController.md b/docs/useListController.md index b9f970162e2..888c30a579b 100644 --- a/docs/useListController.md +++ b/docs/useListController.md @@ -5,9 +5,9 @@ title: "useListController" # `useListController` -The `useListController` hook fetches the data, prepares callbacks for modifying the pagination, filters, sort and selection, and returns them. Its return value match the `ListContext` shape. `useListController` is used internally by the `` and `` components. +The `useListController` hook fetches the data, prepares callbacks for modifying the pagination, filters, sort and selection, and returns them. Its return value match the `ListContext` shape. `useListController` is used internally by the `` and `` components. -You can use it to create a custom List view, although its component counterpart, [``](./ListBase.md), is probably better in most cases. +You can use it to create a custom List view, although its component counterpart, [``](./ListBase.md), is probably better in most cases. ## Usage @@ -43,11 +43,12 @@ const MyList = () => { * [`queryOptions`](./List.md#queryoptions): react-query options for the useQuery call * [`resource`](./List.md#resource): resource name, e.g. 'posts' ; defaults to the current resource context * [`sort`](./List.md#sort-default-sort-field--order), current sort value, e.g. { field: 'published_at', order: 'DESC' } +* [`storeKey`](#storeKey): key used to differenciate the list from another sharing the same resource, in store managed states Here are their default values: ```jsx -import { +import { useListController, defaultExporter, ListContextProvider @@ -64,6 +65,7 @@ const MyList = ({ queryOptions = undefined, resource = '', sort = { field: 'id', order: 'DESC' }, + storeKey = undefined, }) => { const listContext = useListController({ debounce, @@ -76,6 +78,7 @@ const MyList = ({ queryOptions, resource, sort, + storeKey, }); return ( @@ -85,6 +88,28 @@ const MyList = ({ }; ``` +## `storeKey` + +To display multiple lists of the same resource and keep distinct store states for each of them (filters, sorting and pagination), specify unique keys with the `storeKey` property. + +**Note:** Please note that selection state will remain linked to a resource-based key as described [here](https://marmelab.com/react-admin/List.html#disablesyncwithlocation). + +```jsx +// display the top 5 posts + ( +
    { + !params.isLoading && params.data.map(post => ( +
  • {post.title} - {post.votes} votes
  • + )) + }
+ )} +/> +``` + ## Return Value The return value of `useListController` has the following shape: diff --git a/packages/ra-core/src/controller/list/useListController.spec.tsx b/packages/ra-core/src/controller/list/useListController.spec.tsx index c6eef79c0e7..d273e50ce07 100644 --- a/packages/ra-core/src/controller/list/useListController.spec.tsx +++ b/packages/ra-core/src/controller/list/useListController.spec.tsx @@ -235,6 +235,7 @@ describe('useListController', () => { clock.uninstall(); }); }); + describe('showFilter', () => { it('Does not remove previously shown filter when adding a new one', async () => { let currentDisplayedFilters; @@ -409,6 +410,7 @@ describe('useListController', () => { }); }); }); + describe('getListControllerProps', () => { it('should only pick the props injected by the ListController', () => { expect( @@ -449,6 +451,7 @@ describe('useListController', () => { }); }); }); + describe('sanitizeListRestProps', () => { it('should omit the props injected by the ListController', () => { expect( diff --git a/packages/ra-core/src/controller/list/useListController.storeKey.spec.tsx b/packages/ra-core/src/controller/list/useListController.storeKey.spec.tsx new file mode 100644 index 00000000000..704b4297c60 --- /dev/null +++ b/packages/ra-core/src/controller/list/useListController.storeKey.spec.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { + render, + fireEvent, + screen, + waitFor, + act, +} from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import { ListsUsingSameResource } from './useListController.storeKey.stories'; + +describe('useListController', () => { + describe('customStoreKey', () => { + it('should keep distinct two lists of the same resource given different keys', async () => { + render( + + ); + + await waitFor(() => { + expect( + screen.getByLabelText('perPage').getAttribute('data-value') + ).toEqual('3'); + }); + + act(() => { + fireEvent.click(screen.getByLabelText('incrementPerPage')); + }); + + await waitFor(() => { + expect( + screen.getByLabelText('perPage').getAttribute('data-value') + ).toEqual('4'); + }); + + act(() => { + fireEvent.click(screen.getByLabelText('flop')); + }); + expect( + screen.getByLabelText('perPage').getAttribute('data-value') + ).toEqual('3'); + }); + }); +}); diff --git a/packages/ra-core/src/controller/list/useListController.storeKey.stories.tsx b/packages/ra-core/src/controller/list/useListController.storeKey.stories.tsx new file mode 100644 index 00000000000..8e030a8c24c --- /dev/null +++ b/packages/ra-core/src/controller/list/useListController.storeKey.stories.tsx @@ -0,0 +1,129 @@ +import * as React from 'react'; +import { Route } from 'react-router'; +import { Link } from 'react-router-dom'; +import fakeDataProvider from 'ra-data-fakerest'; + +import { CoreAdmin, CustomRoutes, Resource } from '../../core'; +import { localStorageStore } from '../../store'; +import { FakeBrowserDecorator } from '../../storybook/FakeBrowser'; +import { CoreLayoutProps, SortPayload } from '../../types'; +import { ListController } from './ListController'; +import { ListControllerResult } from './useListController'; + +export default { + title: 'ra-core/controller/list/useListController', + decorators: [FakeBrowserDecorator], + parameters: { + initialEntries: ['/top'], + }, +}; + +const styles = { + mainContainer: { + margin: '20px 10px', + }, + + ul: { + marginTop: '20px', + padding: '10px', + }, +}; + +const dataProvider = fakeDataProvider({ + posts: [ + { id: 1, title: 'Post #1', votes: 90 }, + { id: 2, title: 'Post #2', votes: 20 }, + { id: 3, title: 'Post #3', votes: 30 }, + { id: 4, title: 'Post #4', votes: 40 }, + { id: 5, title: 'Post #5', votes: 50 }, + { id: 6, title: 'Post #6', votes: 60 }, + { id: 7, title: 'Post #7', votes: 70 }, + ], +}); + +const listControllerComponent = (storeKey: string, sort?: SortPayload) => { + return ( + ( +
+ + storeKey: {storeKey} + +
+ + perPage: {params.perPage} + +
+
+ {' '} + +
    + {!params.isLoading && + params.data.map(post => ( +
  • + {post.title} - {post.votes} votes +
  • + ))} +
+
+ )} + /> + ); +}; + +const MinimalLayout = (props: CoreLayoutProps) => { + return ( +
+ + Go to Top + {' '} + + Go to Flop + +
+
+ {props.children} +
+ ); +}; +const TopList = () => + listControllerComponent('top', { field: 'votes', order: 'DESC' }); +const FlopList = () => + listControllerComponent('flop', { field: 'votes', order: 'ASC' }); + +export const ListsUsingSameResource = (argsOrProps, context) => { + const history = context?.history || argsOrProps.history; + return ( + + + } /> + + + } /> + + + + ); +}; diff --git a/packages/ra-core/src/controller/list/useListController.ts b/packages/ra-core/src/controller/list/useListController.ts index b70c00c4ca9..7368dc07e93 100644 --- a/packages/ra-core/src/controller/list/useListController.ts +++ b/packages/ra-core/src/controller/list/useListController.ts @@ -33,15 +33,16 @@ export const useListController = ( props: ListControllerProps = {} ): ListControllerResult => { const { + debounce = 500, disableAuthentication, + disableSyncWithLocation, exporter = defaultExporter, + filter, filterDefaultValues, - sort = defaultSort, perPage = 10, - filter, - debounce = 500, - disableSyncWithLocation, queryOptions = {}, + sort = defaultSort, + storeKey, } = props; useAuthenticated({ enabled: !disableAuthentication }); const resource = useResourceContext(props); @@ -62,12 +63,13 @@ export const useListController = ( const notify = useNotify(); const [query, queryModifiers] = useListParams({ - resource, - filterDefaultValues, - sort, - perPage, debounce, disableSyncWithLocation, + filterDefaultValues, + perPage, + resource, + sort, + storeKey, }); const [selectedIds, selectionModifiers] = useRecordSelection(resource); @@ -195,6 +197,7 @@ export interface ListControllerProps { }> & { meta?: any }; resource?: string; sort?: SortPayload; + storeKey?: string; } const defaultSort = { diff --git a/packages/ra-core/src/controller/list/useListParams.ts b/packages/ra-core/src/controller/list/useListParams.ts index 78af6a47c57..e4dcef0a8dd 100644 --- a/packages/ra-core/src/controller/list/useListParams.ts +++ b/packages/ra-core/src/controller/list/useListParams.ts @@ -77,17 +77,19 @@ export interface ListParams { * } = listParamsActions; */ export const useListParams = ({ - resource, - filterDefaultValues, - sort = defaultSort, - perPage = 10, debounce = 500, disableSyncWithLocation = false, + filterDefaultValues, + perPage = 10, + resource, + sort = defaultSort, + storeKey, }: ListParamsOptions): [Parameters, Modifiers] => { + if (!storeKey) storeKey = `${resource}.listParams`; + const location = useLocation(); const navigate = useNavigate(); const [localParams, setLocalParams] = useState(defaultParams); - const storeKey = `${resource}.listParams`; const [params, setParams] = useStore(storeKey, defaultParams); const tempParams = useRef(); const isMounted = useIsMounted(); @@ -95,6 +97,7 @@ export const useListParams = ({ const requestSignature = [ location.search, resource, + storeKey, JSON.stringify(disableSyncWithLocation ? localParams : params), JSON.stringify(filterDefaultValues), JSON.stringify(sort), @@ -367,15 +370,16 @@ export const getNumberOrDefault = ( }; export interface ListParamsOptions { - resource: string; - perPage?: number; - sort?: SortPayload; - // default value for a filter when displayed but not yet set - filterDefaultValues?: FilterPayload; debounce?: number; // Whether to disable the synchronization of the list parameters with // the current location (URL search parameters) disableSyncWithLocation?: boolean; + // default value for a filter when displayed but not yet set + filterDefaultValues?: FilterPayload; + perPage?: number; + resource: string; + sort?: SortPayload; + storeKey?: string; } interface Parameters extends ListParams { From ac766bc5a7599d969298b254f571d78320d02199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=C3=ADbal=20Svarcas?= Date: Thu, 8 Sep 2022 11:20:43 -0300 Subject: [PATCH 2/8] Fix anchor --- docs/useListController.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/useListController.md b/docs/useListController.md index 888c30a579b..c67fbe988c4 100644 --- a/docs/useListController.md +++ b/docs/useListController.md @@ -43,7 +43,7 @@ const MyList = () => { * [`queryOptions`](./List.md#queryoptions): react-query options for the useQuery call * [`resource`](./List.md#resource): resource name, e.g. 'posts' ; defaults to the current resource context * [`sort`](./List.md#sort-default-sort-field--order), current sort value, e.g. { field: 'published_at', order: 'DESC' } -* [`storeKey`](#storeKey): key used to differenciate the list from another sharing the same resource, in store managed states +* [`storeKey`](#storekey): key used to differenciate the list from another sharing the same resource, in store managed states Here are their default values: From 95781eaeb77e2f3118ebaa3076b84dae13e9c088 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser Date: Thu, 8 Sep 2022 18:13:01 +0200 Subject: [PATCH 3/8] code review --- docs/useListController.md | 4 ++- .../useListController.storeKey.stories.tsx | 28 +++++++++++-------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/docs/useListController.md b/docs/useListController.md index c67fbe988c4..a4a399ec5d7 100644 --- a/docs/useListController.md +++ b/docs/useListController.md @@ -92,7 +92,9 @@ const MyList = ({ To display multiple lists of the same resource and keep distinct store states for each of them (filters, sorting and pagination), specify unique keys with the `storeKey` property. -**Note:** Please note that selection state will remain linked to a resource-based key as described [here](https://marmelab.com/react-admin/List.html#disablesyncwithlocation). +In case no `storeKey` is provided, the states will be stored with the following key: `${resource}.listParams`. + +**Note:** Please note that selection state will remain linked to a resource-based key as described [here](./List.md#disablesyncwithlocation). ```jsx // display the top 5 posts diff --git a/packages/ra-core/src/controller/list/useListController.storeKey.stories.tsx b/packages/ra-core/src/controller/list/useListController.storeKey.stories.tsx index 8e030a8c24c..4e977b466dd 100644 --- a/packages/ra-core/src/controller/list/useListController.storeKey.stories.tsx +++ b/packages/ra-core/src/controller/list/useListController.storeKey.stories.tsx @@ -3,7 +3,12 @@ import { Route } from 'react-router'; import { Link } from 'react-router-dom'; import fakeDataProvider from 'ra-data-fakerest'; -import { CoreAdmin, CustomRoutes, Resource } from '../../core'; +import { + CoreAdminContext, + CoreAdminUI, + CustomRoutes, + Resource, +} from '../../core'; import { localStorageStore } from '../../store'; import { FakeBrowserDecorator } from '../../storybook/FakeBrowser'; import { CoreLayoutProps, SortPayload } from '../../types'; @@ -111,19 +116,20 @@ const FlopList = () => export const ListsUsingSameResource = (argsOrProps, context) => { const history = context?.history || argsOrProps.history; return ( - - - } /> - - - } /> - - - + + + } /> + + + } /> + + + + ); }; From 9695b3a515bc6f2e202409302b62a8a9a8de7218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=C3=ADbal=20Svarcas?= Date: Fri, 9 Sep 2022 08:19:45 -0300 Subject: [PATCH 4/8] Fix type --- .../src/controller/list/useListController.storeKey.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-core/src/controller/list/useListController.storeKey.stories.tsx b/packages/ra-core/src/controller/list/useListController.storeKey.stories.tsx index 4e977b466dd..a028b3a9a80 100644 --- a/packages/ra-core/src/controller/list/useListController.storeKey.stories.tsx +++ b/packages/ra-core/src/controller/list/useListController.storeKey.stories.tsx @@ -67,7 +67,7 @@ const listControllerComponent = (storeKey: string, sort?: SortPayload) => {
{' '} - -
    - {!params.isLoading && - params.data.map(post => ( -
  • - {post.title} - {post.votes} votes -
  • - ))} -
- - )} - /> +
+ + storeKey: {storeKey} + +
+ + perPage: {params.perPage} + +
+
+ {' '} + +
    + {!params.isLoading && + params.data.map(post => ( +
  • + {post.title} - {post.votes} votes +
  • + ))} +
+
); }; @@ -108,10 +111,12 @@ const MinimalLayout = (props: CoreLayoutProps) => { ); }; -const TopList = () => - listControllerComponent('top', { field: 'votes', order: 'DESC' }); -const FlopList = () => - listControllerComponent('flop', { field: 'votes', order: 'ASC' }); +const TopPosts = ( + +); +const FlopPosts = ( + +); export const ListsUsingSameResource = (argsOrProps, context) => { const history = context?.history || argsOrProps.history; @@ -123,10 +128,10 @@ export const ListsUsingSameResource = (argsOrProps, context) => { > - } /> + - } /> + diff --git a/packages/ra-ui-materialui/src/list/List.stories.tsx b/packages/ra-ui-materialui/src/list/List.stories.tsx index 3e67d1a4e4c..19020eeb420 100644 --- a/packages/ra-ui-materialui/src/list/List.stories.tsx +++ b/packages/ra-ui-materialui/src/list/List.stories.tsx @@ -1,14 +1,16 @@ import * as React from 'react'; import { Admin, AutocompleteInput } from 'react-admin'; -import { Resource, useListContext } from 'ra-core'; +import { CustomRoutes, Resource, useListContext } from 'ra-core'; import fakeRestDataProvider from 'ra-data-fakerest'; import { createMemoryHistory } from 'history'; -import { Box, Card, Stack, Typography } from '@mui/material'; +import { Box, Card, Stack, Typography, Button } from '@mui/material'; import { List } from './List'; import { Datagrid } from './datagrid'; import { TextField } from '../field'; import { SearchInput, TextInput } from '../input'; +import { Route } from 'react-router'; +import { Link } from 'react-router-dom'; export default { title: 'ra-ui-materialui/list/List' }; @@ -314,3 +316,73 @@ export const Default = () => ( ); + +const NewerBooks = () => ( + + + + + + + + +); + +const OlderBooks = () => ( + + + + + + + + +); + +const StoreKeyDashboard = () => ( + <> + + + + + +); + +export const StoreKey = () => { + history.push('/'); + return ( + + + } /> + } /> + + + + ); +}; From 54469edc16d003633c23a828e1dfc8dd466108d4 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser Date: Mon, 19 Sep 2022 15:07:50 +0200 Subject: [PATCH 8/8] code review --- packages/ra-core/src/controller/list/useListParams.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ra-core/src/controller/list/useListParams.ts b/packages/ra-core/src/controller/list/useListParams.ts index e4dcef0a8dd..674de5a45fa 100644 --- a/packages/ra-core/src/controller/list/useListParams.ts +++ b/packages/ra-core/src/controller/list/useListParams.ts @@ -83,10 +83,8 @@ export const useListParams = ({ perPage = 10, resource, sort = defaultSort, - storeKey, + storeKey = `${resource}.listParams`, }: ListParamsOptions): [Parameters, Modifiers] => { - if (!storeKey) storeKey = `${resource}.listParams`; - const location = useLocation(); const navigate = useNavigate(); const [localParams, setLocalParams] = useState(defaultParams);