Mr. Will
Mr. Will

Reputation: 2308

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.

Upvotes: 1

Views: 1164

Answers (2)

Richard V
Richard V

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)}>
            &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;

Upvotes: 1

Richard V
Richard V

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)}>
            &nbsp;
        </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>&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(
    {
        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

Related Questions