'How to enable bulk actions with Simple List

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.



Solution 1:[1]

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)}>
            &nbsp;
        </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>&nbsp;</Avatar>
                        </ListItemAvatar>
                    )}
                    <ListItemText
                        primary={
                            <div>
                                <Placeholder className={classes.primary} />
                                {hasTertiaryText && (
                                    <span className={classes.tertiary}>
                                        <Placeholder />
                                    </span>
                                )}
                            </div>
                        }
                        secondary={
                            hasSecondaryText ? <Placeholder /> : undefined
                        }
                    />
                    {hasRightAvatarOrIcon && (
                        <ListItemSecondaryAction>
                            <Avatar>&nbsp;</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;

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Richard V