diff --git a/docs/SimpleList.md b/docs/SimpleList.md index 2bf275370c..a8df09b35a 100644 --- a/docs/SimpleList.md +++ b/docs/SimpleList.md @@ -28,7 +28,7 @@ export const PostList = () => ( primaryText={record => record.title} secondaryText={record => `${record.views} views`} tertiaryText={record => new Date(record.published_at).toLocaleDateString()} - linkType={record => record.canEdit ? "edit" : "show"} + rowClick={record => record.canEdit ? "edit" : "show"} rowSx={record => ({ backgroundColor: record.nb_views >= 500 ? '#efe' : 'white' })} /> @@ -44,7 +44,7 @@ export const PostList = () => ( | `primaryText` | Optional | mixed | record representation | The primary text to display. | | `secondaryText` | Optional | mixed | | The secondary text to display. | | `tertiaryText` | Optional | mixed | | The tertiary text to display. | -| `linkType` | Optional |mixed | `"edit"` | The target of each item click. | +| `rowClick` | Optional |mixed | `"edit"` | The action to trigger when the user clicks on a row. | | `leftAvatar` | Optional | function | | A function returning an `` component to display before the primary text. | | `leftIcon` | Optional | function | | A function returning an `` component to display before the primary text. | | `rightAvatar` | Optional | function | | A function returning an `` component to display after the primary text. | @@ -80,9 +80,9 @@ This prop should be a function returning an `` component. When present, This prop should be a function returning an `` component. When present, the `` renders a `` before the `` -## `linkType` +## `rowClick` -The `` items link to the edition page by default. You can also set the `linkType` prop to `show` directly to link to the `` page instead. +The `` items link to the edition page by default. You can also set the `rowClick` prop to `show` directly to link to the `` page instead. ```jsx import { List, SimpleList } from 'react-admin'; @@ -93,17 +93,18 @@ export const PostList = () => ( primaryText={record => record.title} secondaryText={record => `${record.views} views`} tertiaryText={record => new Date(record.published_at).toLocaleDateString()} - linkType="show" + rowClick="show" /> ); ``` -`linkType` accepts the following values: +`rowClick` accepts the following values: -* `linkType="edit"`: links to the edit page. This is the default behavior. -* `linkType="show"`: links to the show page. -* `linkType={false}`: does not create any link. +* `rowClick="edit"`: links to the edit page. This is the default behavior. +* `rowClick="show"`: links to the show page. +* `rowClick={false}`: does not link to anything. +* `rowClick={(id, resource, record) => path}`: path can be any of the above values ## `primaryText` @@ -254,7 +255,7 @@ export const PostList = () => { primaryText={record => record.title} secondaryText={record => `${record.views} views`} tertiaryText={record => new Date(record.published_at).toLocaleDateString()} - linkType={record => record.canEdit ? "edit" : "show"} + rowClick={record => record.canEdit ? "edit" : "show"} /> ) : ( diff --git a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx index 72c071530b..dd308c8282 100644 --- a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx +++ b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx @@ -1,11 +1,7 @@ -import * as React from 'react'; -import { styled } from '@mui/material/styles'; import type { SxProps } from '@mui/material'; -import { isValidElement, ReactNode, ReactElement } from 'react'; import { Avatar, List, - ListProps, ListItem, ListItemAvatar, ListItemButton, @@ -13,22 +9,31 @@ import { ListItemProps, ListItemSecondaryAction, ListItemText, + ListProps, } from '@mui/material'; -import { Link } from 'react-router-dom'; +import { styled } from '@mui/material/styles'; import { Identifier, RaRecord, RecordContextProvider, sanitizeListRestProps, + useGetRecordRepresentation, useListContextWithProps, useResourceContext, - useGetRecordRepresentation, - useCreatePath, useTranslate, } from 'ra-core'; +import * as React from 'react'; +import { isValidElement, ReactElement, ReactNode } from 'react'; +import { Link } from 'react-router-dom'; -import { SimpleListLoading } from './SimpleListLoading'; import { ListNoResults } from '../ListNoResults'; +import { SimpleListLoading } from './SimpleListLoading'; + +import { useGetPathForRecordCallback } from 'ra-core'; +import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { RowClickFunction } from 'react-admin'; /** * The component renders a list of records as a Material UI . @@ -44,7 +49,8 @@ import { ListNoResults } from '../ListNoResults'; * - leftIcon: same * - rightAvatar: same * - rightIcon: same - * - linkType: 'edit' or 'show', or a function returning 'edit' or 'show' based on the record + * - linkType: deprecated 'edit' or 'show', or a function returning 'edit' or 'show' based on the record + * - rowClick: The action to trigger when the user clicks on a row. @see https://marmelab.com/react-admin/Datagrid.html#rowclick * - rowStyle: function returning a style object based on (record, index) * - rowSx: function returning a sx object based on (record, index) * @@ -65,7 +71,7 @@ import { ListNoResults } from '../ListNoResults'; * * ); */ -export const SimpleList = ( +export const SimpleListJarvi = ( props: SimpleListProps ) => { const { @@ -74,7 +80,8 @@ export const SimpleList = ( hasBulkActions, leftAvatar, leftIcon, - linkType = 'edit', + linkType, + rowClick = 'edit', primaryText, rightAvatar, rightIcon, @@ -84,8 +91,9 @@ export const SimpleList = ( rowStyle, ...rest } = props; - const { data, isPending, total } = - useListContextWithProps(props); + const { data, isPending, total } = useListContextWithProps( + props + ); const resource = useResourceContext(props); const getRecordRepresentation = useGetRecordRepresentation(resource); const translate = useTranslate(); @@ -131,7 +139,9 @@ export const SimpleList = ( ( _: primaryText, }) : isValidElement(primaryText) - ? primaryText - : // @ts-ignore - primaryText( - record, - record.id - ) + ? primaryText + : // @ts-ignore + primaryText(record, record.id) : getRecordRepresentation(record)} {!!tertiaryText && @@ -189,14 +196,14 @@ export const SimpleList = ( } ) : isValidElement( - tertiaryText - ) - ? tertiaryText - : // @ts-ignore - tertiaryText( - record, - record.id - )} + tertiaryText + ) + ? tertiaryText + : // @ts-ignore + tertiaryText( + record, + record.id + )} ))} @@ -209,9 +216,9 @@ export const SimpleList = ( _: secondaryText, }) : isValidElement(secondaryText) - ? secondaryText - : // @ts-ignore - secondaryText(record, record.id)) + ? secondaryText + : // @ts-ignore + secondaryText(record, record.id)) } /> {(rightAvatar || rightIcon) && ( @@ -249,7 +256,27 @@ export interface SimpleListProps leftAvatar?: FunctionToElement; leftIcon?: FunctionToElement; primaryText?: FunctionToElement | ReactElement | string; + /** + * @deprecated use rowClick instead + */ linkType?: string | FunctionLinkType | false; + + /** + * The action to trigger when the user clicks on a row. + * + * @see https://marmelab.com/react-admin/Datagrid.html#rowclick + * @example + * import { List, Datagrid } from 'react-admin'; + * + * export const PostList = () => ( + * + * + * ... + * + * + * ); + */ + rowClick?: string | RowClickFunction | false; rightAvatar?: FunctionToElement; rightIcon?: FunctionToElement; secondaryText?: FunctionToElement | ReactElement | string; @@ -270,35 +297,61 @@ const LinkOrNot = ( ) => { const { classes: classesOverride, + //@deprecated: use rowClick instead linkType, + rowClick, resource, id, children, record, ...rest } = props; - const createPath = useCreatePath(); - const type = - typeof linkType === 'function' ? linkType(record, id) : linkType; - if (type === false) { - return ( - - {children} - - ); - } + const navigate = useNavigate(); + const getPathForRecord = useGetPathForRecordCallback(); + const handleClick = useCallback( + async event => { + event.persist(); + const link = + // v this is to maintain compatibility with deprecated linkType + typeof linkType === 'function' + ? linkType(record, id) + : typeof linkType !== 'undefined' + ? linkType + : // v this is the new way to handle links + typeof rowClick === 'function' + ? (record, resource) => + rowClick(record.id, resource, record) + : typeof rowClick !== 'undefined' + ? rowClick + : 'edit'; + const path = await getPathForRecord({ + record, + resource, + link, + }); + console.debug('LinkOrNot handleClick', { + event, + path, + record, + resource, + link, + rowClick, + linkType, + }); + if (path === false || path == null) { + return; + } + navigate(path, { + state: { _scrollToTop: true }, + }); + }, + [record, resource, rowClick, navigate, getPathForRecord] + ); + return ( // @ts-ignore - + {children} ); @@ -307,7 +360,9 @@ const LinkOrNot = ( export type FunctionLinkType = (record: RaRecord, id: Identifier) => string; export interface LinkOrNotProps { - linkType: string | FunctionLinkType | false; + // @deprecated: use rowClick instead + linkType?: string | FunctionLinkType | false; + rowClick?: string | RowClickFunction | false; resource?: string; id: Identifier; record: RaRecord; diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridConfigurable.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridConfigurable.tsx index 345d84c1b7..052717a946 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridConfigurable.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridConfigurable.tsx @@ -1,10 +1,10 @@ -import * as React from 'react'; import { - useResourceContext, usePreference, + useResourceContext, useStore, useTranslate, } from 'ra-core'; +import * as React from 'react'; import { Configurable } from '../../preferences'; import { Datagrid, DatagridProps } from './Datagrid'; @@ -50,6 +50,11 @@ export const DatagridConfigurable = ({ ConfigurableDatagridColumn[] >(`preferences.${finalPreferenceKey}.availableColumns`, []); + const [columns, setColumns] = useStore( + `preferences.${finalPreferenceKey}.columns`, + [] + ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_, setOmit] = useStore( `preferences.${finalPreferenceKey}.omit`, @@ -58,7 +63,7 @@ export const DatagridConfigurable = ({ React.useEffect(() => { // first render, or the preference have been cleared - const columns = React.Children.toArray(props.children) + const newAvailableColumns = React.Children.toArray(props.children) .filter(child => React.isValidElement(child)) .map((child: React.ReactElement, index) => ({ index: String(index), @@ -75,8 +80,74 @@ export const DatagridConfigurable = ({ _: `Unlabeled column #%{column}`, }), })); - if (columns.length !== availableColumns.length) { - setAvailableColumns(columns); + const hasChanged = newAvailableColumns.some(column => { + const availableColumn = availableColumns.find( + availableColumn => + (!!availableColumn.source && + availableColumn.source === column?.source) || + (!!availableColumn.label && + availableColumn.label === column?.label) + ); + return !availableColumn || availableColumn.index !== column.index; + }); + if (hasChanged) { + // first we need to update the columns indexes to match the new availableColumns so we keep the same order + const newColumnsSortedAsOldColumns = columns.flatMap(column => { + const oldColumn = availableColumns.find( + availableColumn => availableColumn.index === column + ); + const newColumn = newAvailableColumns.find( + availableColumn => + (!!availableColumn.source && + availableColumn.source === oldColumn?.source) || + (!!availableColumn.label && + availableColumn.label === oldColumn?.label) + ); + return newColumn?.index ? [newColumn.index] : []; + }); + setColumns([ + // we add the old columns in the same order as before + ...newColumnsSortedAsOldColumns, + // then we add at the new columns which are not omited + ...newAvailableColumns + .filter( + c => + !availableColumns.some( + ac => + (!!ac.source && ac.source === c.source) || + (!!ac.label && ac.label === c.label) + ) && !omit?.includes(c.source as string) + ) + .map(c => c.index), + ]); + + // Then we update the available columns to include the new columns while keeping the same order as before + const newAvailableColumnsSortedAsBefore = [ + // First the existing columns, in the same order + ...(availableColumns + .map(oldAvailableColumn => + newAvailableColumns.find( + c => + (!!c.source && + c.source === oldAvailableColumn.source) || + (!!c.label && + c.label === oldAvailableColumn.label) + ) + ) + .filter(c => !!c) as ConfigurableDatagridColumn[]), // Remove undefined columns + // Then the new columns + ...newAvailableColumns.filter( + c => + !availableColumns.some( + oldAvailableColumn => + (!!oldAvailableColumn.source && + oldAvailableColumn.source === c.source) || + (!!oldAvailableColumn.label && + oldAvailableColumn.label === c.label) + ) + ), + ]; + setAvailableColumns(newAvailableColumnsSortedAsBefore); setOmit(omit); } }, [availableColumns]); // eslint-disable-line react-hooks/exhaustive-deps