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:
@@ -94,7 +94,7 @@ const AccountContainer = ({
|
||||
...(userInfo?.team?.permission.hasManagePer
|
||||
? [
|
||||
{
|
||||
icon: 'support/outlink/apikeyLight',
|
||||
icon: 'key',
|
||||
label: t('account:api_key'),
|
||||
value: TabEnum.apikey
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ const TeamSelector = ({
|
||||
key={'manage'}
|
||||
alignItems={'center'}
|
||||
borderRadius={'md'}
|
||||
cursor={'default'}
|
||||
cursor={'pointer'}
|
||||
gap={3}
|
||||
onClick={() => router.push('/account/team')}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user