4.8.19-feature (#3636)
* feat: sync org from wecom, pref: member list pagination (#3549) * feat: sync org * chore: fe * chore: loading * chore: type * pref: team member list change to pagination. Edit a sort of list apis. * feat: member update avatar * chore: user avatar move to tmb * chore: init scripts move user avatar * chore: sourceMember * fix: list api sourceMember * fix: member sync * fix: pagination * chore: adjust code * chore: move changeOwner to pro * chore: init v4819 script * chore: adjust code * chore: UserBox * perf: scroll page code * perf: list data * docs:更新用户答疑 (#3576) * docs: add custom uid docs (#3572) * fix: pagination bug (#3577) * 4.8.19 test (#3584) * faet: dataset search filter * fix: scroll page * fix: collection list api old version (#3591) * fix: collection list api format * fix: type error of addSourceMemeber * fix: scroll fetch (#3592) * fix: yuque dataset file folder can enter (#3593) * perf: load members;perf: yuque load;fix: workflow llm params cannot close (#3594) * chat openapi doc * feat: dataset openapi doc * perf: load members * perf: member load code * perf: yuque load * fix: workflow llm params cannot close * fix: api dataset reference tag preview (#3600) * perf: doc * feat: chat page config * fix: http parse (#3634) * update doc * fix: http parse * fix code run node reset template (#3633) Co-authored-by: Archer <545436317@qq.com> * docs:faq (#3627) * docs:faq * docsFix * perf: sleep plugin * fix: selector --------- Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com> Co-authored-by: Jiangween <145003935+Jiangween@users.noreply.github.com> Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
billStatusMap,
|
||||
billTypeMap
|
||||
} from '@fastgpt/global/support/wallet/bill/constants';
|
||||
// import { usePagination } from '@/web/common/hooks/usePagination';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { standardSubLevelMap, subModeMap } from '@fastgpt/global/support/wallet/sub/constants';
|
||||
@@ -33,25 +32,23 @@ import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { usePagination } from '@fastgpt/web/hooks/usePagination';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
const BillTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const { commonT } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const [billType, setBillType] = useState<BillTypeEnum | ''>('');
|
||||
const [billType, setBillType] = useState<BillTypeEnum | undefined>(undefined);
|
||||
const [billDetail, setBillDetail] = useState<BillSchemaType>();
|
||||
|
||||
const billTypeList = useMemo(
|
||||
() =>
|
||||
[
|
||||
{ label: t('account_bill:all'), value: '' },
|
||||
{ label: t('account_bill:all'), value: undefined },
|
||||
...Object.entries(billTypeMap).map(([key, value]) => ({
|
||||
label: t(value.label as any),
|
||||
value: key
|
||||
}))
|
||||
] as {
|
||||
label: string;
|
||||
value: BillTypeEnum | '';
|
||||
value: BillTypeEnum | undefined;
|
||||
}[],
|
||||
[t]
|
||||
);
|
||||
@@ -62,8 +59,7 @@ const BillTable = () => {
|
||||
Pagination,
|
||||
getData,
|
||||
total
|
||||
} = usePagination({
|
||||
api: getBills,
|
||||
} = usePagination(getBills, {
|
||||
pageSize: 20,
|
||||
params: {
|
||||
type: billType
|
||||
@@ -110,7 +106,7 @@ const BillTable = () => {
|
||||
<Tr>
|
||||
<Th>#</Th>
|
||||
<Th>
|
||||
<MySelect<BillTypeEnum | ''>
|
||||
<MySelect
|
||||
list={billTypeList}
|
||||
value={billType}
|
||||
size={'sm'}
|
||||
@@ -181,7 +177,6 @@ export default BillTable;
|
||||
|
||||
function BillDetailModal({ bill, onClose }: { bill: BillSchemaType; onClose: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const { commonT } = useI18n();
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getInvoiceRecords } from '@/web/support/wallet/bill/invoice/api';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -30,8 +30,7 @@ const InvoiceTable = () => {
|
||||
isLoading,
|
||||
Pagination,
|
||||
total
|
||||
} = usePagination({
|
||||
api: getInvoiceRecords,
|
||||
} = usePagination(getInvoiceRecords, {
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -160,6 +160,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
color: 'myGray.900'
|
||||
};
|
||||
|
||||
const isSyncMember = feConfigs.register_method?.includes('sync');
|
||||
return (
|
||||
<Box>
|
||||
{/* user info */}
|
||||
@@ -224,6 +225,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
<Box {...labelStyles}>{t('account_info:member_name')}: </Box>
|
||||
<Input
|
||||
flex={'1 0 0'}
|
||||
disabled={isSyncMember}
|
||||
defaultValue={userInfo?.team?.memberName || 'Member'}
|
||||
title={t('account_info:click_modify_nickname')}
|
||||
borderColor={'transparent'}
|
||||
@@ -590,11 +592,6 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
const { feConfigs } = useSystemStore();
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useSystem();
|
||||
const { userInfo, updateUserInfo } = useUserStore();
|
||||
|
||||
const { reset } = useForm<UserUpdateParams>({
|
||||
defaultValues: userInfo as UserType
|
||||
});
|
||||
|
||||
return (
|
||||
<Box>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Flex, useTheme } from '@chakra-ui/react';
|
||||
import { getInforms, readInform } from '@/web/support/user/inform/api';
|
||||
import type { UserInformSchema } from '@fastgpt/global/support/user/inform/type';
|
||||
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
|
||||
import { usePagination } from '@fastgpt/web/hooks/usePagination';
|
||||
import { useLoading } from '@fastgpt/web/hooks/useLoading';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import AccountContainer, { TabEnum } from './components/AccountContainer';
|
||||
import AccountContainer from './components/AccountContainer';
|
||||
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
|
||||
|
||||
const InformTable = () => {
|
||||
@@ -23,8 +22,7 @@ const InformTable = () => {
|
||||
Pagination,
|
||||
getData,
|
||||
pageNum
|
||||
} = usePagination<UserInformSchema>({
|
||||
api: getInforms,
|
||||
} = usePagination(getInforms, {
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import { usePagination } from '@fastgpt/web/hooks/usePagination';
|
||||
import { useLoading } from '@fastgpt/web/hooks/useLoading';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import AccountContainer, { TabEnum } from './components/AccountContainer';
|
||||
import AccountContainer from './components/AccountContainer';
|
||||
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
|
||||
|
||||
const Promotion = () => {
|
||||
@@ -41,8 +41,7 @@ const Promotion = () => {
|
||||
total,
|
||||
pageSize,
|
||||
Pagination
|
||||
} = usePagination({
|
||||
api: getPromotionRecords,
|
||||
} = usePagination(getPromotionRecords, {
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { Box, Button, Flex, Input, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { postCreateTeam, putUpdateTeam } from '@/web/support/user/team/api';
|
||||
import { CreateTeamProps } from '@fastgpt/global/support/user/team/controller.d';
|
||||
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
|
||||
|
||||
export type EditTeamFormDataType = CreateTeamProps & {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export const defaultForm = {
|
||||
name: '',
|
||||
avatar: DEFAULT_TEAM_AVATAR
|
||||
};
|
||||
|
||||
function EditModal({
|
||||
defaultData = defaultForm,
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
defaultData?: EditTeamFormDataType;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { register, setValue, handleSubmit, watch } = useForm<CreateTeamProps>({
|
||||
defaultValues: defaultData
|
||||
});
|
||||
const avatar = watch('avatar');
|
||||
|
||||
const {
|
||||
File,
|
||||
onOpen: onOpenSelectFile,
|
||||
onSelectImage
|
||||
} = useSelectFile({
|
||||
fileType: '.jpg,.png,.svg',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
const { mutate: onclickCreate, isLoading: creating } = useRequest({
|
||||
mutationFn: async (data: CreateTeamProps) => {
|
||||
return postCreateTeam(data);
|
||||
},
|
||||
onSuccess() {
|
||||
onSuccess();
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common:common.Create Success'),
|
||||
errorToast: t('common:common.Create Failed')
|
||||
});
|
||||
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
|
||||
mutationFn: async (data: EditTeamFormDataType) => {
|
||||
if (!data.id) return Promise.resolve('');
|
||||
return putUpdateTeam({
|
||||
name: data.name,
|
||||
avatar: data.avatar
|
||||
});
|
||||
},
|
||||
onSuccess() {
|
||||
onSuccess();
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common:common.Update Success'),
|
||||
errorToast: t('common:common.Update Failed')
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
iconSrc="support/team/group"
|
||||
iconColor="primary.600"
|
||||
title={defaultData.id ? t('user:team.Update Team') : t('user:team.Create Team')}
|
||||
>
|
||||
<ModalBody>
|
||||
<Box color={'myGray.800'} fontWeight={'bold'}>
|
||||
{t('user:team.Set Name')}
|
||||
</Box>
|
||||
<Flex mt={3} alignItems={'center'}>
|
||||
<MyTooltip label={t('common:common.Set Avatar')}>
|
||||
<Avatar
|
||||
flexShrink={0}
|
||||
src={avatar}
|
||||
w={['28px', '32px']}
|
||||
h={['28px', '32px']}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
onClick={onOpenSelectFile}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Input
|
||||
flex={1}
|
||||
ml={4}
|
||||
autoFocus
|
||||
bg={'myWhite.600'}
|
||||
maxLength={20}
|
||||
placeholder={t('user:team.Team Name')}
|
||||
{...register('name', {
|
||||
required: t('common:common.Please Input Name')
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{!!defaultData.id ? (
|
||||
<>
|
||||
<Box flex={1} />
|
||||
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
|
||||
{t('common:common.Close')}
|
||||
</Button>
|
||||
<Button isLoading={updating} onClick={handleSubmit((data) => onclickUpdate(data))}>
|
||||
{t('common:common.Confirm Update')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
w={'100%'}
|
||||
isLoading={creating}
|
||||
onClick={handleSubmit((data) => onclickCreate(data))}
|
||||
>
|
||||
{t('common:common.Confirm Create')}
|
||||
</Button>
|
||||
)}
|
||||
</ModalFooter>
|
||||
<File
|
||||
onSelect={(e) =>
|
||||
onSelectImage(e, {
|
||||
maxH: 300,
|
||||
maxW: 300,
|
||||
callback: (e) => setValue('avatar', e)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(EditModal);
|
||||
@@ -1,127 +0,0 @@
|
||||
import { Input, HStack, ModalBody, Button, ModalFooter } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { TeamContext } from '../context';
|
||||
import { postCreateGroup, putUpdateGroup } from '@/web/support/user/team/group/api';
|
||||
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
|
||||
|
||||
export type GroupFormType = {
|
||||
avatar: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) {
|
||||
const { refetchGroups, groups, refetchMembers } = useContextSelector(TeamContext, (v) => v);
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
File: AvatarSelect,
|
||||
onOpen: onOpenSelectAvatar,
|
||||
onSelectImage
|
||||
} = useSelectFile({
|
||||
fileType: '.jpg, .jpeg, .png',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
const group = useMemo(() => {
|
||||
return groups.find((item) => item._id === editGroupId);
|
||||
}, [editGroupId, groups]);
|
||||
|
||||
const { register, handleSubmit, getValues, setValue } = useForm<GroupFormType>({
|
||||
defaultValues: {
|
||||
name: group?.name || '',
|
||||
avatar: group?.avatar || DEFAULT_TEAM_AVATAR
|
||||
}
|
||||
});
|
||||
|
||||
const { loading: uploadingAvatar, run: onSelectAvatar } = useRequest2(
|
||||
async (file: File[]) => {
|
||||
return onSelectImage(file, {
|
||||
maxW: 300,
|
||||
maxH: 300
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess: (src: string) => {
|
||||
setValue('avatar', src);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const { run: onCreate, loading: isLoadingCreate } = useRequest2(
|
||||
(data: GroupFormType) => {
|
||||
return postCreateGroup({
|
||||
name: data.name,
|
||||
avatar: data.avatar
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()])
|
||||
}
|
||||
);
|
||||
|
||||
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
|
||||
async (data: GroupFormType) => {
|
||||
if (!editGroupId) return;
|
||||
return putUpdateGroup({
|
||||
groupId: editGroupId,
|
||||
name: data.name,
|
||||
avatar: data.avatar
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()])
|
||||
}
|
||||
);
|
||||
|
||||
const isLoading = isLoadingUpdate || isLoadingCreate || uploadingAvatar;
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
onClose={onClose}
|
||||
title={editGroupId ? t('user:team.group.edit') : t('user:team.group.create')}
|
||||
iconSrc={group?.avatar ?? DEFAULT_TEAM_AVATAR}
|
||||
>
|
||||
<ModalBody flex={1} overflow={'auto'} display={'flex'} flexDirection={'column'} gap={4}>
|
||||
<FormLabel w="80px">{t('user:team.avatar_and_name')}</FormLabel>
|
||||
<HStack>
|
||||
<Avatar
|
||||
src={getValues('avatar')}
|
||||
onClick={onOpenSelectAvatar}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
/>
|
||||
<Input
|
||||
bgColor="myGray.50"
|
||||
{...register('name', { required: true })}
|
||||
placeholder={t('user:team.group.name')}
|
||||
/>
|
||||
</HStack>
|
||||
</ModalBody>
|
||||
<ModalFooter alignItems="flex-end">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
onClick={handleSubmit((data) => {
|
||||
if (editGroupId) {
|
||||
onUpdate(data);
|
||||
} else {
|
||||
onCreate(data);
|
||||
}
|
||||
})}
|
||||
>
|
||||
{editGroupId ? t('common:common.Save') : t('common:new_create')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
<AvatarSelect onSelect={onSelectAvatar} />
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default GroupInfoModal;
|
||||
@@ -1,278 +0,0 @@
|
||||
import {
|
||||
Box,
|
||||
ModalBody,
|
||||
Flex,
|
||||
Button,
|
||||
ModalFooter,
|
||||
Checkbox,
|
||||
Grid,
|
||||
HStack
|
||||
} from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import Tag from '@fastgpt/web/components/common/Tag';
|
||||
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { TeamContext } from '../context';
|
||||
import { putUpdateGroup } from '@/web/support/user/team/group/api';
|
||||
import { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
|
||||
export type GroupFormType = {
|
||||
members: {
|
||||
tmbId: string;
|
||||
role: `${GroupMemberRole}`;
|
||||
}[];
|
||||
};
|
||||
|
||||
function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) {
|
||||
// 1. Owner can not be deleted, toast
|
||||
// 2. Owner/Admin can manage members
|
||||
// 3. Owner can add/remove admins
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
const { toast } = useToast();
|
||||
const [hoveredMemberId, setHoveredMemberId] = useState<string | undefined>(undefined);
|
||||
const {
|
||||
members: allMembers,
|
||||
refetchGroups,
|
||||
groups,
|
||||
refetchMembers
|
||||
} = useContextSelector(TeamContext, (v) => v);
|
||||
|
||||
const group = useMemo(() => {
|
||||
return groups.find((item) => item._id === editGroupId);
|
||||
}, [editGroupId, groups]);
|
||||
|
||||
const [members, setMembers] = useState(group?.members || []);
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const filtered = useMemo(() => {
|
||||
return [
|
||||
...allMembers.filter((member) => {
|
||||
if (member.memberName.toLowerCase().includes(searchKey.toLowerCase())) return true;
|
||||
return false;
|
||||
})
|
||||
];
|
||||
}, [searchKey, allMembers]);
|
||||
|
||||
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
|
||||
async () => {
|
||||
if (!editGroupId || !members.length) return;
|
||||
return putUpdateGroup({
|
||||
groupId: editGroupId,
|
||||
memberList: members
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()])
|
||||
}
|
||||
);
|
||||
|
||||
const isSelected = (memberId: string) => {
|
||||
return members.find((item) => item.tmbId === memberId);
|
||||
};
|
||||
|
||||
const myRole = useMemo(() => {
|
||||
if (userInfo?.team.permission.hasManagePer) {
|
||||
return 'owner';
|
||||
}
|
||||
return members.find((item) => item.tmbId === userInfo?.team.tmbId)?.role ?? 'member';
|
||||
}, [members, userInfo]);
|
||||
|
||||
const handleToggleSelect = (memberId: string) => {
|
||||
if (
|
||||
myRole === 'owner' &&
|
||||
memberId === group?.members.find((item) => item.role === 'owner')?.tmbId
|
||||
) {
|
||||
toast({
|
||||
title: t('user:team.group.toast.can_not_delete_owner'),
|
||||
status: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
myRole === 'admin' &&
|
||||
group?.members.find((item) => String(item.tmbId) === memberId)?.role !== 'member'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelected(memberId)) {
|
||||
setMembers(members.filter((item) => item.tmbId !== memberId));
|
||||
} else {
|
||||
setMembers([...members, { tmbId: memberId, role: 'member' }]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleAdmin = (memberId: string) => {
|
||||
if (myRole === 'owner' && isSelected(memberId)) {
|
||||
const oldRole = members.find((item) => item.tmbId === memberId)?.role;
|
||||
if (oldRole === 'admin') {
|
||||
setMembers(
|
||||
members.map((item) => (item.tmbId === memberId ? { ...item, role: 'member' } : item))
|
||||
);
|
||||
} else {
|
||||
setMembers(
|
||||
members.map((item) => (item.tmbId === memberId ? { ...item, role: 'admin' } : item))
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = isLoadingUpdate;
|
||||
return (
|
||||
<MyModal
|
||||
onClose={onClose}
|
||||
title={t('user:team.group.manage_member')}
|
||||
iconSrc={group?.avatar ?? DEFAULT_TEAM_AVATAR}
|
||||
iconColor="primary.600"
|
||||
minW="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'}>
|
||||
{filtered.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
alignItems="center"
|
||||
key={member.tmbId}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'myGray.50',
|
||||
...(!isSelected(member.tmbId) ? { svg: { color: 'myGray.50' } } : {})
|
||||
}}
|
||||
_notLast={{ mb: 2 }}
|
||||
onClick={() => handleToggleSelect(member.tmbId)}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={!!isSelected(member.tmbId)}
|
||||
icon={<MyIcon name={'common/check'} w={'12px'} />}
|
||||
/>
|
||||
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
|
||||
<Box>{member.memberName}</Box>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}>
|
||||
<Box mt={2}>{t('common:chosen') + ': ' + members.length}</Box>
|
||||
<Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'400px'}>
|
||||
{members.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
onMouseEnter={() => setHoveredMemberId(member.tmbId)}
|
||||
onMouseLeave={() => setHoveredMemberId(undefined)}
|
||||
justifyContent="space-between"
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
key={member.tmbId + member.role}
|
||||
_hover={{ bg: 'myGray.50' }}
|
||||
_notLast={{ mb: 2 }}
|
||||
>
|
||||
<HStack>
|
||||
<Avatar
|
||||
src={allMembers.find((item) => item.tmbId === member.tmbId)?.avatar}
|
||||
w="1.5rem"
|
||||
borderRadius={'md'}
|
||||
/>
|
||||
<Box>
|
||||
{allMembers.find((item) => item.tmbId === member.tmbId)?.memberName}
|
||||
</Box>
|
||||
</HStack>
|
||||
<Box mr="auto">
|
||||
{(() => {
|
||||
if (member.role === 'owner') {
|
||||
return (
|
||||
<Tag ml={2} colorSchema="gray">
|
||||
{t('user:team.group.role.owner')}
|
||||
</Tag>
|
||||
);
|
||||
} else if (member.role === 'admin') {
|
||||
return (
|
||||
<Tag ml={2} mr="auto">
|
||||
{t('user:team.group.role.admin')}
|
||||
{myRole === 'owner' && (
|
||||
<MyIcon
|
||||
ml={1}
|
||||
name={'common/closeLight'}
|
||||
w={'1rem'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => handleToggleAdmin(member.tmbId)}
|
||||
/>
|
||||
)}
|
||||
</Tag>
|
||||
);
|
||||
} else if (member.role === 'member') {
|
||||
return (
|
||||
myRole === 'owner' &&
|
||||
hoveredMemberId === member.tmbId && (
|
||||
<Tag
|
||||
ml={2}
|
||||
colorSchema="yellow"
|
||||
cursor={'pointer'}
|
||||
onClick={() => handleToggleAdmin(member.tmbId)}
|
||||
>
|
||||
{t('user:team.group.set_as_admin')}
|
||||
</Tag>
|
||||
)
|
||||
);
|
||||
}
|
||||
})()}
|
||||
</Box>
|
||||
{(myRole === 'owner' || (myRole === 'admin' && member.role === 'member')) && (
|
||||
<MyIcon
|
||||
name={'common/closeLight'}
|
||||
w={'1rem'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => handleToggleSelect(member.tmbId)}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</ModalBody>
|
||||
<ModalFooter alignItems="flex-end">
|
||||
<Button isLoading={isLoading} onClick={onUpdate}>
|
||||
{t('common:common.Save')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default GroupEditModal;
|
||||
@@ -1,196 +0,0 @@
|
||||
import { putUpdateGroup } from '@/web/support/user/team/group/api';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
HStack,
|
||||
Input,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
useDisclosure,
|
||||
Checkbox
|
||||
} from '@chakra-ui/react';
|
||||
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { TeamContext } from '../context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
|
||||
export type ChangeOwnerModalProps = {
|
||||
groupId: string;
|
||||
};
|
||||
|
||||
export function ChangeOwnerModal({
|
||||
onClose,
|
||||
groupId
|
||||
}: ChangeOwnerModalProps & { onClose: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const [inputValue, setInputValue] = React.useState('');
|
||||
const { members: allMembers, groups, refetchGroups } = useContextSelector(TeamContext, (v) => v);
|
||||
const group = useMemo(() => {
|
||||
return groups.find((item) => item._id === groupId);
|
||||
}, [groupId, groups]);
|
||||
|
||||
const memberList = allMembers.filter((item) => {
|
||||
return item.memberName.toLowerCase().includes(inputValue.toLowerCase());
|
||||
});
|
||||
|
||||
const OldOwnerId = useMemo(() => {
|
||||
return group?.members.find((item) => item.role === 'owner')?.tmbId;
|
||||
}, [group]);
|
||||
|
||||
const [keepAdmin, setKeepAdmin] = useState(true);
|
||||
|
||||
const {
|
||||
isOpen: isOpenMemberListMenu,
|
||||
onClose: onCloseMemberListMenu,
|
||||
onOpen: onOpenMemberListMenu
|
||||
} = useDisclosure();
|
||||
|
||||
const [selectedMember, setSelectedMember] = useState<TeamMemberItemType | null>(null);
|
||||
|
||||
const onChangeOwner = async (tmbId: string) => {
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newMemberList = group.members
|
||||
.map((item) => {
|
||||
if (item.tmbId === OldOwnerId) {
|
||||
if (keepAdmin) {
|
||||
return { tmbId: OldOwnerId, role: 'admin' };
|
||||
}
|
||||
return { tmbId: OldOwnerId, role: 'member' };
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.filter((item) => item.tmbId !== tmbId) as any;
|
||||
|
||||
newMemberList.push({ tmbId, role: 'owner' });
|
||||
|
||||
return putUpdateGroup({
|
||||
groupId,
|
||||
memberList: newMemberList
|
||||
});
|
||||
};
|
||||
|
||||
const { runAsync, loading } = useRequest2(onChangeOwner, {
|
||||
onSuccess: () => Promise.all([onClose(), refetchGroups()]),
|
||||
successToast: t('common:permission.change_owner_success'),
|
||||
errorToast: t('common:permission.change_owner_failed')
|
||||
});
|
||||
|
||||
const onConfirm = async () => {
|
||||
if (!selectedMember) {
|
||||
return;
|
||||
}
|
||||
await runAsync(selectedMember.tmbId);
|
||||
};
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
iconSrc="modal/changePer"
|
||||
iconColor="primary.600"
|
||||
onClose={onClose}
|
||||
title={t('common:permission.change_owner')}
|
||||
isLoading={loading}
|
||||
>
|
||||
<ModalBody>
|
||||
<HStack>
|
||||
<Avatar src={group?.avatar} w={'1.75rem'} borderRadius={'md'} />
|
||||
<Box>{group?.name}</Box>
|
||||
</HStack>
|
||||
<Flex mt={4} justify="start" flexDirection="column">
|
||||
<Box fontSize="14px" fontWeight="500" color="myGray.900">
|
||||
{t('common:permission.change_owner_to')}
|
||||
</Box>
|
||||
<Flex mt="4" alignItems="center" position={'relative'}>
|
||||
{selectedMember && (
|
||||
<Avatar
|
||||
src={selectedMember.avatar}
|
||||
w={'20px'}
|
||||
borderRadius={'md'}
|
||||
position="absolute"
|
||||
left={3}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
placeholder={t('common:permission.change_owner_placeholder')}
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value);
|
||||
setSelectedMember(null);
|
||||
}}
|
||||
onFocus={() => {
|
||||
onOpenMemberListMenu();
|
||||
setSelectedMember(null);
|
||||
}}
|
||||
{...(selectedMember && { pl: '10' })}
|
||||
/>
|
||||
</Flex>
|
||||
{isOpenMemberListMenu && memberList.length > 0 && (
|
||||
<Flex
|
||||
mt={2}
|
||||
w={'100%'}
|
||||
flexDirection={'column'}
|
||||
gap={2}
|
||||
p={1}
|
||||
boxShadow="lg"
|
||||
bg="white"
|
||||
borderRadius="md"
|
||||
zIndex={10}
|
||||
maxH={'300px'}
|
||||
overflow={'auto'}
|
||||
>
|
||||
{memberList.map((item) => (
|
||||
<Box
|
||||
key={item.tmbId}
|
||||
p="2"
|
||||
_hover={{ bg: 'myGray.100' }}
|
||||
mx="1"
|
||||
borderRadius="md"
|
||||
cursor={'pointer'}
|
||||
onClickCapture={() => {
|
||||
setInputValue(item.memberName);
|
||||
setSelectedMember(item);
|
||||
onCloseMemberListMenu();
|
||||
}}
|
||||
>
|
||||
<Flex align="center">
|
||||
<Avatar src={item.avatar} w="1.25rem" />
|
||||
<Box ml="2">{item.memberName}</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Box mt="4">
|
||||
<Checkbox
|
||||
isChecked={keepAdmin}
|
||||
onChange={(e) => {
|
||||
setKeepAdmin(e.target.checked);
|
||||
}}
|
||||
>
|
||||
{t('account_team:retain_admin_permissions')}
|
||||
</Checkbox>
|
||||
</Box>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<HStack>
|
||||
<Button onClick={onClose} variant={'whiteBase'}>
|
||||
{t('common:common.Cancel')}
|
||||
</Button>
|
||||
<Button onClick={onConfirm}>{t('common:common.Confirm')}</Button>
|
||||
</HStack>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangeOwnerModal;
|
||||
@@ -1,274 +0,0 @@
|
||||
import AvatarGroup from '@fastgpt/web/components/common/Avatar/AvatarGroup';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
HStack,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { TeamContext } from '../context';
|
||||
import MyMenu, { MenuItemType } from '@fastgpt/web/components/common/MyMenu';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { deleteGroup } from '@/web/support/user/team/group/api';
|
||||
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
|
||||
import MemberTag from '../../../../../components/support/user/team/Info/MemberTag';
|
||||
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({ 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',
|
||||
content: t('account_team:confirm_delete_group')
|
||||
});
|
||||
|
||||
const { groups, refetchGroups, members, refetchMembers } = useContextSelector(
|
||||
TeamContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
const { runAsync: delDeleteGroup } = useRequest2(deleteGroup, {
|
||||
onSuccess: () => {
|
||||
refetchGroups();
|
||||
refetchMembers();
|
||||
}
|
||||
});
|
||||
|
||||
const hasGroupManagePer = (group: (typeof groups)[0]) =>
|
||||
userInfo?.team.permission.hasManagePer ||
|
||||
['admin', 'owner'].includes(
|
||||
group.members.find((item) => item.tmbId === userInfo?.team.tmbId)?.role ?? ''
|
||||
);
|
||||
|
||||
const isGroupOwner = (group: (typeof groups)[0]) =>
|
||||
userInfo?.team.permission.hasManagePer ||
|
||||
group.members.find((item) => item.role === 'owner')?.tmbId === userInfo?.team.tmbId;
|
||||
|
||||
const {
|
||||
isOpen: isOpenChangeOwner,
|
||||
onOpen: onOpenChangeOwner,
|
||||
onClose: onCloseChangeOwner
|
||||
} = useDisclosure();
|
||||
const onChangeOwner = (groupId: string) => {
|
||||
setEditGroupId(groupId);
|
||||
onOpenChangeOwner();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<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
|
||||
? 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={<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
|
||||
},
|
||||
{
|
||||
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} />
|
||||
)}
|
||||
{isOpenGroupInfo && (
|
||||
<GroupInfoModal
|
||||
onClose={() => {
|
||||
onCloseGroupInfo();
|
||||
setEditGroupId(undefined);
|
||||
}}
|
||||
editGroupId={editGroupId}
|
||||
/>
|
||||
)}
|
||||
{isOpenManageGroupMember && (
|
||||
<ManageGroupMemberModal
|
||||
onClose={() => {
|
||||
onCloseManageGroupMember();
|
||||
setEditGroupId(undefined);
|
||||
}}
|
||||
editGroupId={editGroupId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MemberTable;
|
||||
@@ -1,90 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ModalCloseButton, ModalBody, Box, ModalFooter, Button } from '@chakra-ui/react';
|
||||
import TagTextarea from '@/components/common/Textarea/TagTextarea';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { postInviteTeamMember } from '@/web/support/user/team/api';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import type { InviteMemberResponse } from '@fastgpt/global/support/user/team/controller.d';
|
||||
|
||||
const InviteModal = ({
|
||||
teamId,
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
teamId: string;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { ConfirmModal, openConfirm } = useConfirm({
|
||||
title: t('user:team.Invite Member Result Tip'),
|
||||
showCancel: false
|
||||
});
|
||||
|
||||
const [inviteUsernames, setInviteUsernames] = useState<string[]>([]);
|
||||
|
||||
const { runAsync: onInvite, loading: isLoading } = useRequest2(
|
||||
() =>
|
||||
postInviteTeamMember({
|
||||
teamId,
|
||||
usernames: inviteUsernames
|
||||
}),
|
||||
{
|
||||
onSuccess(res: InviteMemberResponse) {
|
||||
onSuccess();
|
||||
openConfirm(
|
||||
() => onClose(),
|
||||
undefined,
|
||||
<Box whiteSpace={'pre-wrap'}>
|
||||
{t('user:team.Invite Member Success Tip', {
|
||||
success: res.invite.length,
|
||||
inValid: res.inValid.map((item) => item.username).join(', '),
|
||||
inTeam: res.inTeam.map((item) => item.username).join(', ')
|
||||
})}
|
||||
</Box>
|
||||
)();
|
||||
},
|
||||
errorToast: t('user:team.Invite Member Failed Tip')
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
iconSrc="common/inviteLight"
|
||||
iconColor="primary.600"
|
||||
title={
|
||||
<Box>
|
||||
<Box>{t('common:user.team.Invite Member')}</Box>
|
||||
<Box color={'myGray.500'} fontSize={'xs'} fontWeight={'normal'}>
|
||||
{t('common:user.team.Invite Member Tips')}
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
maxW={['90vw', '400px']}
|
||||
overflow={'unset'}
|
||||
>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
<ModalBody>
|
||||
<Box mb={2}>{t('common:user.Account')}</Box>
|
||||
<TagTextarea defaultValues={inviteUsernames} onUpdate={setInviteUsernames} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
w={'100%'}
|
||||
h={'34px'}
|
||||
isDisabled={inviteUsernames.length === 0}
|
||||
isLoading={isLoading}
|
||||
onClick={onInvite}
|
||||
>
|
||||
{t('user:team.Confirm Invite')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
<ConfirmModal />
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default InviteModal;
|
||||
@@ -1,225 +0,0 @@
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
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 { 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';
|
||||
|
||||
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 { 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 (
|
||||
<>
|
||||
<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>
|
||||
|
||||
<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} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MemberTable;
|
||||
@@ -1,31 +0,0 @@
|
||||
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;
|
||||
@@ -1,162 +0,0 @@
|
||||
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;
|
||||
@@ -1,197 +0,0 @@
|
||||
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;
|
||||
@@ -1,74 +0,0 @@
|
||||
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;
|
||||
@@ -1,103 +0,0 @@
|
||||
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;
|
||||
@@ -1,354 +0,0 @@
|
||||
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,430 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
HStack,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Text,
|
||||
Tr,
|
||||
Flex,
|
||||
Button
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import {
|
||||
deleteMemberPermission,
|
||||
getTeamClbs,
|
||||
updateMemberPermission
|
||||
} from '@/web/support/user/team/api';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
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 { 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({
|
||||
Tabs,
|
||||
onOpenAddMember
|
||||
}: {
|
||||
Tabs: React.ReactNode;
|
||||
onOpenAddMember: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
|
||||
const collaboratorList = useContextSelector(
|
||||
CollaboratorContext,
|
||||
(state) => state.collaboratorList
|
||||
);
|
||||
const onUpdateCollaborators = useContextSelector(
|
||||
CollaboratorContext,
|
||||
(state) => state.onUpdateCollaborators
|
||||
);
|
||||
const onDelOneCollaborator = useContextSelector(
|
||||
CollaboratorContext,
|
||||
(state) => state.onDelOneCollaborator
|
||||
);
|
||||
|
||||
const [isExpandMember, setExpandMember] = useToggle(true);
|
||||
const [isExpandGroup, setExpandGroup] = useToggle(true);
|
||||
const [isExpandOrg, setExpandOrg] = useToggle(true);
|
||||
|
||||
const { tmbList, groupList, orgList } = useMemo(() => {
|
||||
const tmbList: CollaboratorItemType[] = [];
|
||||
const groupList: CollaboratorItemType[] = [];
|
||||
const orgList: CollaboratorItemType[] = [];
|
||||
|
||||
collaboratorList.forEach((item) => {
|
||||
if (item.tmbId) {
|
||||
tmbList.push(item);
|
||||
} else if (item.groupId) {
|
||||
groupList.push(item);
|
||||
} else if (item.orgId) {
|
||||
orgList.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
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: 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 (
|
||||
<>
|
||||
<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 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;
|
||||
@@ -1,266 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Box, Checkbox, Flex, Grid, HStack } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
import { RequireAtLeastOne } from '@fastgpt/global/common/type/utils';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
|
||||
type memberType = {
|
||||
type: 'member';
|
||||
tmbId: string;
|
||||
memberName: string;
|
||||
avatar: string;
|
||||
};
|
||||
|
||||
type groupType = {
|
||||
type: 'group';
|
||||
_id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
};
|
||||
|
||||
type selectedType = {
|
||||
member: string[];
|
||||
group: string[];
|
||||
};
|
||||
|
||||
function SelectMember({
|
||||
allMembers,
|
||||
selected = { member: [], group: [] },
|
||||
setSelected
|
||||
// mode = 'both'
|
||||
}: {
|
||||
allMembers: {
|
||||
member: memberType[];
|
||||
group: groupType[];
|
||||
};
|
||||
selected?: selectedType;
|
||||
setSelected: React.Dispatch<React.SetStateAction<selectedType>>;
|
||||
mode?: 'member' | 'group' | 'both';
|
||||
}) {
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return [
|
||||
...allMembers.member.filter((member) => {
|
||||
if (member.memberName.toLowerCase().includes(searchKey.toLowerCase())) return true;
|
||||
return false;
|
||||
}),
|
||||
...allMembers.group.filter((member) => {
|
||||
if (member.name.toLowerCase().includes(searchKey.toLowerCase())) return true;
|
||||
return false;
|
||||
})
|
||||
];
|
||||
}, [searchKey, allMembers]);
|
||||
|
||||
const selectedFlated = useMemo(() => {
|
||||
return [
|
||||
...allMembers.member.filter((member) => {
|
||||
return selected.member?.includes(member.tmbId);
|
||||
}),
|
||||
...allMembers.group.filter((member) => {
|
||||
return selected.group?.includes(member._id);
|
||||
})
|
||||
];
|
||||
}, [selected, allMembers]);
|
||||
|
||||
const handleToggleSelect = (member: memberType | groupType) => {
|
||||
if (member.type == 'member') {
|
||||
if (selected.member?.indexOf(member.tmbId) == -1) {
|
||||
setSelected({
|
||||
member: [...selected.member, member.tmbId],
|
||||
group: [...selected.group]
|
||||
});
|
||||
} else {
|
||||
setSelected({
|
||||
member: [...selected.member.filter((item) => item != member.tmbId)],
|
||||
group: [...selected.group]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (selected.group?.indexOf(member._id) == -1) {
|
||||
setSelected({ member: [...selected.member], group: [...selected.group, member._id] });
|
||||
} else {
|
||||
setSelected({
|
||||
member: [...selected.member],
|
||||
group: [...selected.group.filter((item) => item != member._id)]
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isSelected = (member: memberType | groupType) => {
|
||||
if (member.type == 'member') {
|
||||
return selected.member?.includes(member.tmbId);
|
||||
} else {
|
||||
return selected.group?.includes(member._id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid
|
||||
templateColumns="1fr 1fr"
|
||||
borderRadius="8px"
|
||||
border="1px solid"
|
||||
borderColor="myGray.200"
|
||||
h={'100%'}
|
||||
>
|
||||
<Flex flexDirection="column" p="4" h={'100%'} overflow={'auto'}>
|
||||
<SearchInput
|
||||
placeholder={t('user:search_user')}
|
||||
fontSize="sm"
|
||||
bg={'myGray.50'}
|
||||
onChange={(e) => {
|
||||
setSearchKey(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Flex flexDirection="column" mt={3}>
|
||||
{filtered.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
alignItems="center"
|
||||
key={member.type == 'member' ? member.tmbId : member._id}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'myGray.50',
|
||||
...(!isSelected(member) ? { svg: { color: 'myGray.50' } } : {})
|
||||
}}
|
||||
_notLast={{ mb: 2 }}
|
||||
onClick={() => handleToggleSelect(member)}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={!!isSelected(member)}
|
||||
icon={<MyIcon name={'common/check'} w={'12px'} />}
|
||||
/>
|
||||
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
|
||||
<Box>
|
||||
{member.type == 'member'
|
||||
? member.memberName
|
||||
: member.name === DefaultGroupName
|
||||
? userInfo?.team.teamName
|
||||
: member.name}
|
||||
</Box>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex
|
||||
borderLeft="1px"
|
||||
borderColor="myGray.200"
|
||||
flexDirection="column"
|
||||
p="4"
|
||||
h={'100%'}
|
||||
overflow={'auto'}
|
||||
>
|
||||
<Box mt={3}>
|
||||
{t('common:chosen') + ': ' + Number(selected.member.length + selected.group.length)}{' '}
|
||||
</Box>
|
||||
<Box mt={5}>
|
||||
{selectedFlated.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
justifyContent="space-between"
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
key={member.type == 'member' ? member.tmbId : member._id}
|
||||
_hover={{ bg: 'myGray.50' }}
|
||||
_notLast={{ mb: 2 }}
|
||||
>
|
||||
<Avatar src={member.avatar} w="1.5rem" borderRadius={'md'} />
|
||||
<Box w="full">
|
||||
{member.type == 'member'
|
||||
? member.memberName
|
||||
: member.name === DefaultGroupName
|
||||
? userInfo?.team.teamName
|
||||
: member.name}
|
||||
</Box>
|
||||
<MyIcon
|
||||
name={'common/closeLight'}
|
||||
w={'1rem'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => handleToggleSelect(member)}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
// This function is for using with react-hook-form
|
||||
function ControllerWrapper({
|
||||
control,
|
||||
allMembers,
|
||||
mode = 'both',
|
||||
name = 'members'
|
||||
}: {
|
||||
control: Control;
|
||||
allMembers: RequireAtLeastOne<{ member?: memberType[]; group?: groupType[] }>;
|
||||
mode?: 'member' | 'group' | 'both';
|
||||
name?: string;
|
||||
}) {
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field: { value: selected, onChange } }) => (
|
||||
<SelectMember
|
||||
mode={mode}
|
||||
allMembers={
|
||||
(() => {
|
||||
switch (mode) {
|
||||
case 'member':
|
||||
return { member: allMembers.member, group: [] };
|
||||
case 'group':
|
||||
return { member: [], group: allMembers.group };
|
||||
case 'both':
|
||||
return { member: allMembers.member, group: allMembers.group };
|
||||
}
|
||||
})() as Required<typeof allMembers>
|
||||
}
|
||||
selected={(() => {
|
||||
switch (mode) {
|
||||
case 'member':
|
||||
return { member: selected, group: [] };
|
||||
case 'group':
|
||||
return { member: [], group: selected };
|
||||
case 'both':
|
||||
return { member: selected.member, group: selected.group };
|
||||
}
|
||||
})()}
|
||||
setSelected={
|
||||
(({ member, group }: selectedType, _prevState: selectedType) => {
|
||||
switch (mode) {
|
||||
case 'member':
|
||||
onChange(member);
|
||||
return;
|
||||
case 'group':
|
||||
onChange(group);
|
||||
return;
|
||||
case 'both':
|
||||
onChange({ member, group });
|
||||
return;
|
||||
}
|
||||
}) as any // hack: we do not need to handle prevState
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const UnControlledSelectMember = SelectMember;
|
||||
export default ControllerWrapper;
|
||||
@@ -1,142 +0,0 @@
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { createContext } from 'use-context-selector';
|
||||
import type { EditTeamFormDataType } from './EditInfoModal';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { getTeamList, putSwitchTeam } from '@/web/support/user/team/api';
|
||||
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import type { TeamTmbItemType, TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { getGroupList } from '@/web/support/user/team/group/api';
|
||||
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
import { OrgType } from '@fastgpt/global/support/user/team/org/type';
|
||||
|
||||
const EditInfoModal = dynamic(() => import('./EditInfoModal'));
|
||||
|
||||
type TeamModalContextType = {
|
||||
myTeams: TeamTmbItemType[];
|
||||
members: TeamMemberItemType[];
|
||||
groups: MemberGroupListType;
|
||||
isLoading: boolean;
|
||||
onSwitchTeam: (teamId: string) => void;
|
||||
setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>;
|
||||
|
||||
refetchMembers: () => void;
|
||||
refetchTeams: () => void;
|
||||
refetchGroups: () => void;
|
||||
teamSize: number;
|
||||
};
|
||||
|
||||
export const TeamContext = createContext<TeamModalContextType>({
|
||||
myTeams: [],
|
||||
groups: [],
|
||||
members: [],
|
||||
isLoading: false,
|
||||
onSwitchTeam: function (_teamId: string): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
setEditTeamData: function (_value: React.SetStateAction<EditTeamFormDataType | undefined>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
refetchTeams: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
refetchMembers: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
refetchGroups: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
|
||||
teamSize: 0
|
||||
});
|
||||
|
||||
export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { t } = useTranslation();
|
||||
const [editTeamData, setEditTeamData] = useState<EditTeamFormDataType>();
|
||||
const { userInfo, initUserInfo, loadAndGetTeamMembers } = useUserStore();
|
||||
|
||||
const {
|
||||
data: myTeams = [],
|
||||
loading: isLoadingTeams,
|
||||
refresh: refetchTeams
|
||||
} = useRequest2(() => getTeamList(TeamMemberStatusEnum.active), {
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?._id]
|
||||
});
|
||||
|
||||
// member action
|
||||
const {
|
||||
data: members = [],
|
||||
runAsync: refetchMembers,
|
||||
loading: loadingMembers
|
||||
} = useRequest2(
|
||||
() => {
|
||||
if (!userInfo?.team?.teamId) return Promise.resolve([]);
|
||||
return loadAndGetTeamMembers(true);
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?.team?.teamId]
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2(
|
||||
async (teamId: string) => {
|
||||
await putSwitchTeam(teamId);
|
||||
return initUserInfo();
|
||||
},
|
||||
{
|
||||
errorToast: t('common:user.team.Switch Team Failed')
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
data: groups = [],
|
||||
loading: isLoadingGroups,
|
||||
refresh: refetchGroups
|
||||
} = useRequest2(getGroupList, {
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?.team?.teamId]
|
||||
});
|
||||
|
||||
const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers || isLoadingGroups;
|
||||
|
||||
const contextValue = {
|
||||
myTeams,
|
||||
refetchTeams,
|
||||
isLoading,
|
||||
onSwitchTeam,
|
||||
|
||||
// create | update team
|
||||
setEditTeamData,
|
||||
members,
|
||||
refetchMembers,
|
||||
groups,
|
||||
refetchGroups,
|
||||
teamSize: members.length
|
||||
};
|
||||
|
||||
return (
|
||||
<TeamContext.Provider value={contextValue}>
|
||||
{userInfo?.team?.permission && (
|
||||
<>
|
||||
{children}
|
||||
{!!editTeamData && (
|
||||
<EditInfoModal
|
||||
defaultData={editTeamData}
|
||||
onClose={() => setEditTeamData(undefined)}
|
||||
onSuccess={() => {
|
||||
refetchTeams();
|
||||
initUserInfo();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TeamContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamModalContextProvider;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
|
||||
import AccountContainer from '../components/AccountContainer';
|
||||
import { Box, Flex, useDisclosure } from '@chakra-ui/react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import Icon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import TeamSelector from '../components/TeamSelector';
|
||||
@@ -11,14 +11,15 @@ import { useRouter } from 'next/router';
|
||||
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
import { TeamContext, TeamModalContextProvider } from './components/context';
|
||||
import { TeamContext, TeamModalContextProvider } from '@/pageComponents/account/team/context';
|
||||
import dynamic from 'next/dynamic';
|
||||
import MemberTable from './components/MemberTable';
|
||||
|
||||
const PermissionManage = dynamic(() => import('./components/PermissionManage/index'));
|
||||
const GroupManage = dynamic(() => import('./components/GroupManage/index'));
|
||||
|
||||
const OrgManage = dynamic(() => import('./components/OrgManage/index'));
|
||||
const MemberTable = dynamic(() => import('@/pageComponents/account/team/MemberTable'));
|
||||
const PermissionManage = dynamic(
|
||||
() => import('@/pageComponents/account/team/PermissionManage/index')
|
||||
);
|
||||
const GroupManage = dynamic(() => import('@/pageComponents/account/team/GroupManage/index'));
|
||||
const OrgManage = dynamic(() => import('@/pageComponents/account/team/OrgManage/index'));
|
||||
|
||||
export enum TeamTabEnum {
|
||||
member = 'member',
|
||||
@@ -34,7 +35,7 @@ const Team = () => {
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
|
||||
const { setEditTeamData, teamSize, isLoading } = useContextSelector(TeamContext, (v) => v);
|
||||
const { setEditTeamData, isLoading, teamSize } = useContextSelector(TeamContext, (v) => v);
|
||||
|
||||
const Tabs = useMemo(
|
||||
() => (
|
||||
@@ -62,72 +63,81 @@ const Team = () => {
|
||||
|
||||
return (
|
||||
<AccountContainer isLoading={isLoading}>
|
||||
{/* header */}
|
||||
<Flex
|
||||
w={'100%'}
|
||||
h={'3.5rem'}
|
||||
px={'1.56rem'}
|
||||
py={'0.56rem'}
|
||||
borderBottom={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
bg={'myGray.25'}
|
||||
align={'center'}
|
||||
gap={6}
|
||||
justify={'space-between'}
|
||||
>
|
||||
<Flex align={'center'}>
|
||||
<Flex gap={2} color={'myGray.900'}>
|
||||
<Icon name="support/user/usersLight" w={'1.25rem'} h={'1.25rem'} />
|
||||
<Box fontWeight={'500'} fontSize={'1rem'}>
|
||||
{t('account:team')}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex align={'center'} ml={6}>
|
||||
<TeamSelector height={'28px'} />
|
||||
</Flex>
|
||||
{userInfo?.team?.role === TeamMemberRoleEnum.owner && (
|
||||
<Flex align={'center'} justify={'center'} ml={2} p={'0.44rem'}>
|
||||
<MyIcon
|
||||
name="edit"
|
||||
w="18px"
|
||||
cursor="pointer"
|
||||
_hover={{
|
||||
color: 'primary.500'
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!userInfo?.team) return;
|
||||
setEditTeamData({
|
||||
id: userInfo.team.teamId,
|
||||
name: userInfo.team.teamName,
|
||||
avatar: userInfo.team.avatar
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Flex h={'100%'} flexDirection={'column'}>
|
||||
{/* header */}
|
||||
<Flex
|
||||
w={'100%'}
|
||||
h={'3.5rem'}
|
||||
px={'1.56rem'}
|
||||
py={'0.56rem'}
|
||||
borderBottom={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
bg={'myGray.25'}
|
||||
align={'center'}
|
||||
gap={6}
|
||||
justify={'space-between'}
|
||||
>
|
||||
<Flex align={'center'}>
|
||||
<Flex gap={2} color={'myGray.900'}>
|
||||
<Icon name="support/user/usersLight" w={'1.25rem'} h={'1.25rem'} />
|
||||
<Box fontWeight={'500'} fontSize={'1rem'}>
|
||||
{t('account:team')}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
<Flex align={'center'} ml={6}>
|
||||
<TeamSelector height={'28px'} />
|
||||
</Flex>
|
||||
{userInfo?.team?.role === TeamMemberRoleEnum.owner && (
|
||||
<Flex align={'center'} justify={'center'} ml={2} p={'0.44rem'}>
|
||||
<MyIcon
|
||||
name="edit"
|
||||
w="18px"
|
||||
cursor="pointer"
|
||||
_hover={{
|
||||
color: 'primary.500'
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!userInfo?.team) return;
|
||||
setEditTeamData({
|
||||
id: userInfo.team.teamId,
|
||||
name: userInfo.team.teamName,
|
||||
avatar: userInfo.team.avatar
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Box
|
||||
float={'right'}
|
||||
color={'myGray.900'}
|
||||
h={'1.25rem'}
|
||||
px={'0.5rem'}
|
||||
py={'0.125rem'}
|
||||
fontSize={'0.75rem'}
|
||||
borderRadius={'1.25rem'}
|
||||
bg={'myGray.150'}
|
||||
>
|
||||
{t('account_team:total_team_members', { amount: teamSize })}
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{/* table */}
|
||||
<Box
|
||||
float={'right'}
|
||||
color={'myGray.900'}
|
||||
h={'1.25rem'}
|
||||
px={'0.5rem'}
|
||||
py={'0.125rem'}
|
||||
fontSize={'0.75rem'}
|
||||
borderRadius={'1.25rem'}
|
||||
bg={'myGray.150'}
|
||||
py={'1.5rem'}
|
||||
px={'2rem'}
|
||||
flex={'1 0 0'}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
overflow={'auto'}
|
||||
>
|
||||
{t('account_team:total_team_members', { amount: teamSize })}
|
||||
{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>
|
||||
</Flex>
|
||||
|
||||
{/* table */}
|
||||
<Box py={'1.5rem'} px={'2rem'}>
|
||||
{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>
|
||||
</AccountContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,15 +23,16 @@ import DateRangePicker, {
|
||||
import { addDays } from 'date-fns';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { formatNumber } from '@fastgpt/global/common/math/tools';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import AccountContainer, { TabEnum } from '../components/AccountContainer';
|
||||
import AccountContainer from '../components/AccountContainer';
|
||||
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import { getTeamMembers } from '@/web/support/user/team/api';
|
||||
|
||||
const UsageDetail = dynamic(() => import('./UsageDetail'));
|
||||
|
||||
@@ -44,7 +45,7 @@ const UsageTable = () => {
|
||||
});
|
||||
const [usageSource, setUsageSource] = useState<UsageSourceEnum | ''>('');
|
||||
const { isPc } = useSystem();
|
||||
const { userInfo, loadAndGetTeamMembers } = useUserStore();
|
||||
const { userInfo } = useUserStore();
|
||||
const [usageDetail, setUsageDetail] = useState<UsageItemType>();
|
||||
|
||||
const sourceList = useMemo(
|
||||
@@ -63,10 +64,7 @@ const UsageTable = () => {
|
||||
);
|
||||
|
||||
const [selectTmbId, setSelectTmbId] = useState(userInfo?.team?.tmbId);
|
||||
const { data: members = [] } = useQuery(['getMembers', userInfo?.team?.teamId], () => {
|
||||
if (!userInfo?.team?.teamId) return [];
|
||||
return loadAndGetTeamMembers();
|
||||
});
|
||||
const { data: members, ScrollData } = useScrollPagination(getTeamMembers, {});
|
||||
const tmbList = useMemo(
|
||||
() =>
|
||||
members.map((item) => ({
|
||||
@@ -86,14 +84,13 @@ const UsageTable = () => {
|
||||
isLoading,
|
||||
Pagination,
|
||||
getData
|
||||
} = usePagination<UsageItemType>({
|
||||
api: getUserUsages,
|
||||
} = usePagination(getUserUsages, {
|
||||
pageSize: isPc ? 20 : 10,
|
||||
params: {
|
||||
dateStart: dateRange.from || new Date(),
|
||||
dateEnd: addDays(dateRange.to || new Date(), 1),
|
||||
source: usageSource,
|
||||
teamMemberId: selectTmbId
|
||||
source: usageSource as UsageSourceEnum,
|
||||
teamMemberId: selectTmbId ?? ''
|
||||
},
|
||||
defaultRequest: false
|
||||
});
|
||||
@@ -120,6 +117,7 @@ const UsageTable = () => {
|
||||
<MySelect
|
||||
size={'sm'}
|
||||
minW={'100px'}
|
||||
ScrollData={ScrollData}
|
||||
list={tmbList}
|
||||
value={selectTmbId}
|
||||
onchange={setSelectTmbId}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { jiebaSplit } from '@fastgpt/service/common/string/jieba';
|
||||
import { MongoDatasetDataText } from '@fastgpt/service/core/dataset/data/dataTextSchema';
|
||||
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { MongoUser } from '@fastgpt/service/support/user/schema';
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
/*
|
||||
@@ -14,6 +16,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
||||
2. 执行升级脚本,不要删除 MongoDatasetData 里的数据。
|
||||
3. 切换正式版镜像,让 MongoDatasetDataText 生效。
|
||||
4. 删除 MongoDatasetData 里的索引和多余字段。(4819 再删
|
||||
5. 移动 User 表中的 avatar 字段到 TeamMember 表中。
|
||||
*/
|
||||
let success = 0;
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -109,15 +112,26 @@ const initData = async (batchSize: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
const batchUpdateFields = async (batchSize = 2000) => {
|
||||
// Update in batches
|
||||
await MongoDatasetData.updateMany(
|
||||
{ initFullText: { $exists: true } },
|
||||
{
|
||||
$unset: {
|
||||
initFullText: 1,
|
||||
fullTextToken: 1
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
// const batchUpdateFields = async (batchSize = 2000) => {
|
||||
// // Find documents that still have these fields
|
||||
// const documents = await MongoDatasetData.find({ initFullText: { $exists: true } }, '_id')
|
||||
// .limit(batchSize)
|
||||
// .lean();
|
||||
|
||||
// if (documents.length === 0) return;
|
||||
|
||||
// // Update in batches
|
||||
// await MongoDatasetData.updateMany(
|
||||
// { _id: { $in: documents.map((doc) => doc._id) } },
|
||||
// {
|
||||
// $unset: {
|
||||
// initFullText: 1
|
||||
// // fullTextToken: 1
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
|
||||
// success += documents.length;
|
||||
// console.log('Delete success:', success);
|
||||
// await batchUpdateFields(batchSize);
|
||||
// };
|
||||
|
||||
55
projects/app/src/pages/api/admin/initv4819.ts
Normal file
55
projects/app/src/pages/api/admin/initv4819.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { MongoUser } from '@fastgpt/service/support/user/schema';
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
/*
|
||||
简单版迁移:直接升级到最新镜像,会去除 MongoDatasetData 里的索引。直接执行这个脚本。
|
||||
无缝迁移:
|
||||
1. 移动 User 表中的 avatar 字段到 TeamMember 表中。
|
||||
*/
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
await authCert({ req, authRoot: true });
|
||||
await moveUserAvatar();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
const moveUserAvatar = async () => {
|
||||
try {
|
||||
const users = await MongoUser.find({}, '_id avatar');
|
||||
console.log('Total users:', users.length);
|
||||
let success = 0;
|
||||
for await (const user of users) {
|
||||
// @ts-ignore
|
||||
if (!user.avatar) continue;
|
||||
try {
|
||||
await mongoSessionRun(async (session) => {
|
||||
await MongoTeamMember.updateOne(
|
||||
{
|
||||
userId: user._id
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
avatar: (user as any).avatar // 删除 avatar 字段, 因为 Type 改了,所以这里不能直接写 user.avatar
|
||||
}
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
// @ts-ignore
|
||||
user.avatar = undefined;
|
||||
await user.save({ session });
|
||||
});
|
||||
success++;
|
||||
console.log('Move avatar success:', success);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
@@ -8,7 +8,8 @@ async function handler(req: ApiRequestProps<{}, { bufferId?: string }>, res: Nex
|
||||
// If bufferId is the same as the current bufferId, return directly
|
||||
if (bufferId && global.systemInitBufferId && global.systemInitBufferId === bufferId) {
|
||||
return {
|
||||
bufferId: global.systemInitBufferId
|
||||
bufferId: global.systemInitBufferId,
|
||||
systemVersion: global.systemVersion || '0.0.0'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import type { PagingData } from '@/types';
|
||||
import { AppLogsListItemType } from '@/types/app';
|
||||
import { Types } from '@fastgpt/service/common/mongo';
|
||||
import { addDays } from 'date-fns';
|
||||
@@ -10,19 +9,22 @@ import { ChatItemCollectionName } from '@fastgpt/service/core/chat/chatItemSchem
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
|
||||
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
|
||||
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import { addSourceMember } from '@fastgpt/service/support/user/utils';
|
||||
|
||||
async function handler(
|
||||
req: NextApiRequest,
|
||||
_res: NextApiResponse
|
||||
): Promise<PagingData<AppLogsListItemType>> {
|
||||
): Promise<PaginationResponse<AppLogsListItemType>> {
|
||||
const {
|
||||
pageNum = 1,
|
||||
pageSize = 20,
|
||||
appId,
|
||||
dateStart = addDays(new Date(), -7),
|
||||
dateEnd = new Date()
|
||||
} = req.body as GetAppChatLogsParams;
|
||||
|
||||
const { pageSize = 20, offset } = parsePaginationRequest(req);
|
||||
|
||||
if (!appId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
@@ -39,7 +41,7 @@ async function handler(
|
||||
}
|
||||
};
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
const [list, total] = await Promise.all([
|
||||
MongoChat.aggregate(
|
||||
[
|
||||
{ $match: where },
|
||||
@@ -51,7 +53,7 @@ async function handler(
|
||||
updateTime: -1
|
||||
}
|
||||
},
|
||||
{ $skip: (pageNum - 1) * pageSize },
|
||||
{ $skip: offset },
|
||||
{ $limit: pageSize },
|
||||
{
|
||||
$lookup: {
|
||||
@@ -144,10 +146,14 @@ async function handler(
|
||||
MongoChat.countDocuments(where, { ...readFromSecondary })
|
||||
]);
|
||||
|
||||
const listWithSourceMember = await addSourceMember({
|
||||
list: list
|
||||
});
|
||||
|
||||
const listWithoutTmbId = list.filter((item) => !item.tmbId);
|
||||
|
||||
return {
|
||||
pageNum,
|
||||
pageSize,
|
||||
data,
|
||||
list: listWithSourceMember.concat(listWithoutTmbId),
|
||||
total
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { replaceRegChars } from '@fastgpt/global/common/string/tools';
|
||||
import { concatPer } from '@fastgpt/service/support/permission/controller';
|
||||
import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers';
|
||||
import { getOrgIdSetWithParentByTmbId } from '@fastgpt/service/support/permission/org/controllers';
|
||||
import { addSourceMember } from '@fastgpt/service/support/user/utils';
|
||||
|
||||
export type ListAppBody = {
|
||||
parentId?: ParentIdType;
|
||||
@@ -201,19 +202,9 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
|
||||
})
|
||||
.filter((app) => app.permission.hasReadPer);
|
||||
|
||||
return formatApps.map((app) => ({
|
||||
_id: app._id,
|
||||
tmbId: app.tmbId,
|
||||
avatar: app.avatar,
|
||||
type: app.type,
|
||||
name: app.name,
|
||||
intro: app.intro,
|
||||
updateTime: app.updateTime,
|
||||
permission: app.permission,
|
||||
pluginData: app.pluginData,
|
||||
inheritPermission: app.inheritPermission ?? true,
|
||||
private: app.privateApp
|
||||
}));
|
||||
return addSourceMember({
|
||||
list: formatApps
|
||||
});
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
@@ -6,6 +6,8 @@ import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { VersionListItemType } from '@fastgpt/global/core/app/version';
|
||||
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
|
||||
import { addSourceMember } from '@fastgpt/service/support/user/utils';
|
||||
|
||||
export type versionListBody = PaginationProps<{
|
||||
appId: string;
|
||||
@@ -15,41 +17,40 @@ export type versionListResponse = PaginationResponse<VersionListItemType>;
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<versionListBody>,
|
||||
res: NextApiResponse<any>
|
||||
_res: NextApiResponse<any>
|
||||
): Promise<versionListResponse> {
|
||||
const { offset, pageSize, appId } = req.body;
|
||||
const { appId } = req.body;
|
||||
const { offset, pageSize } = parsePaginationRequest(req);
|
||||
|
||||
await authApp({ appId, req, per: WritePermissionVal, authToken: true });
|
||||
|
||||
const [result, total] = await Promise.all([
|
||||
MongoAppVersion.find(
|
||||
{
|
||||
(async () => {
|
||||
const versions = await MongoAppVersion.find({
|
||||
appId
|
||||
},
|
||||
'_id appId versionName time isPublish tmbId'
|
||||
)
|
||||
.sort({
|
||||
time: -1
|
||||
})
|
||||
.skip(offset)
|
||||
.limit(pageSize),
|
||||
.sort({
|
||||
time: -1
|
||||
})
|
||||
.skip(offset)
|
||||
.limit(pageSize)
|
||||
.lean();
|
||||
|
||||
return addSourceMember({
|
||||
list: versions
|
||||
}).then((list) =>
|
||||
list.map((item) => ({
|
||||
...item,
|
||||
isPublish: !!item.isPublish
|
||||
}))
|
||||
);
|
||||
})(),
|
||||
MongoAppVersion.countDocuments({ appId })
|
||||
]);
|
||||
|
||||
const versionList = result.map((item) => {
|
||||
return {
|
||||
_id: item._id,
|
||||
appId: item.appId,
|
||||
versionName: item.versionName,
|
||||
time: item.time,
|
||||
isPublish: item.isPublish,
|
||||
tmbId: item.tmbId
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
total,
|
||||
list: versionList
|
||||
list: result
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ describe('发布应用版本测试', () => {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
chatConfig: {},
|
||||
type: AppTypeEnum.simple,
|
||||
isPublish: false,
|
||||
versionName: '1'
|
||||
};
|
||||
|
||||
@@ -164,6 +164,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
runningAppInfo: {
|
||||
id: appId,
|
||||
teamId: app.teamId,
|
||||
tmbId: app.tmbId
|
||||
},
|
||||
runningUserInfo: {
|
||||
teamId,
|
||||
tmbId
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import { GetHistoriesProps } from '@/global/core/chat/api';
|
||||
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
|
||||
import { addMonths } from 'date-fns';
|
||||
|
||||
export type getHistoriesQuery = {};
|
||||
@@ -17,9 +18,10 @@ export type getHistoriesResponse = {};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<getHistoriesBody, getHistoriesQuery>,
|
||||
res: ApiResponseType<any>
|
||||
_res: ApiResponseType<any>
|
||||
): Promise<PaginationResponse<getHistoriesResponse>> {
|
||||
const { appId, shareId, outLinkUid, teamId, teamToken, offset, pageSize, source } = req.body;
|
||||
const { appId, shareId, outLinkUid, teamId, teamToken, source } = req.body;
|
||||
const { offset, pageSize } = parsePaginationRequest(req);
|
||||
|
||||
const match = await (async () => {
|
||||
if (shareId && outLinkUid) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { filterPublicNodeResponseData } from '@fastgpt/global/core/chat/utils';
|
||||
import { GetChatTypeEnum } from '@/global/core/chat/constants';
|
||||
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import { ChatItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
|
||||
|
||||
export type getPaginationRecordsQuery = {};
|
||||
|
||||
@@ -22,16 +23,11 @@ export type getPaginationRecordsResponse = PaginationResponse<ChatItemType>;
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<getPaginationRecordsBody, getPaginationRecordsQuery>,
|
||||
res: ApiResponseType<any>
|
||||
_res: ApiResponseType<any>
|
||||
): Promise<getPaginationRecordsResponse> {
|
||||
const {
|
||||
appId,
|
||||
chatId,
|
||||
offset,
|
||||
pageSize = 10,
|
||||
loadCustomFeedbacks,
|
||||
type = GetChatTypeEnum.normal
|
||||
} = req.body;
|
||||
const { appId, chatId, loadCustomFeedbacks, type = GetChatTypeEnum.normal } = req.body;
|
||||
|
||||
const { offset, pageSize } = parsePaginationRequest(req);
|
||||
|
||||
if (!appId || !chatId) {
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { ChatInputGuideSchemaType } from '@fastgpt/global/core/chat/inputGuide/type';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
|
||||
|
||||
export type ChatInputGuideProps = PaginationProps<{
|
||||
appId: string;
|
||||
@@ -17,7 +18,8 @@ async function handler(
|
||||
req: ApiRequestProps<ChatInputGuideProps>,
|
||||
res: NextApiResponse<any>
|
||||
): Promise<ChatInputGuideResponse> {
|
||||
const { appId, pageSize, offset, searchKey } = req.body;
|
||||
const { appId, searchKey } = req.body;
|
||||
const { offset, pageSize } = parsePaginationRequest(req);
|
||||
|
||||
await authApp({ req, appId, authToken: true, per: ReadPermissionVal });
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { NextApiRequest } from 'next';
|
||||
import { DatasetTrainingCollectionName } from '@fastgpt/service/core/dataset/training/schema';
|
||||
import { Types } from '@fastgpt/service/common/mongo';
|
||||
import type { DatasetCollectionsListItemType } from '@/global/core/dataset/type.d';
|
||||
import type { GetDatasetCollectionsProps } from '@/global/core/api/datasetReq';
|
||||
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
|
||||
import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
@@ -10,11 +9,10 @@ import { DatasetDataCollectionName } from '@fastgpt/service/core/dataset/data/sc
|
||||
import { startTrainingQueue } from '@/service/core/dataset/training/utils';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { PagingData } from '@/types';
|
||||
import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
|
||||
import { collectionTagsToTagLabel } from '@fastgpt/service/core/dataset/collection/utils';
|
||||
|
||||
async function handler(req: NextApiRequest): Promise<PagingData<DatasetCollectionsListItemType>> {
|
||||
async function handler(req: NextApiRequest) {
|
||||
let {
|
||||
pageNum = 1,
|
||||
pageSize = 10,
|
||||
@@ -24,7 +22,7 @@ async function handler(req: NextApiRequest): Promise<PagingData<DatasetCollectio
|
||||
selectFolder = false,
|
||||
filterTags = [],
|
||||
simple = false
|
||||
} = req.body as GetDatasetCollectionsProps;
|
||||
} = req.body as any;
|
||||
searchText = searchText?.replace(/'/g, '');
|
||||
pageSize = Math.min(pageSize, 30);
|
||||
|
||||
|
||||
192
projects/app/src/pages/api/core/dataset/collection/listV2.ts
Normal file
192
projects/app/src/pages/api/core/dataset/collection/listV2.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import type { NextApiRequest } from 'next';
|
||||
import { DatasetTrainingCollectionName } from '@fastgpt/service/core/dataset/training/schema';
|
||||
import { Types } from '@fastgpt/service/common/mongo';
|
||||
import type { DatasetCollectionsListItemType } from '@/global/core/dataset/type.d';
|
||||
import type { GetDatasetCollectionsProps } from '@/global/core/api/datasetReq';
|
||||
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
|
||||
import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import { DatasetDataCollectionName } from '@fastgpt/service/core/dataset/data/schema';
|
||||
import { startTrainingQueue } from '@/service/core/dataset/training/utils';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
|
||||
import { collectionTagsToTagLabel } from '@fastgpt/service/core/dataset/collection/utils';
|
||||
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
|
||||
|
||||
async function handler(
|
||||
req: NextApiRequest
|
||||
): Promise<PaginationResponse<DatasetCollectionsListItemType>> {
|
||||
let {
|
||||
datasetId,
|
||||
parentId = null,
|
||||
searchText = '',
|
||||
selectFolder = false,
|
||||
filterTags = [],
|
||||
simple = false
|
||||
} = req.body as GetDatasetCollectionsProps;
|
||||
let { pageSize, offset } = parsePaginationRequest(req);
|
||||
pageSize = Math.min(pageSize, 30);
|
||||
searchText = searchText?.replace(/'/g, '');
|
||||
|
||||
// auth dataset and get my role
|
||||
const { teamId, permission } = await authDataset({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
datasetId,
|
||||
per: ReadPermissionVal
|
||||
});
|
||||
|
||||
const match = {
|
||||
teamId: new Types.ObjectId(teamId),
|
||||
datasetId: new Types.ObjectId(datasetId),
|
||||
parentId: parentId ? new Types.ObjectId(parentId) : null,
|
||||
...(selectFolder ? { type: DatasetCollectionTypeEnum.folder } : {}),
|
||||
...(searchText
|
||||
? {
|
||||
name: new RegExp(searchText, 'i')
|
||||
}
|
||||
: {}),
|
||||
...(filterTags.length ? { tags: { $in: filterTags } } : {})
|
||||
};
|
||||
|
||||
const selectField = {
|
||||
_id: 1,
|
||||
parentId: 1,
|
||||
tmbId: 1,
|
||||
name: 1,
|
||||
type: 1,
|
||||
forbid: 1,
|
||||
createTime: 1,
|
||||
updateTime: 1,
|
||||
trainingType: 1,
|
||||
fileId: 1,
|
||||
rawLink: 1,
|
||||
tags: 1,
|
||||
externalFileId: 1
|
||||
};
|
||||
|
||||
// not count data amount
|
||||
if (simple) {
|
||||
const collections = await MongoDatasetCollection.find(match, undefined, {
|
||||
...readFromSecondary
|
||||
})
|
||||
.select(selectField)
|
||||
.sort({
|
||||
updateTime: -1
|
||||
})
|
||||
.lean();
|
||||
|
||||
return {
|
||||
list: await Promise.all(
|
||||
collections.map(async (item) => ({
|
||||
...item,
|
||||
tags: await collectionTagsToTagLabel({
|
||||
datasetId,
|
||||
tags: item.tags
|
||||
}),
|
||||
dataAmount: 0,
|
||||
trainingAmount: 0,
|
||||
permission
|
||||
}))
|
||||
),
|
||||
total: await MongoDatasetCollection.countDocuments(match)
|
||||
};
|
||||
}
|
||||
|
||||
const [collections, total]: [DatasetCollectionsListItemType[], number] = await Promise.all([
|
||||
MongoDatasetCollection.aggregate([
|
||||
{
|
||||
$match: match
|
||||
},
|
||||
{
|
||||
$sort: { updateTime: -1 }
|
||||
},
|
||||
{
|
||||
$skip: offset
|
||||
},
|
||||
{
|
||||
$limit: pageSize
|
||||
},
|
||||
// count training data
|
||||
{
|
||||
$lookup: {
|
||||
from: DatasetTrainingCollectionName,
|
||||
let: { id: '$_id', team_id: match.teamId, dataset_id: match.datasetId },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$and: [{ $eq: ['$teamId', '$$team_id'] }, { $eq: ['$collectionId', '$$id'] }]
|
||||
}
|
||||
}
|
||||
},
|
||||
{ $count: 'count' }
|
||||
],
|
||||
as: 'trainingCount'
|
||||
}
|
||||
},
|
||||
// count collection total data
|
||||
{
|
||||
$lookup: {
|
||||
from: DatasetDataCollectionName,
|
||||
let: { id: '$_id', team_id: match.teamId, dataset_id: match.datasetId },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$and: [
|
||||
{ $eq: ['$teamId', '$$team_id'] },
|
||||
{ $eq: ['$datasetId', '$$dataset_id'] },
|
||||
{ $eq: ['$collectionId', '$$id'] }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{ $count: 'count' }
|
||||
],
|
||||
as: 'dataCount'
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
...selectField,
|
||||
dataAmount: {
|
||||
$ifNull: [{ $arrayElemAt: ['$dataCount.count', 0] }, 0]
|
||||
},
|
||||
trainingAmount: {
|
||||
$ifNull: [{ $arrayElemAt: ['$trainingCount.count', 0] }, 0]
|
||||
}
|
||||
}
|
||||
}
|
||||
]),
|
||||
MongoDatasetCollection.countDocuments(match, {
|
||||
...readFromSecondary
|
||||
})
|
||||
]);
|
||||
|
||||
const list = await Promise.all(
|
||||
collections.map(async (item) => ({
|
||||
...item,
|
||||
tags: await collectionTagsToTagLabel({
|
||||
datasetId,
|
||||
tags: item.tags
|
||||
}),
|
||||
permission
|
||||
}))
|
||||
);
|
||||
|
||||
if (list.find((item) => item.trainingAmount > 0)) {
|
||||
startTrainingQueue();
|
||||
}
|
||||
|
||||
// count collections
|
||||
return {
|
||||
list,
|
||||
total
|
||||
};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -10,6 +10,7 @@ import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import type { DatasetCollectionsListItemType } from '@/global/core/dataset/type.d';
|
||||
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
|
||||
|
||||
export type GetScrollCollectionsProps = PaginationProps<{
|
||||
datasetId: string;
|
||||
@@ -25,8 +26,6 @@ async function handler(
|
||||
): Promise<PaginationResponse<DatasetCollectionsListItemType>> {
|
||||
let {
|
||||
datasetId,
|
||||
pageSize = 10,
|
||||
offset,
|
||||
parentId = null,
|
||||
searchText = '',
|
||||
selectFolder = false,
|
||||
@@ -36,6 +35,7 @@ async function handler(
|
||||
if (!datasetId) {
|
||||
return Promise.reject(CommonErrEnum.missingParams);
|
||||
}
|
||||
let { offset, pageSize } = parsePaginationRequest(req);
|
||||
|
||||
searchText = searchText?.replace(/'/g, '');
|
||||
pageSize = Math.min(pageSize, 30);
|
||||
|
||||
@@ -3,19 +3,21 @@ import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
|
||||
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { PagingData, RequestPaging } from '@/types';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { DatasetDataListItemType } from '@/global/core/dataset/type';
|
||||
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
|
||||
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
|
||||
export type GetDatasetDataListProps = RequestPaging & {
|
||||
export type GetDatasetDataListProps = {
|
||||
searchText?: string;
|
||||
collectionId: string;
|
||||
};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<GetDatasetDataListProps>
|
||||
): Promise<PagingData<DatasetDataListItemType>> {
|
||||
let { pageNum = 1, pageSize = 10, searchText = '', collectionId } = req.body;
|
||||
): Promise<PaginationResponse<DatasetDataListItemType>> {
|
||||
let { searchText = '', collectionId } = req.body;
|
||||
let { offset, pageSize } = parsePaginationRequest(req);
|
||||
|
||||
pageSize = Math.min(pageSize, 30);
|
||||
|
||||
@@ -40,19 +42,17 @@ async function handler(
|
||||
: {})
|
||||
};
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
const [list, total] = await Promise.all([
|
||||
MongoDatasetData.find(match, '_id datasetId collectionId q a chunkIndex')
|
||||
.sort({ chunkIndex: 1, updateTime: -1 })
|
||||
.skip((pageNum - 1) * pageSize)
|
||||
.skip(offset)
|
||||
.limit(pageSize)
|
||||
.lean(),
|
||||
MongoDatasetData.countDocuments(match)
|
||||
]);
|
||||
|
||||
return {
|
||||
pageNum,
|
||||
pageSize,
|
||||
data,
|
||||
list,
|
||||
total
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { DatasetDataListItemType } from '@/global/core/dataset/type';
|
||||
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
|
||||
|
||||
export type GetDatasetDataListProps = PaginationProps & {
|
||||
searchText?: string;
|
||||
@@ -16,7 +17,8 @@ export type GetDatasetDataListRes = PaginationResponse<DatasetDataListItemType>;
|
||||
async function handler(
|
||||
req: ApiRequestProps<GetDatasetDataListProps>
|
||||
): Promise<GetDatasetDataListRes> {
|
||||
let { offset, pageSize = 10, searchText = '', collectionId } = req.body;
|
||||
let { searchText = '', collectionId } = req.body;
|
||||
let { offset, pageSize } = parsePaginationRequest(req);
|
||||
|
||||
pageSize = Math.min(pageSize, 30);
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { DatasetListItemType } from '@fastgpt/global/core/dataset/type.d';
|
||||
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
|
||||
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
|
||||
import { getVectorModel } from '@fastgpt/service/core/ai/model';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller';
|
||||
import {
|
||||
@@ -19,6 +17,8 @@ import { replaceRegChars } from '@fastgpt/global/common/string/tools';
|
||||
import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers';
|
||||
import { concatPer } from '@fastgpt/service/support/permission/controller';
|
||||
import { getOrgIdSetWithParentByTmbId } from '@fastgpt/service/support/permission/org/controllers';
|
||||
import { addSourceMember } from '@fastgpt/service/support/user/utils';
|
||||
import { getVectorModel } from '@fastgpt/service/core/ai/model';
|
||||
|
||||
export type GetDatasetListBody = {
|
||||
parentId: ParentIdType;
|
||||
@@ -167,28 +167,24 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
|
||||
})();
|
||||
|
||||
return {
|
||||
...dataset,
|
||||
_id: dataset._id,
|
||||
avatar: dataset.avatar,
|
||||
name: dataset.name,
|
||||
intro: dataset.intro,
|
||||
type: dataset.type,
|
||||
vectorModel: getVectorModel(dataset.vectorModel),
|
||||
inheritPermission: dataset.inheritPermission,
|
||||
tmbId: dataset.tmbId,
|
||||
updateTime: dataset.updateTime,
|
||||
permission: Per,
|
||||
privateDataset
|
||||
};
|
||||
})
|
||||
.filter((app) => app.permission.hasReadPer);
|
||||
|
||||
const data = formatDatasets.map<DatasetListItemType>((item) => ({
|
||||
_id: item._id,
|
||||
avatar: item.avatar,
|
||||
name: item.name,
|
||||
intro: item.intro,
|
||||
type: item.type,
|
||||
permission: item.permission,
|
||||
vectorModel: getVectorModel(item.vectorModel),
|
||||
inheritPermission: item.inheritPermission,
|
||||
tmbId: item.tmbId,
|
||||
updateTime: item.updateTime,
|
||||
private: item.privateDataset
|
||||
}));
|
||||
|
||||
return data;
|
||||
return addSourceMember({
|
||||
list: formatDatasets
|
||||
});
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
@@ -45,7 +45,11 @@ async function handler(
|
||||
requestOrigin: req.headers.origin,
|
||||
mode: 'debug',
|
||||
runningAppInfo: {
|
||||
id: appId,
|
||||
id: app._id,
|
||||
teamId: app.teamId,
|
||||
tmbId: app.tmbId
|
||||
},
|
||||
runningUserInfo: {
|
||||
teamId,
|
||||
tmbId
|
||||
},
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { MongoUser } from '@fastgpt/service/support/user/schema';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { UserUpdateParams } from '@/types/user';
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
|
||||
/* update user info */
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { getUserDetail } from '@fastgpt/service/support/user/controller';
|
||||
import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller';
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
|
||||
export type UserAccountUpdateQuery = {};
|
||||
export type UserAccountUpdateBody = UserUpdateParams;
|
||||
export type UserAccountUpdateResponse = {};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<UserAccountUpdateBody, UserAccountUpdateQuery>,
|
||||
_res: ApiResponseType<any>
|
||||
@@ -19,21 +20,33 @@ async function handler(
|
||||
const { avatar, timezone } = req.body;
|
||||
|
||||
const { tmbId } = await authCert({ req, authToken: true });
|
||||
const user = await getUserDetail({ tmbId });
|
||||
// const user = await getUserDetail({ tmbId });
|
||||
|
||||
// 更新对应的记录
|
||||
await mongoSessionRun(async (session) => {
|
||||
await MongoUser.updateOne(
|
||||
{
|
||||
_id: user._id
|
||||
},
|
||||
{
|
||||
...(avatar && { avatar }),
|
||||
...(timezone && { timezone })
|
||||
}
|
||||
).session(session);
|
||||
|
||||
await refreshSourceAvatar(avatar, user.avatar, session);
|
||||
const tmb = await MongoTeamMember.findById(tmbId).session(session);
|
||||
if (timezone) {
|
||||
await MongoUser.updateOne(
|
||||
{
|
||||
_id: tmb?.userId
|
||||
},
|
||||
{
|
||||
timezone
|
||||
}
|
||||
).session(session);
|
||||
}
|
||||
// if avatar, update team member avatar
|
||||
if (avatar) {
|
||||
await MongoTeamMember.updateOne(
|
||||
{
|
||||
_id: tmbId
|
||||
},
|
||||
{
|
||||
avatar
|
||||
}
|
||||
).session(session);
|
||||
await refreshSourceAvatar(avatar, tmb?.avatar, session);
|
||||
}
|
||||
});
|
||||
|
||||
return {};
|
||||
|
||||
@@ -280,6 +280,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
teamId: String(app.teamId),
|
||||
tmbId: String(app.tmbId)
|
||||
},
|
||||
runningUserInfo: {
|
||||
teamId,
|
||||
tmbId
|
||||
},
|
||||
uid: String(outLinkUserId || tmbId),
|
||||
|
||||
chatId,
|
||||
|
||||
@@ -164,8 +164,6 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
|
||||
showMarkIcon
|
||||
showVoiceIcon={false}
|
||||
chatType="log"
|
||||
showRawSource
|
||||
showNodeStatus
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
@@ -187,7 +185,12 @@ const Render = (props: Props) => {
|
||||
}, [appId, chatId]);
|
||||
|
||||
return (
|
||||
<ChatItemContextProvider>
|
||||
<ChatItemContextProvider
|
||||
showRouteToAppDetail={true}
|
||||
showRouteToDatasetDetail={true}
|
||||
isShowReadRawSource={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={params}>
|
||||
<DetailLogsModal {...props} />
|
||||
</ChatRecordContextProvider>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
ModalBody,
|
||||
HStack
|
||||
} from '@chakra-ui/react';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import UserBox from '@fastgpt/web/components/common/UserBox';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { getAppChatLogs } from '@/web/core/app/api';
|
||||
@@ -30,8 +30,6 @@ import { cardStyles } from '../constants';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useMount } from 'ahooks';
|
||||
|
||||
const DetailLogsModal = dynamic(() => import('./DetailLogsModal'));
|
||||
|
||||
@@ -40,17 +38,11 @@ const Logs = () => {
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const appId = useContextSelector(AppContext, (v) => v.appId);
|
||||
const { teamMembers, loadAndGetTeamMembers } = useUserStore();
|
||||
|
||||
useMount(() => {
|
||||
loadAndGetTeamMembers();
|
||||
});
|
||||
|
||||
const [dateRange, setDateRange] = useState<DateRangeType>({
|
||||
from: addDays(new Date(), -7),
|
||||
to: new Date()
|
||||
});
|
||||
|
||||
const {
|
||||
isOpen: isOpenMarkDesc,
|
||||
onOpen: onOpenMarkDesc,
|
||||
@@ -63,8 +55,7 @@ const Logs = () => {
|
||||
Pagination,
|
||||
getData,
|
||||
pageNum
|
||||
} = usePagination({
|
||||
api: getAppChatLogs,
|
||||
} = usePagination(getAppChatLogs, {
|
||||
pageSize: 20,
|
||||
params: {
|
||||
appId,
|
||||
@@ -139,15 +130,7 @@ const Logs = () => {
|
||||
{!!item.outLinkUid ? (
|
||||
item.outLinkUid
|
||||
) : (
|
||||
<HStack>
|
||||
<Avatar
|
||||
src={teamMembers?.find((v) => v.tmbId === item.tmbId)?.avatar}
|
||||
w="1.25rem"
|
||||
/>
|
||||
<Box fontSize={'sm'} ml={1}>
|
||||
{teamMembers?.find((v) => v.tmbId === item.tmbId)?.memberName}
|
||||
</Box>
|
||||
</HStack>
|
||||
<UserBox sourceMember={item.sourceMember} />
|
||||
)}
|
||||
</Box>
|
||||
</Td>
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
getWorkflowVersionList,
|
||||
updateAppVersion
|
||||
} from '@/web/core/app/api/version';
|
||||
import { useVirtualScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, BoxProps, Button, Flex, Input } from '@chakra-ui/react';
|
||||
@@ -18,9 +18,7 @@ import Tag from '@fastgpt/web/components/common/Tag';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import type { AppVersionSchemaType, VersionListItemType } from '@fastgpt/global/core/app/version';
|
||||
import type { SimpleAppSnapshotType } from './SimpleApp/useSnapshots';
|
||||
@@ -183,23 +181,18 @@ const TeamCloud = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const { loadAndGetTeamMembers } = useUserStore();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const { scrollDataList, ScrollList, isLoading, fetchData, setData } = useVirtualScrollPagination(
|
||||
getWorkflowVersionList,
|
||||
{
|
||||
itemHeight: 40,
|
||||
overscan: 20,
|
||||
|
||||
pageSize: 30,
|
||||
defaultParams: {
|
||||
appId: appDetail._id
|
||||
}
|
||||
}
|
||||
);
|
||||
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
|
||||
manual: !feConfigs.isPlus
|
||||
const {
|
||||
ScrollData,
|
||||
data: scrollDataList,
|
||||
setData,
|
||||
isLoading
|
||||
} = useScrollPagination(getWorkflowVersionList, {
|
||||
pageSize: 30,
|
||||
params: {
|
||||
appId: appDetail._id
|
||||
},
|
||||
refreshDeps: [appDetail._id]
|
||||
});
|
||||
const [editIndex, setEditIndex] = useState<number | undefined>(undefined);
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(undefined);
|
||||
@@ -237,15 +230,13 @@ const TeamCloud = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollList isLoading={isLoading || isLoadingVersion} flex={'1 0 0'} px={5}>
|
||||
{scrollDataList.map((data, index) => {
|
||||
const item = data.data;
|
||||
const firstPublishedIndex = scrollDataList.findIndex((data) => data.data.isPublish);
|
||||
const tmb = members.find((member) => member.tmbId === item.tmbId);
|
||||
<ScrollData isLoading={isLoading || isLoadingVersion} flex={'1 0 0'} px={5}>
|
||||
{scrollDataList.map((item, index) => {
|
||||
const firstPublishedIndex = scrollDataList.findIndex((data) => data.isPublish);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
key={data.index}
|
||||
key={item._id}
|
||||
alignItems={'center'}
|
||||
py={editIndex !== index ? 2 : 1}
|
||||
px={3}
|
||||
@@ -266,20 +257,33 @@ const TeamCloud = ({
|
||||
h={'72px'}
|
||||
Trigger={
|
||||
<Box>
|
||||
<Avatar src={tmb?.avatar} borderRadius={'50%'} w={'24px'} h={'24px'} />
|
||||
<Avatar
|
||||
src={item.sourceMember.avatar}
|
||||
borderRadius={'50%'}
|
||||
w={'24px'}
|
||||
h={'24px'}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{({ onClose }) => (
|
||||
<Flex alignItems={'center'} h={'full'} pl={5} gap={3}>
|
||||
{() => (
|
||||
<Flex alignItems={'center'} h={'full'} pl={5} gap={2}>
|
||||
<Box>
|
||||
<Avatar src={tmb?.avatar} borderRadius={'50%'} w={'36px'} h={'36px'} />
|
||||
<Avatar
|
||||
src={item.sourceMember.avatar}
|
||||
borderRadius={'50%'}
|
||||
w={'36px'}
|
||||
h={'36px'}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Box fontSize={'14px'} color={'myGray.900'}>
|
||||
{tmb?.memberName}
|
||||
</Box>
|
||||
<Box fontSize={'12px'} color={'myGray.500'}>
|
||||
<Flex gap={1} fontSize={'sm'} color={'myGray.900'}>
|
||||
<Box>{item.sourceMember.name}</Box>
|
||||
{item.sourceMember.status === 'leave' && (
|
||||
<Tag color="gray">{t('common:user_leaved')}</Tag>
|
||||
)}
|
||||
</Flex>
|
||||
<Box fontSize={'xs'} mt={2} color={'myGray.500'}>
|
||||
{formatTime2YMDHMS(item.time)}
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -349,6 +353,6 @@ const TeamCloud = ({
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</ScrollList>
|
||||
</ScrollData>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -91,7 +91,12 @@ const Render = ({ appForm }: Props) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<ChatItemContextProvider>
|
||||
<ChatItemContextProvider
|
||||
showRouteToAppDetail={true}
|
||||
showRouteToDatasetDetail={true}
|
||||
isShowReadRawSource={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
<ChatTest appForm={appForm} />
|
||||
</ChatRecordContextProvider>
|
||||
|
||||
@@ -158,7 +158,12 @@ const Render = (Props: Props) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<ChatItemContextProvider>
|
||||
<ChatItemContextProvider
|
||||
showRouteToAppDetail={true}
|
||||
showRouteToDatasetDetail={true}
|
||||
isShowReadRawSource={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
<ChatTest {...Props} />
|
||||
</ChatRecordContextProvider>
|
||||
|
||||
@@ -50,7 +50,6 @@ import { useWorkflowUtils } from './hooks/useUtils';
|
||||
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart';
|
||||
import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd';
|
||||
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
@@ -89,8 +88,6 @@ enum TemplateTypeEnum {
|
||||
const sliderWidth = 460;
|
||||
|
||||
const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
const { loadAndGetTeamMembers } = useUserStore();
|
||||
|
||||
const [parentId, setParentId] = useState<ParentIdType>('');
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const { feConfigs } = useSystemStore();
|
||||
@@ -99,10 +96,6 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const appId = useContextSelector(WorkflowContext, (v) => v.appId);
|
||||
|
||||
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
|
||||
manual: !feConfigs.isPlus
|
||||
});
|
||||
|
||||
const [templateType, setTemplateType] = useState(TemplateTypeEnum.basic);
|
||||
|
||||
const { data: basicNodes } = useRequest2(
|
||||
@@ -162,19 +155,10 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
searchVal?: string;
|
||||
}) => {
|
||||
if (type === TemplateTypeEnum.teamPlugin) {
|
||||
const teamApps = await getTeamPlugTemplates({
|
||||
return getTeamPlugTemplates({
|
||||
parentId,
|
||||
searchKey: searchVal
|
||||
}).then((res) => res.filter((app) => app.id !== appId));
|
||||
|
||||
return teamApps.map<NodeTemplateListItemType>((app) => {
|
||||
const member = members.find((member) => member.tmbId === app.tmbId);
|
||||
return {
|
||||
...app,
|
||||
author: member?.memberName,
|
||||
authorAvatar: member?.avatar
|
||||
};
|
||||
});
|
||||
}
|
||||
if (type === TemplateTypeEnum.systemPlugin) {
|
||||
return getSystemPlugTemplates({
|
||||
@@ -188,7 +172,7 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
setParentId(parentId);
|
||||
setTemplateType(type);
|
||||
},
|
||||
refreshDeps: [members, searchKey, templateType]
|
||||
refreshDeps: [searchKey, templateType]
|
||||
}
|
||||
);
|
||||
|
||||
@@ -420,7 +404,6 @@ const RenderList = React.memo(function RenderList({
|
||||
templates,
|
||||
type,
|
||||
onClose,
|
||||
parentId,
|
||||
setParentId
|
||||
}: RenderListProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -14,22 +14,18 @@ import RenderToolInput from './render/RenderToolInput';
|
||||
import RenderOutput from './render/RenderOutput';
|
||||
import CodeEditor from '@fastgpt/web/components/common/Textarea/CodeEditor';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { getLatestNodeTemplate } from '@/web/core/workflow/utils';
|
||||
import { CodeNode } from '@fastgpt/global/core/workflow/template/system/sandbox';
|
||||
import { JS_TEMPLATE } from '@fastgpt/global/core/workflow/template/system/sandbox/constants';
|
||||
|
||||
const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { workflowT } = useI18n();
|
||||
const { nodeId, inputs, outputs } = data;
|
||||
const { splitToolInputs, onChangeNode, onResetNode } = useContextSelector(
|
||||
WorkflowContext,
|
||||
(ctx) => ctx
|
||||
);
|
||||
|
||||
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (ctx) => ctx.onChangeNode);
|
||||
|
||||
const { ConfirmModal, openConfirm } = useConfirm({
|
||||
content: workflowT('code.Reset template confirm')
|
||||
content: t('workflow:code.Reset template confirm')
|
||||
});
|
||||
|
||||
const CustomComponent = useMemo(() => {
|
||||
@@ -38,19 +34,24 @@ const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
return (
|
||||
<Box mt={-3}>
|
||||
<Flex mb={2} alignItems={'flex-end'}>
|
||||
<Box flex={'1'}>{'Javascript ' + workflowT('Code')}</Box>
|
||||
<Box flex={'1'}>{'Javascript ' + t('workflow:Code')}</Box>
|
||||
<Box
|
||||
cursor={'pointer'}
|
||||
color={'primary.500'}
|
||||
fontSize={'xs'}
|
||||
onClick={openConfirm(() => {
|
||||
onResetNode({
|
||||
id: nodeId,
|
||||
node: getLatestNodeTemplate(data, CodeNode)
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: JS_TEMPLATE
|
||||
}
|
||||
});
|
||||
})}
|
||||
>
|
||||
{workflowT('code.Reset template')}
|
||||
{t('workflow:code.Reset template')}
|
||||
</Box>
|
||||
</Flex>
|
||||
<CodeEditor
|
||||
@@ -73,37 +74,33 @@ const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [data, nodeId, onChangeNode, onResetNode, openConfirm, workflowT]);
|
||||
}, [nodeId, onChangeNode, openConfirm, t]);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
|
||||
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
|
||||
|
||||
return (
|
||||
<NodeCard minW={'400px'} selected={selected} {...data}>
|
||||
{isTool && (
|
||||
<>
|
||||
<Container>
|
||||
<RenderToolInput nodeId={nodeId} inputs={inputs} />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
<Container>
|
||||
<IOTitle text={t('common:common.Input')} mb={-1} />
|
||||
<RenderInput
|
||||
nodeId={nodeId}
|
||||
flowInputList={commonInputs}
|
||||
CustomComponent={CustomComponent}
|
||||
/>
|
||||
</Container>
|
||||
<Container>
|
||||
<IOTitle text={t('common:common.Output')} />
|
||||
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
<ConfirmModal />
|
||||
</NodeCard>
|
||||
);
|
||||
}, [ConfirmModal, CustomComponent, data, inputs, nodeId, outputs, selected, splitToolInputs, t]);
|
||||
|
||||
return Render;
|
||||
return (
|
||||
<NodeCard minW={'400px'} selected={selected} {...data}>
|
||||
{isTool && (
|
||||
<>
|
||||
<Container>
|
||||
<RenderToolInput nodeId={nodeId} inputs={inputs} />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
<Container>
|
||||
<IOTitle text={t('common:common.Input')} mb={-1} />
|
||||
<RenderInput
|
||||
nodeId={nodeId}
|
||||
flowInputList={commonInputs}
|
||||
CustomComponent={CustomComponent}
|
||||
/>
|
||||
</Container>
|
||||
<Container>
|
||||
<IOTitle text={t('common:common.Output')} />
|
||||
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
<ConfirmModal />
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeCode);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
|
||||
@@ -10,14 +10,14 @@ import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import VariableTip from '@/components/common/Textarea/MyTextarea/VariableTip';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
input: FlowNodeInputItemType;
|
||||
RightComponent?: React.JSX.Element;
|
||||
};
|
||||
|
||||
const InputLabel = ({ nodeId, input }: Props) => {
|
||||
const InputLabel = ({ nodeId, input, RightComponent }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
@@ -68,11 +68,11 @@ const InputLabel = ({ nodeId, input }: Props) => {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Variable picker tip */}
|
||||
{input.renderTypeList[input.selectedTypeIndex ?? 0] === FlowNodeInputTypeEnum.textarea && (
|
||||
{/* Right Component */}
|
||||
{RightComponent && (
|
||||
<>
|
||||
<Box flex={1} />
|
||||
<VariableTip transform={'translateY(2px)'} />
|
||||
<Box flex={'1'} />
|
||||
{RightComponent}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
@@ -8,71 +8,72 @@ import InputLabel from './Label';
|
||||
import type { RenderInputProps } from './type';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
|
||||
const RenderList: {
|
||||
types: FlowNodeInputTypeEnum[];
|
||||
Component: React.ComponentType<RenderInputProps>;
|
||||
}[] = [
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.reference],
|
||||
const RenderList: Record<
|
||||
FlowNodeInputTypeEnum,
|
||||
| {
|
||||
Component: React.ComponentType<RenderInputProps>;
|
||||
LableRightComponent?: React.ComponentType<RenderInputProps>;
|
||||
}
|
||||
| undefined
|
||||
> = {
|
||||
[FlowNodeInputTypeEnum.reference]: {
|
||||
Component: dynamic(() => import('./templates/Reference'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.fileSelect],
|
||||
[FlowNodeInputTypeEnum.fileSelect]: {
|
||||
Component: dynamic(() => import('./templates/Reference'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.select],
|
||||
[FlowNodeInputTypeEnum.select]: {
|
||||
Component: dynamic(() => import('./templates/Select'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.numberInput],
|
||||
[FlowNodeInputTypeEnum.numberInput]: {
|
||||
Component: dynamic(() => import('./templates/NumberInput'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.switch],
|
||||
[FlowNodeInputTypeEnum.switch]: {
|
||||
Component: dynamic(() => import('./templates/Switch'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.selectApp],
|
||||
[FlowNodeInputTypeEnum.selectApp]: {
|
||||
Component: dynamic(() => import('./templates/SelectApp'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.selectLLMModel],
|
||||
[FlowNodeInputTypeEnum.selectLLMModel]: {
|
||||
Component: dynamic(() => import('./templates/SelectLLMModel'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.settingLLMModel],
|
||||
[FlowNodeInputTypeEnum.settingLLMModel]: {
|
||||
Component: dynamic(() => import('./templates/SettingLLMModel'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.selectDataset],
|
||||
Component: dynamic(() => import('./templates/SelectDataset'))
|
||||
[FlowNodeInputTypeEnum.selectDataset]: {
|
||||
Component: dynamic(() =>
|
||||
import('./templates/SelectDataset').then((mod) => mod.SelectDatasetRender)
|
||||
),
|
||||
LableRightComponent: dynamic(() =>
|
||||
import('./templates/SelectDataset').then((mod) => mod.SwitchAuthTmb)
|
||||
)
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.selectDatasetParamsModal],
|
||||
[FlowNodeInputTypeEnum.selectDatasetParamsModal]: {
|
||||
Component: dynamic(() => import('./templates/SelectDatasetParams'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.addInputParam],
|
||||
[FlowNodeInputTypeEnum.addInputParam]: {
|
||||
Component: dynamic(() => import('./templates/DynamicInputs/index'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.JSONEditor],
|
||||
[FlowNodeInputTypeEnum.JSONEditor]: {
|
||||
Component: dynamic(() => import('./templates/JsonEditor'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.settingDatasetQuotePrompt],
|
||||
[FlowNodeInputTypeEnum.settingDatasetQuotePrompt]: {
|
||||
Component: dynamic(() => import('./templates/SettingQuotePrompt'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.input],
|
||||
[FlowNodeInputTypeEnum.input]: {
|
||||
Component: dynamic(() => import('./templates/TextInput'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.textarea],
|
||||
Component: dynamic(() => import('./templates/Textarea'))
|
||||
}
|
||||
];
|
||||
[FlowNodeInputTypeEnum.textarea]: {
|
||||
Component: dynamic(() => import('./templates/Textarea')),
|
||||
LableRightComponent: dynamic(() =>
|
||||
import('./templates/Textarea').then((mod) => mod.TextareaRightComponent)
|
||||
)
|
||||
},
|
||||
|
||||
[FlowNodeInputTypeEnum.customVariable]: undefined,
|
||||
[FlowNodeInputTypeEnum.hidden]: undefined,
|
||||
[FlowNodeInputTypeEnum.custom]: undefined
|
||||
};
|
||||
|
||||
const hideLabelTypeList = [FlowNodeInputTypeEnum.addInputParam];
|
||||
|
||||
@@ -101,7 +102,7 @@ const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props)
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [feConfigs?.isPlus, flowInputList]);
|
||||
}, [filterProInputs]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -110,23 +111,41 @@ const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props)
|
||||
|
||||
const RenderComponent = (() => {
|
||||
if (renderType === FlowNodeInputTypeEnum.custom && CustomComponent?.[input.key]) {
|
||||
return <>{CustomComponent?.[input.key]({ ...input })}</>;
|
||||
return {
|
||||
Component: <>{CustomComponent?.[input.key]({ ...input })}</>
|
||||
};
|
||||
}
|
||||
|
||||
const Component = RenderList.find((item) => item.types.includes(renderType))?.Component;
|
||||
const RenderItem = RenderList[renderType];
|
||||
|
||||
if (!Component) return null;
|
||||
return <Component inputs={filterProInputs} item={input} nodeId={nodeId} />;
|
||||
if (!RenderItem) return null;
|
||||
|
||||
return {
|
||||
Component: (
|
||||
<RenderItem.Component inputs={filterProInputs} item={input} nodeId={nodeId} />
|
||||
),
|
||||
LableRightComponent: RenderItem.LableRightComponent ? (
|
||||
<RenderItem.LableRightComponent
|
||||
inputs={filterProInputs}
|
||||
item={input}
|
||||
nodeId={nodeId}
|
||||
/>
|
||||
) : undefined
|
||||
};
|
||||
})();
|
||||
|
||||
return (
|
||||
<Box key={input.key} _notLast={{ mb }} position={'relative'}>
|
||||
{!!input.label && !hideLabelTypeList.includes(renderType) && (
|
||||
<InputLabel nodeId={nodeId} input={input} />
|
||||
<InputLabel
|
||||
nodeId={nodeId}
|
||||
input={input}
|
||||
RightComponent={RenderComponent?.LableRightComponent}
|
||||
/>
|
||||
)}
|
||||
{!!RenderComponent && (
|
||||
<Box mt={2} className={'nodrag'}>
|
||||
{RenderComponent}
|
||||
{RenderComponent.Component}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import { Box, Button, Flex, Grid, useDisclosure, useTheme } from '@chakra-ui/react';
|
||||
import { Box, Button, Flex, Grid, Switch, useDisclosure, useTheme } from '@chakra-ui/react';
|
||||
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
|
||||
import { SelectedDatasetType } from '@fastgpt/global/core/workflow/api';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
@@ -12,12 +12,17 @@ import dynamic from 'next/dynamic';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
|
||||
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
|
||||
|
||||
const SelectDatasetRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
|
||||
export const SelectDatasetRender = React.memo(function SelectDatasetRender({
|
||||
inputs = [],
|
||||
item,
|
||||
nodeId
|
||||
}: RenderInputProps) {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const [data, setData] = useState({
|
||||
@@ -80,8 +85,9 @@ const SelectDatasetRender = ({ inputs = [], item, nodeId }: RenderInputProps) =>
|
||||
key={item._id}
|
||||
alignItems={'center'}
|
||||
h={10}
|
||||
border={theme.borders.base}
|
||||
borderColor={'myGray.200'}
|
||||
boxShadow={'sm'}
|
||||
bg={'white'}
|
||||
border={'base'}
|
||||
px={2}
|
||||
borderRadius={'md'}
|
||||
>
|
||||
@@ -128,11 +134,47 @@ const SelectDatasetRender = ({ inputs = [], item, nodeId }: RenderInputProps) =>
|
||||
onOpenDatasetSelect,
|
||||
selectedDatasets,
|
||||
selectedDatasetsValue,
|
||||
t,
|
||||
theme.borders.base
|
||||
t
|
||||
]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
});
|
||||
|
||||
export default React.memo(SelectDatasetRender);
|
||||
export const SwitchAuthTmb = React.memo(function SwitchAuthTmb({
|
||||
inputs = [],
|
||||
item,
|
||||
nodeId
|
||||
}: RenderInputProps) {
|
||||
const { t } = useTranslation();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const authTmbIdInput = useMemo(
|
||||
() => inputs.find((v) => v.key === NodeInputKeyEnum.authTmbId),
|
||||
[inputs]
|
||||
);
|
||||
|
||||
return authTmbIdInput ? (
|
||||
<Flex alignItems={'center'}>
|
||||
<Box fontSize={'sm'}>{t('workflow:auth_tmb_id')}</Box>
|
||||
<QuestionTip label={t('workflow:auth_tmb_id_tip')} />
|
||||
<Switch
|
||||
ml={1}
|
||||
size={'sm'}
|
||||
isChecked={!!authTmbIdInput.value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
key: NodeInputKeyEnum.authTmbId,
|
||||
type: 'updateInput',
|
||||
value: {
|
||||
...authTmbIdInput,
|
||||
value: e.target.checked
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
) : null;
|
||||
});
|
||||
|
||||
export default SelectDatasetRender;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AppContext } from '@/pages/app/detail/components/context';
|
||||
import { getEditorVariables } from '../../../../../utils';
|
||||
import { WorkflowNodeEdgeContext } from '../../../../../context/workflowInitContext';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import VariableTip from '@/components/common/Textarea/MyTextarea/VariableTip';
|
||||
|
||||
const TextareaRender = ({ item, nodeId }: RenderInputProps) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -84,3 +85,10 @@ const TextareaRender = ({ item, nodeId }: RenderInputProps) => {
|
||||
};
|
||||
|
||||
export default React.memo(TextareaRender);
|
||||
|
||||
export const TextareaRightComponent = React.memo(function TextareaRightComponent({
|
||||
item,
|
||||
nodeId
|
||||
}: RenderInputProps) {
|
||||
return <VariableTip transform={'translateY(2px)'} />;
|
||||
});
|
||||
|
||||
@@ -141,8 +141,6 @@ export const useChatTest = ({
|
||||
chatId={chatId}
|
||||
showMarkIcon
|
||||
chatType="chat"
|
||||
showRawSource
|
||||
showNodeStatus
|
||||
onStartChat={startChat}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -34,8 +34,8 @@ import { postCopyApp } from '@/web/core/app/api/app';
|
||||
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
|
||||
import UserBox from '@fastgpt/web/components/common/UserBox';
|
||||
const HttpEditModal = dynamic(() => import('./HttpPluginEditModal'));
|
||||
|
||||
const ListItem = () => {
|
||||
@@ -44,8 +44,6 @@ const ListItem = () => {
|
||||
const { parentId = null } = router.query;
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const { loadAndGetTeamMembers } = useUserStore();
|
||||
|
||||
const { openConfirm: openMoveConfirm, ConfirmModal: MoveConfirmModal } = useConfirm({
|
||||
type: 'common',
|
||||
title: t('common:move.confirm'),
|
||||
@@ -115,10 +113,6 @@ const ListItem = () => {
|
||||
successToast: t('app:create_copy_success')
|
||||
});
|
||||
|
||||
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
|
||||
manual: false
|
||||
});
|
||||
|
||||
const { runAsync: onResumeInheritPermission } = useRequest2(
|
||||
() => {
|
||||
return resumeInheritPer(editPerApp!._id);
|
||||
@@ -145,7 +139,6 @@ const ListItem = () => {
|
||||
alignItems={'stretch'}
|
||||
>
|
||||
{myApps.map((app, index) => {
|
||||
const owner = members.find((v) => v.tmbId === app.tmbId);
|
||||
return (
|
||||
<MyTooltip
|
||||
key={app._id}
|
||||
@@ -229,15 +222,12 @@ const ListItem = () => {
|
||||
color={'myGray.500'}
|
||||
>
|
||||
<HStack spacing={3.5}>
|
||||
{owner && (
|
||||
<HStack spacing={1}>
|
||||
<Avatar src={owner.avatar} w={'0.875rem'} borderRadius={'50%'} />
|
||||
<Box maxW={'150px'} className="textEllipsis">
|
||||
{owner.memberName}
|
||||
</Box>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
<UserBox
|
||||
sourceMember={app.sourceMember}
|
||||
fontSize="xs"
|
||||
avatarSize="1rem"
|
||||
spacing={0.5}
|
||||
/>
|
||||
<PermissionIconText
|
||||
private={app.private}
|
||||
color={'myGray.500'}
|
||||
|
||||
@@ -27,13 +27,11 @@ const ChatHeader = ({
|
||||
history,
|
||||
showHistory,
|
||||
apps,
|
||||
onRouteToAppDetail,
|
||||
totalRecordsCount
|
||||
}: {
|
||||
history: ChatItemType[];
|
||||
showHistory?: boolean;
|
||||
apps?: AppListItemType[];
|
||||
onRouteToAppDetail?: () => void;
|
||||
totalRecordsCount: number;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -71,7 +69,7 @@ const ChatHeader = ({
|
||||
)}
|
||||
|
||||
{/* control */}
|
||||
{!isPlugin && <ToolMenu history={history} onRouteToAppDetail={onRouteToAppDetail} />}
|
||||
{!isPlugin && <ToolMenu history={history} />}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,7 +28,6 @@ type HistoryItemType = {
|
||||
const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) => {
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const isUserChatPage = router.pathname === '/chat';
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -46,6 +45,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
|
||||
|
||||
const appName = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.name);
|
||||
const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.avatar);
|
||||
const showRouteToAppDetail = useContextSelector(ChatItemContext, (v) => v.showRouteToAppDetail);
|
||||
|
||||
const concatHistory = useMemo(() => {
|
||||
const formatHistories: HistoryItemType[] = histories.map((item) => {
|
||||
@@ -77,8 +77,8 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
|
||||
});
|
||||
|
||||
const canRouteToDetail = useMemo(
|
||||
() => appId && userInfo?.team.permission.hasWritePer,
|
||||
[appId, userInfo?.team.permission.hasWritePer]
|
||||
() => appId && userInfo?.team.permission.hasWritePer && showRouteToAppDetail,
|
||||
[appId, userInfo?.team.permission.hasWritePer, showRouteToAppDetail]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -287,7 +287,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
|
||||
</ScrollData>
|
||||
|
||||
{/* exec */}
|
||||
{!isPc && isUserChatPage && (
|
||||
{!isPc && !!canRouteToDetail && (
|
||||
<Flex
|
||||
mt={2}
|
||||
borderTop={theme.borders.base}
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
} from '@fastgpt/global/common/parentFolder/type';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
|
||||
const SelectOneResource = dynamic(() => import('@/components/common/folder/SelectOneResource'));
|
||||
|
||||
@@ -22,6 +24,8 @@ const SliderApps = ({ apps, activeAppId }: { apps: AppListItemType[]; activeAppI
|
||||
const router = useRouter();
|
||||
const isTeamChat = router.pathname === '/chat/team';
|
||||
|
||||
const showRouteToAppDetail = useContextSelector(ChatItemContext, (v) => v.showRouteToAppDetail);
|
||||
|
||||
const getAppList = useCallback(async ({ parentId }: GetResourceFolderListProps) => {
|
||||
return getMyApps({
|
||||
parentId,
|
||||
@@ -50,34 +54,36 @@ const SliderApps = ({ apps, activeAppId }: { apps: AppListItemType[]; activeAppI
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'}>
|
||||
<Box mt={4} px={4}>
|
||||
{!isTeamChat && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
py={2}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
_hover={{ bg: 'myGray.200' }}
|
||||
onClick={() => router.push('/app/list')}
|
||||
>
|
||||
<IconButton
|
||||
mr={3}
|
||||
icon={<MyIcon name={'common/backFill'} w={'1rem'} color={'primary.500'} />}
|
||||
bg={'white'}
|
||||
boxShadow={'1px 1px 9px rgba(0,0,0,0.15)'}
|
||||
size={'smSquare'}
|
||||
borderRadius={'50%'}
|
||||
aria-label={''}
|
||||
/>
|
||||
{t('common:core.chat.Exit Chat')}
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
{showRouteToAppDetail && (
|
||||
<>
|
||||
<Box mt={4} px={4}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
py={2}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
_hover={{ bg: 'myGray.200' }}
|
||||
onClick={() => router.push('/app/list')}
|
||||
>
|
||||
<IconButton
|
||||
mr={3}
|
||||
icon={<MyIcon name={'common/backFill'} w={'1rem'} color={'primary.500'} />}
|
||||
bg={'white'}
|
||||
boxShadow={'1px 1px 9px rgba(0,0,0,0.15)'}
|
||||
size={'smSquare'}
|
||||
borderRadius={'50%'}
|
||||
aria-label={''}
|
||||
/>
|
||||
{t('common:core.chat.Exit Chat')}
|
||||
</Flex>
|
||||
</Box>
|
||||
<MyDivider h={2} my={1} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isTeamChat && (
|
||||
<>
|
||||
<MyDivider h={2} my={1} />
|
||||
<HStack
|
||||
px={4}
|
||||
my={2}
|
||||
|
||||
@@ -7,20 +7,19 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatContext } from '@/web/core/chat/context/chatContext';
|
||||
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const ToolMenu = ({
|
||||
history,
|
||||
onRouteToAppDetail
|
||||
}: {
|
||||
history: ChatItemType[];
|
||||
onRouteToAppDetail?: () => void;
|
||||
}) => {
|
||||
const ToolMenu = ({ history }: { history: ChatItemType[] }) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { onExportChat } = useChatBox();
|
||||
|
||||
const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId);
|
||||
const chatData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
|
||||
const showRouteToAppDetail = useContextSelector(ChatItemContext, (v) => v.showRouteToAppDetail);
|
||||
|
||||
return history.length > 0 ? (
|
||||
return (
|
||||
<MyMenu
|
||||
Button={
|
||||
<IconButton
|
||||
@@ -61,14 +60,14 @@ const ToolMenu = ({
|
||||
// }
|
||||
]
|
||||
},
|
||||
...(onRouteToAppDetail
|
||||
...(showRouteToAppDetail
|
||||
? [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'core/app/aiLight',
|
||||
label: t('app:app_detail'),
|
||||
onClick: onRouteToAppDetail
|
||||
onClick: () => router.push(`/app/detail?appId=${chatData.appId}`)
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -76,8 +75,6 @@ const ToolMenu = ({
|
||||
: [])
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Box w={'28px'} h={'28px'} />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const { userInfo } = useUserStore();
|
||||
const { setLastChatAppId, chatId, appId, outLinkAuthData } = useChatStore();
|
||||
|
||||
@@ -186,7 +187,6 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
apps={myApps}
|
||||
history={chatRecords}
|
||||
showHistory
|
||||
onRouteToAppDetail={() => router.push(`/app/detail?appId=${appId}`)}
|
||||
/>
|
||||
|
||||
{/* chat box */}
|
||||
@@ -208,8 +208,6 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
feedbackType={'user'}
|
||||
onStartChat={onStartChat}
|
||||
chatType={'chat'}
|
||||
showRawSource
|
||||
showNodeStatus
|
||||
isReady={!loading}
|
||||
/>
|
||||
)}
|
||||
@@ -221,8 +219,8 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Render = (props: { appId: string }) => {
|
||||
const { appId } = props;
|
||||
const Render = (props: { appId: string; isStandalone?: string }) => {
|
||||
const { appId, isStandalone } = props;
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
@@ -276,7 +274,12 @@ const Render = (props: { appId: string }) => {
|
||||
|
||||
return source === ChatSourceEnum.online ? (
|
||||
<ChatContextProvider params={chatHistoryProviderParams}>
|
||||
<ChatItemContextProvider>
|
||||
<ChatItemContextProvider
|
||||
showRouteToAppDetail={isStandalone !== '1'}
|
||||
showRouteToDatasetDetail={isStandalone !== '1'}
|
||||
isShowReadRawSource={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
<Chat myApps={myApps} />
|
||||
</ChatRecordContextProvider>
|
||||
@@ -289,6 +292,7 @@ export async function getServerSideProps(context: any) {
|
||||
return {
|
||||
props: {
|
||||
appId: context?.query?.appId || '',
|
||||
isStandalone: context?.query?.isStandalone || '',
|
||||
...(await serviceSideProps(context, ['file', 'app', 'chat', 'workflow']))
|
||||
}
|
||||
};
|
||||
|
||||
@@ -55,7 +55,6 @@ type Props = {
|
||||
const OutLink = (props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { showRawSource, showNodeStatus } = props;
|
||||
const {
|
||||
shareId = '',
|
||||
showHistory = '1',
|
||||
@@ -287,8 +286,6 @@ const OutLink = (props: Props) => {
|
||||
feedbackType={'user'}
|
||||
onStartChat={startChat}
|
||||
chatType="share"
|
||||
showRawSource={showRawSource}
|
||||
showNodeStatus={showNodeStatus}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
@@ -340,7 +337,12 @@ const Render = (props: Props) => {
|
||||
|
||||
return source === ChatSourceEnum.share ? (
|
||||
<ChatContextProvider params={chatHistoryProviderParams}>
|
||||
<ChatItemContextProvider>
|
||||
<ChatItemContextProvider
|
||||
showRouteToAppDetail={false}
|
||||
showRouteToDatasetDetail={false}
|
||||
isShowReadRawSource={props.showRawSource}
|
||||
showNodeStatus={props.showNodeStatus}
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
<OutLink {...props} />
|
||||
</ChatRecordContextProvider>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import NextHead from '@/components/common/NextHead';
|
||||
import { getTeamChatInfo } from '@/web/core/chat/api';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -20,8 +20,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import ChatContextProvider, { ChatContext } from '@/web/core/chat/context/chatContext';
|
||||
import { AppListItemType } from '@fastgpt/global/core/app/type';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { InitChatResponse } from '@/global/core/chat/api';
|
||||
import { defaultChatData, GetChatTypeEnum } from '@/global/core/chat/constants';
|
||||
import { GetChatTypeEnum } from '@/global/core/chat/constants';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
|
||||
@@ -226,8 +225,6 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
feedbackType={'user'}
|
||||
onStartChat={startChat}
|
||||
chatType="team"
|
||||
showRawSource
|
||||
showNodeStatus
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
@@ -299,7 +296,12 @@ const Render = (props: Props) => {
|
||||
|
||||
return source === ChatSourceEnum.team ? (
|
||||
<ChatContextProvider params={contextParams}>
|
||||
<ChatItemContextProvider>
|
||||
<ChatItemContextProvider
|
||||
showRouteToAppDetail={false}
|
||||
showRouteToDatasetDetail={false}
|
||||
isShowReadRawSource={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
<Chat {...props} myApps={myApps} />
|
||||
</ChatRecordContextProvider>
|
||||
|
||||
@@ -111,8 +111,7 @@ const CollectionPageContextProvider = ({ children }: { children: ReactNode }) =>
|
||||
isLoading: isGetting,
|
||||
pageNum,
|
||||
pageSize
|
||||
} = usePagination<DatasetCollectionsListItemType>({
|
||||
api: getDatasetCollections,
|
||||
} = usePagination(getDatasetCollections, {
|
||||
pageSize: 20,
|
||||
params: {
|
||||
datasetId,
|
||||
|
||||
@@ -49,7 +49,6 @@ const CustomAPIFileInput = () => {
|
||||
parentId: '',
|
||||
parentName: ''
|
||||
});
|
||||
const [parentUuid, setParentUuid] = useState<string>('');
|
||||
const [paths, setPaths] = useState<ParentTreePathItemType[]>([]);
|
||||
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
@@ -128,7 +127,7 @@ const CustomAPIFileInput = () => {
|
||||
|
||||
const handleItemClick = useCallback(
|
||||
(item: APIFileItem) => {
|
||||
if (item.type === 'folder') {
|
||||
if (item.hasChild) {
|
||||
setPaths((state) => [...state, { parentId: item.id, parentName: item.name }]);
|
||||
return setParent({
|
||||
parentId: item.id,
|
||||
@@ -251,6 +250,7 @@ const CustomAPIFileInput = () => {
|
||||
<Box fontSize={'sm'} fontWeight={'medium'} color={'myGray.900'}>
|
||||
{item.name}
|
||||
</Box>
|
||||
{item.hasChild && <MyIcon name="core/chat/chevronRight" w={'18px'} ml={2} />}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -28,10 +28,10 @@ import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { useFolderDrag } from '@/components/common/folder/useFolderDrag';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import SideTag from './SideTag';
|
||||
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
|
||||
import UserBox from '@fastgpt/web/components/common/UserBox';
|
||||
|
||||
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
|
||||
|
||||
@@ -39,7 +39,6 @@ function List() {
|
||||
const { setLoading } = useSystemStore();
|
||||
const { isPc } = useSystem();
|
||||
const { t } = useTranslation();
|
||||
const { loadAndGetTeamMembers } = useUserStore();
|
||||
const {
|
||||
loadMyDatasets,
|
||||
setMoveDatasetId,
|
||||
@@ -81,10 +80,6 @@ function List() {
|
||||
}
|
||||
});
|
||||
|
||||
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
|
||||
manual: false
|
||||
});
|
||||
|
||||
const editPerDataset = useMemo(
|
||||
() => (editPerDatasetIndex !== undefined ? myDatasets[editPerDatasetIndex] : undefined),
|
||||
[editPerDatasetIndex, myDatasets]
|
||||
@@ -156,7 +151,6 @@ function List() {
|
||||
alignItems={'stretch'}
|
||||
>
|
||||
{formatDatasets.map((dataset, index) => {
|
||||
const owner = members.find((v) => v.tmbId === dataset.tmbId);
|
||||
const vectorModelAvatar = getModelProvider(dataset.vectorModel.provider)?.avatar;
|
||||
|
||||
return (
|
||||
@@ -265,14 +259,12 @@ function List() {
|
||||
color={'myGray.500'}
|
||||
>
|
||||
<HStack spacing={3.5}>
|
||||
{owner && (
|
||||
<HStack spacing={1}>
|
||||
<Avatar src={owner.avatar} w={'0.875rem'} borderRadius={'50%'} />
|
||||
<Box maxW={'150px'} className="textEllipsis" fontSize={'mini'}>
|
||||
{owner.memberName}
|
||||
</Box>
|
||||
</HStack>
|
||||
)}
|
||||
<UserBox
|
||||
sourceMember={dataset.sourceMember}
|
||||
fontSize="xs"
|
||||
avatarSize="1rem"
|
||||
spacing={0.5}
|
||||
/>
|
||||
<PermissionIconText
|
||||
flexShrink={0}
|
||||
private={dataset.private}
|
||||
|
||||
Reference in New Issue
Block a user