Local modifications to ClusterLabs/Anvil by Alteeve
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

285 lines
7.5 KiB

import {
Add as MUIAddIcon,
Delete as MUIDeleteIcon,
Done as MUIDoneIcon,
Edit as MUIEditIcon,
} from '@mui/icons-material';
import {
Box as MUIBox,
List as MUIList,
ListItem as MUIListItem,
ListItemButton,
ListItemIcon as MUIListItemIcon,
SxProps,
Theme,
} from '@mui/material';
import {
FC,
ForwardedRef,
forwardRef,
useCallback,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import { v4 as uuidv4 } from 'uuid';
import { BLUE, BORDER_RADIUS, GREY, RED } from '../lib/consts/DEFAULT_THEME';
import Checkbox from './Checkbox';
import Divider from './Divider';
import FlexBox from './FlexBox';
import IconButton from './IconButton';
import { BodyText } from './Text';
const List = forwardRef(
<T,>(
{
allowCheckAll: isAllowCheckAll = false,
allowEdit: isAllowEdit = false,
allowItemButton: isAllowItemButton = false,
edit: isEdit = false,
flexBoxProps,
header,
headerSpacing = '.3em',
initialCheckAll = false,
insertHeader: isInsertHeader = true,
listEmpty,
listItemIconMinWidth = '56px',
listItemKeyPrefix = uuidv4(),
listItemProps: { sx: listItemSx, ...restListItemProps } = {},
listItems,
listProps: { sx: listSx, ...restListProps } = {},
onAdd,
onDelete,
onEdit,
onAllCheckboxChange,
onItemCheckboxChange,
onItemClick,
renderListItem = (key) => <BodyText>{key}</BodyText>,
renderListItemCheckboxState,
scroll: isScroll = false,
// Input props that depend on other input props.
allowAddItem: isAllowAddItem = isAllowEdit,
allowCheckItem: isAllowCheckItem = isAllowEdit,
allowDelete: isAllowDelete = isAllowEdit,
allowEditItem: isAllowEditItem = isAllowEdit,
}: ListProps<T>,
ref: ForwardedRef<ListForwardedRefContent>,
) => {
const [isCheckAll, setIsCheckAll] = useState<boolean>(initialCheckAll);
const checkAllMinWidth = useMemo(
() => `calc(${listItemIconMinWidth} - ${headerSpacing})`,
[headerSpacing, listItemIconMinWidth],
);
const addItemButton = useMemo(
() =>
isAllowAddItem ? (
<IconButton onClick={onAdd} size="small">
<MUIAddIcon />
</IconButton>
) : undefined,
[isAllowAddItem, onAdd],
);
const deleteItemButton = useMemo(
() =>
isEdit && isAllowDelete ? (
<IconButton
onClick={onDelete}
size="small"
sx={{
backgroundColor: RED,
color: GREY,
'&:hover': { backgroundColor: `${RED}F0` },
}}
>
<MUIDeleteIcon />
</IconButton>
) : undefined,
[isAllowDelete, isEdit, onDelete],
);
const editItemButton = useMemo(() => {
if (isAllowEditItem) {
return (
<IconButton onClick={onEdit} size="small">
{isEdit ? <MUIDoneIcon sx={{ color: BLUE }} /> : <MUIEditIcon />}
</IconButton>
);
}
return undefined;
}, [isAllowEditItem, isEdit, onEdit]);
const checkAllElement = useMemo(() => {
let element;
if (isEdit && isAllowCheckItem) {
element = isAllowCheckAll ? (
<MUIBox sx={{ minWidth: checkAllMinWidth }}>
<Checkbox
checked={isCheckAll}
edge="start"
onChange={(...args) => {
const [, isChecked] = args;
onAllCheckboxChange?.call(null, ...args);
setIsCheckAll(isChecked);
}}
/>
</MUIBox>
) : (
<Divider sx={{ minWidth: checkAllMinWidth }} />
);
}
return element;
}, [
checkAllMinWidth,
isAllowCheckAll,
isAllowCheckItem,
isCheckAll,
isEdit,
onAllCheckboxChange,
]);
const headerElement = useMemo(() => {
const headerType = typeof header;
return isInsertHeader && header ? (
<FlexBox row spacing={headerSpacing} sx={{ height: '2.4em' }}>
{checkAllElement}
{['boolean', 'string'].includes(headerType) ? (
<>
{headerType === 'string' && <BodyText>{header}</BodyText>}
<Divider sx={{ flexGrow: 1 }} />
</>
) : (
header
)}
{deleteItemButton}
{editItemButton}
{addItemButton}
</FlexBox>
) : (
header
);
}, [
addItemButton,
checkAllElement,
deleteItemButton,
editItemButton,
header,
headerSpacing,
isInsertHeader,
]);
const listEmptyElement = useMemo(
() =>
typeof listEmpty === 'string' ? (
<BodyText align="center">{listEmpty}</BodyText>
) : (
listEmpty
),
[listEmpty],
);
const listItemCheckbox = useCallback(
(key: string, checked?: boolean) =>
isEdit && isAllowCheckItem ? (
<MUIListItemIcon sx={{ minWidth: listItemIconMinWidth }}>
<Checkbox
checked={checked}
edge="start"
onChange={(...args) =>
onItemCheckboxChange?.call(null, key, ...args)
}
/>
</MUIListItemIcon>
) : undefined,
[isAllowCheckItem, isEdit, listItemIconMinWidth, onItemCheckboxChange],
);
const listItemElements = useMemo(() => {
let result = listEmptyElement;
if (listItems) {
const entries = Object.entries(listItems);
if (entries.length > 0) {
result = entries.map(([key, value]) => {
const listItem = renderListItem(key, value);
return (
<MUIListItem
{...restListItemProps}
key={`${listItemKeyPrefix}-${key}`}
sx={{ paddingLeft: 0, paddingRight: 0, ...listItemSx }}
>
{listItemCheckbox(
key,
renderListItemCheckboxState?.call(null, key, value),
)}
{isAllowItemButton ? (
<ListItemButton
onClick={(...args) => {
onItemClick?.call(null, value, key, ...args);
}}
sx={{ borderRadius: BORDER_RADIUS }}
>
{listItem}
</ListItemButton>
) : (
listItem
)}
</MUIListItem>
);
});
}
}
return result;
}, [
listEmptyElement,
listItems,
renderListItem,
restListItemProps,
listItemKeyPrefix,
listItemSx,
listItemCheckbox,
renderListItemCheckboxState,
isAllowItemButton,
onItemClick,
]);
const listScrollSx: SxProps<Theme> | undefined = useMemo(
() => (isScroll ? { maxHeight: '100%', overflowY: 'scroll' } : undefined),
[isScroll],
);
useImperativeHandle(
ref,
() => ({
setCheckAll: (value) => setIsCheckAll(value),
}),
[],
);
return (
<FlexBox spacing={0} {...flexBoxProps}>
{headerElement}
<MUIList
{...restListProps}
sx={{ paddingBottom: 0, paddingTop: 0, ...listScrollSx, ...listSx }}
>
{listItemElements}
</MUIList>
</FlexBox>
);
},
);
List.displayName = 'List';
export default List as <T>(
props: ListProps<T> & { ref?: ForwardedRef<ListForwardedRefContent> },
) => ReturnType<FC<ListProps<T>>>;