diff --git a/docs/List.md b/docs/List.md index 62fead24bac..f0259e6eaa2 100644 --- a/docs/List.md +++ b/docs/List.md @@ -2016,6 +2016,7 @@ The `Datagrid` component renders a list of records as a table. It is usually use Here are all the props accepted by the component: * [`body`](#body-element) +* [`header`](#header-element) * [`rowStyle`](#row-style-function) * [`rowClick`](#rowclick) * [`expand`](#expand) @@ -2101,6 +2102,39 @@ const PostList = props => ( export default PostList; ``` +### Header Element + +By default, `` renders its header using ``, an internal react-admin component. You can pass a custom component as the `header` prop to override that default. This can be useful e.g. to add a second header row, or to create headers spanning multiple columns. + +For instance, here is a simple datagrid header that displays column names with no sort and no "select all" button: + +```jsx +import { TableHead, TableRow, TableCell } from '@material-ui/core'; + +const DatagridHeader = ({ children }) => ( + + + {/* empty cell to account for the select row checkbox in the body */} + {Children.map(children, child => ( + + {child.props.source} + + ))} + + +); + +const PostList = props => ( + + }> + {/* ... */} + + +); +``` + +**Tip**: To handle sorting in your custom Datagrid header component, check out the [Building a custom sort control](#building-a-custom-sort-control) section. + ### Row Style Function You can customize the `` row style (applied to the `` element) based on the record, thanks to the `rowStyle` prop, which expects a function. React-admin calls this function for each row, passing the current record and index as arguments. The function should return a style object, which react-admin uses as a `` prop. diff --git a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx index 9d8f499f187..b932f953edb 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx @@ -1,12 +1,13 @@ import * as React from 'react'; import { - isValidElement, - Children, cloneElement, + createElement, + isValidElement, useCallback, useRef, useEffect, FC, + ComponentType, ReactElement, useMemo, } from 'react'; @@ -20,19 +21,12 @@ import { RecordMap, SortPayload, } from 'ra-core'; -import { - Checkbox, - Table, - TableProps, - TableCell, - TableHead, - TableRow, -} from '@material-ui/core'; +import { Table, TableProps } from '@material-ui/core'; import classnames from 'classnames'; import union from 'lodash/union'; import difference from 'lodash/difference'; -import DatagridHeaderCell from './DatagridHeaderCell'; +import { DatagridHeader } from './DatagridHeader'; import DatagridLoading from './DatagridLoading'; import DatagridBody, { PureDatagridBody } from './DatagridBody'; import useDatagridStyles from './useDatagridStyles'; @@ -113,7 +107,8 @@ const Datagrid: FC = React.forwardRef((props, ref) => { const classes = useDatagridStyles(props); const { optimized = false, - body = optimized ? : , + body = optimized ? PureDatagridBody : DatagridBody, + header = DatagridHeader, children, classes: classesOverride, className, @@ -148,42 +143,6 @@ const Datagrid: FC = React.forwardRef((props, ref) => { isRowExpandable, ]); - const updateSortCallback = useCallback( - event => { - event.stopPropagation(); - const newField = event.currentTarget.dataset.field; - const newOrder = - currentSort.field === newField - ? currentSort.order === 'ASC' - ? 'DESC' - : 'ASC' - : event.currentTarget.dataset.order; - - setSort(newField, newOrder); - }, - [currentSort.field, currentSort.order, setSort] - ); - - const updateSort = setSort ? updateSortCallback : null; - - const handleSelectAll = useCallback( - event => { - if (event.target.checked) { - const all = ids.concat( - selectedIds.filter(id => !ids.includes(id)) - ); - onSelect( - isRowSelectable - ? all.filter(id => isRowSelectable(data[id])) - : all - ); - } else { - onSelect([]); - } - }, - [data, ids, onSelect, isRowSelectable, selectedIds] - ); - const lastSelected = useRef(null); useEffect(() => { @@ -253,10 +212,6 @@ const Datagrid: FC = React.forwardRef((props, ref) => { return null; } - const all = isRowSelectable - ? ids.filter(id => isRowSelectable(data[id])) - : ids; - /** * After the initial load, if the data for the list isn't empty, * and even if the data is refreshing (e.g. after a filter change), @@ -270,58 +225,26 @@ const Datagrid: FC = React.forwardRef((props, ref) => { size={size} {...sanitizeListRestProps(rest)} > - - - {expand && ( - - )} - {hasBulkActions && selectedIds && ( - - 0 && - all.length > 0 && - all.every(id => - selectedIds.includes(id) - ) - } - onChange={handleSelectAll} - /> - - )} - {Children.map(children, (field, index) => - isValidElement(field) ? ( - - ) : null - )} - - - {cloneElement( + {createOrCloneElement( + header, + { + children, + classes, + className, + currentSort, + data, + hasExpand: !!expand, + hasBulkActions, + ids, + isRowSelectable, + onSelect, + resource, + selectedIds, + setSort, + }, + children + )} + {createOrCloneElement( body, { basePath, @@ -347,9 +270,15 @@ const Datagrid: FC = React.forwardRef((props, ref) => { ); }); +const createOrCloneElement = (element, props, children) => + isValidElement(element) + ? cloneElement(element, props, children) + : createElement(element, props, children); + Datagrid.propTypes = { basePath: PropTypes.string, - body: PropTypes.element, + // @ts-ignore + body: PropTypes.oneOfType([PropTypes.element, PropTypes.elementType]), children: PropTypes.node.isRequired, classes: PropTypes.object, className: PropTypes.string, @@ -362,6 +291,8 @@ Datagrid.propTypes = { // @ts-ignore expand: PropTypes.oneOfType([PropTypes.element, PropTypes.elementType]), hasBulkActions: PropTypes.bool, + // @ts-ignore + header: PropTypes.oneOfType([PropTypes.element, PropTypes.elementType]), hover: PropTypes.bool, ids: PropTypes.arrayOf(PropTypes.any), loading: PropTypes.bool, @@ -380,7 +311,7 @@ Datagrid.propTypes = { export interface DatagridProps extends Omit { - body?: ReactElement; + body?: ReactElement | ComponentType; classes?: ClassesOverride; className?: string; expand?: @@ -392,6 +323,7 @@ export interface DatagridProps resource: string; }>; hasBulkActions?: boolean; + header?: ReactElement | ComponentType; hover?: boolean; empty?: ReactElement; isRowSelectable?: (record: Record) => boolean; diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridHeader.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridHeader.tsx new file mode 100644 index 00000000000..db6f3994e0c --- /dev/null +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridHeader.tsx @@ -0,0 +1,177 @@ +import * as React from 'react'; +import { Children, isValidElement, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { + useListContext, + useResourceContext, + Identifier, + Record, + RecordMap, + SortPayload, +} from 'ra-core'; +import { Checkbox, TableCell, TableHead, TableRow } from '@material-ui/core'; +import classnames from 'classnames'; + +import DatagridHeaderCell from './DatagridHeaderCell'; +import useDatagridStyles from './useDatagridStyles'; +import { ClassesOverride } from '../../types'; + +/** + * The default Datagrid Header component. + * + * Renders select all checkbox as well as column header buttons used for sorting. + */ +export const DatagridHeader = (props: DatagridHeaderProps) => { + const { + children, + classes, + className, + hasExpand = false, + hasBulkActions = false, + isRowSelectable, + } = props; + const resource = useResourceContext(props); + const { + currentSort, + data, + ids, + onSelect, + selectedIds, + setSort, + } = useListContext(props); + + const updateSortCallback = useCallback( + event => { + event.stopPropagation(); + const newField = event.currentTarget.dataset.field; + const newOrder = + currentSort.field === newField + ? currentSort.order === 'ASC' + ? 'DESC' + : 'ASC' + : event.currentTarget.dataset.order; + + setSort(newField, newOrder); + }, + [currentSort.field, currentSort.order, setSort] + ); + + const updateSort = setSort ? updateSortCallback : null; + + const handleSelectAll = useCallback( + event => { + if (event.target.checked) { + const all = ids.concat( + selectedIds.filter(id => !ids.includes(id)) + ); + onSelect( + isRowSelectable + ? all.filter(id => isRowSelectable(data[id])) + : all + ); + } else { + onSelect([]); + } + }, + [data, ids, onSelect, isRowSelectable, selectedIds] + ); + + const selectableIds = isRowSelectable + ? ids.filter(id => isRowSelectable(data[id])) + : ids; + + return ( + + + {hasExpand && ( + + )} + {hasBulkActions && selectedIds && ( + + 0 && + selectableIds.length > 0 && + selectableIds.every(id => + selectedIds.includes(id) + ) + } + onChange={handleSelectAll} + /> + + )} + {Children.map(children, (field, index) => + isValidElement(field) ? ( + + ) : null + )} + + + ); +}; + +DatagridHeader.propTypes = { + children: PropTypes.node, + classes: PropTypes.object, + className: PropTypes.string, + currentSort: PropTypes.exact({ + field: PropTypes.string, + order: PropTypes.string, + }), + data: PropTypes.any, + hasExpand: PropTypes.bool, + hasBulkActions: PropTypes.bool, + ids: PropTypes.arrayOf(PropTypes.any), + isRowSelectable: PropTypes.func, + isRowExpandable: PropTypes.func, + onSelect: PropTypes.func, + onToggleItem: PropTypes.func, + resource: PropTypes.string, + selectedIds: PropTypes.arrayOf(PropTypes.any), + setSort: PropTypes.func, +}; + +export interface DatagridHeaderProps { + children?: React.ReactNode; + classes?: ClassesOverride; + className?: string; + hasExpand?: boolean; + hasBulkActions?: boolean; + isRowSelectable?: (record: Record) => boolean; + isRowExpandable?: (record: Record) => boolean; + size?: 'medium' | 'small'; + // can be injected when using the component without context + currentSort?: SortPayload; + data?: RecordMap; + ids?: Identifier[]; + onSelect?: (ids: Identifier[]) => void; + onToggleItem?: (id: Identifier) => void; + resource?: string; + selectedIds?: Identifier[]; + setSort?: (sort: string, order?: string) => void; +} + +DatagridHeader.displayName = 'DatagridHeader'; diff --git a/packages/ra-ui-materialui/src/list/datagrid/index.ts b/packages/ra-ui-materialui/src/list/datagrid/index.ts index 3ca2435df39..7eb6e18ce3b 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/index.ts +++ b/packages/ra-ui-materialui/src/list/datagrid/index.ts @@ -16,6 +16,8 @@ import DatagridRow, { import ExpandRowButton, { ExpandRowButtonProps } from './ExpandRowButton'; import useDatagridStyles from './useDatagridStyles'; +export * from './DatagridHeader'; + export { Datagrid, DatagridLoading,