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

Make List children responsible of the empty state #8585

Merged
merged 3 commits into from
Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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 loading state, the Datagrid will display a message indicating there are no results. This message is translatable and its key is `ra.navigation.no_results`.
slax57 marked this conversation as resolved.
Show resolved Hide resolved

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 loading state, the SimpleList will display a message indicating there are no results. This message is translatable and its key is `ra.navigation.no_results`.
slax57 marked this conversation as resolved.
Show resolved Hide resolved

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
16 changes: 16 additions & 0 deletions packages/ra-ui-materialui/src/list/ListNoResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';
import { memo } from 'react';
import CardContent from '@mui/material/CardContent';
import Typography from '@mui/material/Typography';
import { useTranslate } from 'ra-core';

export const ListNoResults = memo(() => {
const translate = useTranslate();
return (
<CardContent>
<Typography variant="body2">
{translate('ra.navigation.no_results')}
slax57 marked this conversation as resolved.
Show resolved Hide resolved
</Typography>
</CardContent>
);
});
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 the empty component.
slax57 marked this conversation as resolved.
Show resolved Hide resolved
*/
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