|
- "use client";
-
- import {
- Box,
- Checkbox,
- Divider,
- FormControl,
- InputLabel,
- ListItemIcon,
- ListItemText,
- ListSubheader,
- MenuItem,
- Select,
- SelectChangeEvent,
- Stack,
- Typography,
- } from "@mui/material";
- import React, { useCallback } from "react";
- import { LabelGroup, LabelWithId, TransferListProps } from "./TransferList";
- import { useTranslation } from "react-i18next";
- import uniqBy from "lodash/uniqBy";
- import groupBy from "lodash/groupBy";
- import intersectionBy from "lodash/intersectionBy";
- import union from "lodash/union";
- import differenceBy from "lodash/differenceBy";
-
- export const MultiSelectList: React.FC<TransferListProps> = ({
- allItems,
- selectedItems,
- selectedItemsLabel,
- allItemsLabel,
- onChange,
- }) => {
- // Keep a map for the original order of items
- const sortMap = React.useMemo(() => {
- return allItems.reduce<{ [id: string]: LabelWithId & { index: number } }>(
- (acc, item, index) => ({ ...acc, [item.id]: { ...item, index } }),
- {},
- );
- }, [allItems]);
- const compareFn = React.useCallback(
- (a: number, b: number) => sortMap[a].index - sortMap[b].index,
- [sortMap],
- );
-
- const handleChange = useCallback(
- (event: SelectChangeEvent<number[]>) => {
- const {
- target: { value },
- } = event;
- const selectedValues =
- typeof value === "string" ? [Number(value)] : value;
-
- onChange(allItems.filter((item) => selectedValues.includes(item.id)));
- },
- [allItems, onChange],
- );
-
- const handleToggleAll = useCallback(() => {
- if (selectedItems.length === allItems.length) {
- onChange([]);
- } else {
- onChange(allItems);
- }
- }, [allItems, onChange, selectedItems.length]);
-
- const handleToggleAllInGroup = useCallback(
- (groupItems: LabelWithId[]) => () => {
- const selectedGroupItems = intersectionBy(
- selectedItems,
- groupItems,
- "id",
- );
- if (selectedGroupItems.length !== groupItems.length) {
- onChange(union(selectedItems, groupItems));
- } else {
- onChange(differenceBy(selectedItems, groupItems, "id"));
- }
- },
- [onChange, selectedItems],
- );
-
- const { t } = useTranslation();
- const groups: LabelGroup[] = uniqBy(
- [
- ...allItems.reduce<LabelGroup[]>((acc, item) => {
- return item.group ? [...acc, item.group] : acc;
- }, []),
- // Items with no group
- { id: 0, name: t("Ungrouped") },
- ],
- "id",
- );
- const groupedItems = groupBy(allItems, (item) => item.group?.id ?? 0);
-
- return (
- <Box>
- <FormControl fullWidth>
- <InputLabel>{selectedItemsLabel}</InputLabel>
- <Select
- multiple
- value={selectedItems.map((item) => item.id)}
- onChange={handleChange}
- renderValue={(values) => {
- return (
- <Stack spacing={2}>
- {values.toSorted(compareFn).map((value) => (
- <Typography key={value} whiteSpace="normal">
- {sortMap[value]?.label}
- </Typography>
- ))}
- </Stack>
- );
- }}
- MenuProps={{
- slotProps: {
- paper: {
- sx: { maxHeight: 400 },
- },
- },
- anchorOrigin: {
- vertical: "top",
- horizontal: "left",
- },
- transformOrigin: {
- vertical: "top",
- horizontal: "left",
- },
- }}
- >
- <ListSubheader disableGutters sx={{ zIndex: 1 }}>
- <Stack
- direction="row"
- paddingY={1}
- paddingX={3}
- onClick={handleToggleAll}
- >
- <ListItemIcon>
- <Checkbox
- disableRipple
- checked={
- selectedItems.length === allItems.length &&
- allItems.length !== 0
- }
- indeterminate={
- selectedItems.length !== allItems.length &&
- selectedItems.length !== 0
- }
- />
- </ListItemIcon>
- <Stack>
- <Typography variant="subtitle2">{allItemsLabel}</Typography>
- <Typography variant="caption">{`${selectedItems.length}/${allItems.length} selected`}</Typography>
- </Stack>
- </Stack>
- <Divider />
- </ListSubheader>
- {groups.flatMap((group) => {
- const groupItems = groupedItems[group.id];
- const selectedGroupItems = intersectionBy(
- selectedItems,
- groupItems,
- "id",
- );
- if (!groupItems) return null;
-
- return [
- <ListSubheader disableSticky key={`${group.id}-${group.name}`}>
- <Stack
- onClick={handleToggleAllInGroup(groupItems)}
- direction="row"
- paddingX={1}
- >
- <Checkbox
- disableRipple
- checked={selectedGroupItems.length === groupItems.length}
- indeterminate={
- selectedGroupItems.length !== groupItems.length &&
- selectedGroupItems.length !== 0
- }
- />
- {group.name}
- </Stack>
- </ListSubheader>,
- ...groupItems.map((item) => {
- return (
- <MenuItem key={item.id} value={item.id} disableRipple>
- <Checkbox
- checked={Boolean(
- selectedItems.find(
- (selected) => selected.id === item.id,
- ),
- )}
- disableRipple
- />
- <ListItemText sx={{ whiteSpace: "normal" }}>
- {item.label}
- </ListItemText>
- </MenuItem>
- );
- }),
- ];
- })}
- </Select>
- </FormControl>
- </Box>
- );
- };
-
- export default MultiSelectList;
|