diff --git a/docs/Datagrid.md b/docs/Datagrid.md index ebd87eb010a..d0653cedf1a 100644 --- a/docs/Datagrid.md +++ b/docs/Datagrid.md @@ -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 = () =>
No books found
; diff --git a/docs/SimpleList.md b/docs/SimpleList.md index 541a62bfdda..684d2485b97 100644 --- a/docs/SimpleList.md +++ b/docs/SimpleList.md @@ -44,6 +44,7 @@ It accepts the following props: * [`rightAvatar`](#rightavatar) * [`rightIcon`](#righticon) * [`rowStyle`](#rowstyle) +* [`empty`](#empty) ## `leftAvatar` @@ -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 = () =>
No books found
; + +const PostList = () => ( + + record.title} + empty={} + /> + +); +``` + ## Using `` On Small Screens To use `` on small screens and a `` on larger screens, use MUI's `useMediaQuery` hook: diff --git a/packages/ra-ui-materialui/src/list/ListNoResults.tsx b/packages/ra-ui-materialui/src/list/ListNoResults.tsx new file mode 100644 index 00000000000..1f2f8acd97a --- /dev/null +++ b/packages/ra-ui-materialui/src/list/ListNoResults.tsx @@ -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 ( + + + {translate('ra.navigation.no_results', { resource })} + + + ); +}); diff --git a/packages/ra-ui-materialui/src/list/ListView.tsx b/packages/ra-ui-materialui/src/list/ListView.tsx index 9931925b5be..c4f0c98d182 100644 --- a/packages/ra-ui-materialui/src/list/ListView.tsx +++ b/packages/ra-ui-materialui/src/list/ListView.tsx @@ -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'; diff --git a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.spec.tsx b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.spec.tsx index d87b7305f04..b1664d11584 100644 --- a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.spec.tsx +++ b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.spec.tsx @@ -130,4 +130,20 @@ describe('', () => { expect(screen.getByText('2').closest('a')).toBeNull(); }); }); + + it('should display a message when there is no result', () => { + render( + + + + ); + expect(screen.queryByText('ra.navigation.no_results')).not.toBeNull(); + }); }); diff --git a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx index da1195ce6a5..575bc06c1b4 100644 --- a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx +++ b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx @@ -27,6 +27,7 @@ import { } from 'ra-core'; import { SimpleListLoading } from './SimpleListLoading'; +import { ListNoResults } from '../ListNoResults'; /** * The component renders a list of records as a MUI . @@ -67,6 +68,7 @@ export const SimpleList = ( ) => { const { className, + empty = DefaultEmpty, hasBulkActions, leftAvatar, leftIcon, @@ -95,6 +97,18 @@ export const SimpleList = ( ); } + /** + * 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 @@ -245,6 +259,7 @@ export type FunctionToElement = ( export interface SimpleListProps extends Omit { className?: string; + empty?: ReactElement; hasBulkActions?: boolean; leftAvatar?: FunctionToElement; leftIcon?: FunctionToElement; @@ -321,3 +336,5 @@ const Root = styled(List, { })({ [`& .${SimpleListClasses.tertiary}`]: { float: 'right', opacity: 0.541176 }, }); + +const DefaultEmpty = ; diff --git a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.spec.tsx b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.spec.tsx index 9b140899ab4..6259abd5cdd 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.spec.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.spec.tsx @@ -268,4 +268,13 @@ describe('', () => { expect(contextValue.onSelect).toHaveBeenCalledTimes(1); }); }); + + it('should display a message when there is no result', () => { + render( + + + + ); + expect(screen.queryByText('ra.navigation.no_results')).not.toBeNull(); + }); }); diff --git a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx index 37b8bad5de0..0c55fa3abab 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx @@ -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 = ; @@ -120,7 +121,7 @@ export const Datagrid: FC = React.forwardRef((props, ref) => { header = DatagridHeader, children, className, - empty, + empty = DefaultEmpty, expand, bulkActionButtons = defaultBulkActionButtons, hover, @@ -210,7 +211,7 @@ export const Datagrid: FC = 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) { @@ -369,3 +370,5 @@ const sanitizeRestProps = props => .reduce((acc, key) => ({ ...acc, [key]: props[key] }), {}); Datagrid.displayName = 'Datagrid'; + +const DefaultEmpty = ; diff --git a/packages/ra-ui-materialui/src/list/index.ts b/packages/ra-ui-materialui/src/list/index.ts index fba6aa18b7f..b18134056e1 100644 --- a/packages/ra-ui-materialui/src/list/index.ts +++ b/packages/ra-ui-materialui/src/list/index.ts @@ -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'; diff --git a/packages/ra-ui-materialui/src/list/pagination/Pagination.spec.tsx b/packages/ra-ui-materialui/src/list/pagination/Pagination.spec.tsx index ff64c111a03..644316f2567 100644 --- a/packages/ra-ui-materialui/src/list/pagination/Pagination.spec.tsx +++ b/packages/ra-ui-materialui/src/list/pagination/Pagination.spec.tsx @@ -21,86 +21,6 @@ describe('', () => { hasPreviousPage: undefined, }; - describe('no results mention', () => { - it('should display a pagination limit when there is no result', () => { - render( - - - - - - ); - expect( - screen.queryByText('ra.navigation.no_results') - ).not.toBeNull(); - }); - - it('should not display a pagination limit when there are results', () => { - render( - - - - - - ); - 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( - - - - - - ); - // 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( - - - - - - ); - // 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( diff --git a/packages/ra-ui-materialui/src/list/pagination/Pagination.tsx b/packages/ra-ui-materialui/src/list/pagination/Pagination.tsx index 4a4f4bda1b5..c8e6637d93c 100644 --- a/packages/ra-ui-materialui/src/list/pagination/Pagination.tsx +++ b/packages/ra-ui-materialui/src/list/pagination/Pagination.tsx @@ -17,13 +17,12 @@ import { } from 'ra-core'; import { PaginationActions } from './PaginationActions'; -import { PaginationLimit } from './PaginationLimit'; export const Pagination: FC = memo(props => { const { rowsPerPageOptions = DefaultRowsPerPageOptions, actions, - limit = DefaultLimit, + limit = null, ...rest } = props; const { @@ -97,7 +96,12 @@ export const Pagination: FC = 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) { @@ -149,7 +153,6 @@ Pagination.propTypes = { rowsPerPageOptions: PropTypes.arrayOf(PropTypes.number), }; -const DefaultLimit = ; const DefaultRowsPerPageOptions = [5, 10, 25, 50]; const emptyArray = []; diff --git a/packages/ra-ui-materialui/src/list/pagination/PaginationLimit.tsx b/packages/ra-ui-materialui/src/list/pagination/PaginationLimit.tsx index 747c7206ead..cc8b9cf9fa3 100644 --- a/packages/ra-ui-materialui/src/list/pagination/PaginationLimit.tsx +++ b/packages/ra-ui-materialui/src/list/pagination/PaginationLimit.tsx @@ -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 (