diff --git a/docs/List.md b/docs/List.md index 3586d408647..2c3aa6983b3 100644 --- a/docs/List.md +++ b/docs/List.md @@ -1978,6 +1978,7 @@ Here are all the props accepted by the component: * [`rowStyle`](#row-style-function) * [`rowClick`](#rowclick) * [`expand`](#expand) +* [`isRowExpandable`](#isrowexpandable) * [`isRowSelectable`](#isrowselectable) * [`optimized`](#performance) @@ -2198,6 +2199,31 @@ const PostList = props => ( ) ``` +### `isRowExpandable` + +You can customize which rows will allow to show an expandable panel below them using the `isRowExpandable` prop. It expects a function that will receive the record of each `` and returns a boolean expression. For instance, this code shows an expand button only for rows that has a detail to show: + +```jsx +const PostPanel = ({ id, record, resource }) => ( +
+); + +const PostList = props => ( + + } + isRowExpandable={row => row.has_detail} + > + + + + + + + +) +``` + ### `isRowSelectable` You can customize which rows will show a selection checkbox using the `isRowSelectable` prop. It expects a function that will receive the record of each `` and returns a boolean expression. For instance, this code shows a checkbox only for rows with an id greater than 300: diff --git a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx index 3f25423a42f..17e69d8d1c4 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx @@ -8,6 +8,7 @@ import { useEffect, FC, ReactElement, + useMemo, } from 'react'; import PropTypes from 'prop-types'; import { @@ -34,6 +35,7 @@ import DatagridLoading from './DatagridLoading'; import DatagridBody, { PureDatagridBody } from './DatagridBody'; import useDatagridStyles from './useDatagridStyles'; import { ClassesOverride } from '../../types'; +import DatagridContextProvider from './DatagridContextProvider'; /** * The Datagrid component renders a list of records as a table. @@ -116,6 +118,7 @@ const Datagrid: FC = React.forwardRef((props, ref) => { hasBulkActions = false, hover, isRowSelectable, + isRowExpandable, resource, rowClick, rowStyle, @@ -137,6 +140,10 @@ const Datagrid: FC = React.forwardRef((props, ref) => { } = useListContext(props); const version = useVersion(); + const contextValue = useMemo(() => ({ isRowExpandable }), [ + isRowExpandable, + ]); + const updateSort = useCallback( event => { event.stopPropagation(); @@ -246,83 +253,87 @@ const Datagrid: FC = React.forwardRef((props, ref) => { * the datagrid displays the current data. */ return ( - - - - {expand && ( - - )} - {hasBulkActions && ( - - 0 && - all.length > 0 && - all.every(id => selectedIds.includes(id)) - } - onChange={handleSelectAll} + +
+ + + {expand && ( + - - )} - {Children.map(children, (field, index) => - isValidElement(field) ? ( - - ) : null - )} - - - {cloneElement( - body, - { - basePath, - className: classes.tbody, - classes, - expand, - rowClick, - data, - hasBulkActions, - hover, - ids, - onToggleItem: handleToggleItem, - resource, - rowStyle, - selectedIds, - isRowSelectable, - version, - }, - children - )} -
+ > + 0 && + all.length > 0 && + all.every(id => + selectedIds.includes(id) + ) + } + onChange={handleSelectAll} + /> + + )} + {Children.map(children, (field, index) => + isValidElement(field) ? ( + + ) : null + )} + + + {cloneElement( + body, + { + basePath, + className: classes.tbody, + classes, + expand, + rowClick, + data, + hasBulkActions, + hover, + ids, + onToggleItem: handleToggleItem, + resource, + rowStyle, + selectedIds, + isRowSelectable, + version, + }, + children + )} + + ); }); @@ -353,6 +364,7 @@ Datagrid.propTypes = { total: PropTypes.number, version: PropTypes.number, isRowSelectable: PropTypes.func, + isRowExpandable: PropTypes.func, }; type RowClickFunction = ( @@ -376,6 +388,7 @@ export interface DatagridProps extends Omit { hasBulkActions?: boolean; hover?: boolean; isRowSelectable?: (record: Record) => boolean; + isRowExpandable?: (record: Record) => boolean; optimized?: boolean; rowClick?: string | RowClickFunction; rowStyle?: (record: Record, index: number) => any; diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridContext.ts b/packages/ra-ui-materialui/src/list/datagrid/DatagridContext.ts new file mode 100644 index 00000000000..119658216ef --- /dev/null +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridContext.ts @@ -0,0 +1,12 @@ +import { createContext } from 'react'; +import { Record as RaRecord } from 'ra-core'; + +const DatagridContext = createContext({}); + +DatagridContext.displayName = 'DatagridContext'; + +export type DatagridContextValue = { + isRowExpandable?: (record: RaRecord) => boolean; +}; + +export default DatagridContext; diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridContextProvider.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridContextProvider.tsx new file mode 100644 index 00000000000..fb0baf972f5 --- /dev/null +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridContextProvider.tsx @@ -0,0 +1,16 @@ +import React, { ReactElement, ReactNode } from 'react'; +import DatagridContext, { DatagridContextValue } from './DatagridContext'; + +const DatagridContextProvider = ({ + children, + value, +}: { + children: ReactNode; + value: DatagridContextValue; +}): ReactElement => ( + + {children} + +); + +export default DatagridContextProvider; diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.spec.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.spec.tsx index e2f691cfbf1..d2c23fb20b8 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.spec.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.spec.tsx @@ -6,6 +6,7 @@ import { createMemoryHistory } from 'history'; import { Router } from 'react-router-dom'; import DatagridRow from './DatagridRow'; +import DatagridContextProvider from './DatagridContextProvider'; const TitleField = ({ record }: any): JSX.Element => ( {record.title} @@ -48,6 +49,46 @@ describe('', () => { }; }; + describe('isRowExpandable', () => { + it('should show the expand button if it returns true', () => { + const contextValue = { isRowExpandable: () => true }; + + const { queryAllByText, getByText } = renderWithRouter( + + } + > + + + + ); + expect(queryAllByText('expanded')).toHaveLength(0); + fireEvent.click(getByText('hello')); + expect(queryAllByText('expanded')).toHaveLength(1); + }); + + it('should not show the expand button if it returns false', () => { + const contextValue = { isRowExpandable: () => false }; + + const { queryAllByText, getByText } = renderWithRouter( + + } + > + + + + ); + expect(queryAllByText('expanded')).toHaveLength(0); + fireEvent.click(getByText('hello')); + expect(queryAllByText('expanded')).toHaveLength(0); + }); + }); + describe('rowClick', () => { it("should redirect to edit page if the 'edit' option is selected", () => { const { getByText, history } = renderWithRouter( diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx index 467c9836b81..a62b2f2382c 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx @@ -31,6 +31,7 @@ import { useHistory } from 'react-router-dom'; import DatagridCell from './DatagridCell'; import ExpandRowButton from './ExpandRowButton'; import useDatagridStyles from './useDatagridStyles'; +import { useDatagridContext } from './useDatagridContext'; const computeNbColumns = (expand, children, hasBulkActions) => expand @@ -59,20 +60,31 @@ const DatagridRow: FC = React.forwardRef((props, ref) => { selectable, ...rest } = props; + + const context = useDatagridContext(); + const expandable = + (!context || + !context.isRowExpandable || + context.isRowExpandable(record)) && + expand; const resource = useResourceContext(props); const [expanded, toggleExpanded] = useExpanded(resource, id); const [nbColumns, setNbColumns] = useState( - computeNbColumns(expand, children, hasBulkActions) + computeNbColumns(expandable, children, hasBulkActions) ); useEffect(() => { // Fields can be hidden dynamically based on permissions; // The expand panel must span over the remaining columns // So we must recompute the number of columns to span on - const newNbColumns = computeNbColumns(expand, children, hasBulkActions); + const newNbColumns = computeNbColumns( + expandable, + children, + hasBulkActions + ); if (newNbColumns !== nbColumns) { setNbColumns(newNbColumns); } - }, [expand, nbColumns, children, hasBulkActions]); + }, [expandable, nbColumns, children, hasBulkActions]); const history = useHistory(); @@ -145,12 +157,14 @@ const DatagridRow: FC = React.forwardRef((props, ref) => { padding="none" className={classes.expandIconCell} > - + {expandable && ( + + )} )} {hasBulkActions && ( @@ -181,7 +195,7 @@ const DatagridRow: FC = React.forwardRef((props, ref) => { ) : null )} - {expand && expanded && ( + {expandable && expanded && ( {isValidElement(expand) diff --git a/packages/ra-ui-materialui/src/list/datagrid/useDatagridContext.ts b/packages/ra-ui-materialui/src/list/datagrid/useDatagridContext.ts new file mode 100644 index 00000000000..726843d94c7 --- /dev/null +++ b/packages/ra-ui-materialui/src/list/datagrid/useDatagridContext.ts @@ -0,0 +1,20 @@ +import { useContext, useMemo } from 'react'; +import { DatagridProps } from './Datagrid'; +import DatagridContext, { DatagridContextValue } from './DatagridContext'; +import merge from 'lodash/merge'; + +export const useDatagridContext = ( + props?: DatagridProps +): DatagridContextValue => { + const context = useContext(DatagridContext); + + return useMemo( + () => + merge( + {}, + context, + props != null ? { isRowExpandable: props.isRowExpandable } : {} + ), + [context, props] + ); +};