Team group (#2864)

* feat(member-group): Team (#2616)

* feat: member-group schema define

* feat(fe): create group

* feat: add group edit modal

* feat(fe): add avatar group component

* feat: edit group
fix: permission select menu style

* feat: bio-mode support for select-member component

* fix: avatar group key unique

* feat: group manage

* feat: divide member into group and clbs

* feat: finish team permission

* chore: adjust

* fix: get clbs

* perf: groups code

* pref: member group for team (#2706)

* chore: fe adjust
fix: remove the member from groups when removing from team
feat: change the groups avatar when updating the team's avatar

* chore: DefaultGroupName as a constant string ''

* fix: create default group when create team for root

* feat: comment

* feat: 4811 init

* pref: member group for team (#2732)

* chore: default group name

* feat: get default group when get by tmbid

* feat(fe): adjust

* member ui

* fix: delete group (#2736)

* perf: init4811

* pref: member group (#2818)

* fix: update clb per then refetch clb list

* fix: calculate group permission

* feat(fe): group tag

* refactor(fe): team and group manage

* feat: manage group member

* feat: add group transfer owner modal

* feat: group manage member

* chore: adjust the file structure

* pref: member group

* chore: adjust fe style

* fix: ts error

* chore: fe adjust

* chore: fe adjust

* chore: adjust

* chore: adjust the code

* perf: i18n and schema name

* pref: member-group (#2862)

* feat: group list ordered by updateTime

* fix: transfer ownership of group when deleting member

* fix: i18n fix

* feat: can not set member as admin/owner when user is not active

* fix: GroupInfoModal hover input do not change color

* fix(fe): searchinput do not scroll

* perf: team group ui

* doc

* remove enum

---------

Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
This commit is contained in:
Archer
2024-10-09 18:32:10 +08:00
committed by GitHub
parent 7afa8f00b8
commit 3a4b4a866b
80 changed files with 2670 additions and 751 deletions

View File

@@ -0,0 +1,10 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="20" height="20" fill="url(#paint0_linear_12282_71879)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.61043 5.58085C7.70468 5.58085 6.97042 6.31511 6.97042 7.22086C6.97042 8.1266 7.70468 8.86086 8.61043 8.86086C9.51618 8.86086 10.2504 8.1266 10.2504 7.22086C10.2504 6.31511 9.51618 5.58085 8.61043 5.58085ZM5.80376 7.22086C5.80376 5.67077 7.06035 4.41418 8.61043 4.41418C10.1605 4.41418 11.4171 5.67077 11.4171 7.22086C11.4171 8.77094 10.1605 10.0275 8.61043 10.0275C7.06035 10.0275 5.80376 8.77094 5.80376 7.22086ZM11.4047 4.94026C11.5256 4.64163 11.8657 4.49754 12.1643 4.61842C13.1918 5.03434 13.9184 6.04205 13.9184 7.22086C13.9184 8.39966 13.1918 9.40737 12.1643 9.82329C11.8657 9.94417 11.5256 9.80008 11.4047 9.50145C11.2838 9.20283 11.4279 8.86275 11.7266 8.74187C12.3287 8.49814 12.7517 7.9082 12.7517 7.22086C12.7517 6.53351 12.3287 5.94357 11.7266 5.69985C11.4279 5.57896 11.2838 5.23888 11.4047 4.94026ZM7.7561 11.0842H9.46476C9.96526 11.0842 10.3704 11.0842 10.7004 11.1067C11.0402 11.1299 11.3427 11.1789 11.6299 11.2978C12.3176 11.5827 12.864 12.1291 13.1489 12.8168C13.2678 13.104 13.3168 13.4066 13.34 13.7463C13.3625 14.0763 13.3625 14.4814 13.3625 14.9819V15.0025C13.3625 15.3247 13.1014 15.5859 12.7792 15.5859C12.457 15.5859 12.1959 15.3247 12.1959 15.0025C12.1959 14.4766 12.1955 14.1113 12.1761 13.8257C12.1569 13.5454 12.1213 13.3846 12.071 13.2633C11.9046 12.8614 11.5853 12.5422 11.1835 12.3757C11.0621 12.3254 10.9013 12.2898 10.621 12.2707C10.3354 12.2512 9.97013 12.2509 9.44418 12.2509H7.77668C7.25072 12.2509 6.88547 12.2512 6.59987 12.2707C6.31951 12.2898 6.15877 12.3254 6.03741 12.3757C5.63556 12.5422 5.31629 12.8614 5.14984 13.2633C5.09957 13.3846 5.06394 13.5454 5.04481 13.8257C5.02532 14.1113 5.025 14.4766 5.025 15.0025C5.025 15.3247 4.76384 15.5859 4.44167 15.5859C4.1195 15.5859 3.85834 15.3247 3.85834 15.0025L3.85834 14.9819C3.85833 14.4814 3.85833 14.0763 3.88085 13.7463C3.90403 13.4066 3.95301 13.104 4.07198 12.8168C4.35684 12.1291 4.90323 11.5827 5.59094 11.2978C5.87816 11.1789 6.1807 11.1299 6.52046 11.1067C6.85047 11.0842 7.2556 11.0842 7.7561 11.0842ZM13.3259 11.5922C13.4062 11.2802 13.7243 11.0924 14.0363 11.1727C15.2467 11.4842 16.1417 12.5823 16.1417 13.8909V15.0025C16.1417 15.3247 15.8805 15.5859 15.5584 15.5859C15.2362 15.5859 14.975 15.3247 14.975 15.0025V13.8909C14.975 13.1275 14.4529 12.4846 13.7455 12.3025C13.4335 12.2222 13.2456 11.9042 13.3259 11.5922Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_12282_71879" x1="1.5" y1="20" x2="20" y2="1.6056e-06" gradientUnits="userSpaceOnUse">
<stop stop-color="#53A3FF"/>
<stop offset="1" stop-color="#68BFFF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -66,6 +66,7 @@ export function ChangeOwnerModal({
<MyModal
isOpen
iconSrc="modal/changePer"
iconColor="primary.600"
onClose={onClose}
title={t('common:permission.change_owner')}
isLoading={loading}
@@ -76,7 +77,7 @@ export function ChangeOwnerModal({
<Box>{name}</Box>
</HStack>
<Flex mt={4} justify="start" flexDirection="column">
<Box fontSize="14px" fontWeight="500" color="myGray.900">
<Box fontSize="14px" fontWeight="500">
{t('common:permission.change_owner_to')}
</Box>
<Flex mt="4" alignItems="center" position={'relative'}>
@@ -162,3 +163,5 @@ export function ChangeOwnerModal({
</MyModal>
);
}
export default ChangeOwnerModal;

View File

@@ -0,0 +1,59 @@
import {
Box,
Flex,
Popover,
PopoverContent,
PopoverTrigger,
useDisclosure
} from '@chakra-ui/react';
import Tag from '@fastgpt/web/components/common/Tag';
import React from 'react';
type Props = {
max: number;
names?: string[];
};
function GroupTags({ max, names }: Props) {
const length = names?.length || 0;
const { isOpen, onToggle, onClose } = useDisclosure();
return (
<Flex flexWrap="wrap" rowGap={2}>
{names?.slice(0, max).map((name, index) => (
<Tag key={index} colorSchema={'gray'} ml={2}>
{name.length > 10 ? name.slice(0, 10) + '...' : name}
</Tag>
))}
<Popover
isOpen={isOpen}
trigger={'hover'}
onOpen={onToggle}
onClose={onClose}
placement="bottom"
>
<PopoverTrigger>
<Box>
{length > max && (
<Tag colorSchema={'gray'} ml={2} cursor={'pointer'}>
{'+' + (length - max)}
</Tag>
)}
</Box>
</PopoverTrigger>
<PopoverContent w={'fit-content'} bg={'white'} px={4} py={2}>
<Flex rowGap={2} flexWrap={'wrap'} columnGap={2}>
{names?.slice(max)?.map((name, index) => (
<Tag key={index + length} colorSchema={'gray'}>
{name}
</Tag>
))}
</Flex>
</PopoverContent>
</Popover>
</Flex>
);
}
export default GroupTags;

View File

@@ -1,15 +1,13 @@
import {
Flex,
Box,
Grid,
ModalBody,
InputGroup,
InputLeftElement,
Input,
Checkbox,
ModalFooter,
Button,
useToast
Button
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -67,7 +65,7 @@ function AddMemberModal({ onClose }: AddModalPropsType) {
const { mutate: onConfirm, isLoading: isUpdating } = useRequest({
mutationFn: () => {
return onUpdateCollaborators({
tmbIds: selectedMemberIdList,
members: selectedMemberIdList,
permission: selectedPermission
});
},
@@ -184,7 +182,9 @@ function AddMemberModal({ onClose }: AddModalPropsType) {
_notLast={{ mb: 2 }}
>
<Avatar src={member.avatar} w="24px" />
<Box w="full">{member.memberName}</Box>
<Box w="full" ml={2}>
{member.memberName}
</Box>
<MyIcon
name="common/closeLight"
w="16px"

View File

@@ -1,15 +1,4 @@
import {
ModalBody,
Table,
TableContainer,
Tbody,
Th,
Thead,
Tr,
Td,
Box,
Flex
} from '@chakra-ui/react';
import { ModalBody, Table, TableContainer, Tbody, Th, Thead, Tr, Td, Flex } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
@@ -18,7 +7,7 @@ import PermissionTags from './PermissionTags';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { CollaboratorContext } from './context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { useUserStore } from '@/web/support/user/useUserStore';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
@@ -38,16 +27,17 @@ function ManageModal({ onClose }: ManageModalProps) {
onDelOneCollaborator(tmbId)
);
const { mutate: onUpdate, isLoading: isUpdating } = useRequest({
mutationFn: ({ tmbId, per }: { tmbId: string; per: PermissionValueType }) => {
return onUpdateCollaborators({
tmbIds: [tmbId],
const { runAsync: onUpdate, loading: isUpdating } = useRequest2(
({ tmbId, per }: { tmbId: string; per: PermissionValueType }) =>
onUpdateCollaborators({
members: [tmbId],
permission: per
});
},
successToast: t('common.Update Success'),
errorToast: 'Error'
});
}),
{
successToast: t('common.Update Success'),
errorToast: 'Error'
}
);
const loading = isDeleting || isUpdating;

View File

@@ -2,12 +2,12 @@ import {
ButtonProps,
Flex,
Menu,
MenuButton,
MenuList,
Box,
Radio,
useOutsideClick,
HStack
HStack,
MenuButton
} from '@chakra-ui/react';
import React, { useMemo, useRef, useState } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -46,18 +46,17 @@ function PermissionSelect({
offset = [0, 5],
Button,
width = 'auto',
onDelete,
...props
onDelete
}: PermissionSelectProps) {
const { t } = useTranslation();
const { permission, permissionList } = useContextSelector(CollaboratorContext, (v) => v);
const ref = useRef<HTMLDivElement>(null);
const closeTimer = useRef<any>();
const ref = useRef<HTMLButtonElement>(null);
const closeTimer = useRef<NodeJS.Timeout>();
const [isOpen, setIsOpen] = useState(false);
const permissionSelectList = useMemo(() => {
const list = Object.entries(permissionList).map(([key, value]) => {
const list = Object.entries(permissionList).map(([_, value]) => {
return {
name: value.name,
value: value.value,
@@ -85,15 +84,15 @@ function PermissionSelect({
return permissionList['read'].value;
}, [permissionList, value]);
const selectedMultipleValues = useMemo(() => {
const per = new Permission({ per: value });
return permissionSelectList.multipleCheckBoxList
.filter((item) => {
return per.checkPer(item.value);
})
.map((item) => item.value);
}, [permissionSelectList.multipleCheckBoxList, value]);
// const selectedMultipleValues = useMemo(() => {
// const per = new Permission({ per: value });
//
// return permissionSelectList.multipleCheckBoxList
// .filter((item) => {
// return per.checkPer(item.value);
// })
// .map((item) => item.value);
// }, [permissionSelectList.multipleCheckBoxList, value]);
const onSelectPer = (per: PermissionValueType) => {
if (per === value) return;
@@ -111,7 +110,7 @@ function PermissionSelect({
return (
<Menu offset={offset} isOpen={isOpen} autoSelect={false} direction={'ltr'}>
<Box
ref={ref}
w="fit-content"
onMouseEnter={() => {
if (trigger === 'hover') {
setIsOpen(true);
@@ -126,7 +125,8 @@ function PermissionSelect({
}
}}
>
<Box
<MenuButton
ref={ref}
position={'relative'}
onClickCapture={() => {
if (trigger === 'click') {
@@ -134,25 +134,8 @@ function PermissionSelect({
}
}}
>
<MenuButton
w={'100%'}
h={'100%'}
position={'absolute'}
top={0}
right={0}
bottom={0}
left={0}
/>
<Flex
alignItems={'center'}
justifyContent={'center'}
position={'relative'}
cursor={'pointer'}
userSelect={'none'}
>
{Button}
</Flex>
</Box>
{Button}
</MenuButton>
<MenuList
minW={isOpen ? `${width}px !important` : 0}
p="3"

View File

@@ -22,7 +22,7 @@ export type MemberManagerInputPropsType = {
permission: Permission;
onGetCollaboratorList: () => Promise<CollaboratorItemType[]>;
permissionList: PermissionListType;
onUpdateCollaborators: (props: UpdateClbPermissionProps) => any;
onUpdateCollaborators: (props: any) => any; // TODO: type. should be UpdatePermissionBody after app and dataset permission refactored
onDelOneCollaborator: (tmbId: string) => any;
refreshDeps?: any[];
};

View File

@@ -0,0 +1,21 @@
import { Box, HStack } from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import React from 'react';
type Props = {
name: string;
avatar: string;
};
function MemberTag({ name, avatar }: Props) {
return (
<HStack>
<Avatar src={avatar} w={['18px', '22px']} rounded="50%" />
<Box maxW={'150px'} className={'textEllipsis'}>
{name}
</Box>
</HStack>
);
}
export default MemberTag;

View File

@@ -7,29 +7,41 @@ import { useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { DragHandleIcon } from '@chakra-ui/icons';
import MemberTable from './components/MemberTable';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { TeamModalContext } from './context';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { delLeaveTeam } from '@/web/support/user/team/api';
import dynamic from 'next/dynamic';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
enum TabListEnum {
member = 'member',
permission = 'permission'
permission = 'permission',
group = 'group'
}
const TeamTagModal = dynamic(() => import('../TeamTagModal'));
const InviteModal = dynamic(() => import('./components/InviteModal'));
const PermissionManage = dynamic(() => import('./components/PermissionManage/index'));
const GroupManage = dynamic(() => import('./components/GroupManage/index'));
const GroupInfoModal = dynamic(() => import('./components/GroupManage/GroupInfoModal'));
const ManageGroupMemberModal = dynamic(() => import('./components/GroupManage/GroupManageMember'));
function TeamCard() {
const { toast } = useToast();
const { t } = useTranslation();
const { myTeams, refetchTeams, members, refetchMembers, setEditTeamData, onSwitchTeam } =
useContextSelector(TeamModalContext, (v) => v);
const {
myTeams,
refetchTeams,
members,
refetchMembers,
setEditTeamData,
onSwitchTeam,
searchKey,
setSearchKey
} = useContextSelector(TeamModalContext, (v) => v);
const { userInfo, teamPlanStatus } = useUserStore();
const { feConfigs } = useSystemStore();
@@ -37,8 +49,9 @@ function TeamCard() {
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('common:user.team.member.Confirm Leave')
});
const { mutate: onLeaveTeam, isLoading: isLoadingLeaveTeam } = useRequest({
mutationFn: async (teamId?: string) => {
const { runAsync: onLeaveTeam, loading: isLoadingLeaveTeam } = useRequest2(
async (teamId?: string) => {
if (!teamId) return;
const defaultTeam = myTeams.find((item) => item.defaultTeam) || myTeams[0];
// change to personal team
@@ -46,19 +59,45 @@ function TeamCard() {
onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam(teamId);
},
onSuccess() {
refetchTeams();
},
errorToast: t('common:user.team.Leave Team Failed')
});
{
onSuccess() {
refetchTeams();
},
errorToast: t('common:user.team.Leave Team Failed')
}
);
const {
isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync
} = useDisclosure();
const {
isOpen: isOpenGroupInfo,
onOpen: onOpenGroupInfo,
onClose: onCloseGroupInfo
} = useDisclosure();
const {
isOpen: isOpenManageGroupMember,
onOpen: onOpenManageGroupMember,
onClose: onCloseManageGroupMember
} = useDisclosure();
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const [editGroupId, setEditGroupId] = useState<string>();
const onEditGroup = (groupId: string) => {
setEditGroupId(groupId);
onOpenGroupInfo();
};
const onManageMember = (groupId: string) => {
setEditGroupId(groupId);
onOpenManageGroupMember();
};
const Tablist = useMemo(
() => [
{
@@ -73,6 +112,11 @@ function TeamCard() {
),
value: TabListEnum.member
},
{
icon: 'support/team/group',
label: t('user:team.group.group'),
value: TabListEnum.group
},
{
icon: 'support/team/key',
label: t('common:common.Role'),
@@ -99,15 +143,15 @@ function TeamCard() {
borderBottomColor={'myGray.100'}
mb={2}
>
<Box fontSize={['sm', 'md']} fontWeight={'bold'} alignItems={'center'}>
<Box fontSize={['sm', 'md']} fontWeight={'bold'} alignItems={'center'} color={'myGray.900'}>
{userInfo?.team.teamName}
</Box>
{userInfo?.team.role === TeamMemberRoleEnum.owner && (
<MyIcon
name="edit"
w={'14px'}
w="14px"
ml={2}
cursor={'pointer'}
cursor="pointer"
_hover={{
color: 'primary.500'
}}
@@ -124,21 +168,32 @@ function TeamCard() {
</Flex>
<Flex px={5} alignItems={'center'} justifyContent={'space-between'}>
<LightRowTabs<TabListEnum>
overflow={'auto'}
list={Tablist}
value={tab}
onChange={setTab}
></LightRowTabs>
<LightRowTabs<TabListEnum> overflow={'auto'} list={Tablist} value={tab} onChange={setTab} />
{/* ctrl buttons */}
<Flex alignItems={'center'}>
{tab === TabListEnum.member &&
userInfo?.team.permission.hasManagePer &&
feConfigs?.show_team_chat && (
<Button
variant={'whitePrimary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="core/dataset/tag" w={'16px'} />}
onClick={() => {
onOpenTeamTagsAsync();
}}
>
{t('common:user.team.Team Tags Async')}
</Button>
)}
{tab === TabListEnum.member && userInfo?.team.permission.hasManagePer && (
<Button
variant={'whitePrimary'}
size="sm"
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/inviteLight" w={'14px'} color={'primary.500'} />}
leftIcon={<MyIcon name="common/inviteLight" w={'16px'} color={'white'} />}
onClick={() => {
if (
teamPlanStatus?.standardConstants?.maxTeamMember &&
@@ -158,24 +213,10 @@ function TeamCard() {
{t('common:user.team.Invite Member')}
</Button>
)}
{userInfo?.team.permission.hasManagePer && feConfigs?.show_team_chat && (
{tab === TabListEnum.member && !userInfo?.team.permission.isOwner && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<DragHandleIcon w={'14px'} color={'primary.500'} />}
onClick={() => {
onOpenTeamTagsAsync();
}}
>
{t('common:user.team.Team Tags Async')}
</Button>
)}
{!userInfo?.team.permission.isOwner && (
<Button
variant={'whitePrimary'}
size="sm"
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
@@ -187,11 +228,36 @@ function TeamCard() {
{t('common:user.team.Leave Team')}
</Button>
)}
{tab === TabListEnum.group && userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="support/permission/collaborator" w={'14px'} />}
onClick={onOpenGroupInfo}
>
{t('user:team.group.create')}
</Button>
)}
{tab === TabListEnum.permission && (
<Box ml="auto">
<SearchInput
placeholder={t('user:team.group.search_placeholder')}
w="200px"
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
/>
</Box>
)}
</Flex>
</Flex>
<Box mt={3} flex={'1 0 0'} overflow={'auto'}>
{tab === TabListEnum.member && <MemberTable />}
{tab === TabListEnum.group && (
<GroupManage onEditGroup={onEditGroup} onManageMember={onManageMember} />
)}
{tab === TabListEnum.permission && <PermissionManage />}
</Box>
@@ -203,6 +269,24 @@ function TeamCard() {
/>
)}
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
{isOpenGroupInfo && (
<GroupInfoModal
onClose={() => {
onCloseGroupInfo();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
{isOpenManageGroupMember && (
<ManageGroupMemberModal
onClose={() => {
onCloseManageGroupMember();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
<ConfirmLeaveTeamModal />
</Flex>
);

View File

@@ -1,4 +1,4 @@
import { Box, Button, Flex, IconButton, Text } from '@chakra-ui/react';
import { Box, Button, Flex, IconButton } from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -28,7 +28,7 @@ function TeamList() {
h={'40px'}
borderBottom={'1.5px solid rgba(0, 0, 0, 0.05)'}
>
<Box flex={['0 0 auto', 1]} fontSize={['sm', 'md']}>
<Box flex={['0 0 auto', 1]} fontSize={['sm', 'md']} fontWeight={'bold'}>
{t('common:common.Team')}
</Box>
{/* if there is no team */}

View File

@@ -13,6 +13,7 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
import { postCreateTeam, putUpdateTeam } from '@/web/support/user/team/api';
import { CreateTeamProps } from '@fastgpt/global/support/user/team/controller.d';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
export type EditTeamFormDataType = CreateTeamProps & {
id?: string;
@@ -20,7 +21,7 @@ export type EditTeamFormDataType = CreateTeamProps & {
export const defaultForm = {
name: '',
avatar: '/icon/logo.svg'
avatar: DEFAULT_TEAM_AVATAR
};
function EditModal({
@@ -98,7 +99,8 @@ function EditModal({
<MyModal
isOpen
onClose={onClose}
iconSrc="/imgs/modal/team.svg"
iconSrc="support/team/group"
iconColor="primary.600"
title={defaultData.id ? t('common:user.team.Update Team') : t('common:user.team.Create Team')}
>
<ModalBody>

View File

@@ -0,0 +1,128 @@
import { Input, HStack, ModalBody, Button, ModalFooter } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import Avatar from '@fastgpt/web/components/common/Avatar';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useTranslation } from 'next-i18next';
import React, { useMemo } from 'react';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { useForm } from 'react-hook-form';
import { useContextSelector } from 'use-context-selector';
import { TeamModalContext } from '../../context';
import { postCreateGroup, putUpdateGroup } from '@/web/support/user/team/group/api';
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
export type GroupFormType = {
avatar: string;
name: string;
};
function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) {
const { refetchGroups, groups, refetchMembers } = useContextSelector(TeamModalContext, (v) => v);
const { t } = useTranslation();
const { File: AvatarSelect, onOpen: onOpenSelectAvatar } = useSelectFile({
fileType: '.jpg, .jpeg, .png',
multiple: false
});
const group = useMemo(() => {
return groups.find((item) => item._id === editGroupId);
}, [editGroupId, groups]);
const { register, handleSubmit, getValues, setValue } = useForm<GroupFormType>({
defaultValues: {
name: group?.name || '',
avatar: group?.avatar || DEFAULT_TEAM_AVATAR
}
});
const { loading: uploadingAvatar, run: onSelectAvatar } = useRequest2(
async (file: File[]) => {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.groupAvatar,
file: file[0],
maxW: 300,
maxH: 300
});
return src;
},
{
onSuccess: (src: string) => {
setValue('avatar', src);
}
}
);
const { run: onCreate, loading: isLoadingCreate } = useRequest2(
(data: GroupFormType) => {
return postCreateGroup({
name: data.name,
avatar: data.avatar
});
},
{
onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()])
}
);
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
async (data: GroupFormType) => {
if (!editGroupId) return;
return putUpdateGroup({
groupId: editGroupId,
name: data.name,
avatar: data.avatar
});
},
{
onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()])
}
);
const isLoading = isLoadingUpdate || isLoadingCreate || uploadingAvatar;
return (
<MyModal
onClose={onClose}
title={editGroupId ? t('user:team.group.edit') : t('user:team.group.create')}
iconSrc={group?.avatar ?? DEFAULT_TEAM_AVATAR}
>
<ModalBody flex={1} overflow={'auto'} display={'flex'} flexDirection={'column'} gap={4}>
<FormLabel w="80px">{t('user:team.avatar_and_name')}</FormLabel>
<HStack>
<Avatar
src={getValues('avatar')}
onClick={onOpenSelectAvatar}
cursor={'pointer'}
borderRadius={'md'}
/>
<Input
bgColor="myGray.50"
{...register('name', { required: true })}
placeholder={t('user:team.group.name')}
/>
</HStack>
</ModalBody>
<ModalFooter alignItems="flex-end">
<Button
isLoading={isLoading}
onClick={handleSubmit((data) => {
if (editGroupId) {
onUpdate(data);
} else {
onCreate(data);
}
})}
>
{editGroupId ? t('common:common.Save') : t('common:new_create')}
</Button>
</ModalFooter>
<AvatarSelect onSelect={onSelectAvatar} />
</MyModal>
);
}
export default GroupInfoModal;

View File

@@ -0,0 +1,275 @@
import {
Box,
ModalBody,
Flex,
Button,
ModalFooter,
Checkbox,
Grid,
HStack
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar';
import Tag from '@fastgpt/web/components/common/Tag';
import { useTranslation } from 'next-i18next';
import React, { useMemo, useState } from 'react';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useContextSelector } from 'use-context-selector';
import { TeamModalContext } from '../../context';
import { putUpdateGroup } from '@/web/support/user/team/group/api';
import { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
export type GroupFormType = {
members: {
tmbId: string;
role: `${GroupMemberRole}`;
}[];
};
function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) {
// 1. Owner can not be deleted, toast
// 2. Owner/Admin can manage members
// 3. Owner can add/remove admins
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { toast } = useToast();
const [hoveredMemberId, setHoveredMemberId] = useState<string | undefined>(undefined);
const {
members: allMembers,
refetchGroups,
groups,
refetchMembers
} = useContextSelector(TeamModalContext, (v) => v);
const group = useMemo(() => {
return groups.find((item) => item._id === editGroupId);
}, [editGroupId, groups]);
const [members, setMembers] = useState(group?.members || []);
const [searchKey, setSearchKey] = useState('');
const filtered = useMemo(() => {
return [
...allMembers.filter((member) => {
if (member.memberName.toLowerCase().includes(searchKey.toLowerCase())) return true;
return false;
})
];
}, [searchKey, allMembers]);
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
async () => {
if (!editGroupId || !members.length) return;
return putUpdateGroup({
groupId: editGroupId,
memberList: members
});
},
{
onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()])
}
);
const isSelected = (memberId: string) => {
return members.find((item) => item.tmbId === memberId);
};
const myRole = useMemo(() => {
if (userInfo?.team.permission.hasManagePer) {
return 'owner';
}
return members.find((item) => item.tmbId === userInfo?.team.tmbId)?.role ?? 'member';
}, [members, userInfo]);
const handleToggleSelect = (memberId: string) => {
if (
myRole === 'owner' &&
memberId === group?.members.find((item) => item.role === 'owner')?.tmbId
) {
toast({
title: t('user:team.group.toast.can_not_delete_owner'),
status: 'error'
});
return;
}
if (
myRole === 'admin' &&
group?.members.find((item) => String(item.tmbId) === memberId)?.role !== 'member'
) {
return;
}
if (isSelected(memberId)) {
setMembers(members.filter((item) => item.tmbId !== memberId));
} else {
setMembers([...members, { tmbId: memberId, role: 'member' }]);
}
};
const handleToggleAdmin = (memberId: string) => {
if (myRole === 'owner' && isSelected(memberId)) {
const oldRole = members.find((item) => item.tmbId === memberId)?.role;
if (oldRole === 'admin') {
setMembers(
members.map((item) => (item.tmbId === memberId ? { ...item, role: 'member' } : item))
);
} else {
setMembers(
members.map((item) => (item.tmbId === memberId ? { ...item, role: 'admin' } : item))
);
}
}
};
const isLoading = isLoadingUpdate;
return (
<MyModal
onClose={onClose}
title={t('user:team.group.manage_member')}
iconSrc={group?.avatar ?? DEFAULT_TEAM_AVATAR}
iconColor="primary.600"
minW={['70vw', '800px']}
>
<ModalBody flex={1} display={'flex'} flexDirection={'column'} gap={4}>
<Grid
templateColumns="1fr 1fr"
borderRadius="8px"
border="1px solid"
borderColor="myGray.200"
>
<Flex flexDirection="column" p="4">
<SearchInput
placeholder={t('user:search_user')}
fontSize="sm"
bg={'myGray.50'}
onChange={(e) => {
setSearchKey(e.target.value);
}}
/>
<Flex flexDirection="column" mt={3} flexGrow="1" overflow={'auto'} maxH={'400px'}>
{filtered.map((member) => {
return (
<HStack
py="2"
px={3}
borderRadius={'md'}
alignItems="center"
key={member.tmbId}
cursor={'pointer'}
_hover={{
bg: 'myGray.50',
...(!isSelected(member.tmbId) ? { svg: { color: 'myGray.50' } } : {})
}}
_notLast={{ mb: 2 }}
onClick={() => handleToggleSelect(member.tmbId)}
>
<Checkbox
isChecked={!!isSelected(member.tmbId)}
icon={<MyIcon name={'common/check'} w={'12px'} />}
/>
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box>{member.memberName}</Box>
</HStack>
);
})}
</Flex>
</Flex>
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}>
<Box mt={2}>{t('common:chosen') + ': ' + members.length}</Box>
<Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'400px'}>
{members.map((member) => {
return (
<HStack
onMouseEnter={() => setHoveredMemberId(member.tmbId)}
onMouseLeave={() => setHoveredMemberId(undefined)}
justifyContent="space-between"
py="2"
px={3}
borderRadius={'md'}
key={member.tmbId + member.role}
_hover={{ bg: 'myGray.50' }}
_notLast={{ mb: 2 }}
>
<HStack>
<Avatar
src={allMembers.find((item) => item.tmbId === member.tmbId)?.avatar}
w="1.5rem"
borderRadius={'md'}
/>
<Box>
{allMembers.find((item) => item.tmbId === member.tmbId)?.memberName}
</Box>
</HStack>
<Box mr="auto">
{(() => {
if (member.role === 'owner') {
return (
<Tag ml={2} colorSchema="gray">
{t('user:team.group.role.owner')}
</Tag>
);
} else if (member.role === 'admin') {
return (
<Tag ml={2} mr="auto">
{t('user:team.group.role.admin')}
{myRole === 'owner' && (
<MyIcon
ml={1}
name={'common/closeLight'}
w={'1rem'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
onClick={() => handleToggleAdmin(member.tmbId)}
/>
)}
</Tag>
);
} else if (member.role === 'member') {
return (
myRole === 'owner' &&
hoveredMemberId === member.tmbId && (
<Tag
ml={2}
colorSchema="yellow"
cursor={'pointer'}
onClick={() => handleToggleAdmin(member.tmbId)}
>
{t('user:team.group.set_as_admin')}
</Tag>
)
);
}
})()}
</Box>
{(myRole === 'owner' || (myRole === 'admin' && member.role === 'member')) && (
<MyIcon
name={'common/closeLight'}
w={'1rem'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
onClick={() => handleToggleSelect(member.tmbId)}
/>
)}
</HStack>
);
})}
</Flex>
</Flex>
</Grid>
</ModalBody>
<ModalFooter alignItems="flex-end">
<Button isLoading={isLoading} onClick={onUpdate}>
{t('common:common.Save')}
</Button>
</ModalFooter>
</MyModal>
);
}
export default GroupEditModal;

View File

@@ -0,0 +1,200 @@
import { putUpdateGroup } from '@/web/support/user/team/group/api';
import {
Box,
Flex,
HStack,
Input,
ModalBody,
ModalFooter,
Button,
useDisclosure,
Checkbox
} from '@chakra-ui/react';
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import React, { useMemo, useState } from 'react';
import { TeamModalContext } from '../../context';
import { useContextSelector } from 'use-context-selector';
export type ChangeOwnerModalProps = {
groupId: string;
};
export function ChangeOwnerModal({
onClose,
groupId
}: ChangeOwnerModalProps & { onClose: () => void }) {
const { t } = useTranslation();
const [inputValue, setInputValue] = React.useState('');
const {
members: allMembers,
groups,
refetchGroups
} = useContextSelector(TeamModalContext, (v) => v);
const group = useMemo(() => {
return groups.find((item) => item._id === groupId);
}, [groupId, groups]);
const memberList = allMembers.filter((item) => {
return item.memberName.toLowerCase().includes(inputValue.toLowerCase());
});
const OldOwnerId = useMemo(() => {
return group?.members.find((item) => item.role === 'owner')?.tmbId;
}, [group]);
const [keepAdmin, setKeepAdmin] = useState(true);
const {
isOpen: isOpenMemberListMenu,
onClose: onCloseMemberListMenu,
onOpen: onOpenMemberListMenu
} = useDisclosure();
const [selectedMember, setSelectedMember] = useState<TeamMemberItemType | null>(null);
const onChangeOwner = async (tmbId: string) => {
if (!group) {
return;
}
const newMemberList = group.members
.map((item) => {
if (item.tmbId === OldOwnerId) {
if (keepAdmin) {
return { tmbId: OldOwnerId, role: 'admin' };
}
return { tmbId: OldOwnerId, role: 'member' };
}
return item;
})
.filter((item) => item.tmbId !== tmbId) as any;
newMemberList.push({ tmbId, role: 'owner' });
return putUpdateGroup({
groupId,
memberList: newMemberList
});
};
const { runAsync, loading } = useRequest2(onChangeOwner, {
onSuccess: () => Promise.all([onClose(), refetchGroups()]),
successToast: t('common:permission.change_owner_success'),
errorToast: t('common:permission.change_owner_failed')
});
const onConfirm = async () => {
if (!selectedMember) {
return;
}
await runAsync(selectedMember.tmbId);
};
return (
<MyModal
isOpen
iconSrc="modal/changePer"
iconColor="primary.600"
onClose={onClose}
title={t('common:permission.change_owner')}
isLoading={loading}
>
<ModalBody>
<HStack>
<Avatar src={group?.avatar} w={'1.75rem'} borderRadius={'md'} />
<Box>{group?.name}</Box>
</HStack>
<Flex mt={4} justify="start" flexDirection="column">
<Box fontSize="14px" fontWeight="500" color="myGray.900">
{t('common:permission.change_owner_to')}
</Box>
<Flex mt="4" alignItems="center" position={'relative'}>
{selectedMember && (
<Avatar
src={selectedMember.avatar}
w={'20px'}
borderRadius={'md'}
position="absolute"
left={3}
/>
)}
<Input
placeholder={t('common:permission.change_owner_placeholder')}
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
setSelectedMember(null);
}}
onFocus={() => {
onOpenMemberListMenu();
setSelectedMember(null);
}}
{...(selectedMember && { pl: '10' })}
/>
</Flex>
{isOpenMemberListMenu && memberList.length > 0 && (
<Flex
mt={2}
w={'100%'}
flexDirection={'column'}
gap={2}
p={1}
boxShadow="lg"
bg="white"
borderRadius="md"
zIndex={10}
maxH={'300px'}
overflow={'auto'}
>
{memberList.map((item) => (
<Box
key={item.tmbId}
p="2"
_hover={{ bg: 'myGray.100' }}
mx="1"
borderRadius="md"
cursor={'pointer'}
onClickCapture={() => {
setInputValue(item.memberName);
setSelectedMember(item);
onCloseMemberListMenu();
}}
>
<Flex align="center">
<Avatar src={item.avatar} w="1.25rem" />
<Box ml="2">{item.memberName}</Box>
</Flex>
</Box>
))}
</Flex>
)}
<Box mt="4">
<Checkbox
isChecked={keepAdmin}
onChange={(e) => {
setKeepAdmin(e.target.checked);
}}
>
{t('user:team.group.keep_admin')}
</Checkbox>
</Box>
</Flex>
</ModalBody>
<ModalFooter>
<HStack>
<Button onClick={onClose} variant={'whiteBase'}>
{t('common:common.Cancel')}
</Button>
<Button onClick={onConfirm}>{t('common:common.Confirm')}</Button>
</HStack>
</ModalFooter>
</MyModal>
);
}
export default ChangeOwnerModal;

View File

@@ -0,0 +1,217 @@
import AvatarGroup from '@fastgpt/web/components/common/Avatar/AvatarGroup';
import {
Box,
HStack,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
useDisclosure
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useContextSelector } from 'use-context-selector';
import { TeamModalContext } from '../../context';
import MyMenu, { MenuItemType } from '@fastgpt/web/components/common/MyMenu';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { deleteGroup } from '@/web/support/user/team/group/api';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import MemberTag from '../../../Info/MemberTag';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic';
import { useState } from 'react';
const ChangeOwnerModal = dynamic(() => import('./GroupTransferOwnerModal'));
function MemberTable({
onEditGroup,
onManageMember
}: {
onEditGroup: (groupId: string) => void;
onManageMember: (groupId: string) => void;
}) {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const [editGroupId, setEditGroupId] = useState<string>();
const { ConfirmModal: ConfirmDeleteGroupModal, openConfirm: openDeleteGroupModal } = useConfirm({
type: 'delete',
content: t('user:team.group.delete_confirm')
});
const { groups, refetchGroups, members, refetchMembers } = useContextSelector(
TeamModalContext,
(v) => v
);
const { runAsync: delDeleteGroup } = useRequest2(deleteGroup, {
onSuccess: () => {
refetchGroups();
refetchMembers();
}
});
const hasGroupManagePer = (group: (typeof groups)[0]) =>
userInfo?.team.permission.hasManagePer ||
['admin', 'owner'].includes(
group.members.find((item) => item.tmbId === userInfo?.team.tmbId)?.role ?? ''
);
const isGroupOwner = (group: (typeof groups)[0]) =>
userInfo?.team.permission.hasManagePer ||
group.members.find((item) => item.role === 'owner')?.tmbId === userInfo?.team.tmbId;
const {
isOpen: isOpenChangeOwner,
onOpen: onOpenChangeOwner,
onClose: onCloseChangeOwner
} = useDisclosure();
const onChangeOwner = (groupId: string) => {
setEditGroupId(groupId);
onOpenChangeOwner();
};
return (
<MyBox>
<TableContainer overflow={'unset'} fontSize={'sm'} mx="6">
<Table overflow={'unset'}>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="6px">
{t('user:team.group.name')}
</Th>
<Th bg="myGray.100">{t('user:owner')}</Th>
<Th bg="myGray.100">{t('user:team.group.members')}</Th>
<Th bg="myGray.100" borderRightRadius="6px">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
<Tbody>
{groups?.map((group) => (
<Tr key={group._id} overflow={'unset'}>
<Td>
<HStack>
<MemberTag
name={
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
}
avatar={group.avatar}
/>
<Box>
({group.name === DefaultGroupName ? members.length : group.members.length})
</Box>
</HStack>
</Td>
<Td>
<MemberTag
name={
group.name === DefaultGroupName
? members.find((item) => item.role === 'owner')?.memberName ?? ''
: members.find(
(item) =>
item.tmbId ===
group.members.find((item) => item.role === 'owner')?.tmbId
)?.memberName ?? ''
}
avatar={
group.name === DefaultGroupName
? members.find((item) => item.role === 'owner')?.avatar ?? ''
: members.find(
(i) =>
i.tmbId === group.members.find((item) => item.role === 'owner')?.tmbId
)?.avatar ?? ''
}
/>
</Td>
<Td>
{group.name === DefaultGroupName ? (
<AvatarGroup avatars={members.map((v) => v.avatar)} groupId={group._id} />
) : hasGroupManagePer(group) ? (
<MyTooltip label={t('user:team.group.manage_member')}>
<Box cursor="pointer" onClick={() => onManageMember(group._id)}>
<AvatarGroup
avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
)}
groupId={group._id}
/>
</Box>
</MyTooltip>
) : (
<AvatarGroup
avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
)}
groupId={group._id}
/>
)}
</Td>
<Td>
{hasGroupManagePer(group) && group.name !== DefaultGroupName && (
<MyMenu
Button={<MyIcon name={'edit'} cursor={'pointer'} w="1rem" />}
menuList={[
{
children: [
{
label: t('user:team.group.edit_info'),
icon: 'edit',
onClick: () => {
onEditGroup(group._id);
}
},
{
label: t('user:team.group.manage_member'),
icon: 'support/team/group',
onClick: () => {
onManageMember(group._id);
}
},
...(isGroupOwner(group)
? [
{
label: t('user:team.group.transfer_owner'),
icon: 'modal/changePer',
onClick: () => {
onChangeOwner(group._id);
},
type: 'primary' as MenuItemType
},
{
label: t('common:common.Delete'),
icon: 'delete',
onClick: () => {
openDeleteGroupModal(() => delDeleteGroup(group._id))();
},
type: 'danger' as MenuItemType
}
]
: [])
]
}
]}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
<ConfirmDeleteGroupModal />
{isOpenChangeOwner && editGroupId && (
<ChangeOwnerModal groupId={editGroupId} onClose={onCloseChangeOwner} />
)}
</MyBox>
);
}
export default MemberTable;

View File

@@ -1,20 +1,12 @@
import React, { useMemo, useState } from 'react';
import React, { useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { ModalCloseButton, ModalBody, Box, ModalFooter, Button } from '@chakra-ui/react';
import TagTextarea from '@/components/common/Textarea/TagTextarea';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { postInviteTeamMember } from '@/web/support/user/team/api';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import type { InviteMemberResponse } from '@fastgpt/global/support/user/team/controller.d';
import MySelect from '@fastgpt/web/components/common/MySelect';
import {
ManagePermissionVal,
ReadPermissionVal,
WritePermissionVal
} from '@fastgpt/global/support/permission/constant';
import { useI18n } from '@/web/context/I18n';
import { useUserStore } from '@/web/support/user/useUserStore';
const InviteModal = ({
teamId,
@@ -26,69 +18,43 @@ const InviteModal = ({
onSuccess: () => void;
}) => {
const { t } = useTranslation();
const { userT } = useI18n();
const { ConfirmModal, openConfirm } = useConfirm({
title: t('common:user.team.Invite Member Result Tip'),
showCancel: false
});
const { userInfo } = useUserStore();
const [inviteUsernames, setInviteUsernames] = useState<string[]>([]);
const inviteTypes = useMemo(
() => [
{
label: userT('permission.Read'),
description: userT('permission.Read desc'),
value: ReadPermissionVal
},
{
label: userT('permission.Write'),
description: userT('permission.Write tip'),
value: WritePermissionVal
},
...(userInfo?.team?.permission.isOwner
? [
{
label: userT('permission.Manage'),
description: userT('permission.Manage tip'),
value: ManagePermissionVal
}
]
: [])
],
[userInfo?.team?.permission.isOwner, userT]
);
const [selectedInviteType, setSelectInviteType] = useState(inviteTypes[0].value);
const { mutate: onInvite, isLoading } = useRequest({
mutationFn: () => {
return postInviteTeamMember({
const { runAsync: onInvite, loading: isLoading } = useRequest2(
() =>
postInviteTeamMember({
teamId,
usernames: inviteUsernames,
permission: selectedInviteType
});
},
onSuccess(res: InviteMemberResponse) {
onSuccess();
openConfirm(
() => onClose(),
undefined,
<Box whiteSpace={'pre-wrap'}>
{t('user.team.Invite Member Success Tip', {
success: res.invite.length,
inValid: res.inValid.map((item) => item.username).join(', '),
inTeam: res.inTeam.map((item) => item.username).join(', ')
})}
</Box>
)();
},
errorToast: t('common:user.team.Invite Member Failed Tip')
});
usernames: inviteUsernames
}),
{
onSuccess(res: InviteMemberResponse) {
onSuccess();
openConfirm(
() => onClose(),
undefined,
<Box whiteSpace={'pre-wrap'}>
{t('user.team.Invite Member Success Tip', {
success: res.invite.length,
inValid: res.inValid.map((item) => item.username).join(', '),
inTeam: res.inTeam.map((item) => item.username).join(', ')
})}
</Box>
)();
},
errorToast: t('common:user.team.Invite Member Failed Tip')
}
);
return (
<MyModal
isOpen
iconSrc="/imgs/modal/team.svg"
iconSrc="common/inviteLight"
iconColor="primary.600"
title={
<Box>
<Box>{t('common:user.team.Invite Member')}</Box>
@@ -104,9 +70,6 @@ const InviteModal = ({
<ModalBody>
<Box mb={2}>{t('common:user.Account')}</Box>
<TagTextarea defaultValues={inviteUsernames} onUpdate={setInviteUsernames} />
<Box mt={4}>
<MySelect list={inviteTypes} value={selectedInviteType} onchange={setSelectInviteType} />
</Box>
</ModalBody>
<ModalFooter>
<Button

View File

@@ -1,105 +1,91 @@
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import {
Box,
HStack,
MenuButton,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr
} from '@chakra-ui/react';
import {
TeamMemberRoleEnum,
TeamMemberStatusMap
} from '@fastgpt/global/support/user/team/constant';
import { Box, HStack, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { useUserStore } from '@/web/support/user/useUserStore';
import { TeamModalContext } from '../context';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import MyBox from '@fastgpt/web/components/common/MyBox';
import PermissionTags from '@/components/support/permission/PermissionTags';
import { TeamPermissionList } from '@fastgpt/global/support/permission/user/constant';
import PermissionSelect from '@/components/support/permission/MemberManager/PermissionSelect';
import { CollaboratorContext } from '@/components/support/permission/MemberManager/context';
import { delRemoveMember } from '@/web/support/user/team/api';
import Tag from '@fastgpt/web/components/common/Tag';
import Icon from '@fastgpt/web/components/common/Icon';
import GroupTags from '@/components/support/permission/Group/GroupTags';
import { useContextSelector } from 'use-context-selector';
import { TeamModalContext } from '../context';
function MemberTable() {
const { userInfo } = useUserStore();
const { t } = useTranslation();
const { members, refetchMembers } = useContextSelector(TeamModalContext, (v) => v);
const { onUpdateCollaborators } = useContextSelector(CollaboratorContext, (v) => v);
const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm({
type: 'delete'
});
const { members, groups, refetchMembers, refetchGroups } = useContextSelector(
TeamModalContext,
(v) => v
);
return (
<MyBox>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<TableContainer overflow={'unset'} fontSize={'sm'} mx="6">
<Table overflow={'unset'}>
<Thead bg={'myWhite.400'}>
<Tr>
<Th borderRadius={'none !important'}>{t('common:common.Username')}</Th>
<Th>{t('common:common.Permission')}</Th>
<Th>{t('common:common.Status')}</Th>
<Th borderRadius={'none !important'}>{t('common:common.Action')}</Th>
<Thead>
<Tr bgColor={'white !important'}>
<Th borderLeftRadius="6px" bgColor="myGray.100">
{t('common:common.Username')}
</Th>
<Th bgColor="myGray.100">{t('user:team.belong_to_group')}</Th>
<Th borderRightRadius="6px" bgColor="myGray.100">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
<Tbody>
{members.map((item) => (
{members?.map((item) => (
<Tr key={item.userId} overflow={'unset'}>
<Td>
<HStack>
<Avatar src={item.avatar} w={['18px', '22px']} />
<Box maxW={'150px'} className={'textEllipsis'}>
<Avatar src={item.avatar} w={['18px', '22px']} borderRadius={'50%'} />
<Box className={'textEllipsis'}>
{item.memberName}
{item.status === 'waiting' && (
<Tag ml="2" colorSchema="yellow">
{t('common:user.team.member.waiting')}
</Tag>
)}
</Box>
</HStack>
</Td>
<Td>
<PermissionTags
permission={item.permission}
permissionList={TeamPermissionList}
<Td maxW={'300px'}>
<GroupTags
names={groups
?.filter((group) => group.members.map((m) => m.tmbId).includes(item.tmbId))
.map((g) => g.name)}
max={3}
/>
</Td>
<Td color={TeamMemberStatusMap[item.status].color}>
{t(TeamMemberStatusMap[item.status]?.label || ('' as any))}
</Td>
<Td>
{userInfo?.team.permission.hasManagePer &&
item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
<PermissionSelect
value={item.permission.value}
Button={
<MenuButton
_hover={{
color: 'primary.600'
}}
borderRadius={'md'}
px={2}
py={1}
lineHeight={1}
>
<MyIcon name={'edit'} cursor={'pointer'} w="1rem" />
</MenuButton>
}
onChange={(permission) => {
onUpdateCollaborators({
tmbIds: [item.tmbId],
permission
});
<Icon
name={'common/trash'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'red.600',
bgColor: 'myGray.100'
}}
onDelete={() => {
onClick={() => {
openRemoveMember(
() => delRemoveMember(item.tmbId).then(refetchMembers),
() =>
delRemoveMember(item.tmbId).then(() =>
Promise.all([refetchGroups(), refetchMembers()])
),
undefined,
t('user.team.Remove Member Confirm Tip', {
t('common:user.team.Remove Member Confirm Tip', {
username: item.memberName
})
)();

View File

@@ -1,170 +0,0 @@
import {
Box,
Button,
ModalBody,
ModalCloseButton,
ModalFooter,
Grid,
Input,
Flex,
Checkbox,
CloseButton,
InputGroup,
InputLeftElement
} from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { updateMemberPermission } from '@/web/support/user/team/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
import { TeamModalContext } from '../../context';
import { useI18n } from '@/web/context/I18n';
function AddManagerModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) {
const { t } = useTranslation();
const { userT } = useI18n();
const { userInfo } = useUserStore();
const { members, refetchMembers } = useContextSelector(TeamModalContext, (v) => v);
const [selected, setSelected] = useState<typeof members>([]);
const [searchKey, setSearchKey] = useState('');
const { mutate: submit, isLoading } = useRequest({
mutationFn: async () => {
return updateMemberPermission({
permission: ManagePermissionVal,
tmbIds: selected.map((item) => {
return item.tmbId;
})
});
},
onSuccess: () => {
refetchMembers();
onSuccess();
},
successToast: t('common:common.Success'),
errorToast: t('common:common.failed')
});
const filterMembers = useMemo(() => {
return members.filter((member) => {
if (member.permission.isOwner) return false;
if (!searchKey) return true;
return !!member.memberName.includes(searchKey);
});
}, [members, searchKey]);
return (
<MyModal
isOpen
iconSrc={'modal/AddClb'}
maxW={['90vw']}
minW={['900px']}
overflow={'unset'}
title={userT('team.Add manager')}
>
<ModalCloseButton onClick={onClose} />
<ModalBody py={6} px={10}>
<Grid
templateColumns="1fr 1fr"
h="448px"
borderRadius="8px"
border="1px solid"
borderColor="myGray.200"
>
<Flex flexDirection="column" p="4">
<InputGroup alignItems="center" size={'sm'}>
<InputLeftElement>
<MyIcon name="common/searchLight" w="16px" color={'myGray.500'} />
</InputLeftElement>
<Input
placeholder={t('user:search_user')}
fontSize="sm"
bg={'myGray.50'}
onChange={(e) => {
setSearchKey(e.target.value);
}}
/>
</InputGroup>
<Flex flexDirection="column" mt={3}>
{filterMembers.map((member) => {
return (
<Flex
py="2"
px={3}
borderRadius={'md'}
alignItems="center"
key={member.tmbId}
cursor={'pointer'}
_hover={{
bg: 'myGray.50',
...(!selected.includes(member) ? { svg: { color: 'myGray.50' } } : {})
}}
_notLast={{ mb: 2 }}
onClick={() => {
if (selected.indexOf(member) == -1) {
setSelected([...selected, member]);
} else {
setSelected([...selected.filter((item) => item.tmbId != member.tmbId)]);
}
}}
>
<Checkbox
isChecked={selected.includes(member)}
icon={<MyIcon name={'common/check'} w={'12px'} />}
/>
<Avatar ml={2} src={member.avatar} w="1.5rem" />
{member.memberName}
</Flex>
);
})}
</Flex>
</Flex>
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4">
<Box mt={3}>{t('common:chosen') + ': ' + selected.length} </Box>
<Box mt={5}>
{selected.map((member) => {
return (
<Flex
alignItems="center"
justifyContent="space-between"
py="2"
px={3}
borderRadius={'md'}
key={member.tmbId}
_hover={{ bg: 'myGray.50' }}
_notLast={{ mb: 2 }}
>
<Avatar src={member.avatar} w="1.5rem" />
<Box w="full">{member.memberName}</Box>
<MyIcon
name={'common/closeLight'}
w={'1rem'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
onClick={() =>
setSelected([...selected.filter((item) => item.tmbId != member.tmbId)])
}
/>
</Flex>
);
})}
</Box>
</Flex>
</Grid>
</ModalBody>
<ModalFooter alignItems="flex-end">
<Button h={'30px'} isLoading={isLoading} onClick={submit}>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
}
export default AddManagerModal;

View File

@@ -1,116 +1,286 @@
import React from 'react';
import { Box, Button, Flex, Tag, TagLabel, useDisclosure } from '@chakra-ui/react';
import {
Box,
Checkbox,
HStack,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { delMemberPermission } from '@/web/support/user/team/api';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getTeamClbs, updateMemberPermission } from '@/web/support/user/team/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import { TeamModalContext } from '../../context';
import { TeamPermissionList } from '@fastgpt/global/support/permission/user/constant';
import dynamic from 'next/dynamic';
import MyBox from '@fastgpt/web/components/common/MyBox';
import MyTag from '@fastgpt/web/components/common/Tag/index';
const AddManagerModal = dynamic(() => import('./AddManager'));
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MemberTag from '../../../Info/MemberTag';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import {
TeamManagePermissionVal,
TeamWritePermissionVal
} from '@fastgpt/global/support/permission/user/constant';
import { TeamPermission } from '@fastgpt/global/support/permission/user/controller';
import { useCreation } from 'ahooks';
function PermissionManage() {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { members, refetchMembers } = useContextSelector(TeamModalContext, (v) => v);
const { groups, refetchMembers, refetchGroups, members, searchKey } = useContextSelector(
TeamModalContext,
(v) => v
);
const {
isOpen: isOpenAddManager,
onOpen: onOpenAddManager,
onClose: onCloseAddManager
} = useDisclosure();
const { runAsync: refetchClbs, data: clbs = [] } = useRequest2(getTeamClbs, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const { mutate: removeManager, isLoading: isRemovingManager } = useRequest({
mutationFn: async (memberId: string) => {
return delMemberPermission(memberId);
},
successToast: t('user:delete.admin_success'),
errorToast: t('user:delete.admin_failed'),
const filteredGroups = useCreation(
() => groups?.filter((group) => group.name.toLowerCase().includes(searchKey.toLowerCase())),
[groups, searchKey]
);
const filteredMembers = useCreation(
() =>
members
?.filter((member) => member.memberName.toLowerCase().includes(searchKey.toLowerCase()))
.map((member) => {
const clb = clbs?.find((clb) => String(clb.tmbId) === String(member.tmbId));
const permission =
member.role === 'owner'
? new TeamPermission({ isOwner: true })
: new TeamPermission({ per: clb?.permission });
return { ...member, permission };
}),
[clbs, members, searchKey]
);
const { runAsync: onUpdateMemberPermission } = useRequest2(updateMemberPermission, {
onSuccess: () => {
refetchGroups();
refetchMembers();
refetchClbs();
}
});
return (
<MyBox h={'100%'} isLoading={isRemovingManager} bg={'white'}>
<Flex
mx={'5'}
flexDirection={'row'}
alignItems={'center'}
rowGap={'8'}
justifyContent={'space-between'}
>
<Flex>
<Box fontSize={['sm', 'md']} fontWeight={'bold'} alignItems={'center'}>
{t('common:user.team.role.Admin')}
</Box>
<Box
fontSize={['xs']}
color={'myGray.500'}
bgColor={'myGray.100'}
alignItems={'center'}
alignContent={'center'}
px={'3'}
ml={3}
borderRadius={'sm'}
>
{t(TeamPermissionList['manage'].description as any)}
</Box>
</Flex>
{userInfo?.team.role === 'owner' && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'common/inviteLight'} w={'14px'} color={'primary.500'} />}
onClick={() => {
onOpenAddManager();
}}
>
{t('user:team.Add manager')}
</Button>
)}
</Flex>
<Flex mt="4" mx="4" flexWrap={'wrap'} gap={3}>
{members.map((member) => {
if (member.permission.hasManagePer && !member.permission.isOwner) {
return (
<MyTag key={member.tmbId} px="4" py="2" type="fill" colorSchema="gray">
<Avatar src={member.avatar} w="1.25rem" />
<Box fontSize={'sm'} ml={1}>
{member.memberName}
</Box>
{userInfo?.team.role === 'owner' && (
<MyIcon
ml={4}
name="common/trash"
w="1rem"
color="myGray.500"
cursor="pointer"
_hover={{ color: 'red.600' }}
onClick={() => {
removeManager(member.tmbId);
}}
/>
)}
</MyTag>
);
const { runAsync: onAddPermission, loading: addLoading } = useRequest2(
async ({
groupId,
memberId,
per
}: {
groupId?: string;
memberId?: string;
per: 'write' | 'manage';
}) => {
if (groupId) {
const group = groups?.find((group) => group._id === groupId);
if (group) {
const permission = new TeamPermission({ per: group.permission.value });
switch (per) {
case 'write':
permission.addPer(TeamWritePermissionVal);
return onUpdateMemberPermission({
groupId: group._id,
permission: permission.value
});
case 'manage':
permission.addPer(TeamManagePermissionVal);
return onUpdateMemberPermission({
groupId: group._id,
permission: permission.value
});
}
})}
</Flex>
}
}
if (memberId) {
const member = filteredMembers?.find((member) => String(member.tmbId) === memberId);
if (member) {
const permission = new TeamPermission({ per: member.permission.value });
switch (per) {
case 'write':
permission.addPer(TeamWritePermissionVal);
return onUpdateMemberPermission({
memberId: String(member.tmbId),
permission: permission.value
});
case 'manage':
permission.addPer(TeamManagePermissionVal);
return onUpdateMemberPermission({
memberId: String(member.tmbId),
permission: permission.value
});
}
}
}
}
);
{isOpenAddManager && (
<AddManagerModal onClose={onCloseAddManager} onSuccess={onCloseAddManager} />
)}
</MyBox>
const { runAsync: onRemovePermission, loading: removeLoading } = useRequest2(
async ({
groupId,
memberId,
per
}: {
groupId?: string;
memberId?: string;
per: 'write' | 'manage';
}) => {
if (groupId) {
const group = groups?.find((group) => group._id === groupId);
if (group) {
const permission = new TeamPermission({ per: group.permission.value });
switch (per) {
case 'write':
permission.removePer(TeamWritePermissionVal);
return onUpdateMemberPermission({
groupId: group._id,
permission: permission.value
});
case 'manage':
permission.removePer(TeamManagePermissionVal);
return onUpdateMemberPermission({
groupId: group._id,
permission: permission.value
});
}
}
}
if (memberId) {
const member = members?.find((member) => String(member.tmbId) === memberId);
if (member) {
const permission = new TeamPermission({ per: member.permission.value }); // Hint: member.permission is read-only
switch (per) {
case 'write':
permission.removePer(TeamWritePermissionVal);
return onUpdateMemberPermission({
memberId: String(member.tmbId),
permission: permission.value
});
case 'manage':
permission.removePer(TeamManagePermissionVal);
return onUpdateMemberPermission({
memberId: String(member.tmbId),
permission: permission.value
});
}
}
}
}
);
const userManage = userInfo?.permission.hasManagePer;
return (
<TableContainer fontSize={'sm'} mx="6">
<Table>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="md" maxW={'150px'}>
{t('user:team.group.group')} / {t('user:team.group.members')}
<QuestionTip ml="1" label={t('user:team.group.permission_tip')} />
</Th>
<Th bg="myGray.100">
<Box mx="auto" w="fit-content">
{t('user:team.group.permission.write')}
</Box>
</Th>
<Th bg="myGray.100" borderRightRadius="md">
<Box mx="auto" w="fit-content">
{t('user:team.group.permission.manage')}
<QuestionTip ml="1" label={t('user:team.group.manage_tip')} />
</Box>
</Th>
</Tr>
</Thead>
<Tbody>
{filteredGroups?.map((group) => (
<Tr key={group._id} overflow={'unset'} border="none">
<Td border="none">
<MemberTag
name={
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
}
avatar={group.avatar}
/>
</Td>
<Td border="none">
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userManage}
isChecked={group.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onAddPermission({ groupId: group._id, per: 'write' })
: onRemovePermission({ groupId: group._id, per: 'write' })
}
/>
</Box>
</Td>
<Td border="none">
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userInfo?.permission.isOwner}
isChecked={group.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onAddPermission({ groupId: group._id, per: 'manage' })
: onRemovePermission({ groupId: group._id, per: 'manage' })
}
/>
</Box>
</Td>
</Tr>
))}
{filteredGroups?.length > 0 && filteredMembers?.length > 0 && (
<Tr borderBottom={'1px solid'} borderColor={'myGray.300'} />
)}
{filteredMembers?.map((member) => (
<Tr key={member.tmbId} overflow={'unset'} border="none">
<Td border="none">
<HStack>
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box>{member.memberName}</Box>
</HStack>
</Td>
<Td border="none">
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={member.permission.isOwner || !userManage}
isChecked={member.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onAddPermission({ memberId: String(member.tmbId), per: 'write' })
: onRemovePermission({ memberId: String(member.tmbId), per: 'write' })
}
/>
</Box>
</Td>
<Td border="none">
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={member.permission.isOwner || !userInfo?.permission.isOwner}
isChecked={member.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onAddPermission({ memberId: String(member.tmbId), per: 'manage' })
: onRemovePermission({ memberId: String(member.tmbId), per: 'manage' })
}
/>
</Box>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
);
}

View File

@@ -0,0 +1,279 @@
import React, { useMemo, useState } from 'react';
import {
Box,
Checkbox,
Flex,
Grid,
HStack,
Input,
InputGroup,
InputLeftElement
} from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useTranslation } from 'next-i18next';
import { Control, Controller } from 'react-hook-form';
import { RequireAtLeastOne } from '@fastgpt/global/common/type/utils';
import { useUserStore } from '@/web/support/user/useUserStore';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
type memberType = {
type: 'member';
tmbId: string;
memberName: string;
avatar: string;
};
type groupType = {
type: 'group';
_id: string;
name: string;
avatar: string;
};
type selectedType = {
member: string[];
group: string[];
};
function SelectMember({
allMembers,
selected = { member: [], group: [] },
setSelected
// mode = 'both'
}: {
allMembers: {
member: memberType[];
group: groupType[];
};
selected?: selectedType;
setSelected: React.Dispatch<React.SetStateAction<selectedType>>;
mode?: 'member' | 'group' | 'both';
}) {
const [searchKey, setSearchKey] = useState('');
const { t } = useTranslation();
const { userInfo } = useUserStore();
const filtered = useMemo(() => {
return [
...allMembers.member.filter((member) => {
if (member.memberName.toLowerCase().includes(searchKey.toLowerCase())) return true;
return false;
}),
...allMembers.group.filter((member) => {
if (member.name.toLowerCase().includes(searchKey.toLowerCase())) return true;
return false;
})
];
}, [searchKey, allMembers]);
const selectedFlated = useMemo(() => {
return [
...allMembers.member.filter((member) => {
return selected.member?.includes(member.tmbId);
}),
...allMembers.group.filter((member) => {
return selected.group?.includes(member._id);
})
];
}, [selected, allMembers]);
const handleToggleSelect = (member: memberType | groupType) => {
if (member.type == 'member') {
if (selected.member?.indexOf(member.tmbId) == -1) {
setSelected({
member: [...selected.member, member.tmbId],
group: [...selected.group]
});
} else {
setSelected({
member: [...selected.member.filter((item) => item != member.tmbId)],
group: [...selected.group]
});
}
} else {
if (selected.group?.indexOf(member._id) == -1) {
setSelected({ member: [...selected.member], group: [...selected.group, member._id] });
} else {
setSelected({
member: [...selected.member],
group: [...selected.group.filter((item) => item != member._id)]
});
}
}
};
const isSelected = (member: memberType | groupType) => {
if (member.type == 'member') {
return selected.member?.includes(member.tmbId);
} else {
return selected.group?.includes(member._id);
}
};
return (
<Grid
templateColumns="1fr 1fr"
borderRadius="8px"
border="1px solid"
borderColor="myGray.200"
h={'100%'}
>
<Flex flexDirection="column" p="4" h={'100%'} overflow={'auto'}>
<InputGroup alignItems="center" size={'sm'}>
<InputLeftElement>
<MyIcon name="common/searchLight" w="16px" color={'myGray.500'} />
</InputLeftElement>
<Input
placeholder={t('user:search_user')}
fontSize="sm"
bg={'myGray.50'}
onChange={(e) => {
setSearchKey(e.target.value);
}}
/>
</InputGroup>
<Flex flexDirection="column" mt={3}>
{filtered.map((member) => {
return (
<HStack
py="2"
px={3}
borderRadius={'md'}
alignItems="center"
key={member.type == 'member' ? member.tmbId : member._id}
cursor={'pointer'}
_hover={{
bg: 'myGray.50',
...(!isSelected(member) ? { svg: { color: 'myGray.50' } } : {})
}}
_notLast={{ mb: 2 }}
onClick={() => handleToggleSelect(member)}
>
<Checkbox
isChecked={!!isSelected(member)}
icon={<MyIcon name={'common/check'} w={'12px'} />}
/>
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box>
{member.type == 'member'
? member.memberName
: member.name === DefaultGroupName
? userInfo?.team.teamName
: member.name}
</Box>
</HStack>
);
})}
</Flex>
</Flex>
<Flex
borderLeft="1px"
borderColor="myGray.200"
flexDirection="column"
p="4"
h={'100%'}
overflow={'auto'}
>
<Box mt={3}>
{t('common:chosen') + ': ' + Number(selected.member.length + selected.group.length)}{' '}
</Box>
<Box mt={5}>
{selectedFlated.map((member) => {
return (
<HStack
justifyContent="space-between"
py="2"
px={3}
borderRadius={'md'}
key={member.type == 'member' ? member.tmbId : member._id}
_hover={{ bg: 'myGray.50' }}
_notLast={{ mb: 2 }}
>
<Avatar src={member.avatar} w="1.5rem" borderRadius={'md'} />
<Box w="full">
{member.type == 'member'
? member.memberName
: member.name === DefaultGroupName
? userInfo?.team.teamName
: member.name}
</Box>
<MyIcon
name={'common/closeLight'}
w={'1rem'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
onClick={() => handleToggleSelect(member)}
/>
</HStack>
);
})}
</Box>
</Flex>
</Grid>
);
}
// This function is for using with react-hook-form
function ControllerWrapper({
control,
allMembers,
mode = 'both',
name = 'members'
}: {
control: Control;
allMembers: RequireAtLeastOne<{ member?: memberType[]; group?: groupType[] }>;
mode?: 'member' | 'group' | 'both';
name?: string;
}) {
return (
<Controller
control={control}
name={name}
render={({ field: { value: selected, onChange } }) => (
<SelectMember
mode={mode}
allMembers={
(() => {
switch (mode) {
case 'member':
return { member: allMembers.member, group: [] };
case 'group':
return { member: [], group: allMembers.group };
case 'both':
return { member: allMembers.member, group: allMembers.group };
}
})() as Required<typeof allMembers>
}
selected={(() => {
switch (mode) {
case 'member':
return { member: selected, group: [] };
case 'group':
return { member: [], group: selected };
case 'both':
return { member: selected.member, group: selected.group };
}
})()}
setSelected={
(({ member, group }: selectedType, _prevState: selectedType) => {
switch (mode) {
case 'member':
onChange(member);
return;
case 'group':
onChange(group);
return;
case 'both':
onChange({ member, group });
return;
}
}) as any // hack: we do not need to handle prevState
}
/>
)}
/>
);
}
export const UnControlledSelectMember = SelectMember;
export default ControllerWrapper;

View File

@@ -1,54 +1,57 @@
import React, { ReactNode, useCallback, useState } from 'react';
import React, { ReactNode, useState } from 'react';
import { createContext } from 'use-context-selector';
import type { EditTeamFormDataType } from './components/EditInfoModal';
import dynamic from 'next/dynamic';
import { useQuery } from '@tanstack/react-query';
import {
delMemberPermission,
getTeamList,
putSwitchTeam,
updateMemberPermission
} from '@/web/support/user/team/api';
import { getTeamList, putSwitchTeam } from '@/web/support/user/team/api';
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
import { useUserStore } from '@/web/support/user/useUserStore';
import type { TeamTmbItemType, TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import CollaboratorContextProvider from '@/components/support/permission/MemberManager/context';
import { TeamPermissionList } from '@fastgpt/global/support/permission/user/constant';
import {
CollaboratorItemType,
UpdateClbPermissionProps
} from '@fastgpt/global/support/permission/collaborator';
import { getGroupList } from '@/web/support/user/team/group/api';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
const EditInfoModal = dynamic(() => import('./components/EditInfoModal'));
type TeamModalContextType = {
myTeams: TeamTmbItemType[];
refetchTeams: () => void;
members: TeamMemberItemType[];
groups: MemberGroupListType;
isLoading: boolean;
onSwitchTeam: (teamId: string) => void;
setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>;
members: TeamMemberItemType[];
refetchMembers: () => void;
refetchTeams: () => void;
refetchGroups: () => void;
searchKey: string;
setSearchKey: React.Dispatch<React.SetStateAction<string>>;
};
export const TeamModalContext = createContext<TeamModalContextType>({
myTeams: [],
isLoading: false,
onSwitchTeam: function (teamId: string): void {
throw new Error('Function not implemented.');
},
setEditTeamData: function (value: React.SetStateAction<EditTeamFormDataType | undefined>): void {
throw new Error('Function not implemented.');
},
groups: [],
members: [],
isLoading: false,
onSwitchTeam: function (_teamId: string): void {
throw new Error('Function not implemented.');
},
setEditTeamData: function (_value: React.SetStateAction<EditTeamFormDataType | undefined>): void {
throw new Error('Function not implemented.');
},
refetchTeams: function (): void {
throw new Error('Function not implemented.');
},
refetchMembers: function (): void {
throw new Error('Function not implemented.');
},
refetchGroups: function (): void {
throw new Error('Function not implemented.');
},
searchKey: '',
setSearchKey: function (_value: React.SetStateAction<string>): void {
throw new Error('Function not implemented.');
}
});
@@ -56,12 +59,16 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
const { t } = useTranslation();
const [editTeamData, setEditTeamData] = useState<EditTeamFormDataType>();
const { userInfo, initUserInfo, loadAndGetTeamMembers } = useUserStore();
const [searchKey, setSearchKey] = useState('');
const {
data: myTeams = [],
isFetching: isLoadingTeams,
refetch: refetchTeams
} = useQuery(['getTeams', userInfo?._id], () => getTeamList(TeamMemberStatusEnum.active));
loading: isLoadingTeams,
refresh: refetchTeams
} = useRequest2(() => getTeamList(TeamMemberStatusEnum.active), {
manual: false,
refreshDeps: [userInfo?._id]
});
// member action
const {
@@ -79,71 +86,59 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
}
);
const onGetClbList = useCallback(() => {
return refetchMembers().then((res) =>
res.map<CollaboratorItemType>((member) => ({
teamId: member.teamId,
tmbId: member.tmbId,
permission: member.permission,
name: member.memberName,
avatar: member.avatar
}))
);
}, [refetchMembers]);
const { runAsync: onUpdatePer, loading: isUpdatingPer } = useRequest2(
(props: UpdateClbPermissionProps) => {
return updateMemberPermission(props);
}
);
const { mutate: onSwitchTeam, isLoading: isSwitchingTeam } = useRequest({
mutationFn: async (teamId: string) => {
const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2(
async (teamId: string) => {
await putSwitchTeam(teamId);
return initUserInfo();
},
errorToast: t('common:user.team.Switch Team Failed')
{
errorToast: t('common:user.team.Switch Team Failed')
}
);
const {
data: groups = [],
loading: isLoadingGroups,
refresh: refetchGroups
} = useRequest2(getGroupList, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers || isUpdatingPer;
const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers || isLoadingGroups;
const contextValue = {
myTeams,
refetchTeams,
isLoading,
onSwitchTeam,
searchKey,
setSearchKey,
// create | update team
setEditTeamData,
members,
refetchMembers
refetchMembers,
groups,
refetchGroups
};
return (
<TeamModalContext.Provider value={contextValue}>
{userInfo?.team?.permission && (
<CollaboratorContextProvider
permission={userInfo?.team?.permission}
permissionList={TeamPermissionList}
onGetCollaboratorList={onGetClbList}
onUpdateCollaborators={onUpdatePer}
onDelOneCollaborator={delMemberPermission}
>
{() => (
<>
{children}
{!!editTeamData && (
<EditInfoModal
defaultData={editTeamData}
onClose={() => setEditTeamData(undefined)}
onSuccess={() => {
refetchTeams();
initUserInfo();
}}
/>
)}
</>
<>
{children}
{!!editTeamData && (
<EditInfoModal
defaultData={editTeamData}
onClose={() => setEditTeamData(undefined)}
onSuccess={() => {
refetchTeams();
initUserInfo();
}}
/>
)}
</CollaboratorContextProvider>
</>
)}
</TeamModalContext.Provider>
);

View File

@@ -1,9 +1,7 @@
import React from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { Box } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { createContext, useContextSelector } from 'use-context-selector';
import TeamList from './TeamList';
import TeamCard from './TeamCard';
@@ -14,7 +12,6 @@ export const TeamContext = createContext<{}>({} as any);
type Props = { onClose: () => void };
const TeamManageModal = ({ onClose }: Props) => {
const { Loading } = useLoading();
const { isLoading } = useContextSelector(TeamModalContext, (v) => v);
return (
@@ -26,15 +23,15 @@ const TeamManageModal = ({ onClose }: Props) => {
w={'100%'}
h={'550px'}
isCentered
bg={'myWhite.600'}
bg={'myGray.50'}
overflow={'hidden'}
isLoading={isLoading}
>
<Box display={['block', 'flex']} flex={1} position={'relative'} overflow={'auto'}>
<TeamList />
<Box h={'100%'} flex={'1 0 0'}>
<TeamCard />
</Box>
<Loading loading={isLoading} fixed={false} />
</Box>
</MyModal>
</>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Box, Button, Flex, Image, useDisclosure } from '@chakra-ui/react';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useTranslation } from 'next-i18next';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';

View File

@@ -0,0 +1,64 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { FastGPTProUrl } from '@fastgpt/service/common/system/constants';
import { POST } from '@fastgpt/service/common/api/plusRequest';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { MongoMemberGroupModel } from '@fastgpt/service/support/permission/memberGroup/memberGroupSchema';
import { delay } from '@fastgpt/global/common/system/utils';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
/*
1. 给每个 team 创建一个默认的 group
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
await authCert({ req, authRoot: true });
const teamList = await MongoTeam.find({}, '_id');
console.log('Total team', teamList.length);
let success = 0;
async function createGroup(teamId: string) {
try {
await MongoMemberGroupModel.updateOne(
{
teamId,
name: DefaultGroupName
},
{
$set: {
teamId: teamId,
name: DefaultGroupName
}
},
{
upsert: true
}
);
} catch (error) {
console.log(error);
await delay(500);
return createGroup(teamId);
}
}
for await (const team of teamList) {
await createGroup(team._id);
console.log(++success);
}
jsonRes(res, {
message: 'success'
});
} catch (error) {
console.log(error);
jsonRes(res, {
code: 500,
error
});
}
}

View File

@@ -7,8 +7,7 @@ import {
Input,
Textarea,
ModalFooter,
ModalBody,
useDisclosure
ModalBody
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
@@ -35,10 +34,10 @@ import {
} from '@fastgpt/global/support/permission/app/constant';
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator';
import { resumeInheritPer } from '@/web/core/app/api';
import { useI18n } from '@/web/context/I18n';
import ResumeInherit from '@/components/support/permission/ResumeInheritText';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
const InfoModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
@@ -61,7 +60,6 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
defaultValues: appDetail
});
const avatar = getValues('avatar');
const name = getValues('name');
// submit config
const { runAsync: saveSubmitSuccess, loading: btnLoading } = useRequest2(
@@ -129,31 +127,33 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
[setValue, t, toast]
);
const onUpdateCollaborators = async ({ tmbIds, permission }: UpdateClbPermissionProps) => {
await postUpdateAppCollaborators({
tmbIds,
const onUpdateCollaborators = ({
members,
permission
}: {
members: string[];
permission: PermissionValueType;
}) => {
return postUpdateAppCollaborators({
members,
permission,
appId: appDetail._id
});
};
const onDelCollaborator = async (tmbId: string) => {
await deleteAppCollaborators({
const onDelCollaborator = (tmbId: string) => {
return deleteAppCollaborators({
appId: appDetail._id,
tmbId
});
};
const { runAsync: resumeInheritPermission } = useRequest2(
() => resumeInheritPer(appDetail._id),
// () => putAppById(appDetail._id, { inheritPermission: true }),
{
errorToast: t('common:resume_failed'),
onSuccess: () => {
reloadApp();
}
const { runAsync: resumeInheritPermission } = useRequest2(() => resumeInheritPer(appDetail._id), {
errorToast: t('common:resume_failed'),
onSuccess: () => {
reloadApp();
}
);
});
return (
<MyModal
@@ -223,7 +223,14 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
permission={appDetail.permission}
onGetCollaboratorList={() => getCollaboratorList(appDetail._id)}
permissionList={AppPermissionList}
onUpdateCollaborators={onUpdateCollaborators}
onUpdateCollaborators={(props) => {
if (props.members) {
return onUpdateCollaborators({
permission: props.permission,
members: props.members
});
}
}}
onDelOneCollaborator={onDelCollaborator}
refreshDeps={[appDetail.inheritPermission]}
isInheritPermission={appDetail.inheritPermission}

View File

@@ -424,14 +424,14 @@ const ListItem = () => {
onGetCollaboratorList: () => getCollaboratorList(editPerApp._id),
permissionList: AppPermissionList,
onUpdateCollaborators: ({
tmbIds,
members = [], // TODO: remove the default value after group is ready
permission
}: {
tmbIds: string[];
members?: string[];
permission: number;
}) => {
return postUpdateAppCollaborators({
tmbIds,
members,
permission,
appId: editPerApp._id
});

View File

@@ -285,14 +285,14 @@ const MyApps = () => {
onGetCollaboratorList: () => getCollaboratorList(folderDetail._id),
permissionList: AppPermissionList,
onUpdateCollaborators: ({
tmbIds,
members = [], // TODO: remove the default value after group is ready
permission
}: {
tmbIds: string[];
members?: string[];
permission: number;
}) => {
return postUpdateAppCollaborators({
tmbIds,
members,
permission,
appId: folderDetail._id
});

View File

@@ -178,6 +178,7 @@ const TagManageModal = ({ onClose }: { onClose: () => void }) => {
isOpen
onClose={onClose}
iconSrc="core/dataset/tag"
iconColor={'primary.600'}
title={t('dataset:tag.manage')}
w={'580px'}
h={'600px'}

View File

@@ -441,14 +441,14 @@ function List() {
onGetCollaboratorList: () => getCollaboratorList(editPerDataset._id),
permissionList: DatasetPermissionList,
onUpdateCollaborators: ({
tmbIds,
members = [], // TODO: remove default value after group is ready
permission
}: {
tmbIds: string[];
members?: string[];
permission: number;
}) => {
return postUpdateDatasetCollaborators({
tmbIds,
members,
permission,
datasetId: editPerDataset._id
});

View File

@@ -1,14 +1,5 @@
import React, { useCallback, useMemo, useState } from 'react';
import {
Box,
Flex,
Image,
Button,
useDisclosure,
InputGroup,
InputLeftElement,
Input
} from '@chakra-ui/react';
import { Box, Flex, Button, InputGroup, InputLeftElement, Input } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { serviceSideProps } from '@/web/common/utils/i18n';
@@ -249,14 +240,14 @@ const Dataset = () => {
onGetCollaboratorList: () => getCollaboratorList(folderDetail._id),
permissionList: DatasetPermissionList,
onUpdateCollaborators: ({
tmbIds,
members = [], // TODO: remove the default value after group is ready
permission
}: {
tmbIds: string[];
members?: string[];
permission: number;
}) => {
return postUpdateDatasetCollaborators({
tmbIds,
members,
permission,
datasetId: folderDetail._id
});

View File

@@ -1,5 +1,5 @@
import { GET, POST, PUT, DELETE } from '@/web/common/api/request';
import { UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator';
import { UpdatePermissionBody } from '@fastgpt/global/support/permission/collaborator';
import {
CreateTeamProps,
InviteMemberProps,
@@ -15,6 +15,7 @@ import {
} from '@fastgpt/global/support/user/team/type.d';
import { FeTeamPlanStatusType, TeamSubSchema } from '@fastgpt/global/support/wallet/sub/type';
import { TeamInvoiceHeaderType } from '@fastgpt/global/support/user/team/type';
import { ResourcePermissionType } from '@fastgpt/global/support/permission/type';
/* --------------- team ---------------- */
export const getTeamList = (status: `${TeamMemberSchema['status']}`) =>
@@ -39,11 +40,12 @@ export const updateInviteResult = (data: UpdateInviteProps) =>
export const delLeaveTeam = (teamId: string) =>
DELETE('/proApi/support/user/team/member/leave', { teamId });
export const getTeamClbs = () =>
GET<ResourcePermissionType[]>(`/proApi/support/user/team/collaborator/list`);
/* -------------- team collaborator -------------------- */
export const updateMemberPermission = (data: UpdateClbPermissionProps) =>
PUT('/proApi/support/user/team/collaborator/update', data);
export const delMemberPermission = (tmbId: string) =>
DELETE('/proApi/support/user/team/collaborator/delete', { tmbId });
export const updateMemberPermission = (data: UpdatePermissionBody) =>
PUT('/proApi/support/user/team/collaborator/updatePermission', data);
/* --------------- team tags ---------------- */
export const getTeamsTags = () => GET<TeamTagSchema[]>(`/proApi/support/user/team/tag/list`);

View File

@@ -0,0 +1,17 @@
import { DELETE, GET, POST, PUT } from '@/web/common/api/request';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import {
postCreateGroupData,
putUpdateGroupData
} from '@fastgpt/global/support/user/team/group/api';
export const getGroupList = () => GET<MemberGroupListType>('/proApi/support/user/team/group/list');
export const postCreateGroup = (data: postCreateGroupData) =>
POST('/proApi/support/user/team/group/create', data);
export const deleteGroup = (groupId: string) =>
DELETE('/proApi/support/user/team/group/delete', { groupId });
export const putUpdateGroup = (data: putUpdateGroupData) =>
PUT('/proApi/support/user/team/group/update', data);

View File

@@ -9,6 +9,8 @@ import { getTeamPlanStatus } from './team/api';
import { getTeamMembers } from '@/web/support/user/team/api';
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import { getGroupList } from './team/group/api';
type State = {
systemMsgReadId: string;
@@ -24,6 +26,9 @@ type State = {
teamMembers: TeamMemberItemType[];
loadAndGetTeamMembers: (init?: boolean) => Promise<TeamMemberItemType[]>;
teamMemberGroups: MemberGroupListType;
loadAndGetGroups: (init?: boolean) => Promise<MemberGroupListType>;
};
export const useUserStore = create<State>()(
@@ -98,6 +103,21 @@ export const useUserStore = create<State>()(
state.teamMembers = res;
});
return res;
},
teamMemberGroups: [],
loadAndGetGroups: async (init = false) => {
if (!useSystemStore.getState()?.feConfigs?.isPlus) return [];
const randomRefresh = Math.random() > 0.7;
if (!randomRefresh && !init && get().teamMemberGroups.length)
return Promise.resolve(get().teamMemberGroups);
const res = await getGroupList();
set((state) => {
state.teamMemberGroups = res;
});
return res;
}
})),