mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-29 09:44:47 +00:00
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:
@@ -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;
|
||||
|
@@ -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;
|
@@ -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"
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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"
|
||||
|
@@ -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[];
|
||||
};
|
||||
|
@@ -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;
|
@@ -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>
|
||||
);
|
||||
|
@@ -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 */}
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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
|
||||
|
@@ -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
|
||||
})
|
||||
)();
|
||||
|
@@ -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;
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -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;
|
@@ -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>
|
||||
);
|
||||
|
@@ -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>
|
||||
</>
|
||||
|
@@ -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';
|
||||
|
Reference in New Issue
Block a user