Skip to content

Commit

Permalink
Merge pull request #8585 from marmelab/deprecate-pagination-limit
Browse files Browse the repository at this point in the history
Make List children responsible of the empty state
  • Loading branch information
slax57 authored Feb 10, 2023
2 parents e63118c + 0474696 commit e76e3eb
Show file tree
Hide file tree
Showing 12 changed files with 99 additions and 95 deletions.
5 changes: 3 additions & 2 deletions docs/Datagrid.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,9 @@ const CustomResetViewsButton = () => {

## `empty`

It's possible that a Datagrid will have no records to display. If the Datagrid's parent component handles the loading state, the Datagrid will return `null` and render nothing.
Passing through a component to the `empty` prop will cause the Datagrid to render the `empty` component instead of `null`.
It's possible that a Datagrid will have no records to display. If the Datagrid's parent component does not handle the empty state, the Datagrid will display a message indicating there are no results. This message is translatable and its key is `ra.navigation.no_results`.

You can customize the empty state by passing a component to the `empty` prop:

```jsx
const CustomEmpty = () => <div>No books found</div>;
Expand Down
20 changes: 20 additions & 0 deletions docs/SimpleList.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ It accepts the following props:
* [`rightAvatar`](#rightavatar)
* [`rightIcon`](#righticon)
* [`rowStyle`](#rowstyle)
* [`empty`](#empty)

## `leftAvatar`

Expand Down Expand Up @@ -179,6 +180,25 @@ See [`primaryText`](#primarytext)

See [`primaryText`](#primarytext)

## `empty`

It's possible that a SimpleList will have no records to display. If the SimpleList's parent component does not handle the empty state, the SimpleList will display a message indicating there are no results. This message is translatable and its key is `ra.navigation.no_results`.

You can customize the empty state by passing a component to the `empty` prop:

```jsx
const CustomEmpty = () => <div>No books found</div>;

const PostList = () => (
<List>
<SimpleList
primaryText={record => record.title}
empty={<CustomEmpty />}
/>
</List>
);
```

## Using `<SimpleList>` On Small Screens

To use `<SimpleList>` on small screens and a `<Datagrid>` on larger screens, use MUI's `useMediaQuery` hook:
Expand Down
17 changes: 17 additions & 0 deletions packages/ra-ui-materialui/src/list/ListNoResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as React from 'react';
import { memo } from 'react';
import CardContent from '@mui/material/CardContent';
import Typography from '@mui/material/Typography';
import { useResourceContext, useTranslate } from 'ra-core';

export const ListNoResults = memo(() => {
const translate = useTranslate();
const resource = useResourceContext();
return (
<CardContent>
<Typography variant="body2">
{translate('ra.navigation.no_results', { resource })}
</Typography>
</CardContent>
);
});
8 changes: 1 addition & 7 deletions packages/ra-ui-materialui/src/list/ListView.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import * as React from 'react';
import { styled } from '@mui/material/styles';
import {
Children,
cloneElement,
ReactElement,
ReactNode,
ElementType,
} from 'react';
import { cloneElement, ReactElement, ReactNode, ElementType } from 'react';
import PropTypes from 'prop-types';
import { SxProps } from '@mui/system';
import Card from '@mui/material/Card';
Expand Down
16 changes: 16 additions & 0 deletions packages/ra-ui-materialui/src/list/SimpleList/SimpleList.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,20 @@ describe('<SimpleList />', () => {
expect(screen.getByText('2').closest('a')).toBeNull();
});
});

it('should display a message when there is no result', () => {
render(
<ListContext.Provider
value={{
isLoading: false,
data: [],
total: 0,
resource: 'posts',
}}
>
<SimpleList />
</ListContext.Provider>
);
expect(screen.queryByText('ra.navigation.no_results')).not.toBeNull();
});
});
17 changes: 17 additions & 0 deletions packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from 'ra-core';

import { SimpleListLoading } from './SimpleListLoading';
import { ListNoResults } from '../ListNoResults';

/**
* The <SimpleList> component renders a list of records as a MUI <List>.
Expand Down Expand Up @@ -67,6 +68,7 @@ export const SimpleList = <RecordType extends RaRecord = any>(
) => {
const {
className,
empty = DefaultEmpty,
hasBulkActions,
leftAvatar,
leftIcon,
Expand Down Expand Up @@ -95,6 +97,18 @@ export const SimpleList = <RecordType extends RaRecord = any>(
);
}

/**
* Once loaded, the data for the list may be empty. Instead of
* displaying the table header with zero data rows,
* the SimpleList the empty component.
*/
if (data == null || data.length === 0 || total === 0) {
if (empty) {
return empty;
}

return null;
}
const renderAvatar = (
record: RecordType,
avatarCallback: FunctionToElement<RecordType>
Expand Down Expand Up @@ -245,6 +259,7 @@ export type FunctionToElement<RecordType extends RaRecord = any> = (
export interface SimpleListProps<RecordType extends RaRecord = any>
extends Omit<ListProps, 'classes'> {
className?: string;
empty?: ReactElement;
hasBulkActions?: boolean;
leftAvatar?: FunctionToElement<RecordType>;
leftIcon?: FunctionToElement<RecordType>;
Expand Down Expand Up @@ -321,3 +336,5 @@ const Root = styled(List, {
})({
[`& .${SimpleListClasses.tertiary}`]: { float: 'right', opacity: 0.541176 },
});

const DefaultEmpty = <ListNoResults />;
9 changes: 9 additions & 0 deletions packages/ra-ui-materialui/src/list/datagrid/Datagrid.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,4 +268,13 @@ describe('<Datagrid />', () => {
expect(contextValue.onSelect).toHaveBeenCalledTimes(1);
});
});

it('should display a message when there is no result', () => {
render(
<Wrapper listContext={{ ...contextValue, data: [], total: 0 }}>
<Datagrid />
</Wrapper>
);
expect(screen.queryByText('ra.navigation.no_results')).not.toBeNull();
});
});
7 changes: 5 additions & 2 deletions packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import DatagridContextProvider from './DatagridContextProvider';
import { DatagridClasses, DatagridRoot } from './useDatagridStyles';
import { BulkActionsToolbar } from '../BulkActionsToolbar';
import { BulkDeleteButton } from '../../button';
import { ListNoResults } from '../ListNoResults';

const defaultBulkActionButtons = <BulkDeleteButton />;

Expand Down Expand Up @@ -120,7 +121,7 @@ export const Datagrid: FC<DatagridProps> = React.forwardRef((props, ref) => {
header = DatagridHeader,
children,
className,
empty,
empty = DefaultEmpty,
expand,
bulkActionButtons = defaultBulkActionButtons,
hover,
Expand Down Expand Up @@ -210,7 +211,7 @@ export const Datagrid: FC<DatagridProps> = React.forwardRef((props, ref) => {
/**
* Once loaded, the data for the list may be empty. Instead of
* displaying the table header with zero data rows,
* the datagrid displays nothing or a custom empty component.
* the Datagrid displays the empty component.
*/
if (data == null || data.length === 0 || total === 0) {
if (empty) {
Expand Down Expand Up @@ -369,3 +370,5 @@ const sanitizeRestProps = props =>
.reduce((acc, key) => ({ ...acc, [key]: props[key] }), {});

Datagrid.displayName = 'Datagrid';

const DefaultEmpty = <ListNoResults />;
1 change: 1 addition & 0 deletions packages/ra-ui-materialui/src/list/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './List';
export * from './ListActions';
export * from './listFieldTypes';
export * from './ListGuesser';
export * from './ListNoResults';
export * from './ListToolbar';
export * from './ListView';
export * from './pagination';
Expand Down
80 changes: 0 additions & 80 deletions packages/ra-ui-materialui/src/list/pagination/Pagination.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,86 +21,6 @@ describe('<Pagination />', () => {
hasPreviousPage: undefined,
};

describe('no results mention', () => {
it('should display a pagination limit when there is no result', () => {
render(
<ThemeProvider theme={theme}>
<ListPaginationContext.Provider
value={{ ...defaultProps, total: 0 }}
>
<Pagination />
</ListPaginationContext.Provider>
</ThemeProvider>
);
expect(
screen.queryByText('ra.navigation.no_results')
).not.toBeNull();
});

it('should not display a pagination limit when there are results', () => {
render(
<ThemeProvider theme={theme}>
<ListPaginationContext.Provider
value={{ ...defaultProps, total: 1 }}
>
<Pagination />
</ListPaginationContext.Provider>
</ThemeProvider>
);
expect(screen.queryByText('ra.navigation.no_results')).toBeNull();
});

it('should display a pagination limit on an out of bounds page (more than total pages)', async () => {
jest.spyOn(console, 'error').mockImplementationOnce(() => {});
const setPage = jest.fn().mockReturnValue(null);
render(
<ThemeProvider theme={theme}>
<ListPaginationContext.Provider
value={{
...defaultProps,
total: 10,
page: 2, // Query the page 2 but there is only 1 page
perPage: 10,
setPage,
}}
>
<Pagination />
</ListPaginationContext.Provider>
</ThemeProvider>
);
// mui TablePagination displays no more a warning in that case
// Then useEffect fallbacks on a valid page
expect(
screen.queryByText('ra.navigation.no_results')
).not.toBeNull();
});

it('should display a pagination limit on an out of bounds page (less than 0)', async () => {
jest.spyOn(console, 'error').mockImplementationOnce(() => {});
const setPage = jest.fn().mockReturnValue(null);
render(
<ThemeProvider theme={theme}>
<ListPaginationContext.Provider
value={{
...defaultProps,
total: 10,
page: -2, // Query the page -2 😱
perPage: 10,
setPage,
}}
>
<Pagination />
</ListPaginationContext.Provider>
</ThemeProvider>
);
// mui TablePagination displays no more a warning in that case
// Then useEffect fallbacks on a valid page
expect(
screen.queryByText('ra.navigation.no_results')
).not.toBeNull();
});
});

describe('Total pagination', () => {
it('should display a next button when there are more results', () => {
render(
Expand Down
11 changes: 7 additions & 4 deletions packages/ra-ui-materialui/src/list/pagination/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@ import {
} from 'ra-core';

import { PaginationActions } from './PaginationActions';
import { PaginationLimit } from './PaginationLimit';

export const Pagination: FC<PaginationProps> = memo(props => {
const {
rowsPerPageOptions = DefaultRowsPerPageOptions,
actions,
limit = DefaultLimit,
limit = null,
...rest
} = props;
const {
Expand Down Expand Up @@ -97,7 +96,12 @@ export const Pagination: FC<PaginationProps> = memo(props => {

// Avoid rendering TablePagination if "page" value is invalid
if (total === 0 || page < 1 || (total != null && page > totalPages)) {
return limit;
if (limit != null && process.env.NODE_ENV === 'development') {
console.warn(
'The Pagination limit prop is deprecated. Empty state should be handled by the component displaying data (Datagrid, SimpleList).'
);
}
return null;
}

if (isSmall) {
Expand Down Expand Up @@ -149,7 +153,6 @@ Pagination.propTypes = {
rowsPerPageOptions: PropTypes.arrayOf(PropTypes.number),
};

const DefaultLimit = <PaginationLimit />;
const DefaultRowsPerPageOptions = [5, 10, 25, 50];
const emptyArray = [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import CardContent from '@mui/material/CardContent';
import Typography from '@mui/material/Typography';
import { useTranslate } from 'ra-core';

/**
* @deprecated Empty state should be handled by the component displaying data (Datagrid, SimpleList).
*/
export const PaginationLimit = memo(() => {
const translate = useTranslate();
return (
Expand Down

0 comments on commit e76e3eb

Please sign in to comment.