Reputation: 2308
I need to create a different view for a list so that it can be viewed on mobile devices. It was suggested I use SimpleList, but I still want the user to be able to select multiple items in the list and complete bulk actions. Is there a way to do this? There isn't much documentation on this scenario in the React Admin docs.
Upvotes: 1
Views: 1164
Reputation: 71
Answer for React-Admin v4.
As I have decided to update all of my makeStyle code using tss-react/mui, you will need to install it prior to using this version of SelectSimpleList. (npm i tss-react/mui)
Using this updated version, no changes to your code 'should' be required in order for it to function. bulkActionButtons have also been added and should function.
import * as React from 'react';
import PropTypes from 'prop-types';
import { isValidElement } from 'react';
import {
Avatar,
List,
ListItem,
ListItemAvatar,
ListItemIcon,
ListItemSecondaryAction,
ListItemText
} from '@mui/material';
import { makeStyles } from 'tss-react/mui';
import { Link } from 'react-router-dom';
import {
useCreatePath,
sanitizeListRestProps,
useListContext,
useResourceContext,
RecordContextProvider,
} from 'ra-core';
import Checkbox from '@mui/material/Checkbox';
import { useTimeout } from 'ra-core';
import classnames from 'classnames';
import { BulkActionsToolbar } from 'react-admin';
import { BulkDeleteButton } from 'react-admin';
const defaultBulkActionButtons = <BulkDeleteButton />;
const useStylesPlaceholder = makeStyles()((theme) =>{
return {
root: {
backgroundColor: theme.palette.grey[300],
display: 'flex',
}
}
});
const Placeholder = props => {
const { classes } = useStylesPlaceholder(props);
return (
<span className={classnames(classes.root, props.className)}>
</span>
);
};
const useStylesLoading = makeStyles()((theme) => {
return {
primary: {
width: '30vw',
display: 'inline-block',
marginBottom: theme.spacing(),
},
tertiary: { float: 'right', opacity: 0.541176, minWidth: '10vw' }
}
})
const times = (nbChildren, fn) =>
Array.from({ length: nbChildren }, (_, key) => fn(key));
const SimpleListLoading = props => {
const {
classes: classesOverride,
className,
hasLeftAvatarOrIcon,
hasRightAvatarOrIcon,
hasSecondaryText,
hasTertiaryText,
nbFakeLines = 5,
...rest
} = props;
const { classes } = useStylesLoading(props);
const oneSecondHasPassed = useTimeout(1000);
return oneSecondHasPassed ? (
<List className={className} {...rest}>
{times(nbFakeLines, key => (
<ListItem key={key}>
{hasLeftAvatarOrIcon && (
<ListItemAvatar>
<Avatar> </Avatar>
</ListItemAvatar>
)}
<ListItemText
primary={
<div>
<Placeholder className={classes.primary} />
{hasTertiaryText && (
<span className={classes.tertiary}>
<Placeholder />
</span>
)}
</div>
}
secondary={
hasSecondaryText ? <Placeholder /> : undefined
}
/>
{hasRightAvatarOrIcon && (
<ListItemSecondaryAction>
<Avatar> </Avatar>
</ListItemSecondaryAction>
)}
</ListItem>
))}
</List>
) : null;
};
SimpleListLoading.propTypes = {
className: PropTypes.string,
hasLeftAvatarOrIcon: PropTypes.bool,
hasRightAvatarOrIcon: PropTypes.bool,
hasSecondaryText: PropTypes.bool,
hasTertiaryText: PropTypes.bool,
nbFakeLines: PropTypes.number,
};
const useStyles = makeStyles()((theme) => {
return {
tertiary: { float: 'right', opacity: 0.541176 },
}
})
/**
* The <SimpleList> component renders a list of records as a material-ui <List>.
* It is usually used as a child of react-admin's <List> and <ReferenceManyField> components.
*
* Also widely used on Mobile.
*
* Props:
* - primaryText: function returning a React element (or some text) based on the record
* - secondaryText: same
* - tertiaryText: same
* - leftAvatar: function returning a React element based on the record
* - leftIcon: same
* - rightAvatar: same
* - rightIcon: same
* - linkType: 'edit' or 'show', or a function returning 'edit' or 'show' based on the record
* - rowStyle: function returning a style object based on (record, index)
*
* @example // Display all posts as a List
* const postRowStyle = (record, index) => ({
* backgroundColor: record.views >= 500 ? '#efe' : 'white',
* });
* export const PostList = (props) => (
* <List {...props}>
* <SimpleList
* primaryText={record => record.title}
* secondaryText={record => `${record.views} views`}
* tertiaryText={record =>
* new Date(record.published_at).toLocaleDateString()
* }
* rowStyle={postRowStyle}
* />
* </List>
* );
*/
const SelectSimpleList = props => {
const {
className,
classes: classesOverride,
bulkActionButtons = defaultBulkActionButtons,
leftAvatar,
leftIcon,
linkType = 'edit',
primaryText,
rightAvatar,
rightIcon,
secondaryText,
tertiaryText,
rowStyle,
isRowSelectable,
...rest
} = props;
const hasBulkActions = !!bulkActionButtons !== false;
const resource = useResourceContext(props);
const { data, isLoading, total, onToggleItem, selectedIds } = useListContext(props);
const { classes } = useStyles(props);
if (isLoading === true) {
return (
<SimpleListLoading
classes={classes}
className={className}
hasBulkActions={hasBulkActions}
hasLeftAvatarOrIcon={!!leftIcon || !!leftAvatar}
hasRightAvatarOrIcon={!!rightIcon || !!rightAvatar}
hasSecondaryText={!!secondaryText}
hasTertiaryText={!!tertiaryText}
/>
);
}
const isSelected = id => {
if (selectedIds.includes(id)){
return true;
}
return false;
}
return (
total > 0 && (
<>
{bulkActionButtons !== false ? (
<BulkActionsToolbar selectedIds={selectedIds}>
{isValidElement(bulkActionButtons)
? bulkActionButtons
: defaultBulkActionButtons}
</BulkActionsToolbar>
) : null}
<List className={className} {...sanitizeListRestProps(rest)}>
{data.map((record, rowIndex) => (
<RecordContextProvider key={record.id} value={record}>
<LinkOrNot
linkType={linkType}
resource={resource}
id={record.id}
key={record.id}
record={record}
style={
rowStyle
? rowStyle(record, rowIndex)
: undefined
}
>
{
!!isRowSelectable ? (
<>
{
!!isRowSelectable(record) ? (
<Checkbox
checked={isSelected(record.id)}
onChange={() => onToggleItem(record.id)}
color="primary"
onClick={(e) => e.stopPropagation()}
inputProps={{ 'aria-label': 'selected checkbox' }}
/>
) : (
<div style={{width: '46px'}} />
)
}
</>
) : (
<Checkbox
checked={isSelected(record.id)}
onChange={() => onToggleItem(record.id)}
color="primary"
onClick={(e) => e.stopPropagation()}
inputProps={{ 'aria-label': 'selected checkbox' }}
/>
)
}
{leftIcon && (
<ListItemIcon>
{leftIcon(record, record.id)}
</ListItemIcon>
)}
{leftAvatar && (
<ListItemAvatar>
<Avatar>{leftAvatar(record, record.id)}</Avatar>
</ListItemAvatar>
)}
<ListItemText
primary={
<div>
{primaryText(record, record.id)}
{tertiaryText && (
<span className={classes.tertiary}>
{tertiaryText(record, record.id)}
</span>
)}
</div>
}
secondary={
secondaryText && secondaryText(record, record.id)
}
/>
{(rightAvatar || rightIcon) && (
<ListItemSecondaryAction>
{rightAvatar && (
<Avatar>
{rightAvatar(record, record.id)}
</Avatar>
)}
{rightIcon && (
<ListItemIcon>
{rightIcon(record, record.id)}
</ListItemIcon>
)}
</ListItemSecondaryAction>
)}
</LinkOrNot>
</RecordContextProvider>
))}
</List>
</>
)
);
};
SelectSimpleList.propTypes = {
className: PropTypes.string,
classes: PropTypes.object,
leftAvatar: PropTypes.func,
leftIcon: PropTypes.func,
linkType: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool,
PropTypes.func,
]),
primaryText: PropTypes.func,
rightAvatar: PropTypes.func,
rightIcon: PropTypes.func,
secondaryText: PropTypes.func,
tertiaryText: PropTypes.func,
rowStyle: PropTypes.func,
};
const useLinkOrNotStyles = makeStyles()((theme) => {
return {
link: {
textDecoration: 'none',
color: 'inherit',
}
}
})
const LinkOrNot = ({
classes: classesOverride,
linkType,
resource,
id,
children,
record,
...rest
}) => {
const { classes } = useLinkOrNotStyles({ classes: classesOverride });
const createPath = useCreatePath();
const type =
typeof linkType === 'function' ? linkType(record, id) : linkType;
return type === false ? (
<ListItem
// @ts-ignore
component="div"
className={classes.link}
{...rest}
>
{children}
</ListItem>
) : (
// @ts-ignore
<ListItem
component={Link}
button={true}
to={createPath({ resource, id, type })}
className={classes.link}
{...rest}
>
{children}
</ListItem>
);
};
export default SelectSimpleList;
Upvotes: 1
Reputation: 71
I did not have enough reputation to vote this up, however, using the current SimpleList, I have made a selectable (bulk actions) version that works for my purposes.
It may be useful for you as well.
It's in regular JS, not TypeScript like the original.
To use it, simply import the file such as:
import SelectSimpleList from './SelectSimpleList'
and use it exactly the same way as the original SimpleList.
SelectSimpleList.js:
import * as React from 'react';
import PropTypes from 'prop-types';
import {
Avatar,
List,
//ListProps,
ListItem,
ListItemAvatar,
ListItemIcon,
ListItemSecondaryAction,
ListItemText,
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { Link } from 'react-router-dom';
import {
linkToRecord,
sanitizeListRestProps,
useListContext,
//Record,
//RecordMap,
//Identifier,
} from 'ra-core';
import Checkbox from '@material-ui/core/Checkbox';
import { useTimeout } from 'ra-core';
import classnames from 'classnames';
const useStylesPlaceholder = makeStyles(
theme => ({
root: {
backgroundColor: theme.palette.grey[300],
display: 'flex',
},
}),
{ name: 'RaPlaceholder' }
);
const Placeholder = props => {
const classes = useStylesPlaceholder(props);
return (
<span className={classnames(classes.root, props.className)}>
</span>
);
};
const useStylesLoading = makeStyles(
theme => ({
primary: {
width: '30vw',
display: 'inline-block',
marginBottom: theme.spacing(),
},
tertiary: { float: 'right', opacity: 0.541176, minWidth: '10vw' },
}),
{ name: 'RaSimpleListLoading' },
);
const times = (nbChildren, fn) =>
Array.from({ length: nbChildren }, (_, key) => fn(key));
const SimpleListLoading = props => {
const {
classes: classesOverride,
className,
hasLeftAvatarOrIcon,
hasRightAvatarOrIcon,
hasSecondaryText,
hasTertiaryText,
nbFakeLines = 5,
...rest
} = props;
const classes = useStylesLoading(props);
const oneSecondHasPassed = useTimeout(1000);
return oneSecondHasPassed ? (
<List className={className} {...rest}>
{times(nbFakeLines, key => (
<ListItem key={key}>
{hasLeftAvatarOrIcon && (
<ListItemAvatar>
<Avatar> </Avatar>
</ListItemAvatar>
)}
<ListItemText
primary={
<div>
<Placeholder className={classes.primary} />
{hasTertiaryText && (
<span className={classes.tertiary}>
<Placeholder />
</span>
)}
</div>
}
secondary={
hasSecondaryText ? <Placeholder /> : undefined
}
/>
{hasRightAvatarOrIcon && (
<ListItemSecondaryAction>
<Avatar> </Avatar>
</ListItemSecondaryAction>
)}
</ListItem>
))}
</List>
) : null;
};
SimpleListLoading.propTypes = {
className: PropTypes.string,
hasLeftAvatarOrIcon: PropTypes.bool,
hasRightAvatarOrIcon: PropTypes.bool,
hasSecondaryText: PropTypes.bool,
hasTertiaryText: PropTypes.bool,
nbFakeLines: PropTypes.number,
};
const useStyles = makeStyles(
{
tertiary: { float: 'right', opacity: 0.541176 },
},
{ name: 'RaSimpleList' }
);
/**
* The <SimpleList> component renders a list of records as a material-ui <List>.
* It is usually used as a child of react-admin's <List> and <ReferenceManyField> components.
*
* Also widely used on Mobile.
*
* Props:
* - primaryText: function returning a React element (or some text) based on the record
* - secondaryText: same
* - tertiaryText: same
* - leftAvatar: function returning a React element based on the record
* - leftIcon: same
* - rightAvatar: same
* - rightIcon: same
* - linkType: 'edit' or 'show', or a function returning 'edit' or 'show' based on the record
* - rowStyle: function returning a style object based on (record, index)
*
* @example // Display all posts as a List
* const postRowStyle = (record, index) => ({
* backgroundColor: record.views >= 500 ? '#efe' : 'white',
* });
* export const PostList = (props) => (
* <List {...props}>
* <SimpleList
* primaryText={record => record.title}
* secondaryText={record => `${record.views} views`}
* tertiaryText={record =>
* new Date(record.published_at).toLocaleDateString()
* }
* rowStyle={postRowStyle}
* />
* </List>
* );
*/
const SelectSimpleList = props => {
const {
className,
classes: classesOverride,
hasBulkActions,
leftAvatar,
leftIcon,
linkType = 'edit',
primaryText,
rightAvatar,
rightIcon,
secondaryText,
tertiaryText,
rowStyle,
...rest
} = props;
const { basePath, data, ids, loaded, total, onToggleItem, selectedIds } = useListContext(props);
const classes = useStyles(props);
if (loaded === false) {
return (
<SimpleListLoading
classes={classes}
className={className}
hasLeftAvatarOrIcon={!!leftIcon || !!leftAvatar}
hasRightAvatarOrIcon={!!rightIcon || !!rightAvatar}
hasSecondaryText={!!secondaryText}
hasTertiaryText={!!tertiaryText}
/>
);
}
const isSelected = id => {
if (selectedIds.includes(id)){
return true;
}
return false;
}
return (
total > 0 && (
<List className={className} {...sanitizeListRestProps(rest)}>
{ids.map((id, rowIndex) => (
<LinkOrNot
linkType={linkType}
basePath={basePath}
id={id}
key={id}
record={data[id]}
>
<ListItem
//onClick={() => {onToggleItem(id)}}
button={!!linkType}
style={
rowStyle
? rowStyle(data[id], rowIndex)
: undefined
}
>
<Checkbox
checked={isSelected(id)}
onChange={() => onToggleItem(id)}
color="primary"
onClick={(e) => e.stopPropagation()}
inputProps={{ 'aria-label': 'primary checkbox' }}
/>
{leftIcon && (
<ListItemIcon>
{leftIcon(data[id], id)}
</ListItemIcon>
)}
{leftAvatar && (
<ListItemAvatar>
<Avatar>{leftAvatar(data[id], id)}</Avatar>
</ListItemAvatar>
)}
<ListItemText
primary={
<div>
{primaryText(data[id], id)}
{tertiaryText && (
<span className={classes.tertiary}>
{tertiaryText(data[id], id)}
</span>
)}
</div>
}
secondary={
secondaryText && secondaryText(data[id], id)
}
/>
{(rightAvatar || rightIcon) && (
<ListItemSecondaryAction>
{rightAvatar && (
<Avatar>
{rightAvatar(data[id], id)}
</Avatar>
)}
{rightIcon && (
<ListItemIcon>
{rightIcon(data[id], id)}
</ListItemIcon>
)}
</ListItemSecondaryAction>
)}
</ListItem>
</LinkOrNot>
))}
</List>
)
);
};
SelectSimpleList.propTypes = {
className: PropTypes.string,
classes: PropTypes.object,
leftAvatar: PropTypes.func,
leftIcon: PropTypes.func,
linkType: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool,
PropTypes.func,
]),
primaryText: PropTypes.func,
rightAvatar: PropTypes.func,
rightIcon: PropTypes.func,
secondaryText: PropTypes.func,
tertiaryText: PropTypes.func,
rowStyle: PropTypes.func,
};
const useLinkOrNotStyles = makeStyles(
{
link: {
textDecoration: 'none',
color: 'inherit',
},
},
{ name: 'RaLinkOrNot' }
);
const LinkOrNot = ({
classes: classesOverride,
linkType,
basePath,
id,
children,
record,
}) => {
const classes = useLinkOrNotStyles({ classes: classesOverride });
const link =
typeof linkType === 'function' ? linkType(record, id) : linkType;
return link === 'edit' || link === true ? (
<Link to={linkToRecord(basePath, id)} className={classes.link}>
{children}
</Link>
) : link === 'show' ? (
<Link
to={`${linkToRecord(basePath, id)}/show`}
className={classes.link}
>
{children}
</Link>
) : (
<span>{children}</span>
);
};
export default SelectSimpleList;
Upvotes: 1