如何使用简单列表启用批量操作

How to enable bulk actions with Simple List

我需要为列表创建不同的视图,以便可以在移动设备上查看它。有人建议我使用 SimpleList,但我仍然希望用户能够 select 列表中的多个项目并完成批量操作。有没有办法做到这一点? React Admin 文档中没有太多关于这种情况的文档。

我没有足够的声誉来投票,但是,使用当前的 SimpleList,我制作了一个适合我的目的的可选(批量操作)版本。

它可能对你也有用。

它使用的是常规 JS,而不是原始的 TypeScript。

要使用它,只需导入文件,例如:

import SelectSimpleList from './SelectSimpleList'

并以与原始 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;

React-Admin v4.

的答案

由于我决定使用 tss-react/mui 更新我所有的 makeStyle 代码,您需要在使用此版本的 SelectSimpleList 之前安装它。 (npm i tss-react/mui)

使用此更新版本,无需更改代码 'should' 即可正常运行。还添加了 bulkActionButtons 并且应该起作用。

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;