Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to create independent store configurations for different lists of same resource #8073

Merged
merged 10 commits into from
Sep 19, 2022
66 changes: 66 additions & 0 deletions docs/List.md
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,72 @@ export const PostList = () => (

For more details on list sort, see the [Sorting The List](./ListTutorial.md#sorting-the-list) section below.

## `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.

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).

In the example below, both lists `NewerBooks` and `OlderBooks` use the same resource ('books'), but their controller states are stored separately (under the store keys `'newerBooks'` and `'olderBooks'` respectively). This allows to use both components in the same app, each having its own state (filters, sorting and pagination).

```jsx
import {
Admin,
CustomRoutes,
Resource,
List,
Datagrid,
TextField,
} from 'react-admin';
import { Route } from 'react-router';

const NewerBooks = () => (
<List
resource="books"
storeKey="newerBooks"
sort={{ field: 'year', order: 'DESC' }}
>
<Datagrid>
<TextField source="id" />
<TextField source="title" />
<TextField source="author" />
<TextField source="year" />
</Datagrid>
</List>
);

const OlderBooks = () => (
<List
resource="books"
storeKey="olderBooks"
sort={{ field: 'year', order: 'ASC' }}
>
<Datagrid>
<TextField source="id" />
<TextField source="title" />
<TextField source="author" />
<TextField source="year" />
</Datagrid>
</List>
);

const Admin = () => {
return (
<Admin dataProvider={dataProvider}>
<CustomRoutes>
<Route path="/newerBooks" element={<NewerBooks />} />
<Route path="/olderBooks" element={<OlderBooks />} />
</CustomRoutes>
<Resource name="books" />
</Admin>
);
};
```

**Tip:** The `storeKey` is actually passed to the underlying `useListController` hook, which you can use directly for more complex scenarios. See the [`useListController` doc](./useListController.md#storekey) for more info.

## `title`

The default title for a list view is "[resource] list" (e.g. "Posts list"). Use the `title` prop to customize the List view title:
Expand Down
53 changes: 50 additions & 3 deletions docs/useListController.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<List>` and `<ListBase>` 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 `<List>` and `<ListBase>` components.

You can use it to create a custom List view, although its component counterpart, [`<ListBase>`](./ListBase.md), is probably better in most cases.
You can use it to create a custom List view, although its component counterpart, [`<ListBase>`](./ListBase.md), is probably better in most cases.

## Usage

Expand Down Expand Up @@ -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
Expand All @@ -64,6 +65,7 @@ const MyList = ({
queryOptions = undefined,
resource = '',
sort = { field: 'id', order: 'DESC' },
storeKey = undefined,
}) => {
const listContext = useListController({
debounce,
Expand All @@ -76,6 +78,7 @@ const MyList = ({
queryOptions,
resource,
sort,
storeKey,
});
return (
<ListContextProvider value={listContext}>
Expand All @@ -85,6 +88,50 @@ 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.
slax57 marked this conversation as resolved.
Show resolved Hide resolved

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).

In the example below, both lists `TopPosts` and `FlopPosts` use the same resource ('posts'), but their controller states are stored separately (under the store keys `'top'` and `'flop'` respectively).

```jsx
import { useListController } from 'react-admin';

const OrderedPostList = ({
storeKey,
sort,
}) => {
const params = useListController({
resource: 'posts',
sort,
storeKey,
});
return (
<div>
<ul style={styles.ul}>
{!params.isLoading &&
params.data.map(post => (
<li key={`post_${post.id}`}>
{post.title} - {post.votes} votes
</li>
))}
</ul>
</div>
);
};

const TopPosts = (
<OrderedPostList storeKey="top" sort={{ field: 'votes', order: 'DESC' }} />
);
const FlopPosts = (
<OrderedPostList storeKey="flop" sort={{ field: 'votes', order: 'ASC' }} />
);
```

## Return Value

The return value of `useListController` has the following shape:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -409,6 +410,7 @@ describe('useListController', () => {
});
});
});

describe('getListControllerProps', () => {
it('should only pick the props injected by the ListController', () => {
expect(
Expand Down Expand Up @@ -449,6 +451,7 @@ describe('useListController', () => {
});
});
});

describe('sanitizeListRestProps', () => {
it('should omit the props injected by the ListController', () => {
expect(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as React from 'react';
slax57 marked this conversation as resolved.
Show resolved Hide resolved
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(
<ListsUsingSameResource
history={createMemoryHistory({
initialEntries: ['/top'],
})}
/>
);

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');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import * as React from 'react';
import { Route } from 'react-router';
import { Link } from 'react-router-dom';
import fakeDataProvider from 'ra-data-fakerest';

import {
CoreAdminContext,
CoreAdminUI,
CustomRoutes,
Resource,
} from '../../core';
import { localStorageStore } from '../../store';
import { FakeBrowserDecorator } from '../../storybook/FakeBrowser';
import { CoreLayoutProps, SortPayload } from '../../types';
import { useListController } 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 OrderedPostList = ({
storeKey,
sort,
}: {
storeKey: string;
sort?: SortPayload;
}) => {
const params = useListController({
resource: 'posts',
debounce: 200,
perPage: 3,
sort,
storeKey,
});
return (
<div>
<span aria-label="storeKey" data-value={storeKey}>
storeKey: {storeKey}
</span>
<br />
<span aria-label="perPage" data-value={params.perPage}>
perPage: {params.perPage}
</span>
<br />
<br />
<button
aria-label="incrementPerPage"
disabled={params.perPage > params.data?.length ?? false}
onClick={() => params.setPerPage(++params.perPage)}
>
Increment perPage
</button>{' '}
<button
aria-label="decrementPerPage"
disabled={params.perPage <= 0}
onClick={() => params.setPerPage(--params.perPage)}
>
Decrement perPage
</button>
<ul style={styles.ul}>
{!params.isLoading &&
params.data.map(post => (
<li key={`post_${post.id}`}>
{post.title} - {post.votes} votes
</li>
))}
</ul>
</div>
);
};

const MinimalLayout = (props: CoreLayoutProps) => {
return (
<div style={styles.mainContainer}>
<Link aria-label="top" to={`/top`}>
Go to Top List
</Link>{' '}
<Link aria-label="flop" to={`/flop`}>
Go to Flop List
</Link>
<br />
<br />
{props.children}
</div>
);
};
const TopPosts = (
<OrderedPostList storeKey="top" sort={{ field: 'votes', order: 'DESC' }} />
);
const FlopPosts = (
<OrderedPostList storeKey="flop" sort={{ field: 'votes', order: 'ASC' }} />
);

export const ListsUsingSameResource = (argsOrProps, context) => {
const history = context?.history || argsOrProps.history;
return (
<CoreAdminContext
history={history}
store={localStorageStore()}
slax57 marked this conversation as resolved.
Show resolved Hide resolved
dataProvider={dataProvider}
>
<CoreAdminUI layout={MinimalLayout}>
<CustomRoutes>
<Route path="/top" element={TopPosts} />
</CustomRoutes>
<CustomRoutes>
<Route path="/flop" element={FlopPosts} />
</CustomRoutes>
<Resource name="posts" />
</CoreAdminUI>
</CoreAdminContext>
);
};
Loading