V4.8.15 feature (#3331)

* feat: add customize toolkit (#3205)

* chaoyang

* fix-auth

* add toolkit

* add order

* plugin usage

* fix

* delete console:

* Fix: Fix fullscreen preview top positioning and improve Markdown rendering logic (#3247)

* 完成任务:修复全屏预览顶部固定问题,优化 Markdown 渲染逻辑

* 有问题修改

* 问题再修改

* 修正问题

* fix: plugin standalone display issue (#3254)

* 4.8.15 test (#3246)

* o1 config

* perf: system plugin code

* 调整系统插件代码。增加html 渲染安全配置。 (#3258)

* perf: base64 picker

* perf: list app or dataset

* perf: plugin config code

* 小窗适配等问题 (#3257)

* 小窗适配等问题

* git问题

* 小窗剩余问题

* feat: system plugin auth and lock version (#3265)

* feat: system plugin auth and lock version

* update comment

* 4.8.15 test (#3267)

* tmp log

* perf: login direct

* perf: iframe html code

* remove log

* fix: plugin standalone display (#3277)

* refactor: 页面拆分&i18n拆分 (#3281)

* refactor: account组件拆成独立页面

* script: 新增i18n json文件创建脚本

* refactor: 页面i18n拆分

* i18n: add en&hant

* 4.8.15 test (#3285)

* tmp log

* remove log

* fix: watch avatar refresh

* perf: i18n code

* fix(plugin): use intro instead of userguide (#3290)

* Universal SSO (#3292)

* tmp log

* remove log

* feat: common oauth

* readme

* perf: sso provider

* remove sso code

* perf: refresh plugins

* feat: add api dataset (#3272)

* add api-dataset

* fix api-dataset

* fix api dataset

* fix ts

* perf: create collection code (#3301)

* tmp log

* remove log

* perf: i18n change

* update version doc

* feat: question guide from chatId

* perf: create collection code

* fix: request api

* fix: request api

* fix: tts auth and response type (#3303)

* perf: md splitter

* fix: tts auth and response type

* fix: api file dataset (#3307)

* perf: api dataset init (#3310)

* perf: collection schema

* perf: api dataset init

* refactor: 团队管理独立页面 (#3302)

* ui: 团队管理独立页面

* 代码优化

* fix

* perf: sync collection and ui check (#3314)

* perf: sync collection

* remove script

* perf: update api server

* perf: api dataset parent

* perf: team ui

* perf: team 18n

* update team ui

* perf: ui check

* perf: i18n

* fix: debug variables & cronjob & system plugin callback load (#3315)

* fix: debug variables & cronjob & system plugin callback load

* fix type

* fix

* fix

* fix: plugin dataset quote;perf: system variables init (#3316)

* fix: plugin dataset quote

* perf: system variables init

* perf: node templates ui;fix: dataset import ui (#3318)

* fix: dataset import ui

* perf: node templates ui

* perf: ui refresh

* feat:套餐改名和套餐跳转配置 (#3309)

* fixing:except Sidebar

* 去除了多余的代码

* 修正了套餐说明的代码

* 修正了误删除的show_git代码

* 修正了名字部分等代码

* 修正了问题,遗留了其他和ui讨论不一致的部分

* 4.8.15 test (#3319)

* remove log

* pref: bill ui

* pref: bill ui

* perf: log

* html渲染文档 (#3270)

* html渲染文档

* 文档有点小问题

* feat: doc (#3322)

* 集合重训练 (#3282)

* rebaser

* 一点补充

* 小问题

* 其他问题修正,删除集合保留文件的参数还没找到...

* reTraining

* delete uesless

* 删除了一行错误代码

* 集合重训练部分

* fixing

* 删除console代码

* feat: navbar item config (#3326)

* perf: custom navbar code;perf: retraining code;feat: api dataset and dataset api doc (#3329)

* feat: api dataset and dataset api doc

* perf: retraining code

* perf: custom navbar code

* fix: ts (#3330)

* fix: ts

* fix: ts

* retraining ui

* perf: api collection filter

* perf: retrining button

---------

Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: Jiangween <145003935+Jiangween@users.noreply.github.com>
Co-authored-by: papapatrick <109422393+Patrickill@users.noreply.github.com>
This commit is contained in:
Archer
2024-12-06 10:56:53 +08:00
committed by GitHub
parent b188544386
commit 1aebe5f185
307 changed files with 7383 additions and 3981 deletions

View File

@@ -1,295 +0,0 @@
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useContextSelector } from 'use-context-selector';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
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 MemberTable from './components/MemberTable';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { TeamModalContext } from './context';
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',
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,
searchKey,
setSearchKey
} = useContextSelector(TeamModalContext, (v) => v);
const { userInfo, teamPlanStatus } = useUserStore();
const { feConfigs } = useSystemStore();
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('common:user.team.member.Confirm Leave')
});
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
// get members
onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam(teamId);
},
{
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(
() => [
{
icon: 'support/team/memberLight',
label: (
<Flex alignItems={'center'}>
<Box ml={1}>{t('common:user.team.Member')}</Box>
<Box ml={2} bg={'myGray.100'} borderRadius={'20px'} px={3} fontSize={'xs'}>
{members.length}
</Box>
</Flex>
),
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'),
value: TabListEnum.permission
}
],
[members.length, t]
);
const [tab, setTab] = useState(Tablist[0].value);
return (
<Flex
flexDirection={'column'}
bg={'white'}
minH={['50vh', 'auto']}
h={'100%'}
borderRadius={['8px 8px 0 0', '8px 0 0 8px']}
>
<Flex
alignItems={'center'}
px={5}
py={4}
borderBottom={'1.5px solid'}
borderBottomColor={'myGray.100'}
mb={2}
>
<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"
ml={2}
cursor="pointer"
_hover={{
color: 'primary.500'
}}
onClick={() => {
if (!userInfo?.team) return;
setEditTeamData({
id: userInfo.team.teamId,
name: userInfo.team.teamName,
avatar: userInfo.team.avatar
});
}}
/>
)}
</Flex>
<Flex px={5} alignItems={'center'} justifyContent={'space-between'}>
<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={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/inviteLight" w={'16px'} color={'white'} />}
onClick={() => {
if (
teamPlanStatus?.standardConstants?.maxTeamMember &&
teamPlanStatus.standardConstants.maxTeamMember <= members.length
) {
toast({
status: 'warning',
title: t('user.team.Over Max Member Tip', {
max: teamPlanStatus.standardConstants.maxTeamMember
})
});
} else {
onOpenInvite();
}
}}
>
{t('common:user.team.Invite Member')}
</Button>
)}
{tab === TabListEnum.member && !userInfo?.team.permission.isOwner && (
<Button
variant={'whitePrimary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
isLoading={isLoadingLeaveTeam}
onClick={() => {
openLeaveConfirm(() => onLeaveTeam(userInfo?.team?.teamId))();
}}
>
{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>
{isOpenInvite && userInfo?.team?.teamId && (
<InviteModal
teamId={userInfo.team.teamId}
onClose={onCloseInvite}
onSuccess={refetchMembers}
/>
)}
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
{isOpenGroupInfo && (
<GroupInfoModal
onClose={() => {
onCloseGroupInfo();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
{isOpenManageGroupMember && (
<ManageGroupMemberModal
onClose={() => {
onCloseManageGroupMember();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
<ConfirmLeaveTeamModal />
</Flex>
);
}
export default TeamCard;

View File

@@ -1,103 +0,0 @@
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';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { defaultForm } from './components/EditInfoModal';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useContextSelector } from 'use-context-selector';
import { TeamModalContext } from './context';
function TeamList() {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { myTeams, onSwitchTeam, setEditTeamData } = useContextSelector(TeamModalContext, (v) => v);
return (
<Flex
flexDirection={'column'}
w={['auto', '270px']}
h={['auto', '100%']}
pt={3}
px={5}
mb={[2, 0]}
>
<Flex
alignItems={'center'}
py={2}
h={'40px'}
borderBottom={'1.5px solid rgba(0, 0, 0, 0.05)'}
>
<Box flex={['0 0 auto', 1]} fontSize={['sm', 'md']} fontWeight={'bold'}>
{t('common:common.Team')}
</Box>
{/* if there is no team */}
{myTeams.length < 1 && (
<IconButton
variant={'ghost'}
border={'none'}
icon={
<MyIcon
name={'common/addCircleLight'}
w={['16px', '18px']}
color={'primary.500'}
cursor={'pointer'}
/>
}
aria-label={''}
onClick={() => setEditTeamData(defaultForm)}
/>
)}
</Flex>
<Box flex={['auto', '1 0 0']} overflow={'auto'}>
{myTeams.map((team) => (
<Flex
key={team.teamId}
alignItems={'center'}
mt={3}
borderRadius={'md'}
p={3}
cursor={'default'}
gap={3}
{...(userInfo?.team?.teamId === team.teamId
? {
bg: 'primary.200'
}
: {
_hover: {
bg: 'myGray.100'
}
})}
>
<Avatar src={team.avatar} w={['18px', '22px']} />
<Box
flex={'1 0 0'}
w={0}
fontSize={'sm'}
{...(team.role === TeamMemberRoleEnum.owner
? {
fontWeight: 'bold'
}
: {})}
>
{team.teamName}
</Box>
{userInfo?.team?.teamId === team.teamId ? (
<MyIcon name={'common/tickFill'} w={'16px'} color={'primary.500'} />
) : (
<Button
size={'xs'}
variant={'whitePrimary'}
onClick={() => onSwitchTeam(team.teamId)}
>
{t('common:user.team.Check Team')}
</Button>
)}
</Flex>
))}
</Box>
</Flex>
);
}
export default TeamList;

View File

@@ -1,162 +0,0 @@
import React, { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'next-i18next';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { Box, Button, Flex, Input, ModalBody, ModalFooter } from '@chakra-ui/react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
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;
};
export const defaultForm = {
name: '',
avatar: DEFAULT_TEAM_AVATAR
};
function EditModal({
defaultData = defaultForm,
onClose,
onSuccess
}: {
defaultData?: EditTeamFormDataType;
onClose: () => void;
onSuccess: () => void;
}) {
const { t } = useTranslation();
const { toast } = useToast();
const { register, setValue, handleSubmit, watch } = useForm<CreateTeamProps>({
defaultValues: defaultData
});
const avatar = watch('avatar');
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png,.svg',
multiple: false
});
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.teamAvatar,
file,
maxW: 300,
maxH: 300
});
setValue('avatar', src);
} catch (err: any) {
toast({
title: getErrText(err, t('common:common.Select File Failed')),
status: 'warning'
});
}
},
[setValue, t, toast]
);
const { mutate: onclickCreate, isLoading: creating } = useRequest({
mutationFn: async (data: CreateTeamProps) => {
return postCreateTeam(data);
},
onSuccess() {
onSuccess();
onClose();
},
successToast: t('common:common.Create Success'),
errorToast: t('common:common.Create Failed')
});
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
mutationFn: async (data: EditTeamFormDataType) => {
if (!data.id) return Promise.resolve('');
return putUpdateTeam({
name: data.name,
avatar: data.avatar
});
},
onSuccess() {
onSuccess();
onClose();
},
successToast: t('common:common.Update Success'),
errorToast: t('common:common.Update Failed')
});
return (
<MyModal
isOpen
onClose={onClose}
iconSrc="support/team/group"
iconColor="primary.600"
title={defaultData.id ? t('common:user.team.Update Team') : t('common:user.team.Create Team')}
>
<ModalBody>
<Box color={'myGray.800'} fontWeight={'bold'}>
{t('common:user.team.Set Name')}
</Box>
<Flex mt={3} alignItems={'center'}>
<MyTooltip label={t('common:common.Set Avatar')}>
<Avatar
flexShrink={0}
src={avatar}
w={['28px', '32px']}
h={['28px', '32px']}
cursor={'pointer'}
borderRadius={'md'}
onClick={onOpenSelectFile}
/>
</MyTooltip>
<Input
flex={1}
ml={4}
autoFocus
bg={'myWhite.600'}
maxLength={20}
placeholder={t('common:user.team.Team Name')}
{...register('name', {
required: t('common:common.Please Input Name')
})}
/>
</Flex>
</ModalBody>
<ModalFooter>
{!!defaultData.id ? (
<>
<Box flex={1} />
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button isLoading={updating} onClick={handleSubmit((data) => onclickUpdate(data))}>
{t('common:common.Confirm Update')}
</Button>
</>
) : (
<Button
w={'100%'}
isLoading={creating}
onClick={handleSubmit((data) => onclickCreate(data))}
>
{t('common:common.Confirm Create')}
</Button>
)}
</ModalFooter>
<File onSelect={onSelectFile} />
</MyModal>
);
}
export default React.memo(EditModal);

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,90 +0,0 @@
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 { 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';
const InviteModal = ({
teamId,
onClose,
onSuccess
}: {
teamId: string;
onClose: () => void;
onSuccess: () => void;
}) => {
const { t } = useTranslation();
const { ConfirmModal, openConfirm } = useConfirm({
title: t('common:user.team.Invite Member Result Tip'),
showCancel: false
});
const [inviteUsernames, setInviteUsernames] = useState<string[]>([]);
const { runAsync: onInvite, loading: isLoading } = useRequest2(
() =>
postInviteTeamMember({
teamId,
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="common/inviteLight"
iconColor="primary.600"
title={
<Box>
<Box>{t('common:user.team.Invite Member')}</Box>
<Box color={'myGray.500'} fontSize={'xs'} fontWeight={'normal'}>
{t('common:user.team.Invite Member Tips')}
</Box>
</Box>
}
maxW={['90vw', '400px']}
overflow={'unset'}
>
<ModalCloseButton onClick={onClose} />
<ModalBody>
<Box mb={2}>{t('common:user.Account')}</Box>
<TagTextarea defaultValues={inviteUsernames} onUpdate={setInviteUsernames} />
</ModalBody>
<ModalFooter>
<Button
w={'100%'}
h={'34px'}
isDisabled={inviteUsernames.length === 0}
isLoading={isLoading}
onClick={onInvite}
>
{t('common:user.team.Confirm Invite')}
</Button>
</ModalFooter>
<ConfirmModal />
</MyModal>
);
};
export default InviteModal;

View File

@@ -1,107 +0,0 @@
import Avatar from '@fastgpt/web/components/common/Avatar';
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 { useUserStore } from '@/web/support/user/useUserStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import MyBox from '@fastgpt/web/components/common/MyBox';
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 { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm({
type: 'delete'
});
const { members, groups, refetchMembers, refetchGroups } = useContextSelector(
TeamModalContext,
(v) => v
);
return (
<MyBox>
<TableContainer overflow={'unset'} fontSize={'sm'} mx="6">
<Table overflow={'unset'}>
<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) => (
<Tr key={item.userId} overflow={'unset'}>
<Td>
<HStack>
<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 maxW={'300px'}>
<GroupTags
names={groups
?.filter((group) => group.members.map((m) => m.tmbId).includes(item.tmbId))
.map((g) => g.name)}
max={3}
/>
</Td>
<Td>
{userInfo?.team.permission.hasManagePer &&
item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
<Icon
name={'common/trash'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'red.600',
bgColor: 'myGray.100'
}}
onClick={() => {
openRemoveMember(
() =>
delRemoveMember(item.tmbId).then(() =>
Promise.all([refetchGroups(), refetchMembers()])
),
undefined,
t('common:user.team.Remove Member Confirm Tip', {
username: item.memberName
})
)();
}}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
<ConfirmRemoveMemberModal />
</TableContainer>
</MyBox>
);
}
export default MemberTable;

View File

@@ -1,287 +0,0 @@
import React from '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 { 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 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 { groups, refetchMembers, refetchGroups, members, searchKey } = useContextSelector(
TeamModalContext,
(v) => v
);
const { runAsync: refetchClbs, data: clbs = [] } = useRequest2(getTeamClbs, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
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();
}
});
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
});
}
}
}
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
});
}
}
}
}
);
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>
);
}
export default PermissionManage;

View File

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

View File

@@ -1,145 +0,0 @@
import React, { ReactNode, useState } from 'react';
import { createContext } from 'use-context-selector';
import type { EditTeamFormDataType } from './components/EditInfoModal';
import dynamic from 'next/dynamic';
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 { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
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[];
members: TeamMemberItemType[];
groups: MemberGroupListType;
isLoading: boolean;
onSwitchTeam: (teamId: string) => void;
setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>;
refetchMembers: () => void;
refetchTeams: () => void;
refetchGroups: () => void;
searchKey: string;
setSearchKey: React.Dispatch<React.SetStateAction<string>>;
};
export const TeamModalContext = createContext<TeamModalContextType>({
myTeams: [],
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.');
}
});
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 = [],
loading: isLoadingTeams,
refresh: refetchTeams
} = useRequest2(() => getTeamList(TeamMemberStatusEnum.active), {
manual: false,
refreshDeps: [userInfo?._id]
});
// member action
const {
data: members = [],
runAsync: refetchMembers,
loading: loadingMembers
} = useRequest2(
() => {
if (!userInfo?.team?.teamId) return Promise.resolve([]);
return loadAndGetTeamMembers(true);
},
{
manual: false,
refreshDeps: [userInfo?.team?.teamId]
}
);
const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2(
async (teamId: string) => {
await putSwitchTeam(teamId);
return initUserInfo();
},
{
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 || isLoadingGroups;
const contextValue = {
myTeams,
refetchTeams,
isLoading,
onSwitchTeam,
searchKey,
setSearchKey,
// create | update team
setEditTeamData,
members,
refetchMembers,
groups,
refetchGroups
};
return (
<TeamModalContext.Provider value={contextValue}>
{userInfo?.team?.permission && (
<>
{children}
{!!editTeamData && (
<EditInfoModal
defaultData={editTeamData}
onClose={() => setEditTeamData(undefined)}
onSuccess={() => {
refetchTeams();
initUserInfo();
}}
/>
)}
</>
)}
</TeamModalContext.Provider>
);
};

View File

@@ -1,50 +0,0 @@
import React from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { Box } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { createContext, useContextSelector } from 'use-context-selector';
import TeamList from './TeamList';
import TeamCard from './TeamCard';
import { TeamModalContext, TeamModalContextProvider } from './context';
export const TeamContext = createContext<{}>({} as any);
type Props = { onClose: () => void };
const TeamManageModal = ({ onClose }: Props) => {
const { isLoading } = useContextSelector(TeamModalContext, (v) => v);
return (
<>
<MyModal
isOpen
onClose={onClose}
maxW={['90vw', '1000px']}
w={'100%'}
h={'550px'}
isCentered
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>
</Box>
</MyModal>
</>
);
};
const Render = (props: Props) => {
const { userInfo } = useUserStore();
return !!userInfo?.team ? (
<TeamModalContextProvider>
<TeamManageModal {...props} />
</TeamModalContextProvider>
) : null;
};
export default React.memo(Render);

View File

@@ -1,70 +0,0 @@
import React from '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';
import dynamic from 'next/dynamic';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
const TeamManageModal = dynamic(() => import('../TeamManageModal'));
const TeamMenu = () => {
const { feConfigs } = useSystemStore();
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { toast } = useToast();
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Button
variant={'whitePrimary'}
userSelect={'none'}
w={'100%'}
h={'34px'}
justifyContent={'space-between'}
px={3}
css={{
'& span': {
display: 'block'
}
}}
transform={'none !important'}
rightIcon={<MyIcon w={'1rem'} name={'common/select'} />}
onClick={() => {
if (feConfigs.isPlus) {
onOpen();
} else {
toast({
status: 'warning',
title: t('common:common.system.Commercial version function')
});
}
}}
>
<MyTooltip label={t('common:user.team.Select Team')}>
<Flex w={'100%'} alignItems={'center'}>
{userInfo?.team ? (
<>
<Avatar src={userInfo.team.avatar} w={'1rem'} />
<Box ml={2}>{userInfo.team.teamName}</Box>
</>
) : (
<>
<Box w={'8px'} h={'8px'} mr={3} borderRadius={'50%'} bg={'#67c13b'} />
{t('common:user.team.Personal Team')}
</>
)}
</Flex>
</MyTooltip>
</Button>
{isOpen && <TeamManageModal onClose={onClose} />}
</>
);
};
export default TeamMenu;