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

Partial pagination #7120

Merged
merged 16 commits into from
Jan 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1539,6 +1539,34 @@ In general, you should use `isLoading`. It's false as long as the data has never

The new props are actually returned by react-query's `useQuery` hook. Check [their documentation](https://react-query.tanstack.com/reference/useQuery) for more information.

## Changes In Translation Messages

The `ra.navigation.prev` message was renamed to `ra.navigation.previous`. Update your translation files accordingly.

```diff
const messages = {
ra: {
navigation: {
no_results: 'No results found',
no_more_results:
'The page number %{page} is out of boundaries. Try the previous page.',
page_out_of_boundaries: 'Page number %{page} out of boundaries',
page_out_from_end: 'Cannot go after last page',
page_out_from_begin: 'Cannot go before page 1',
page_range_info: '%{offsetBegin}-%{offsetEnd} of %{total}',
partial_page_range_info:
'%{offsetBegin}-%{offsetEnd} of more than %{offsetEnd}',
current_page: 'Page %{page}',
page: 'Go to page %{page}',
next: 'Go to next page',
- prev: 'Go to previous page',
+ previous: 'Go to previous page',
page_rows_per_page: 'Rows per page:',
skip_nav: 'Skip to content',
},
// ...
```

## Unit Tests for Data Provider Dependent Components Need A QueryClientContext

If you were using components dependent on the dataProvider hooks in isolation (e.g. in unit or integration tests), you now need to wrap them inside a `<QueryClientContext>` component, to let the access react-query's `QueryClient` instance.
Expand Down
12 changes: 6 additions & 6 deletions cypress/support/ListPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ export default url => ({
filterMenuItem: source => `.new-filter-item[data-key="${source}"]`,
hideFilterButton: source =>
`.filter-field[data-source="${source}"] .hide-filter`,
nextPage: '.next-page',
pageNumber: n => `.page-number[data-page='${n - 1}']`,
previousPage: '.previous-page',
nextPage: "button[aria-label='Go to next page']",
previousPage: "button[aria-label='Go to previous page']",
pageNumber: n => `button[aria-label='Go to page ${n}']`,
recordRows: '.datagrid-body tr',
viewsColumn: '.datagrid-body tr td:nth-child(7)',
datagridHeaders: 'th',
Expand Down Expand Up @@ -53,15 +53,15 @@ export default url => ({
},

nextPage() {
cy.get(this.elements.nextPage).click({ force: true });
cy.get(this.elements.nextPage).click();
},

previousPage() {
cy.get(this.elements.previousPage).click({ force: true });
cy.get(this.elements.previousPage).click();
},

goToPage(n) {
return cy.get(this.elements.pageNumber(n)).click({ force: true });
return cy.get(this.elements.pageNumber(n)).click();
},

addCommentableFilter() {
Expand Down
34 changes: 30 additions & 4 deletions docs/DataProviderWriting.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ Data Providers methods must return a Promise for an object with a `data` propert

| Method | Response format |
| ------------------ | --------------------------------------------------------------- |
| `getList` | `{ data: {Record[]}, total: {int}, validUntil?: {Date} }` |
| `getOne` | `{ data: {Record}, validUntil?: {Date} }` |
| `getMany` | `{ data: {Record[]}, validUntil?: {Date} }` |
| `getList` | `{ data: {Record[]}, total: {int} }` |
| `getOne` | `{ data: {Record} }` |
| `getMany` | `{ data: {Record[]} }` |
| `getManyReference` | `{ data: {Record[]}, total: {int} }` |
| `create` | `{ data: {Record} }` |
| `update` | `{ data: {Record} }` |
Expand Down Expand Up @@ -198,7 +198,33 @@ dataProvider.deleteMany('posts', { ids: [123, 234] })
// }
```

**Tip**: The `validUntil` field in the response is optional. It enables the Application cache, a client-side optimization to speed up rendering and reduce network traffic. Check [the Caching documentation](./Caching.md#application-cache) for more details.
## Partial Pagination

The `getList()` and `getManyReference()` methods return paginated responses. Sometimes, executing a "count" server-side to return the `total` number of records is expensive. In this case, you can omit the `total` property in the response, and pass a `pageInfo` object instead, specifying if there are previous and next pages:

```js
dataProvider.getList('posts', {
pagination: { page: 1, perPage: 5 },
sort: { field: 'title', order: 'ASC' },
filter: { author_id: 12 },
})
.then(response => console.log(response));
// {
// data: [
// { id: 126, title: "allo?", author_id: 12 },
// { id: 127, title: "bien le bonjour", author_id: 12 },
// { id: 124, title: "good day sunshine", author_id: 12 },
// { id: 123, title: "hello, world", author_id: 12 },
// { id: 125, title: "howdy partner", author_id: 12 },
// ],
// pageInfo: {
// hasPreviousPage: false,
// hasNextPage: true,
// }
// }
```

React-admin's `<Pagination>` component will automatically handle the `pageInfo` object and display the appropriate pagination controls.

## Error Format

Expand Down
53 changes: 34 additions & 19 deletions docs/ListTutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,9 @@ The [`<Pagination>`](./Pagination.md) component gets the following constants fro
* `page`: The current page number (integer). First page is `1`.
* `perPage`: The number of records per page.
* `setPage`: `Function(page: number) => void`. A function that set the current page number.
* `total`: The total number of records.
* `total`: The total number of records (may be undefined when the data provider uses [Partial pagination](./DataProviderWriting.md#partial-pagination)).
* `hasPreviousPage`: True if the page number is greater than 1.
* `hasNextPage`: True if the page number is lower than the total number of pages.
* `actions`: A component that displays the pagination buttons (default: `<PaginationActions>`)
* `limit`: An element that is displayed if there is no data to show (default: `<PaginationLimit>`)

Expand All @@ -639,24 +641,29 @@ import ChevronLeft from '@mui/icons-material/ChevronLeft';
import ChevronRight from '@mui/icons-material/ChevronRight';

const PostPagination = () => {
const { page, perPage, total, setPage } = useListContext();
const nbPages = Math.ceil(total / perPage) || 1;
const { page, hasPreviousPage, hasNextPage, setPage } = useListContext();
if (!hasPreviousPage && !hasNextPage) return null;
return (
nbPages > 1 &&
<Toolbar>
{page > 1 &&
<Button color="primary" key="prev" onClick={() => setPage(page - 1)}>
<ChevronLeft />
Prev
</Button>
}
{page !== nbPages &&
<Button color="primary" key="next" onClick={() => setPage(page + 1)}>
Next
<ChevronRight />
</Button>
}
</Toolbar>
<Toolbar>
{hasPreviousPage &&
<Button
key="previous"
onClick={() => setPage(page - 1)}
startIcon={<ChevronLeft />}
>
Previous
</Button>
}
{hasNextPage &&
<Button
key="next"
onClick={() => setPage(page + 1)}
startIcon={<ChevronRight />}
>
Next
</Button>
}
</Toolbar>
);
}

Expand All @@ -676,7 +683,15 @@ import {
PaginationActions as RaPaginationActions,
} from 'react-admin';

export const PaginationActions = props => <RaPaginationActions {...props} color="secondary" />;
export const PaginationActions = props => (
<RaPaginationActions
{...props}
// these props are passed down to the MUI <Pagination> component
color="primary"
showFirstButton
showLastButton
/>
);

export const Pagination = props => <RaPagination {...props} ActionsComponent={PaginationActions} />;

Expand Down
67 changes: 58 additions & 9 deletions docs/useGetList.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,79 @@ title: "useGetList"

This hook calls `dataProvider.getList()` when the component mounts. It's ideal for getting a list of records. It supports filtering, sorting, and pagination.


## Syntax

```jsx
// syntax
const { data, total, isLoading, error, refetch } = useGetList(
resource,
{ pagination, sort, filter, meta },
options
);
```

// example
## Usage

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

const LatestNews = () => {
const { data, isLoading, error } = useGetList(
const { data, total, isLoading, error } = useGetList(
'posts',
{ pagination: { page: 1, perPage: 10 }, sort: { field: 'published_at', order: 'DESC' } }
{
pagination: { page: 1, perPage: 10 },
sort: { field: 'published_at', order: 'DESC' }
}
);
if (isLoading) { return <Loading />; }
if (error) { return <p>ERROR</p>; }
return (
<ul>
{data.map(record =>
<li key={record.id}>{record.title}</li>
)}
</ul>
<>
<h1>Latest news</h1>
<ul>
{data.map(record =>
<li key={record.id}>{record.title}</li>
)}
</ul>
<p>{data.length} / {total} articles</p>
</>
);
};
```

## Partial Pagination

If your data provider doesn't return the `total` number of records (see [Partial Pagination](./DataProviderWriting.md#partial-pagination)), you can use the `pageInfo` field to determine if there are more records to fetch.

```jsx
import { useState } from 'react';
import { useGetList } from 'react-admin';

const LatestNews = () => {
const [page, setPage] = useState(1);
const { data, pageInfo, isLoading, error } = useGetList(
'posts',
{
pagination: { page, perPage: 10 },
sort: { field: 'published_at', order: 'DESC' }
}
);
if (isLoading) { return <Loading />; }
if (error) { return <p>ERROR</p>; }
const { hasNextPage, hasPreviousPage } = pageInfo;

const getNextPage = () => setPage(page + 1);

return (
<>
<h1>Latest news</h1>
<ul>
{data.map(record =>
<li key={record.id}>{record.title}</li>
)}
</ul>
{hasNextPage && <button onClick={getNextPage}>More articles</button>}
</>
);
};
```
46 changes: 44 additions & 2 deletions docs/useGetManyReference.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ title: "useGetManyReference"

This hook calls `dataProvider.getManyReference()` when the component mounts. It queries the data provider for a list of records related to another one (e.g. all the comments for a post). It supports filtering, sorting, and pagination.

## Syntax

```jsx
// syntax
const { data, total, isLoading, error, refetch } = useGetManyReference(
resource,
{ target, id, pagination, sort, filter, meta },
options
);
```

// example
## Usage

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

const PostComments = ({ record }) => {
Expand All @@ -40,3 +44,41 @@ const PostComments = ({ record }) => {
);
};
```

## Partial Pagination

If your data provider doesn't return the `total` number of records (see [Partial Pagination](./DataProviderWriting.md#partial-pagination)), you can use the `pageInfo` field to determine if there are more records to fetch.

```jsx
import { useState } from 'react';
import { useGetManyReference } from 'react-admin';

const PostComments = ({ record }) => {
const [page, setPage] = useState(1);
const { data, isLoading, pageInfo, error } = useGetManyReference(
'comments',
{
target: 'post_id',
id: record.id,
pagination: { page, perPage: 10 },
sort: { field: 'published_at', order: 'DESC' }
}
);
if (isLoading) { return <Loading />; }
if (error) { return <p>ERROR</p>; }
const { hasNextPage, hasPreviousPage } = pageInfo;

const getNextPage = () => setPage(page + 1);

return (
<>
<ul>
{data.map(comment => (
<li key={comment.id}>{comment.body}</li>
))}
</ul>
{hasNextPage && <button onClick={getNextPage}>More comments</button>}
</>
);
};
```
2 changes: 2 additions & 0 deletions docs/useList.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ const {
perPage, // the number of results per page. Defaults to 25
setPage, // a callback to change the page, e.g. setPage(3)
setPerPage, // a callback to change the number of results per page, e.g. setPerPage(25)
hasPreviousPage, // boolean, true if the current page is not the first one
hasNextPage, // boolean, true if the current page is not the last one
// sorting
sort, // a sort object { field, order }, e.g. { field: 'date', order: 'DESC' }
setSort, // a callback to change the sort, e.g. setSort({ field: 'name', order: 'ASC' })
Expand Down
2 changes: 2 additions & 0 deletions docs/useListContext.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ const {
perPage, // the number of results per page. Defaults to 25
setPage, // a callback to change the page, e.g. setPage(3)
setPerPage, // a callback to change the number of results per page, e.g. setPerPage(25)
hasPreviousPage, // boolean, true if the current page is not the first one
hasNextPage, // boolean, true if the current page is not the last one
// sorting
sort, // a sort object { field, order }, e.g. { field: 'date', order: 'DESC' }
setSort, // a callback to change the sort, e.g. setSort({ field: 'name', orfer: 'ASC' })
Expand Down
2 changes: 2 additions & 0 deletions docs/useListController.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ const {
perPage, // the number of results per page. Defaults to 25
setPage, // a callback to change the page, e.g. setPage(3)
setPerPage, // a callback to change the number of results per page, e.g. setPerPage(25)
hasPreviousPage, // boolean, true if the current page is not the first one
hasNextPage, // boolean, true if the current page is not the last one
// sorting
sort, // a sort object { field, order }, e.g. { field: 'date', order: 'DESC' }
setSort, // a callback to change the sort, e.g. setSort({ field: 'name', order: 'ASC' })
Expand Down
Loading