| @@ -65,30 +65,3 @@ export const fetchProjectCategories = cache(async () => { | |||
| }, | |||
| ); | |||
| }); | |||
| const mockProjects: ProjectResult[] = [ | |||
| { | |||
| id: 1, | |||
| code: "M1001", | |||
| name: "Consultancy Project A", | |||
| category: "Confirmed Project", | |||
| team: "TW", | |||
| client: "Client A", | |||
| }, | |||
| { | |||
| id: 2, | |||
| code: "M1002", | |||
| name: "Consultancy Project B", | |||
| category: "Project to be bidded", | |||
| team: "WY", | |||
| client: "Client B", | |||
| }, | |||
| { | |||
| id: 3, | |||
| code: "S1001", | |||
| name: "Consultancy Project C", | |||
| category: "Confirmed Project", | |||
| team: "WY", | |||
| client: "Client C", | |||
| }, | |||
| ]; | |||
| @@ -20,6 +20,9 @@ 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, | |||
| @@ -53,15 +56,28 @@ export const MultiSelectList: React.FC<TransferListProps> = ({ | |||
| [allItems, onChange], | |||
| ); | |||
| const handleToggleAll = useCallback( | |||
| () => () => { | |||
| if (selectedItems.length === allItems.length) { | |||
| 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(allItems); | |||
| onChange(differenceBy(selectedItems, groupItems, "id")); | |||
| } | |||
| }, | |||
| [allItems, onChange, selectedItems.length], | |||
| [onChange, selectedItems], | |||
| ); | |||
| const { t } = useTranslation(); | |||
| @@ -117,7 +133,7 @@ export const MultiSelectList: React.FC<TransferListProps> = ({ | |||
| direction="row" | |||
| paddingY={1} | |||
| paddingX={3} | |||
| onClick={handleToggleAll()} | |||
| onClick={handleToggleAll} | |||
| > | |||
| <ListItemIcon> | |||
| <Checkbox | |||
| @@ -141,11 +157,30 @@ export const MultiSelectList: React.FC<TransferListProps> = ({ | |||
| </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}`}> | |||
| {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 ( | |||
| @@ -19,6 +19,8 @@ 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; | |||
| @@ -47,6 +49,10 @@ interface ItemListProps { | |||
| items: LabelWithId[], | |||
| checkedItems: LabelWithId[], | |||
| ) => React.MouseEventHandler; | |||
| handleToggleAllInGroup: ( | |||
| groupItems: LabelWithId[], | |||
| checkedItems: LabelWithId[], | |||
| ) => React.MouseEventHandler; | |||
| handleToggle: (item: LabelWithId) => React.MouseEventHandler; | |||
| } | |||
| @@ -56,6 +62,7 @@ const ItemList: React.FC<ItemListProps> = ({ | |||
| label, | |||
| handleToggle, | |||
| handleToggleAll, | |||
| handleToggleAllInGroup, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const groups: LabelGroup[] = uniqBy( | |||
| @@ -111,14 +118,34 @@ const ItemList: React.FC<ItemListProps> = ({ | |||
| > | |||
| {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 | |||
| sx={{ paddingBlock: 2, lineHeight: 1.8 }} | |||
| 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) => { | |||
| @@ -210,6 +237,18 @@ const TransferList: React.FC<TransferListProps> = ({ | |||
| 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 | |||
| @@ -218,6 +257,7 @@ const TransferList: React.FC<TransferListProps> = ({ | |||
| label={allItemsLabel} | |||
| handleToggleAll={handleToggleAll} | |||
| handleToggle={handleToggle} | |||
| handleToggleAllInGroup={handleToggleAllInGroup} | |||
| /> | |||
| <ItemList | |||
| items={rightList} | |||
| @@ -225,6 +265,7 @@ const TransferList: React.FC<TransferListProps> = ({ | |||
| label={selectedItemsLabel} | |||
| handleToggleAll={handleToggleAll} | |||
| handleToggle={handleToggle} | |||
| handleToggleAllInGroup={handleToggleAllInGroup} | |||
| /> | |||
| <Stack | |||
| spacing={1} | |||