2025-06-03 10:41:16 +08:00

270 lines
8.1 KiB
TypeScript

import {
type ButtonProps,
Flex,
Menu,
MenuList,
Box,
Radio,
useOutsideClick,
HStack,
MenuButton,
Checkbox
} from '@chakra-ui/react';
import React, { useMemo, useRef, useState } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { type PermissionValueType } from '@fastgpt/global/support/permission/type';
import { useContextSelector } from 'use-context-selector';
import { Permission } from '@fastgpt/global/support/permission/controller';
import { CollaboratorContext } from './context';
import { useTranslation } from 'next-i18next';
import MyDivider from '@fastgpt/web/components/common/MyDivider';
export type PermissionSelectProps = {
value?: PermissionValueType;
onChange: (value: PermissionValueType) => void;
trigger?: 'hover' | 'click';
offset?: [number, number];
Button: React.ReactNode;
onDelete?: () => void;
} & Omit<ButtonProps, 'onChange' | 'value'>;
const MenuStyle = {
py: 2,
px: 3,
_hover: {
bg: 'myGray.50'
},
borderRadius: 'md',
cursor: 'pointer',
fontSize: 'sm'
};
function PermissionSelect({
value,
onChange,
trigger = 'click',
offset = [0, 5],
Button,
width = 'auto',
onDelete
}: PermissionSelectProps) {
const { t } = useTranslation();
const ref = useRef<HTMLButtonElement>(null);
const closeTimer = useRef<NodeJS.Timeout>();
const { permission, permissionList } = useContextSelector(CollaboratorContext, (v) => v);
const [isOpen, setIsOpen] = useState(false);
const permissionSelectList = useMemo(() => {
if (!permissionList) return { singleCheckBoxList: [], multipleCheckBoxList: [] };
const list = Object.entries(permissionList).map(([_, value]) => {
return {
name: value.name,
value: value.value,
description: value.description,
checkBoxType: value.checkBoxType
};
});
return {
singleCheckBoxList: list
.filter((item) => item.checkBoxType === 'single')
.filter((item) => {
if (permission.isOwner) return true;
if (item.value === permissionList['manage'].value) return false;
return true;
}),
multipleCheckBoxList: list.filter((item) => item.checkBoxType === 'multiple')
};
}, [permission.isOwner, permissionList]);
const selectedSingleValue = useMemo(() => {
if (!permissionList) return undefined;
const per = new Permission({ per: value, permissionList });
if (per.hasManagePer) return permissionList['manage'].value;
if (per.hasWritePer) return permissionList['write'].value;
return permissionList['read'].value;
}, [permissionList, value]);
const selectedMultipleValues = useMemo(() => {
const per = new Permission({ per: value, permissionList });
return permissionSelectList.multipleCheckBoxList
.filter((item) => {
return per.checkPer(item.value);
})
.map((item) => item.value);
}, [permissionList, permissionSelectList.multipleCheckBoxList, value]);
const onSelectPer = (perValue: PermissionValueType) => {
if (perValue === value) return;
const per = new Permission({ per: perValue, permissionList });
per.addPer(...selectedMultipleValues);
onChange(per.value);
};
useOutsideClick({
ref: ref,
handler: () => {
setIsOpen(false);
}
});
return selectedSingleValue !== undefined ? (
<Menu offset={offset} isOpen={isOpen} autoSelect={false} direction={'ltr'}>
<Box
w="fit-content"
onMouseEnter={() => {
if (trigger === 'hover') {
setIsOpen(true);
}
clearTimeout(closeTimer.current);
}}
onMouseLeave={() => {
if (trigger === 'hover') {
closeTimer.current = setTimeout(() => {
setIsOpen(false);
}, 100);
}
}}
>
<MenuButton
ref={ref}
position={'relative'}
onClickCapture={() => {
if (trigger === 'click') {
setIsOpen(!isOpen);
}
}}
>
{Button}
</MenuButton>
<MenuList
minW={isOpen ? `${width}px !important` : 0}
p="3"
border={'1px solid #fff'}
boxShadow={
'0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);'
}
zIndex={99}
overflowY={'auto'}
whiteSpace={'pre-wrap'}
>
{/* The list of single select permissions */}
{permissionSelectList.singleCheckBoxList.map((item) => {
const change = () => {
const per = new Permission({ per: value, permissionList });
per.removePer(selectedSingleValue);
per.addPer(item.value);
onSelectPer(per.value);
};
return (
<Flex
key={item.value}
{...(selectedSingleValue === item.value
? {
color: 'primary.600'
}
: {})}
{...MenuStyle}
onClick={change}
maxW={['70vw', '260px']}
>
<Radio isChecked={selectedSingleValue === item.value} />
<Box ml={4}>
<Box>{t(item.name as any)}</Box>
<Box color={'myGray.500'} fontSize={'mini'}>
{t(item.description as any)}
</Box>
</Box>
</Flex>
);
})}
{permissionSelectList.multipleCheckBoxList.length > 0 && (
<>
<MyDivider my={2} />
{permissionSelectList.multipleCheckBoxList.map((item) => {
const isChecked = selectedMultipleValues.includes(item.value);
const isDisabled = new Permission({
per: selectedSingleValue,
permissionList
}).checkPer(item.value);
const change = () => {
if (isDisabled) return;
const per = new Permission({ per: value, permissionList });
if (isChecked) {
per.removePer(item.value);
} else {
per.addPer(item.value);
}
onChange(per.value);
};
return (
<Flex
key={item.value}
{...(isChecked
? {
color: 'primary.600'
}
: {})}
{...MenuStyle}
maxW={['70vw', '260px']}
{...(isDisabled
? {
cursor: 'not-allowed'
}
: {
cursor: 'pointer'
})}
onClick={change}
>
<Checkbox
size="lg"
isChecked={isChecked}
// onChange={(e) => {
// e.stopPropagation();
// e.preventDefault();
// }}
isDisabled={isDisabled}
/>
<Box ml={4}>
<Box>{t(item.name as any)}</Box>
<Box color={'myGray.500'} fontSize={'mini'}>
{t(item.description as any)}
</Box>
</Box>
</Flex>
);
})}
</>
)}
{onDelete && (
<>
<MyDivider my={2} h={'2px'} borderColor={'myGray.200'} />
<HStack
{...MenuStyle}
onClick={() => {
onDelete();
setIsOpen(false);
}}
>
<MyIcon name="delete" w="20px" color="red.600" />
<Box color="red.600">{t('common:Remove')}</Box>
</HStack>
</>
)}
</MenuList>
</Box>
</Menu>
) : null;
}
export default React.memo(PermissionSelect);