|
- "use client";
-
- import * as React from "react";
- import List from "@mui/material/List";
- import ListItem from "@mui/material/ListItem";
- import ListItemText from "@mui/material/ListItemText";
- import ListItemIcon from "@mui/material/ListItemIcon";
- import Checkbox from "@mui/material/Checkbox";
- import IconButton from "@mui/material/Fab";
- import Divider from "@mui/material/Divider";
- import ChevronLeft from "@mui/icons-material/ChevronLeft";
- import ChevronRight from "@mui/icons-material/ChevronRight";
- import intersection from "lodash/intersection";
- import differenceBy from "lodash/differenceBy";
- import Stack from "@mui/material/Stack";
- import Paper from "@mui/material/Paper";
- import Typography from "@mui/material/Typography";
- import ListSubheader from "@mui/material/ListSubheader";
- import groupBy from "lodash/groupBy";
- import uniqBy from "lodash/uniqBy";
- import { useTranslation } from "react-i18next";
- import union from "lodash/union";
- import intersectionBy from "lodash/intersectionBy";
-
- export interface LabelGroup {
- id: number;
- name: string;
- }
-
- export interface LabelWithId {
- id: number;
- label: string;
- group?: LabelGroup;
- }
-
- export interface TransferListProps {
- allItems: LabelWithId[];
- selectedItems: LabelWithId[];
- onChange: (selectedItems: LabelWithId[]) => void;
- allItemsLabel: string;
- selectedItemsLabel: string;
- }
-
- interface ItemListProps {
- items: LabelWithId[];
- checkedItems: LabelWithId[];
- label: string;
- handleToggleAll: (
- items: LabelWithId[],
- checkedItems: LabelWithId[],
- ) => React.MouseEventHandler;
- handleToggleAllInGroup: (
- groupItems: LabelWithId[],
- checkedItems: LabelWithId[],
- ) => React.MouseEventHandler;
- handleToggle: (item: LabelWithId) => React.MouseEventHandler;
- }
-
- const ItemList: React.FC<ItemListProps> = ({
- items,
- checkedItems,
- label,
- handleToggle,
- handleToggleAll,
- handleToggleAllInGroup,
- }) => {
- const { t } = useTranslation();
- const groups: LabelGroup[] = uniqBy(
- [
- ...items.reduce<LabelGroup[]>((acc, item) => {
- return item.group ? [...acc, item.group] : acc;
- }, []),
- // Items with no group
- { id: 0, name: t("Ungrouped") },
- ],
- "id",
- );
- const groupedItems = groupBy(items, (item) => item.group?.id ?? 0);
-
- return (
- <Paper sx={{ width: "100%" }} variant="outlined">
- <List
- sx={{
- height: 400,
- bgcolor: "background.paper",
- overflow: "auto",
- }}
- disablePadding
- dense
- component="ul"
- subheader={
- <ListSubheader
- sx={{ zIndex: 2 }}
- disableGutters
- component="li"
- onClick={handleToggleAll(items, checkedItems)}
- >
- <Stack direction="row" paddingY={1} paddingX={2}>
- <ListItemIcon>
- <Checkbox
- checked={
- checkedItems.length === items.length && items.length !== 0
- }
- indeterminate={
- checkedItems.length !== items.length &&
- checkedItems.length !== 0
- }
- disabled={items.length === 0}
- />
- </ListItemIcon>
- <Stack>
- <Typography variant="subtitle2">{label}</Typography>
- <Typography variant="caption">{`${checkedItems.length}/${
- items.length
- } ${t("selected")}`}</Typography>
- </Stack>
- </Stack>
- <Divider />
- </ListSubheader>
- }
- >
- {groups.map((group) => {
- const groupItems = groupedItems[group.id];
- const selectedGroupItems = intersectionBy(
- checkedItems,
- groupItems,
- "id",
- );
- if (!groupItems) return null;
-
- return (
- <React.Fragment key={group.id}>
- <ListSubheader
- disableSticky
- onClick={handleToggleAllInGroup(groupItems, checkedItems)}
- sx={{
- paddingBlock: 2,
- lineHeight: 1.8,
- display: "flex",
- alignItems: "center",
- }}
- >
- <ListItemIcon>
- <Checkbox
- checked={selectedGroupItems.length === groupItems.length}
- indeterminate={
- selectedGroupItems.length !== groupItems.length &&
- selectedGroupItems.length !== 0
- }
- />
- </ListItemIcon>
- {group.name}
- </ListSubheader>
- {groupItems.map((item) => {
- return (
- <ListItem key={item.id} onClick={handleToggle(item)}>
- <ListItemIcon>
- <Checkbox
- checked={checkedItems.includes(item)}
- tabIndex={-1}
- />
- </ListItemIcon>
- <ListItemText primary={item.label} />
- </ListItem>
- );
- })}
- </React.Fragment>
- );
- })}
- </List>
- </Paper>
- );
- };
-
- const TransferList: React.FC<TransferListProps> = ({
- allItems,
- selectedItems,
- allItemsLabel,
- selectedItemsLabel,
- onChange,
- }) => {
- // Keep a map for the original order of items
- const sortMap = React.useMemo(() => {
- return allItems.reduce<{ [id: string]: number }>(
- (acc, item, index) => ({ ...acc, [item.id]: index }),
- {},
- );
- }, [allItems]);
- const compareFn = React.useCallback(
- (a: LabelWithId, b: LabelWithId) => sortMap[a.id] - sortMap[b.id],
- [sortMap],
- );
-
- const [checkedList, setCheckedList] = React.useState<LabelWithId[]>([]);
- const [leftList, setLeftList] = React.useState<LabelWithId[]>(
- differenceBy(allItems, selectedItems, "id"),
- );
-
- React.useEffect(() => {
- setLeftList(differenceBy(allItems, selectedItems, "id"));
- }, [allItems, selectedItems]);
-
- const rightList = selectedItems;
-
- const leftListChecked = intersection(checkedList, leftList);
- const rightListChecked = intersection(checkedList, rightList);
-
- const handleToggle = React.useCallback(
- (value: LabelWithId) => () => {
- const isChecked = checkedList.includes(value);
- const newCheckedList = isChecked
- ? differenceBy(checkedList, [value], "id")
- : [...checkedList, value];
-
- setCheckedList(newCheckedList);
- },
- [checkedList],
- );
-
- const handleToggleAll = React.useCallback(
- (items: LabelWithId[], checkedItems: LabelWithId[]) => () => {
- if (checkedItems.length === items.length) {
- setCheckedList(differenceBy(checkedList, checkedItems, "id"));
- } else {
- setCheckedList([...checkedList, ...items]);
- }
- },
- [checkedList],
- );
-
- const handleCheckedRight = () => {
- onChange([...selectedItems, ...leftListChecked].sort(compareFn));
- setLeftList(differenceBy(leftList, leftListChecked, "id").sort(compareFn));
- setCheckedList(differenceBy(checkedList, leftListChecked, "id"));
- };
-
- const handleCheckedLeft = () => {
- setLeftList([...leftList, ...rightListChecked].sort(compareFn));
- onChange(differenceBy(rightList, rightListChecked, "id").sort(compareFn));
- setCheckedList(differenceBy(checkedList, rightListChecked, "id"));
- };
-
- const handleToggleAllInGroup = React.useCallback(
- (groupItems: LabelWithId[], checkedItems: LabelWithId[]) => () => {
- const selectedGroupItems = intersectionBy(checkedItems, groupItems, "id");
- if (selectedGroupItems.length !== groupItems.length) {
- setCheckedList(union(checkedList, groupItems));
- } else {
- setCheckedList(differenceBy(checkedList, groupItems, "id"));
- }
- },
- [checkedList],
- );
-
- return (
- <Stack spacing={2} direction="row" alignItems="center" position="relative">
- <ItemList
- items={leftList}
- checkedItems={leftListChecked}
- label={allItemsLabel}
- handleToggleAll={handleToggleAll}
- handleToggle={handleToggle}
- handleToggleAllInGroup={handleToggleAllInGroup}
- />
- <ItemList
- items={rightList}
- checkedItems={rightListChecked}
- label={selectedItemsLabel}
- handleToggleAll={handleToggleAll}
- handleToggle={handleToggle}
- handleToggleAllInGroup={handleToggleAllInGroup}
- />
- <Stack
- spacing={1}
- position="absolute"
- margin="0 !important"
- left="50%"
- sx={{ transform: "translateX(-50%)" }}
- >
- <IconButton
- color="secondary"
- size="small"
- onClick={handleCheckedRight}
- disabled={leftListChecked.length === 0}
- >
- <ChevronRight />
- </IconButton>
- <IconButton
- color="secondary"
- size="small"
- onClick={handleCheckedLeft}
- disabled={rightListChecked.length === 0}
- >
- <ChevronLeft />
- </IconButton>
- </Stack>
- </Stack>
- );
- };
-
- export default TransferList;
|