V4.8.18 feature (#3565)

* feat: org CRUD (#3380)

* feat: add org schema

* feat: org manage UI

* feat: OrgInfoModal

* feat: org tree view

* feat: org management

* fix: init root org

* feat: org permission for app

* feat: org support for dataset

* fix: disable org role control

* styles: opt type signatures

* fix: remove unused permission

* feat: delete org collaborator

* perf: Team org ui (#3499)

* perf: org ui

* perf: org ui

* feat: org auth for app & dataset (#3498)

* feat: auth org resource permission

* feat: org auth support for app & dataset

* perf: org permission check (#3500)

* i18n (#3501)

* name

* i18n

* feat: support dataset changeOwner (#3483)

* feat: support dataset changeOwner

* chore: update dataset change owner api

* feat: permission manage UI for org (#3503)

* perf: password check;perf: image upload check;perf: sso login check (#3509)

* perf: password check

* perf: image upload check

* perf: sso login check

* force show update notification modal & fix login page text (#3512)

* fix login page English text

* update notification modal

* perf: notify account (#3515)

* perf(plugin): improve searXNG empty result handling and documentation (#3507)

* perf(plugin): improve searXNG empty result handling and documentation

* 修改了文档和代码部分无搜索的结果的反馈

* refactor: org pathId (#3516)

* optimize payment process (#3517)

* feat: support wecom sso (#3518)

* feat: support wecom sso

* chore: remove unused wecom js-sdk dependency

* fix qrcode script (#3520)

* fix qrcode script

* i18n

* perf: full text collection and search code;perf: rename function (#3519)

* perf: full text collection and search code

* perf: rename function

* perf: notify modal

* remove invalid code

* perf: sso login

* perf: pay process

* 4.8.18 test (#3524)

* perf: remove local token

* perf: index

* perf: file encoding;perf: leave team code;@c121914yu perf: full text search code (#3528)

* perf: text encoding

* perf: leave team code

* perf: full text search code

* fix: http status

* perf: embedding search and vector avatar

* perf: async read file (#3531)

* refactor: team permission  manager (#3535)

* perf: classify org, group and member

* refactor: team per manager

* fix: missing functions

* 4.8.18 test (#3543)

* perf: login check

* doc

* perf: llm model config

* perf: team clb config

* fix: MemberModal UI (#3553)

* fix: adapt MemberModal title and icon

* fix: adapt member modal

* fix: search input placeholder

* fix: add button text

* perf: org permission (#3556)

* docs:用户答疑的官方文档补充 (#3540)

* docs:用户答疑的官方文档补充

* 问题回答的内容修补

* share link random avatar (#3541)

* share link random avatar

* fix

* delete unused code

* share page avatar (#3558)

* feat: init 4818

* share page avatar

* feat: tmp upgrade code (#3559)

* feat: tmp upgrade code

* fulltext search test

* update action

* full text tmp code (#3561)

* full text tmp code

* fix: init

* fix: init

* remove tmp code

* remove tmp code

* 4818-alpha

* 4.8.18 test (#3562)

* full text tmp code

* fix: init

* upgrade code

* account log

* account log

* perf: dockerfile

* upgrade code

* chore: update docs app template submission (#3564)

---------

Co-authored-by: a.e. <49438478+I-Info@users.noreply.github.com>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: Jiangween <145003935+Jiangween@users.noreply.github.com>
This commit is contained in:
Archer
2025-01-11 15:15:38 +08:00
committed by GitHub
parent bb669ca3ff
commit 10d8c56e23
205 changed files with 5305 additions and 2428 deletions

View File

@@ -94,7 +94,7 @@ const AccountContainer = ({
...(userInfo?.team?.permission.hasManagePer
? [
{
icon: 'support/outlink/apikeyLight',
icon: 'key',
label: t('account:api_key'),
value: TabEnum.apikey
}

View File

@@ -75,7 +75,7 @@ const TeamSelector = ({
key={'manage'}
alignItems={'center'}
borderRadius={'md'}
cursor={'default'}
cursor={'pointer'}
gap={3}
onClick={() => router.push('/account/team')}
>

View File

@@ -1,111 +0,0 @@
import React from 'react';
import { ModalBody, Box, Flex, Input, ModalFooter, Button, HStack } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { updateNotificationAccount } from '@/web/support/user/api';
import Icon from '@fastgpt/web/components/common/Icon';
import { useSendCode } from '@/web/support/user/hooks/useSendCode';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useSystemStore } from '@/web/common/system/useSystemStore';
type FormType = {
account: string;
verifyCode: string;
};
const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { initUserInfo } = useUserStore();
const { feConfigs } = useSystemStore();
const { register, handleSubmit, watch } = useForm<FormType>({
defaultValues: {
account: '',
verifyCode: ''
}
});
const account = watch('account');
const verifyCode = watch('verifyCode');
const { runAsync: onSubmit, loading: isLoading } = useRequest2(
(data: FormType) => {
return updateNotificationAccount(data);
},
{
onSuccess() {
initUserInfo();
onClose();
},
successToast: t('account_info:bind_notification_success'),
errorToast: t('account_info:bind_notification_error')
}
);
const { SendCodeBox } = useSendCode({ type: 'bindNotification' });
const placeholder = feConfigs?.bind_notification_method
?.map((item) => {
switch (item) {
case 'email':
return t('account_info:email_label');
case 'phone':
return t('account_info:phone_label');
}
})
.join('/');
return (
<>
<MyModal
isOpen
iconSrc="common/settingLight"
w={'32rem'}
title={t('account_info:notification_receiving_hint')}
>
<ModalBody px={10}>
<Flex flexDirection="column">
<HStack px="6" py="3" color="primary.600" bgColor="primary.50" borderRadius="md">
<Icon name="common/info" w="1rem" />
<Box fontSize={'sm'}>{t('account_info:bind_notification_hint')}</Box>
</HStack>
<Flex mt="4" alignItems="center">
<Box flex={'0 0 70px'}>{t('account_info:user_account')}</Box>
<Input
flex={1}
bg={'myGray.50'}
{...register('account', { required: true })}
placeholder={placeholder}
></Input>
</Flex>
<Flex mt="6" alignItems="center" position={'relative'}>
<Box flex={'0 0 70px'}>{t('account_info:verification_code')}</Box>
<Input
flex={1}
bg={'myGray.50'}
{...register('verifyCode', { required: true })}
placeholder={t('account_info:code_required')}
></Input>
<SendCodeBox username={account} />
</Flex>
</Flex>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('account_info:cancel')}
</Button>
<Button
isLoading={isLoading}
isDisabled={!account || !verifyCode}
onClick={handleSubmit((data) => onSubmit(data))}
>
{t('account_info:confirm')}
</Button>
</ModalFooter>
</MyModal>
</>
);
};
export default UpdateNotificationModal;

View File

@@ -3,8 +3,10 @@ import { ModalBody, Box, Flex, Input, ModalFooter, Button } from '@chakra-ui/rea
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { updatePasswordByOld } from '@/web/support/user/api';
import { PasswordRule } from '@/web/support/user/login/constants';
import { useToast } from '@fastgpt/web/hooks/useToast';
type FormType = {
oldPsw: string;
@@ -14,7 +16,9 @@ type FormType = {
const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { register, handleSubmit } = useForm<FormType>({
const { toast } = useToast();
const { register, handleSubmit, getValues } = useForm<FormType>({
defaultValues: {
oldPsw: '',
newPsw: '',
@@ -22,19 +26,25 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
}
});
const { mutate: onSubmit, isLoading } = useRequest({
mutationFn: (data: FormType) => {
if (data.newPsw !== data.confirmPsw) {
return Promise.reject(t('account_info:password_mismatch'));
}
return updatePasswordByOld(data);
},
const { runAsync: onSubmit, loading: isLoading } = useRequest2(updatePasswordByOld, {
onSuccess() {
onClose();
},
successToast: t('account_info:password_update_success'),
errorToast: t('account_info:password_update_error')
});
const onSubmitErr = (err: Record<string, any>) => {
const val = Object.values(err)[0];
if (!val) return;
if (val.message) {
toast({
status: 'warning',
title: val.message,
duration: 3000,
isClosable: true
});
}
};
return (
<MyModal
@@ -45,34 +55,39 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
>
<ModalBody>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('account_info:old_password') + ':'}</Box>
<Box flex={'0 0 70px'} fontSize={'sm'}>
{t('account_info:old_password') + ':'}
</Box>
<Input flex={1} type={'password'} {...register('oldPsw', { required: true })}></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 70px'}>{t('account_info:new_password') + ':'}</Box>
<Box flex={'0 0 70px'} fontSize={'sm'}>
{t('account_info:new_password') + ':'}
</Box>
<Input
flex={1}
type={'password'}
placeholder={t('account_info:password_tip')}
{...register('newPsw', {
required: true,
maxLength: {
value: 60,
message: t('account_info:password_length_error')
pattern: {
value: PasswordRule,
message: t('account_info:password_tip')
}
})}
></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 70px'}>{t('account_info:confirm_password') + ':'}</Box>
<Box flex={'0 0 70px'} fontSize={'sm'}>
{t('account_info:confirm_password') + ':'}
</Box>
<Input
flex={1}
type={'password'}
placeholder={t('user:password.confirm')}
{...register('confirmPsw', {
required: true,
maxLength: {
value: 60,
message: t('account_info:password_length_error')
}
validate: (val) => (getValues('newPsw') === val ? true : t('user:password.not_match'))
})}
></Input>
</Flex>
@@ -81,7 +96,7 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('account_info:cancel')}
</Button>
<Button isLoading={isLoading} onClick={handleSubmit((data) => onSubmit(data))}>
<Button isLoading={isLoading} onClick={handleSubmit((data) => onSubmit(data), onSubmitErr)}>
{t('account_info:confirm')}
</Button>
</ModalFooter>

View File

@@ -9,8 +9,7 @@ import {
Link,
Progress,
Grid,
BoxProps,
FlexProps
BoxProps
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { UserUpdateParams } from '@/types/user';
@@ -20,7 +19,6 @@ import type { UserType } from '@fastgpt/global/support/user/type.d';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next';
import Avatar from '@fastgpt/web/components/common/Avatar';
@@ -29,7 +27,6 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tools';
import { putUpdateMemberName } from '@/web/support/user/team/api';
import { getDocPath } from '@/web/common/system/doc';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import {
StandardSubLevelEnum,
standardSubLevelMap
@@ -49,7 +46,9 @@ import TeamSelector from '../components/TeamSelector';
const StandDetailModal = dynamic(() => import('./components/standardDetailModal'), { ssr: false });
const ConversionModal = dynamic(() => import('./components/ConversionModal'));
const UpdatePswModal = dynamic(() => import('./components/UpdatePswModal'));
const UpdateNotification = dynamic(() => import('./components/UpdateNotificationModal'));
const UpdateNotification = dynamic(
() => import('@/components/support/user/inform/UpdateNotificationModal')
);
const CommunityModal = dynamic(() => import('@/components/CommunityModal'));
const ModelPriceModal = dynamic(() =>
@@ -131,7 +130,11 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
onClose: onCloseUpdateNotification,
onOpen: onOpenUpdateNotification
} = useDisclosure();
const { File, onOpen: onOpenSelectFile } = useSelectFile({
const {
File,
onOpen: onOpenSelectFile,
onSelectImage
} = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
@@ -151,32 +154,6 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
[reset, t, toast, updateUserInfo]
);
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file || !userInfo) return;
try {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.userAvatar,
file,
maxW: 300,
maxH: 300
});
onclickSave({
...userInfo,
avatar: src
});
} catch (err: any) {
toast({
title: typeof err === 'string' ? err : t('account_info:avatar_selection_exception'),
status: 'warning'
});
}
},
[onclickSave, t, toast, userInfo]
);
const labelStyles: BoxProps = {
flex: '0 0 80px',
fontSize: 'sm',
@@ -329,7 +306,21 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
)}
{isOpenUpdatePsw && <UpdatePswModal onClose={onCloseUpdatePsw} />}
{isOpenUpdateNotification && <UpdateNotification onClose={onCloseUpdateNotification} />}
<File onSelect={onSelectFile} />
<File
onSelect={(e) =>
onSelectImage(e, {
maxW: 300,
maxH: 300,
callback: (src) => {
if (!userInfo) return;
onclickSave({
...userInfo,
avatar: src
});
}
})
}
/>
</Box>
);
};

View File

@@ -0,0 +1,84 @@
import React, { useMemo, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { Box, Flex, ModalBody } from '@chakra-ui/react';
import { MultipleRowArraySelect } from '@fastgpt/web/components/common/MySelect/MultipleRowSelect';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { ModelProviderList } from '@fastgpt/global/core/ai/provider';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { HUGGING_FACE_ICON } from '@fastgpt/global/common/system/constants';
import { getModelFromList } from '@fastgpt/global/core/ai/model';
const DefaultModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { llmModelList, vectorModelList, whisperModel, audioSpeechModelList, reRankModelList } =
useSystemStore();
const [value, setValue] = useState<string[]>([]);
const modelList = useMemo(() => {
return [
...llmModelList,
...vectorModelList,
...audioSpeechModelList,
...reRankModelList,
whisperModel
].map((item) => ({
provider: item.provider,
name: item.name,
model: item.model
}));
}, [llmModelList, vectorModelList, whisperModel, audioSpeechModelList, reRankModelList]);
const selectorList = useMemo(() => {
const renderList = ModelProviderList.map<{
label: React.JSX.Element;
value: string;
children: { label: string | React.ReactNode; value: string }[];
}>((provider) => ({
label: (
<Flex alignItems={'center'} py={1}>
<Avatar
borderRadius={'0'}
mr={2}
src={provider?.avatar || HUGGING_FACE_ICON}
fallbackSrc={HUGGING_FACE_ICON}
w={'1rem'}
/>
<Box>{t(provider.name as any)}</Box>
</Flex>
),
value: provider.id,
children: []
}));
for (const item of modelList) {
const modelData = getModelFromList(modelList, item.model);
const provider =
renderList.find((item) => item.value === (modelData?.provider || 'Other')) ??
renderList[renderList.length - 1];
provider.children.push({
label: modelData.name,
value: modelData.model
});
}
return renderList.filter((item) => item.children.length > 0);
}, [modelList, t]);
console.log(selectorList);
return (
<MyModal
isOpen
title={t('account:add_default_model')}
iconSrc="common/model"
iconColor="primary.600"
onClose={onClose}
>
<ModalBody>11</ModalBody>
</MyModal>
);
};
export default DefaultModal;

View File

@@ -1,15 +1,72 @@
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import React from 'react';
import React, { useState } from 'react';
import AccountContainer from '../components/AccountContainer';
import { Box } from '@chakra-ui/react';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import ModelTable from '@/components/core/ai/ModelTable';
import { useUserStore } from '@/web/support/user/useUserStore';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import { useTranslation } from 'next-i18next';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import dynamic from 'next/dynamic';
const DefaultModal = dynamic(() => import('./components/DefaultModal'), {
ssr: false
});
const ModelProvider = () => {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const isRoot = userInfo?.username === 'root';
const [tab, setTab] = useState<'model' | 'channel'>('model');
const { isOpen: isOpenDefault, onOpen: onOpenDefault, onClose: onCloseDefault } = useDisclosure();
return (
<AccountContainer>
<Box h={'100%'} py={4} px={6}>
<ModelTable />
</Box>
<Flex h={'100%'} flexDirection={'column'} gap={4} py={4} px={6}>
{/* Header */}
{/* <Flex justifyContent={'space-between'}>
<FillRowTabs<'model' | 'channel'>
list={[
{ label: t('account:active_model'), value: 'model' },
{ label: t('account:channel'), value: 'channel' }
]}
value={tab}
px={8}
py={1}
onChange={setTab}
/>
{tab === 'model' && (
<MyMenu
trigger="hover"
size="mini"
Button={<Button>{t('account:create_model')}</Button>}
menuList={[
{
children: [
{
label: t('account:default_model'),
onClick: onOpenDefault
},
{
label: t('account:custom_model')
}
]
}
]}
/>
)}
{tab === 'channel' && <Button>{t('account:create_channel')}</Button>}
</Flex> */}
<Box flex={'1 0 0'}>
{tab === 'model' && <ModelTable />}
{/* {tab === 'channel' && <ChannelTable />} */}
</Box>
</Flex>
{isOpenDefault && <DefaultModal onClose={onCloseDefault} />}
</AccountContainer>
);
};

View File

@@ -1,10 +1,8 @@
import React, { useCallback } from 'react';
import React 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';
@@ -12,7 +10,6 @@ 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 & {
@@ -41,33 +38,15 @@ function EditModal({
});
const avatar = watch('avatar');
const { File, onOpen: onOpenSelectFile } = useSelectFile({
const {
File,
onOpen: onOpenSelectFile,
onSelectImage
} = 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);
@@ -154,7 +133,15 @@ function EditModal({
</Button>
)}
</ModalFooter>
<File onSelect={onSelectFile} />
<File
onSelect={(e) =>
onSelectImage(e, {
maxH: 300,
maxW: 300,
callback: (e) => setValue('avatar', e)
})
}
/>
</MyModal>
);
}

View File

@@ -6,9 +6,7 @@ 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 { TeamContext } from '../context';
@@ -23,7 +21,11 @@ export type GroupFormType = {
function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) {
const { refetchGroups, groups, refetchMembers } = useContextSelector(TeamContext, (v) => v);
const { t } = useTranslation();
const { File: AvatarSelect, onOpen: onOpenSelectAvatar } = useSelectFile({
const {
File: AvatarSelect,
onOpen: onOpenSelectAvatar,
onSelectImage
} = useSelectFile({
fileType: '.jpg, .jpeg, .png',
multiple: false
});
@@ -41,13 +43,10 @@ function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGro
const { loading: uploadingAvatar, run: onSelectAvatar } = useRequest2(
async (file: File[]) => {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.groupAvatar,
file: file[0],
return onSelectImage(file, {
maxW: 300,
maxH: 300
});
return src;
},
{
onSuccess: (src: string) => {

View File

@@ -1,6 +1,8 @@
import AvatarGroup from '@fastgpt/web/components/common/Avatar/AvatarGroup';
import {
Box,
Button,
Flex,
HStack,
Table,
TableContainer,
@@ -26,19 +28,35 @@ import MemberTag from '../../../../../components/support/user/team/Info/MemberTa
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic';
import { useState } from 'react';
import IconButton from '../OrgManage/IconButton';
const ChangeOwnerModal = dynamic(() => import('./GroupTransferOwnerModal'));
const GroupInfoModal = dynamic(() => import('./GroupInfoModal'));
const ManageGroupMemberModal = dynamic(() => import('./GroupManageMember'));
function MemberTable({
onEditGroup,
onManageMember
}: {
onEditGroup: (groupId: string) => void;
onManageMember: (groupId: string) => void;
}) {
function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const [editGroupId, setEditGroupId] = useState<string>();
const {
isOpen: isOpenGroupInfo,
onOpen: onOpenGroupInfo,
onClose: onCloseGroupInfo
} = useDisclosure();
const {
isOpen: isOpenManageGroupMember,
onOpen: onOpenManageGroupMember,
onClose: onCloseManageGroupMember
} = useDisclosure();
const onEditGroup = (groupId: string) => {
setEditGroupId(groupId);
onOpenGroupInfo();
};
const onManageMember = (groupId: string) => {
setEditGroupId(groupId);
onOpenManageGroupMember();
};
const { ConfirmModal: ConfirmDeleteGroupModal, openConfirm: openDeleteGroupModal } = useConfirm({
type: 'delete',
@@ -72,145 +90,184 @@ function MemberTable({
onOpen: onOpenChangeOwner,
onClose: onCloseChangeOwner
} = useDisclosure();
const onChangeOwner = (groupId: string) => {
setEditGroupId(groupId);
onOpenChangeOwner();
};
return (
<MyBox>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="6px">
{t('account_team:group_name')}
</Th>
<Th bg="myGray.100">{t('account_team:owner')}</Th>
<Th bg="myGray.100">{t('account_team:member')}</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>
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
{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>
)}
</Flex>
<MyBox flex={'1 0 0'} overflow={'auto'}>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="6px">
{t('account_team:group_name')}
</Th>
<Th bg="myGray.100">{t('account_team:owner')}</Th>
<Th bg="myGray.100">{t('account_team:member')}</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 ? userInfo?.team.teamName ?? '' : group.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 ?? ''
}
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('account_team: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('account_team:edit_info'),
icon: 'edit',
onClick: () => {
onEditGroup(group._id);
}
},
{
label: t('account_team:manage_member'),
icon: 'support/team/group',
onClick: () => {
onManageMember(group._id);
}
},
...(isGroupOwner(group)
? [
{
label: t('account_team:transfer_ownership'),
icon: 'modal/changePer',
onClick: () => {
onChangeOwner(group._id);
</Td>
<Td>
{group.name === DefaultGroupName ? (
<AvatarGroup avatars={members.map((v) => v.avatar)} groupId={group._id} />
) : hasGroupManagePer(group) ? (
<MyTooltip label={t('account_team: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={<IconButton name={'more'} />}
menuList={[
{
children: [
{
label: t('account_team:edit_info'),
icon: 'edit',
onClick: () => {
onEditGroup(group._id);
}
},
{
label: t('account_team:manage_member'),
icon: 'support/team/group',
onClick: () => {
onManageMember(group._id);
}
},
...(isGroupOwner(group)
? [
{
label: t('account_team:transfer_ownership'),
icon: 'modal/changePer',
onClick: () => {
onChangeOwner(group._id);
},
type: 'primary' as MenuItemType
},
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>
{
label: t('common:common.Delete'),
icon: 'delete',
onClick: () => {
openDeleteGroupModal(() => delDeleteGroup(group._id))();
},
type: 'danger' as MenuItemType
}
]
: [])
]
}
]}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</MyBox>
<ConfirmDeleteGroupModal />
{isOpenChangeOwner && editGroupId && (
<ChangeOwnerModal groupId={editGroupId} onClose={onCloseChangeOwner} />
)}
</MyBox>
{isOpenGroupInfo && (
<GroupInfoModal
onClose={() => {
onCloseGroupInfo();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
{isOpenManageGroupMember && (
<ManageGroupMemberModal
onClose={() => {
onCloseManageGroupMember();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
</>
);
}

View File

@@ -1,106 +1,224 @@
import Avatar from '@fastgpt/web/components/common/Avatar';
import { Box, HStack, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import {
Box,
Button,
Flex,
HStack,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
useDisclosure
} 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 { TeamContext } from './context';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyIcon from '@fastgpt/web/components/common/Icon';
import dynamic from 'next/dynamic';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { delLeaveTeam } from '@/web/support/user/team/api';
function MemberTable() {
const { userInfo } = useUserStore();
const InviteModal = dynamic(() => import('./InviteModal'));
const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { toast } = useToast();
const { userInfo, teamPlanStatus } = useUserStore();
const { feConfigs, setNotSufficientModalType } = useSystemStore();
const { groups, refetchGroups, myTeams, refetchTeams, members, refetchMembers, onSwitchTeam } =
useContextSelector(TeamContext, (v) => v);
const {
isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync
} = useDisclosure();
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm({
type: 'delete'
});
const { members, groups, refetchMembers, refetchGroups } = useContextSelector(
TeamContext,
(v) => v
const { runAsync: onLeaveTeam } = useRequest2(
async () => {
const defaultTeam = myTeams.find((item) => item.defaultTeam) || myTeams[0];
// change to personal team
onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam();
},
{
onSuccess() {
refetchTeams();
},
errorToast: t('account_team:user_team_leave_team_failed')
}
);
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('account_team:confirm_leave_team')
});
return (
<MyBox>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
<Tr bgColor={'white !important'}>
<Th borderLeftRadius="6px" bgColor="myGray.100">
{t('account_team:user_name')}
</Th>
<Th bgColor="myGray.100">{t('account_team:member_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('account_team: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('account_team:remove_tip', {
username: item.memberName
})
)();
}}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
<HStack>
{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('account_team:label_sync')}
</Button>
)}
{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('common:user.team.Over Max Member Tip', {
max: teamPlanStatus.standardConstants.maxTeamMember
})
});
setNotSufficientModalType(TeamErrEnum.teamMemberOverSize);
} else {
onOpenInvite();
}
}}
>
{t('account_team:user_team_invite_member')}
</Button>
)}
{!userInfo?.team.permission.isOwner && (
<Button
variant={'whitePrimary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
onClick={() => openLeaveConfirm(onLeaveTeam)()}
>
{t('account_team:user_team_leave_team')}
</Button>
)}
</HStack>
</Flex>
<ConfirmRemoveMemberModal />
</TableContainer>
</MyBox>
<Box flex={'1 0 0'} overflow={'auto'}>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
<Tr bgColor={'white !important'}>
<Th borderLeftRadius="6px" bgColor="myGray.100">
{t('account_team:user_name')}
</Th>
<Th bgColor="myGray.100">{t('account_team:member_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('account_team: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('account_team:remove_tip', {
username: item.memberName
})
)();
}}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
<ConfirmRemoveMemberModal />
</TableContainer>
</Box>
<ConfirmLeaveTeamModal />
{isOpenInvite && userInfo?.team?.teamId && (
<InviteModal
teamId={userInfo.team.teamId}
onClose={onCloseInvite}
onSuccess={refetchMembers}
/>
)}
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
</>
);
}

View File

@@ -0,0 +1,31 @@
import { IconProps } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
function IconButton({
name,
w = '1rem',
h = '1rem',
...props
}: {
name: IconNameType;
} & IconProps) {
return (
<MyIcon
name={name}
w={w}
h={h}
transition={'background 0.1s'}
cursor={'pointer'}
p="1"
rounded={'sm'}
_hover={{
bg: 'myGray.05',
color: 'primary.600'
}}
{...props}
/>
);
}
export default IconButton;

View File

@@ -0,0 +1,162 @@
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { postCreateOrg, putUpdateOrg } from '@/web/support/user/team/org/api';
import { Button, HStack, Input, ModalBody, ModalFooter, Textarea } from '@chakra-ui/react';
import { DEFAULT_ORG_AVATAR } from '@fastgpt/global/common/system/constants';
import Avatar from '@fastgpt/web/components/common/Avatar';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import { useForm } from 'react-hook-form';
export type OrgFormType = {
_id: string;
avatar: string;
description?: string;
name: string;
path: string;
parentId?: string;
};
export const defaultOrgForm: OrgFormType = {
_id: '',
avatar: '',
description: '',
name: '',
path: ''
};
function OrgInfoModal({
editOrg,
onClose,
onSuccess
}: {
editOrg: OrgFormType;
onClose: () => void;
onSuccess: () => void;
}) {
const { t } = useTranslation();
const isEdit = !!editOrg._id;
const { register, handleSubmit, setValue, watch } = useForm<OrgFormType>({
defaultValues: {
name: editOrg.name,
avatar: editOrg.avatar,
description: editOrg.description
}
});
const avatar = watch('avatar');
const { run: onCreate, loading: isLoadingCreate } = useRequest2(
async (data: OrgFormType) => {
if (!editOrg.parentId) return;
return postCreateOrg({
name: data.name,
avatar: data.avatar,
parentId: editOrg.parentId,
description: data.description
});
},
{
successToast: t('common:common.Create Success'),
onSuccess: () => {
onClose();
onSuccess();
}
}
);
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
async (data: OrgFormType) => {
if (!editOrg._id) return;
return putUpdateOrg({
orgId: editOrg._id,
name: data.name,
avatar: data.avatar,
description: data.description
});
},
{
successToast: t('common:common.Update Success'),
onSuccess: () => {
onClose();
onSuccess();
}
}
);
const {
File: AvatarSelect,
onOpen: onOpenSelectAvatar,
onSelectImage
} = useSelectFile({
fileType: '.jpg, .jpeg, .png',
multiple: false
});
const { loading: uploadingAvatar, run: onSelectAvatar } = useRequest2(
async (file: File[]) => {
return onSelectImage(file, {
maxW: 300,
maxH: 300
});
},
{
onSuccess: (src: string) => {
setValue('avatar', src);
}
}
);
const isLoading = uploadingAvatar || isLoadingUpdate || isLoadingCreate;
return (
<MyModal
isOpen
onClose={onClose}
title={isEdit ? t('account_team:edit_org_info') : t('account_team:create_org')}
iconSrc={'modal/edit'}
>
<ModalBody flex={1} overflow={'auto'} display={'flex'} flexDirection={'column'} gap={4}>
<FormLabel w="80px">{t('user:team.avatar_and_name')}</FormLabel>
<HStack>
<Avatar
src={avatar || DEFAULT_ORG_AVATAR}
onClick={onOpenSelectAvatar}
cursor={'pointer'}
borderRadius={'md'}
/>
<Input
bgColor="myGray.50"
{...register('name', { required: true })}
placeholder={t('account_team:org_name')}
/>
</HStack>
<FormLabel w="80px">{t('account_team:org_description')}</FormLabel>
<Textarea
bgColor="myGray.50"
{...register('description')}
placeholder={t('account_team:org_description')}
/>
</ModalBody>
<ModalFooter alignItems="flex-end">
<Button
isLoading={isLoading}
onClick={handleSubmit((data) => {
if (isEdit) {
onUpdate(data);
} else {
onCreate(data);
}
})}
>
{isEdit ? t('common:common.Save') : t('common:new_create')}
</Button>
</ModalFooter>
<AvatarSelect onSelect={onSelectAvatar} />
</MyModal>
);
}
export default OrgInfoModal;

View File

@@ -0,0 +1,197 @@
import { putUpdateOrgMembers } from '@/web/support/user/team/org/api';
import {
Box,
Button,
Checkbox,
Flex,
Grid,
HStack,
ModalBody,
ModalFooter
} from '@chakra-ui/react';
import type { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import type React from 'react';
import { useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '../context';
import { OrgType } from '@fastgpt/global/support/user/team/org/type';
import dynamic from 'next/dynamic';
export type GroupFormType = {
members: {
tmbId: string;
role: `${GroupMemberRole}`;
}[];
};
function CheckboxIcon({
name
}: {
isChecked?: boolean;
isIndeterminate?: boolean;
name: IconNameType;
}) {
return <MyIcon name={name} w="12px" />;
}
function OrgMemberManageModal({
currentOrg,
refetchOrgs,
onClose
}: {
currentOrg: OrgType;
refetchOrgs: () => void;
onClose: () => void;
}) {
const { t } = useTranslation();
const allMembers = useContextSelector(TeamContext, (v) => v.members);
const [selectedMembers, setSelectedMembers] = useState<string[]>(
currentOrg.members.map((item) => item.tmbId)
);
const [searchKey, setSearchKey] = useState('');
const filterMembers = useMemo(() => {
if (!searchKey) return allMembers;
const regx = new RegExp(searchKey, 'i');
return allMembers.filter((member) => regx.test(member.memberName));
}, [searchKey, allMembers]);
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
() => {
return putUpdateOrgMembers({
orgId: currentOrg._id,
members: selectedMembers.map((tmbId) => ({
tmbId
}))
});
},
{
successToast: t('common:common.Update Success'),
onSuccess() {
refetchOrgs();
onClose();
}
}
);
const isSelected = (memberId: string) => {
return selectedMembers.find((tmbId) => tmbId === memberId);
};
const handleToggleSelect = (memberId: string) => {
if (isSelected(memberId)) {
setSelectedMembers((state) => state.filter((tmbId) => tmbId !== memberId));
} else {
setSelectedMembers((state) => [...state, memberId]);
}
};
const isLoading = isLoadingUpdate;
return (
<MyModal
onClose={onClose}
isOpen
title={t('user:team.group.manage_member')}
iconSrc={currentOrg?.avatar}
minW="800px"
h={'100%'}
isCentered
>
<ModalBody flex={1}>
<Grid
border="1px solid"
borderColor="myGray.200"
borderRadius="0.5rem"
gridTemplateColumns="1fr 1fr"
h={'100%'}
>
<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'}>
{filterMembers.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={<CheckboxIcon name={'common/check'} />}
pointerEvents="none"
/>
<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')}:${selectedMembers.length}`}</Box>
<Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'400px'}>
{selectedMembers.map((tmbId) => {
const member = allMembers.find((item) => item.tmbId === tmbId)!;
return (
<HStack
justifyContent="space-between"
py="2"
px={3}
borderRadius={'md'}
key={tmbId}
_hover={{ bg: 'myGray.50' }}
_notLast={{ mb: 2 }}
>
<HStack>
<Avatar src={member?.avatar} w="1.5rem" borderRadius={'md'} />
<Box>{member?.memberName}</Box>
</HStack>
<MyIcon
name={'common/closeLight'}
w={'1rem'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
onClick={() => handleToggleSelect(tmbId)}
/>
</HStack>
);
})}
</Flex>
</Flex>
</Grid>
</ModalBody>
<ModalFooter alignItems="flex-end">
<Button isLoading={isLoading} onClick={onUpdate}>
{t('common:common.Save')}
</Button>
</ModalFooter>
</MyModal>
);
}
export default OrgMemberManageModal;

View File

@@ -0,0 +1,74 @@
import { putMoveOrg } from '@/web/support/user/team/org/api';
import { Button, ModalBody, ModalFooter } from '@chakra-ui/react';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useMemo, useState } from 'react';
import OrgTree from './OrgTree';
import dynamic from 'next/dynamic';
import { useUserStore } from '@/web/support/user/useUserStore';
function OrgMoveModal({
movingOrg,
orgs,
onClose,
onSuccess
}: {
movingOrg: OrgType;
orgs: OrgType[];
onClose: () => void;
onSuccess: () => void;
}) {
const { t } = useTranslation();
const [selectedOrg, setSelectedOrg] = useState<OrgType>();
const { userInfo } = useUserStore();
const team = userInfo?.team!;
const { runAsync: onMoveOrg, loading } = useRequest2(putMoveOrg, {
onSuccess: () => {
onClose();
onSuccess();
}
});
const filterMovingOrgs = useMemo(
() => orgs.filter((org) => org._id !== movingOrg._id),
[movingOrg._id, orgs]
);
return (
<MyModal
isOpen
onClose={onClose}
title={t('account_team:move_org')}
iconSrc="common/file/move"
iconColor="primary.600"
>
<ModalBody>
<OrgTree
orgs={filterMovingOrgs}
selectedOrg={selectedOrg}
setSelectedOrg={setSelectedOrg}
/>
</ModalBody>
<ModalFooter>
<Button
isDisabled={!selectedOrg}
isLoading={loading}
onClick={() => {
if (!selectedOrg) return;
return onMoveOrg({
orgId: movingOrg._id,
targetOrgId: selectedOrg._id
});
}}
>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
}
export default OrgMoveModal;

View File

@@ -0,0 +1,103 @@
import { Box, HStack, VStack } from '@chakra-ui/react';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useToggle } from 'ahooks';
import { useMemo } from 'react';
import IconButton from './IconButton';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
function OrgTreeNode({
org,
list,
selectedOrg,
setSelectedOrg,
index = 0
}: {
org: OrgType;
list: OrgType[];
selectedOrg?: OrgType;
setSelectedOrg: (org?: OrgType) => void;
index?: number;
}) {
const children = useMemo(
() => list.filter((item) => item.path === getOrgChildrenPath(org)),
[org, list]
);
const [isExpanded, toggleIsExpanded] = useToggle(index === 0);
return (
<Box userSelect={'none'}>
<HStack
borderRadius="sm"
_hover={{ bg: 'myGray.100' }}
py={1}
pr={2}
pl={index === 0 ? '0.5rem' : `${1.75 * (index - 1) + 0.5}rem`}
cursor={'pointer'}
{...(selectedOrg === org
? {
bg: 'primary.50 !important',
onClick: () => setSelectedOrg(undefined)
}
: {
onClick: () => setSelectedOrg(org)
})}
>
{index > 0 && (
<IconButton
name={isExpanded ? 'common/downArrowFill' : 'common/rightArrowFill'}
color={'myGray.500'}
p={0}
w={'1.25rem'}
visibility={children.length > 0 ? 'visible' : 'hidden'}
onClick={(e) => {
e.stopPropagation();
toggleIsExpanded.toggle();
}}
/>
)}
<HStack
flex={'1 0 0'}
onClick={() => setSelectedOrg(org)}
cursor={'pointer'}
borderRadius={'xs'}
>
<Avatar src={org.avatar} w={'1.25rem'} borderRadius={'xs'} />
<Box>{org.name}</Box>
</HStack>
</HStack>
{isExpanded &&
children.length > 0 &&
children.map((child) => (
<Box key={child._id} mt={0.5}>
<OrgTreeNode
org={child}
index={index + 1}
list={list}
selectedOrg={selectedOrg}
setSelectedOrg={setSelectedOrg}
/>
</Box>
))}
</Box>
);
}
function OrgTree({
orgs,
selectedOrg,
setSelectedOrg
}: {
orgs: OrgType[];
selectedOrg?: OrgType;
setSelectedOrg: (org?: OrgType) => void;
}) {
const root = orgs[0];
if (!root) return;
return (
<OrgTreeNode org={root} list={orgs} setSelectedOrg={setSelectedOrg} selectedOrg={selectedOrg} />
);
}
export default OrgTree;

View File

@@ -0,0 +1,354 @@
import { deleteOrg, deleteOrgMember } from '@/web/support/user/team/org/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import {
Box,
Divider,
Flex,
HStack,
Table,
TableContainer,
Tag,
Tbody,
Td,
Th,
Thead,
Tr,
VStack
} from '@chakra-ui/react';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import MemberTag from '@/components/support/user/team/Info/MemberTag';
import { TeamContext } from '../context';
import { getOrgList } from '@/web/support/user/team/org/api';
import IconButton from './IconButton';
import { defaultOrgForm, type OrgFormType } from './OrgInfoModal';
import dynamic from 'next/dynamic';
import MyBox from '@fastgpt/web/components/common/MyBox';
import Path from '@/components/common/folder/Path';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
const OrgInfoModal = dynamic(() => import('./OrgInfoModal'));
const OrgMemberManageModal = dynamic(() => import('./OrgMemberManageModal'));
const OrgMoveModal = dynamic(() => import('./OrgMoveModal'));
function ActionButton({
icon,
text,
onClick
}: {
icon: IconNameType;
text: string;
onClick: () => void;
}) {
return (
<HStack
gap={'8px'}
w="100%"
transition={'background 0.1s'}
cursor={'pointer'}
p="4px"
rounded={'sm'}
_hover={{
bg: 'myGray.05',
color: 'primary.600'
}}
onClick={onClick}
>
<MyIcon name={icon} w="1rem" h="1rem" />
<Box fontSize={'sm'}>{text}</Box>
</HStack>
);
}
function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { userInfo, isTeamAdmin } = useUserStore();
const { members } = useContextSelector(TeamContext, (v) => v);
const [parentPath, setParentPath] = useState('');
const {
data: orgs = [],
loading: isLoadingOrgs,
refresh: refetchOrgs
} = useRequest2(getOrgList, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const currentOrgs = useMemo(() => {
if (orgs.length === 0) return [];
// Auto select the first org(root org is team)
if (parentPath === '') {
setParentPath(getOrgChildrenPath(orgs[0]));
return [];
}
return orgs
.filter((org) => org.path === parentPath)
.map((item) => {
return {
...item,
// Member + org
count:
item.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(item)).length
};
});
}, [orgs, parentPath]);
const currentOrg = useMemo(() => {
const splitPath = parentPath.split('/');
const currentOrgId = splitPath[splitPath.length - 1];
if (!currentOrgId) return;
return orgs.find((org) => org.pathId === currentOrgId);
}, [orgs, parentPath]);
const paths = useMemo(() => {
const splitPath = parentPath.split('/').filter(Boolean);
return splitPath
.map((id) => {
const org = orgs.find((org) => org.pathId === id)!;
if (org.path === '') return;
return {
parentId: getOrgChildrenPath(org),
parentName: org.name
};
})
.filter(Boolean) as ParentTreePathItemType[];
}, [parentPath, orgs]);
const [editOrg, setEditOrg] = useState<OrgFormType>();
const [manageMemberOrg, setManageMemberOrg] = useState<OrgType>();
const [movingOrg, setMovingOrg] = useState<OrgType>();
// Delete org
const { ConfirmModal: ConfirmDeleteOrgModal, openConfirm: openDeleteOrgModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_org')
});
const deleteOrgHandler = (orgId: string) => openDeleteOrgModal(() => deleteOrgReq(orgId))();
const { runAsync: deleteOrgReq } = useRequest2(deleteOrg, {
onSuccess: () => {
refetchOrgs();
}
});
// Delete member
const { ConfirmModal: ConfirmDeleteMember, openConfirm: openDeleteMemberModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_member')
});
const { runAsync: deleteMemberReq } = useRequest2(deleteOrgMember, {
onSuccess: () => {
refetchOrgs();
}
});
return (
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
</Flex>
<MyBox flex={'1 0 0'} overflow={'auto'} isLoading={isLoadingOrgs}>
<Box mb={3}>
<Path paths={paths} rootName={userInfo?.team?.teamName} onClick={setParentPath} />
</Box>
<Flex w={'100%'} gap={'4'}>
{/* Table */}
<TableContainer overflow={'unset'} fontSize={'sm'} flexGrow={1}>
<Table overflow={'unset'}>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="6px">
{t('common:Name')}
</Th>
<Th bg="myGray.100" borderRightRadius="6px">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
<Tbody>
{currentOrgs.map((org) => (
<Tr key={org._id} overflow={'unset'}>
<Td>
<HStack
cursor={'pointer'}
onClick={() => setParentPath(getOrgChildrenPath(org))}
>
<MemberTag name={org.name} avatar={org.avatar} />
<Tag size="sm">{org.count}</Tag>
<MyIcon
name="core/chat/chevronRight"
w={'1rem'}
h={'1rem'}
color={'myGray.500'}
/>
</HStack>
</Td>
<Td w={'6rem'}>
{isTeamAdmin && (
<MyMenu
trigger="hover"
Button={<IconButton name="more" />}
menuList={[
{
children: [
{
icon: 'edit',
label: t('account_team:edit_info'),
onClick: () => setEditOrg(org)
},
{
icon: 'common/file/move',
label: t('common:Move'),
onClick: () => setMovingOrg(org)
},
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () => deleteOrgHandler(org._id)
}
]
}
]}
/>
)}
</Td>
</Tr>
))}
{currentOrg?.members.map((member) => {
const memberInfo = members.find((m) => m.tmbId === member.tmbId);
if (!memberInfo) return null;
return (
<Tr key={member.tmbId}>
<Td>
<MemberTag name={memberInfo.memberName} avatar={memberInfo.avatar} />
</Td>
<Td w={'6rem'}>
{isTeamAdmin && (
<MyMenu
trigger={'hover'}
Button={<IconButton name="more" />}
menuList={[
{
children: [
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () =>
openDeleteMemberModal(() =>
deleteMemberReq(currentOrg._id, member.tmbId)
)()
}
]
}
]}
/>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
{/* Slider */}
<VStack w={'180px'} alignItems={'start'}>
<HStack gap={'6px'}>
<Avatar src={currentOrg?.avatar} w={'1rem'} h={'1rem'} rounded={'xs'} />
<Box fontWeight={500} color={'myGray.900'}>
{currentOrg?.name}
</Box>
{currentOrg?.path !== '' && (
<IconButton name="edit" onClick={() => setEditOrg(currentOrg)} />
)}
</HStack>
<Box fontSize={'xs'}>{currentOrg?.description || t('common:common.no_intro')}</Box>
<Divider my={'20px'} />
<Box fontWeight={500} fontSize="sm" color="myGray.900">
{t('common:common.Action')}
</Box>
{currentOrg && isTeamAdmin && (
<VStack gap="13px" w="100%">
<ActionButton
icon="common/add2"
text={t('account_team:create_sub_org')}
onClick={() => {
setEditOrg({
...defaultOrgForm,
parentId: currentOrg?._id
});
}}
/>
<ActionButton
icon="common/administrator"
text={t('account_team:manage_member')}
onClick={() => setManageMemberOrg(currentOrg)}
/>
{currentOrg?.path !== '' && (
<>
<ActionButton
icon="common/file/move"
text={t('account_team:move_org')}
onClick={() => setMovingOrg(currentOrg)}
/>
<ActionButton
icon="delete"
text={t('account_team:delete_org')}
onClick={() => deleteOrgHandler(currentOrg._id)}
/>
</>
)}
</VStack>
)}
</VStack>
</Flex>
{!!editOrg && (
<OrgInfoModal
editOrg={editOrg}
onClose={() => setEditOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!movingOrg && (
<OrgMoveModal
orgs={orgs}
movingOrg={movingOrg}
onClose={() => setMovingOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!manageMemberOrg && (
<OrgMemberManageModal
currentOrg={manageMemberOrg}
refetchOrgs={refetchOrgs}
onClose={() => setManageMemberOrg(undefined)}
/>
)}
<ConfirmDeleteOrgModal />
<ConfirmDeleteMember />
</MyBox>
</>
);
}
export default OrgTable;

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import {
Box,
Checkbox,
@@ -9,279 +9,422 @@ import {
Td,
Th,
Thead,
Tr
Text,
Tr,
Flex,
Button
} 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 {
deleteMemberPermission,
getTeamClbs,
updateMemberPermission
} from '@/web/support/user/team/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import { TeamContext } from '../context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MemberTag from '../../../../../components/support/user/team/Info/MemberTag';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import {
TeamManagePermissionVal,
TeamPermissionList,
TeamWritePermissionVal
} from '@fastgpt/global/support/permission/user/constant';
import { TeamPermission } from '@fastgpt/global/support/permission/user/controller';
import { useCreation } from 'ahooks';
import { useToggle } from 'ahooks';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import MyBox from '@fastgpt/web/components/common/MyBox';
import CollaboratorContextProvider, {
CollaboratorContext
} from '@/components/support/permission/MemberManager/context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
import { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator';
function PermissionManage() {
function PermissionManage({
Tabs,
onOpenAddMember
}: {
Tabs: React.ReactNode;
onOpenAddMember: () => void;
}) {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { groups, refetchMembers, refetchGroups, members, searchKey } = useContextSelector(
TeamContext,
(v) => v
const collaboratorList = useContextSelector(
CollaboratorContext,
(state) => state.collaboratorList
);
const onUpdateCollaborators = useContextSelector(
CollaboratorContext,
(state) => state.onUpdateCollaborators
);
const onDelOneCollaborator = useContextSelector(
CollaboratorContext,
(state) => state.onDelOneCollaborator
);
const { runAsync: refetchClbs, data: clbs = [] } = useRequest2(getTeamClbs, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const [isExpandMember, setExpandMember] = useToggle(true);
const [isExpandGroup, setExpandGroup] = useToggle(true);
const [isExpandOrg, setExpandOrg] = useToggle(true);
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 });
const { tmbList, groupList, orgList } = useMemo(() => {
const tmbList: CollaboratorItemType[] = [];
const groupList: CollaboratorItemType[] = [];
const orgList: CollaboratorItemType[] = [];
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
});
}
}
collaboratorList.forEach((item) => {
if (item.tmbId) {
tmbList.push(item);
} else if (item.groupId) {
groupList.push(item);
} else if (item.orgId) {
orgList.push(item);
}
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
});
}
}
});
return {
tmbList,
groupList,
orgList
};
}, [collaboratorList]);
const { runAsync: onUpdatePermission, loading: addLoading } = useRequest2(
async ({ id, type, per }: { id: string; type: 'add' | 'remove'; per: 'write' | 'manage' }) => {
const clb = collaboratorList.find(
(clb) => clb.tmbId === id || clb.groupId === id || clb.orgId === id
);
if (!clb) return;
const updatePer = per === 'write' ? TeamWritePermissionVal : TeamManagePermissionVal;
const permission = new TeamPermission({ per: clb.permission.value });
if (type === 'add') {
permission.addPer(updatePer);
} else {
permission.removePer(updatePer);
}
return onUpdateCollaborators({
...(clb.tmbId && { members: [clb.tmbId] }),
...(clb.groupId && { groups: [clb.groupId] }),
...(clb.orgId && { orgs: [clb.orgId] }),
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 { runAsync: onDeleteMemberPermission, loading: deleteLoading } =
useRequest2(onDelOneCollaborator);
const userManage = userInfo?.permission.hasManagePer;
const hasDeletePer = (per: TeamPermission) => {
if (userInfo?.permission.isOwner) return true;
if (userManage && !per.hasManagePer) return true;
return false;
};
return (
<TableContainer fontSize={'sm'}>
<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>
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
<Box ml="auto">
{/* <SearchInput
placeholder={t('user:search_group_org_user')}
w="200px"
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
/> */}
</Box>
{userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/add2" w={'14px'} />}
onClick={onOpenAddMember}
>
{t('user:permission.Add')}
</Button>
)}
</Flex>
<MyBox isLoading={addLoading || deleteLoading}>
<TableContainer fontSize={'sm'}>
<Table>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="md" maxW={'150px'}>
{`${t('user:team.group.members')} / ${t('user:team.org.org')} / ${t('user:team.group.group')}`}
<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">
<Box mx="auto" w="fit-content">
{t('user:team.group.permission.manage')}
<QuestionTip ml="1" label={t('user:team.group.manage_tip')} />
</Box>
</Th>
<Th bg="myGray.100" borderRightRadius="md">
<Box mx="auto" w="fit-content">
{t('common:common.Action')}
</Box>
</Th>
</Tr>
</Thead>
<Tbody>
<>
<Tr userSelect={'none'}>
<HStack pl={3} pt={3} pb={isExpandMember && !!tmbList.length ? 0 : 3}>
<MyIconButton
icon={isExpandMember ? 'common/downArrowFill' : 'common/rightArrowFill'}
onClick={setExpandMember.toggle}
/>
<Box color={'myGray.900'}>{t('user:team.group.members')}</Box>
</HStack>
</Tr>
{isExpandMember &&
tmbList.map((member) => (
<Tr key={member.tmbId}>
<Td pl={10}>
<HStack>
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box>{member.name}</Box>
</HStack>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={member.permission.isOwner || !userManage}
isChecked={member.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id: member.tmbId!,
type: 'add',
per: 'write'
})
: onUpdatePermission({
id: member.tmbId!,
type: 'remove',
per: 'write'
})
}
/>
</Box>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={member.permission.isOwner || !userInfo?.permission.isOwner}
isChecked={member.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id: member.tmbId!,
type: 'add',
per: 'manage'
})
: onUpdatePermission({
id: member.tmbId!,
type: 'remove',
per: 'manage'
})
}
/>
</Box>
</Td>
<Td>
{hasDeletePer(member.permission) &&
userInfo?.team.tmbId !== member.tmbId && (
<Box mx="auto" w="fit-content">
<MyIconButton
icon="common/trash"
onClick={() =>
onDeleteMemberPermission({ tmbId: String(member.tmbId) })
}
/>
</Box>
)}
</Td>
</Tr>
))}
</>
<>
<Tr borderBottom={'1px solid'} borderColor={'myGray.200'} />
<Tr userSelect={'none'}>
<HStack pl={3} pt={3} pb={isExpandOrg && !!orgList.length ? 0 : 3}>
<MyIconButton
icon={isExpandOrg ? 'common/downArrowFill' : 'common/rightArrowFill'}
onClick={setExpandOrg.toggle}
/>
<Text>{t('user:team.org.org')}</Text>
</HStack>
</Tr>
{isExpandOrg &&
orgList.map((org) => (
<Tr key={org.orgId}>
<Td pl={10}>
<MemberTag name={org.name} avatar={org.avatar} />
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userManage}
isChecked={org.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({ id: org.orgId!, type: 'add', per: 'write' })
: onUpdatePermission({
id: org.orgId!,
type: 'remove',
per: 'write'
})
}
/>
</Box>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userInfo?.permission.isOwner}
isChecked={org.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({ id: org.orgId!, type: 'add', per: 'manage' })
: onUpdatePermission({
id: org.orgId!,
type: 'remove',
per: 'manage'
})
}
/>
</Box>
</Td>
<Td>
{hasDeletePer(org.permission) && (
<Box mx="auto" w="fit-content">
<MyIconButton
icon="common/trash"
onClick={() => onDeleteMemberPermission({ orgId: org.orgId! })}
/>
</Box>
)}
</Td>
</Tr>
))}
</>
<>
<Tr borderBottom={'1px solid'} borderColor={'myGray.200'} />
<Tr userSelect={'none'}>
<HStack pl={3} pt={3} pb={isExpandGroup && !!groupList.length ? 0 : 3}>
<MyIconButton
icon={isExpandGroup ? 'common/downArrowFill' : 'common/rightArrowFill'}
onClick={setExpandGroup.toggle}
/>
<Text>{t('user:team.group.group')}</Text>
</HStack>
</Tr>
{isExpandGroup &&
groupList.map((group) => (
<Tr key={group.groupId}>
<Td pl={10}>
<MemberTag
name={
group.name === DefaultGroupName
? userInfo?.team.teamName ?? ''
: group.name
}
avatar={group.avatar}
/>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userManage}
isChecked={group.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id: group.groupId!,
type: 'add',
per: 'write'
})
: onUpdatePermission({
id: group.groupId!,
type: 'remove',
per: 'write'
})
}
/>
</Box>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userInfo?.permission.isOwner}
isChecked={group.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id: group.groupId!,
type: 'add',
per: 'manage'
})
: onUpdatePermission({
id: group.groupId!,
type: 'remove',
per: 'manage'
})
}
/>
</Box>
</Td>
<Td>
{hasDeletePer(group.permission) && (
<Box mx="auto" w="fit-content">
<MyIconButton
icon="common/trash"
onClick={() => onDeleteMemberPermission({ groupId: group.groupId! })}
/>
</Box>
)}
</Td>
</Tr>
))}
</>
</Tbody>
</Table>
</TableContainer>
</MyBox>
</>
);
}
export default PermissionManage;
export const Render = ({ Tabs }: { Tabs: React.ReactNode }) => {
const { userInfo } = useUserStore();
return userInfo?.team ? (
<CollaboratorContextProvider
permission={userInfo?.team.permission}
permissionList={TeamPermissionList}
onGetCollaboratorList={getTeamClbs}
onUpdateCollaborators={updateMemberPermission}
onDelOneCollaborator={deleteMemberPermission}
refreshDeps={[userInfo?.team.teamId]}
addPermissionOnly={true}
>
{({ onOpenAddMember }) => <PermissionManage Tabs={Tabs} onOpenAddMember={onOpenAddMember} />}
</CollaboratorContextProvider>
) : null;
};
export default Render;

View File

@@ -10,6 +10,7 @@ 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';
import { OrgType } from '@fastgpt/global/support/user/team/org/type';
const EditInfoModal = dynamic(() => import('./EditInfoModal'));
@@ -24,8 +25,6 @@ type TeamModalContextType = {
refetchMembers: () => void;
refetchTeams: () => void;
refetchGroups: () => void;
searchKey: string;
setSearchKey: React.Dispatch<React.SetStateAction<string>>;
teamSize: number;
};
@@ -50,10 +49,6 @@ export const TeamContext = createContext<TeamModalContextType>({
throw new Error('Function not implemented.');
},
searchKey: '',
setSearchKey: function (_value: React.SetStateAction<string>): void {
throw new Error('Function not implemented.');
},
teamSize: 0
});
@@ -61,7 +56,6 @@ 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 = [],
@@ -114,8 +108,6 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
refetchTeams,
isLoading,
onSwitchTeam,
searchKey,
setSearchKey,
// create | update team
setEditTeamData,

View File

@@ -1,110 +1,67 @@
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import AccountContainer from '../components/AccountContainer';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import { Box, Flex, useDisclosure } from '@chakra-ui/react';
import Icon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import TeamSelector from '../components/TeamSelector';
import { useUserStore } from '@/web/support/user/useUserStore';
import React, { useState } from 'react';
import React, { useMemo } from 'react';
import { useContextSelector } from 'use-context-selector';
import { useRouter } from 'next/router';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { delLeaveTeam } from '@/web/support/user/team/api';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { TeamContext, TeamModalContextProvider } from './components/context';
import dynamic from 'next/dynamic';
import TeamTagModal from '@/components/support/user/team/TeamTagModal';
import MemberTable from './components/MemberTable';
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'));
const OrgManage = dynamic(() => import('./components/OrgManage/index'));
export enum TeamTabEnum {
member = 'member',
org = 'org',
group = 'group',
permission = 'permission'
}
const Team = () => {
const router = useRouter();
const { toast } = useToast();
const { t } = useTranslation();
const { userInfo, teamPlanStatus } = useUserStore();
const { feConfigs } = useSystemStore();
const {
myTeams,
refetchTeams,
members,
refetchMembers,
setEditTeamData,
onSwitchTeam,
searchKey,
setSearchKey,
teamSize,
isLoading
} = useContextSelector(TeamContext, (v) => v);
const { teamTab = TeamTabEnum.member } = router.query as { teamTab: `${TeamTabEnum}` };
const {
isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync
} = useDisclosure();
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const {
isOpen: isOpenGroupInfo,
onOpen: onOpenGroupInfo,
onClose: onCloseGroupInfo
} = useDisclosure();
const {
isOpen: isOpenManageGroupMember,
onOpen: onOpenManageGroupMember,
onClose: onCloseManageGroupMember
} = useDisclosure();
const { t } = useTranslation();
const { userInfo } = useUserStore();
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('account_team:user_team_leave_team_failed')
}
const { setEditTeamData, teamSize, isLoading } = useContextSelector(TeamContext, (v) => v);
const Tabs = useMemo(
() => (
<FillRowTabs
list={[
{ label: t('account_team:member'), value: TeamTabEnum.member },
{ label: t('account_team:org'), value: TeamTabEnum.org },
{ label: t('account_team:group'), value: TeamTabEnum.group },
{ label: t('account_team:permission'), value: TeamTabEnum.permission }
]}
px={'1rem'}
value={teamTab}
onChange={(e) => {
router.replace({
query: {
...router.query,
teamTab: e
}
});
}}
/>
),
[router, t, teamTab]
);
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('account_team:confirm_leave_team')
});
const [editGroupId, setEditGroupId] = useState<string>();
const onEditGroup = (groupId: string) => {
setEditGroupId(groupId);
onOpenGroupInfo();
};
const onManageMember = (groupId: string) => {
setEditGroupId(groupId);
onOpenManageGroupMember();
};
return (
<AccountContainer isLoading={isLoading}>
{/* header */}
@@ -168,142 +125,11 @@ const Team = () => {
{/* table */}
<Box py={'1.5rem'} px={'2rem'}>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
<FillRowTabs
list={[
{ label: t('account_team:member'), value: TeamTabEnum.member },
{ label: t('account_team:group'), value: TeamTabEnum.group },
{ label: t('account_team:permission'), value: TeamTabEnum.permission }
]}
px={'1rem'}
value={teamTab}
onChange={(e) => {
router.replace({
query: {
...router.query,
teamTab: e
}
});
}}
/>
<Flex alignItems={'center'}>
{teamTab === TeamTabEnum.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('account_team:label_sync')}
</Button>
)}
{teamTab === TeamTabEnum.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('account_team:user_team_invite_member')}
</Button>
)}
{teamTab === TeamTabEnum.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('account_team:user_team_leave_team')}
</Button>
)}
{teamTab === TeamTabEnum.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>
)}
{teamTab === TeamTabEnum.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'}>
{teamTab === TeamTabEnum.member && <MemberTable />}
{teamTab === TeamTabEnum.group && (
<GroupManage onEditGroup={onEditGroup} onManageMember={onManageMember} />
)}
{teamTab === TeamTabEnum.permission && <PermissionManage />}
</Box>
{teamTab === TeamTabEnum.member && <MemberTable Tabs={Tabs} />}
{teamTab === TeamTabEnum.org && <OrgManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.group && <GroupManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.permission && <PermissionManage Tabs={Tabs} />}
</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 />
</AccountContainer>
);
};