Archer d5f188a1a4
doc (#4798)
* doc

* fix: i18n

* fix: scroll load
2025-05-13 12:16:32 +08:00

463 lines
17 KiB
TypeScript

import Path from '@/components/common/folder/Path';
import { getTeamMembers } from '@/web/support/user/team/api';
import { getGroupList } from '@/web/support/user/team/group/api';
import useOrg from '@/web/support/user/team/org/hooks/useOrg';
import { useUserStore } from '@/web/support/user/useUserStore';
import { ChevronDownIcon } from '@chakra-ui/icons';
import { Box, Button, Flex, Grid, HStack, ModalBody, ModalFooter, Text } from '@chakra-ui/react';
import {
DEFAULT_ORG_AVATAR,
DEFAULT_TEAM_AVATAR,
DEFAULT_USER_AVATAR
} from '@fastgpt/global/common/system/constants';
import { type UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator';
import { type MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { type OrgListItemType } from '@fastgpt/global/support/user/team/org/type';
import { type TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import MyAvatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { useTranslation } from 'next-i18next';
import { type ValueOf } from 'next/dist/shared/lib/constants';
import { useMemo, useRef, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context';
import MemberItemCard from './MemberItemCard';
import PermissionSelect from './PermissionSelect';
const HoverBoxStyle = {
bgColor: 'myGray.50',
cursor: 'pointer'
};
function MemberModal({
onClose,
addPermissionOnly: addOnly = false
}: {
onClose: () => void;
addPermissionOnly?: boolean;
}) {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList);
const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>();
const {
paths,
onClickOrg,
members: orgMembers,
MemberScrollData: OrgMemberScrollData,
onPathClick,
orgs,
searchKey,
setSearchKey
} = useOrg({ withPermission: false });
const {
data: members,
ScrollData: TeamMemberScrollData,
refreshList
} = useScrollPagination(getTeamMembers, {
pageSize: 15,
params: {
withPermission: true,
withOrgs: true,
status: 'active',
searchKey
},
throttleWait: 500,
debounceWait: 200,
refreshDeps: [searchKey]
});
const {
data: groups = [],
loading: loadingGroupsAndOrgs,
runAsync: refreshGroups
} = useRequest2(
async () => {
if (!userInfo?.team?.teamId) return [];
return getGroupList<false>({
withMembers: false,
searchKey
});
},
{
manual: false,
refreshDeps: [userInfo?.team?.teamId]
}
);
const [selectedOrgList, setSelectedOrgIdList] = useState<OrgListItemType[]>([]);
const [selectedMemberList, setSelectedMemberList] = useState<
Omit<TeamMemberItemType, 'permission' | 'teamId'>[]
>([]);
const [selectedGroupList, setSelectedGroupList] = useState<MemberGroupListItemType<false>[]>([]);
const permissionList = useContextSelector(CollaboratorContext, (v) => v.permissionList);
const getPerLabelList = useContextSelector(CollaboratorContext, (v) => v.getPerLabelList);
const [selectedPermission, setSelectedPermission] = useState<number | undefined>(
permissionList?.read?.value
);
const perLabel = useMemo(() => {
if (selectedPermission === undefined) return '';
return getPerLabelList(selectedPermission!).join('、');
}, [getPerLabelList, selectedPermission]);
const onUpdateCollaborators = useContextSelector(
CollaboratorContext,
(v) => v.onUpdateCollaborators
);
const { runAsync: onConfirm, loading: isUpdating } = useRequest2(
() =>
onUpdateCollaborators({
members: selectedMemberList.map((item) => item.tmbId),
groups: selectedGroupList.map((item) => item._id),
orgs: selectedOrgList.map((item) => item._id),
permission: addOnly ? undefined : selectedPermission!
} as UpdateClbPermissionProps<ValueOf<typeof addOnly>>),
{
successToast: t('common:add_success'),
onSuccess() {
onClose();
}
}
);
const entryList = useRef([
{ label: t('user:team.group.members'), icon: DEFAULT_USER_AVATAR, value: 'member' },
{ label: t('user:team.org.org'), icon: DEFAULT_ORG_AVATAR, value: 'org' },
{ label: t('user:team.group.group'), icon: DEFAULT_TEAM_AVATAR, value: 'group' }
]);
const selectedList = useMemo(() => {
return [
...selectedOrgList.map((item) => ({
id: `org-${item._id}`,
avatar: item.avatar,
name: item.name,
onDelete: () => setSelectedOrgIdList(selectedOrgList.filter((v) => v._id !== item._id)),
orgs: undefined
})),
...selectedGroupList.map((item) => ({
id: `group-${item._id}`,
avatar: item.avatar,
name: item.name === DefaultGroupName ? userInfo?.team.teamName : item.name,
onDelete: () => setSelectedGroupList(selectedGroupList.filter((v) => v._id !== item._id)),
orgs: undefined
})),
...selectedMemberList.map((item) => ({
id: `member-${item.tmbId}`,
avatar: item.avatar,
name: item.memberName,
onDelete: () =>
setSelectedMemberList(selectedMemberList.filter((v) => v.tmbId !== item.tmbId)),
orgs: item.orgs
}))
];
}, [selectedOrgList, selectedGroupList, selectedMemberList, userInfo?.team.teamName]);
return (
<MyModal
isOpen
onClose={onClose}
iconSrc={addOnly ? 'keyPrimary' : 'modal/AddClb'}
title={addOnly ? t('user:team.add_permission') : t('user:team.add_collaborator')}
minW="800px"
maxW={'60vw'}
h={'100%'}
maxH={'90vh'}
isCentered
isLoading={loadingGroupsAndOrgs}
>
<ModalBody flex={'1'}>
<Grid
border="1px solid"
borderColor="myGray.200"
borderRadius="0.5rem"
gridTemplateColumns="1fr 1fr"
h={'100%'}
>
<Flex
h={'100%'}
flexDirection="column"
borderRight="1px solid"
borderColor="myGray.200"
p="4"
>
<SearchInput
placeholder={t('user:search_group_org_user')}
bgColor="myGray.50"
onChange={(e) => setSearchKey(e.target.value)}
/>
<Flex flexDirection="column" mt="3" overflow={'auto'} flex={'1 0 0'} h={0}>
{/* Entry */}
{!searchKey && !filterClass && (
<>
{entryList.current.map((item) => {
return (
<HStack
key={item.value}
justifyContent="space-between"
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
_notLast={{ mb: 1 }}
onClick={() => setFilterClass(item.value as any)}
>
<MyAvatar src={item.icon} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{item.label}
</Box>
<MyIcon name="core/chat/chevronRight" w="16px" />
</HStack>
);
})}
</>
)}
{/* Path */}
{!searchKey && filterClass && (
<Box mb={1}>
<Path
paths={[
{
parentId: filterClass,
parentName:
filterClass === 'member'
? t('user:team.group.members')
: filterClass === 'org'
? t('user:team.org.org')
: t('user:team.group.group')
},
...paths
]}
onClick={(parentId) => {
if (parentId === '') {
setFilterClass(undefined);
onPathClick('');
} else if (
parentId === 'member' ||
parentId === 'org' ||
parentId === 'group'
) {
setFilterClass(parentId);
onPathClick('');
} else {
onPathClick(parentId);
}
}}
rootName={t('common:Team')}
/>
</Box>
)}
{(filterClass === 'member' || searchKey) &&
(() => {
const Members = members?.map((member) => {
const onChange = () => {
setSelectedMemberList((state) => {
if (state.find((v) => v.tmbId === member.tmbId)) {
return state.filter((v) => v.tmbId !== member.tmbId);
}
return [...state, member];
});
};
const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId);
return (
<MemberItemCard
addOnly={addOnly}
avatar={member.avatar}
key={member.tmbId}
name={member.memberName}
permission={collaborator?.permission.value}
onChange={onChange}
isChecked={!!selectedMemberList.find((v) => v.tmbId === member.tmbId)}
orgs={member.orgs}
/>
);
});
return searchKey ? (
Members
) : (
<TeamMemberScrollData
flexDirection={'column'}
gap={1}
userSelect={'none'}
height={'fit-content'}
>
{Members}
</TeamMemberScrollData>
);
})()}
{(filterClass === 'org' || searchKey) &&
(() => {
const Orgs = orgs?.map((org) => {
const onChange = () => {
setSelectedOrgIdList((state) => {
if (state.find((v) => v._id === org._id)) {
return state.filter((v) => v._id !== org._id);
}
return [...state, org];
});
};
const collaborator = collaboratorList?.find((v) => v.orgId === org._id);
return (
<MemberItemCard
avatar={org.avatar}
key={org._id}
name={org.name}
onChange={onChange}
addOnly={addOnly}
permission={collaborator?.permission.value}
isChecked={!!selectedOrgList.find((v) => String(v._id) === String(org._id))}
rightSlot={
org.total && (
<MyIcon
name="core/chat/chevronRight"
w="16px"
p="4px"
rounded={'6px'}
_hover={{
bgColor: 'myGray.200'
}}
onClick={(e) => {
onClickOrg(org);
// setPath(getOrgChildrenPath(org));
e.stopPropagation();
}}
/>
)
}
/>
);
});
return searchKey ? (
Orgs
) : (
<OrgMemberScrollData>
{Orgs}
{orgMembers.map((member) => {
const isChecked = !!selectedMemberList.find(
(v) => v.tmbId === member.tmbId
);
const collaborator = collaboratorList?.find(
(v) => v.tmbId === member.tmbId
);
return (
<MemberItemCard
avatar={member.avatar}
key={member.tmbId}
name={member.memberName}
onChange={() => {
setSelectedMemberList((state) => {
if (state.find((v) => v.tmbId === member.tmbId)) {
return state.filter((v) => v.tmbId !== member.tmbId);
}
return [...state, member];
});
}}
isChecked={isChecked}
permission={collaborator?.permission.value}
addOnly={addOnly && !!member.permission.value}
orgs={member.orgs}
/>
);
})}
</OrgMemberScrollData>
);
})()}
{(filterClass === 'group' || searchKey) &&
groups?.map((group) => {
const onChange = () => {
setSelectedGroupList((state) => {
if (state.find((v) => v._id === group._id)) {
return state.filter((v) => v._id !== group._id);
}
return [...state, group];
});
};
const collaborator = collaboratorList?.find((v) => v.groupId === group._id);
return (
<MemberItemCard
avatar={group.avatar}
key={group._id}
name={
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
}
permission={collaborator?.permission.value}
onChange={onChange}
isChecked={!!selectedGroupList.find((v) => v._id === group._id)}
addOnly={addOnly}
/>
);
})}
</Flex>
</Flex>
<Flex h={'100%'} p="4" flexDirection="column">
<Box>
{`${t('user:has_chosen')}: `}
{selectedMemberList.length + selectedGroupList.length + selectedOrgList.length}
</Box>
<Flex flexDirection="column" mt="2" gap={1} overflow={'auto'} flex={'1 0 0'} h={0}>
{selectedList.map((item) => {
return (
<MemberItemCard
key={item.id}
avatar={item.avatar}
name={item.name ?? ''}
onChange={item.onDelete}
onDelete={item.onDelete}
orgs={item?.orgs}
/>
);
})}
</Flex>
</Flex>
</Grid>
</ModalBody>
<ModalFooter>
{!addOnly && !!permissionList && (
<PermissionSelect
value={selectedPermission}
Button={
<Flex
alignItems={'center'}
bg={'myGray.50'}
border="base"
fontSize={'sm'}
px={3}
borderRadius={'md'}
h={'32px'}
>
{t(perLabel as any)}
<ChevronDownIcon fontSize={'md'} />
</Flex>
}
onChange={(v) => setSelectedPermission(v)}
/>
)}
{addOnly && (
<HStack bg={'blue.50'} color={'blue.600'} padding={'6px 12px'} rounded={'5px'}>
<MyIcon name="common/info" w="1rem" h="1rem" />
<Text fontSize="12px">{t('user:permission_add_tip')}</Text>
</HStack>
)}
<Button isLoading={isUpdating} ml="4" h={'32px'} onClick={onConfirm}>
{t('common:Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
}
export default MemberModal;