feat: org CRUD (#3380)

* feat: add org schema

* feat: org manage UI

* feat: OrgInfoModal

* feat: org tree view

* feat: org management

* fix: init root org

* feat: org permission for app

* feat: org support for dataset

* fix: disable org role control

* styles: opt type signatures

* fix: remove unused permission

* feat: delete org collaborator
This commit is contained in:
a.e.
2024-12-30 13:49:56 +08:00
committed by archer
parent bb669ca3ff
commit 1fc77a126a
46 changed files with 1934 additions and 191 deletions

View File

@@ -1,27 +1,27 @@
import { useUserStore } from '@/web/support/user/useUserStore';
import { ChevronDownIcon } from '@chakra-ui/icons';
import {
Flex,
Box,
ModalBody,
Checkbox,
ModalFooter,
Button,
Checkbox,
Flex,
Grid,
HStack
HStack,
ModalBody,
ModalFooter
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import MyAvatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
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 { useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags';
import { CollaboratorContext } from './context';
import { useUserStore } from '@/web/support/user/useUserStore';
import { ChevronDownIcon } from '@chakra-ui/icons';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
export type AddModalPropsType = {
onClose: () => void;
@@ -30,22 +30,28 @@ export type AddModalPropsType = {
function AddMemberModal({ onClose, mode = 'member' }: AddModalPropsType) {
const { t } = useTranslation();
const { userInfo, loadAndGetTeamMembers, loadAndGetGroups, myGroups } = useUserStore();
const { userInfo, loadAndGetTeamMembers, loadAndGetGroups, myGroups, loadAndGetOrgs, myOrgs } =
useUserStore();
const { permissionList, collaboratorList, onUpdateCollaborators, getPerLabelList, permission } =
useContextSelector(CollaboratorContext, (v) => v);
const [searchText, setSearchText] = useState<string>('');
const { data: [members = [], groups = []] = [], loading: loadingMembersAndGroups } = useRequest2(
async () => {
if (!userInfo?.team?.teamId) return [[], []];
return await Promise.all([loadAndGetTeamMembers(true), loadAndGetGroups(true)]);
},
{
manual: false,
refreshDeps: [userInfo?.team?.teamId]
}
);
const { data: [members = [], groups = [], orgs = []] = [], loading: loadingMembersAndGroups } =
useRequest2(
async () => {
if (!userInfo?.team?.teamId) return [[], []];
return Promise.all([
loadAndGetTeamMembers(true),
loadAndGetGroups(true),
loadAndGetOrgs(true)
]);
},
{
manual: false,
refreshDeps: [userInfo?.team?.teamId]
}
);
const filterMembers = useMemo(() => {
return members.filter((item) => {
@@ -65,8 +71,20 @@ function AddMemberModal({ onClose, mode = 'member' }: AddModalPropsType) {
});
}, [groups, searchText, myGroups, mode, permission]);
const filterOrgs = useMemo(() => {
if (mode !== 'all') return [];
return orgs.filter((item) => {
if (item.path === '') return false; // exclude root org
if (!permission.isOwner && myOrgs.find((i) => String(i._id) !== String(item._id)))
return false;
if (!searchText) return true;
return item.name.includes(searchText);
});
}, [orgs, searchText, myOrgs, mode, permission]);
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
const [selectedGroupIdList, setSelectedGroupIdList] = useState<string[]>([]);
const [selectedOrgIdList, setSelectedOrgIdList] = useState<string[]>([]);
const [selectedPermission, setSelectedPermission] = useState(permissionList['read'].value);
const perLabel = useMemo(() => {
return getPerLabelList(selectedPermission).join('、');
@@ -77,6 +95,7 @@ function AddMemberModal({ onClose, mode = 'member' }: AddModalPropsType) {
onUpdateCollaborators({
members: selectedMemberIdList,
groups: selectedGroupIdList,
orgs: selectedOrgIdList,
permission: selectedPermission
}),
{
@@ -115,6 +134,44 @@ function AddMemberModal({ onClose, mode = 'member' }: AddModalPropsType) {
/>
<Flex flexDirection="column" mt="2" overflow={'auto'} maxH="400px">
{filterOrgs.map((org) => {
const onChange = () => {
setSelectedOrgIdList((state) => {
if (state.includes(org._id)) {
return state.filter((v) => v !== org._id);
}
return [...state, org._id];
});
};
const collaborator = collaboratorList.find((v) => v.orgId === org._id);
return (
<HStack
justifyContent="space-between"
key={org._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer',
...(!selectedOrgIdList.includes(org._id)
? { svg: { color: 'myGray.50' } }
: {})
}}
onClick={onChange}
>
<Checkbox isChecked={selectedOrgIdList.includes(org._id)} />
<MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{org.name}
</Box>
{!!collaborator && (
<PermissionTags permission={collaborator.permission.value} />
)}
</HStack>
);
})}
{filterGroups.map((group) => {
const onChange = () => {
setSelectedGroupIdList((state) => {
@@ -198,10 +255,44 @@ function AddMemberModal({ onClose, mode = 'member' }: AddModalPropsType) {
</Flex>
<Flex p="4" flexDirection="column">
<Box>
{t('user:has_chosen') + ': '}{' '}
{selectedMemberIdList.length + selectedGroupIdList.length}
{`${t('user:has_chosen')}: `}
{selectedMemberIdList.length + selectedGroupIdList.length + selectedOrgIdList.length}
</Box>
<Flex flexDirection="column" mt="2" overflow={'auto'} maxH="400px">
{selectedOrgIdList.map((orgId) => {
const org = orgs.find((v) => String(v._id) === orgId);
return (
<HStack
justifyContent="space-between"
key={orgId}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer',
...(!selectedOrgIdList.includes(orgId) ? { svg: { color: 'myGray.50' } } : {})
}}
onClick={() =>
setSelectedOrgIdList(selectedOrgIdList.filter((v) => v !== orgId))
}
>
<MyAvatar src={org?.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{org?.name}
</Box>
<MyIcon
name="common/closeLight"
w="16px"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
/>
</HStack>
);
})}
{selectedGroupIdList.map((groupId) => {
const onChange = () => {
setSelectedGroupIdList((state) => {

View File

@@ -1,19 +1,19 @@
import { ModalBody, Table, TableContainer, Tbody, Th, Thead, Tr, Td, Flex } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { Flex, ModalBody, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import Avatar from '@fastgpt/web/components/common/Avatar';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Loading from '@fastgpt/web/components/common/MyLoading';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { CollaboratorContext } from './context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useUserStore } from '@/web/support/user/useUserStore';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import Loading from '@fastgpt/web/components/common/MyLoading';
import { useTranslation } from 'next-i18next';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
export type ManageModalProps = {
onClose: () => void;
};
@@ -65,7 +65,7 @@ function ManageModal({ onClose }: ManageModalProps) {
>
<Td border="none">
<Flex alignItems="center">
<Avatar src={item.avatar} w="24px" mr={2} />
<Avatar src={item.avatar} rounded={'50%'} w="24px" mr={2} />
{item.name === DefaultGroupName ? userInfo?.team.teamName : item.name}
</Flex>
</Td>
@@ -85,14 +85,20 @@ function ManageModal({ onClose }: ManageModalProps) {
onUpdate({
members: item.tmbId ? [item.tmbId] : undefined,
groups: item.groupId ? [item.groupId] : undefined,
orgs: item.orgId ? [item.orgId] : undefined,
permission
});
}}
onDelete={() => {
onDelete({
tmbId: item.tmbId,
groupId: item.groupId
} as RequireOnlyOne<{ tmbId: string; groupId: string }>);
groupId: item.groupId,
orgId: item.orgId
} as RequireOnlyOne<{
tmbId: string;
groupId: string;
orgId: string;
}>);
}}
/>
)}

View File

@@ -1,13 +1,13 @@
import { Box, BoxProps, Flex } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { Box, type BoxProps, Flex } from '@chakra-ui/react';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyBox from '@fastgpt/web/components/common/MyBox';
import Tag, { type TagProps } from '@fastgpt/web/components/common/Tag';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context';
import Tag, { TagProps } from '@fastgpt/web/components/common/Tag';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useTranslation } from 'next-i18next';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { useUserStore } from '@/web/support/user/useUserStore';
export type MemberListCardProps = BoxProps & { tagStyle?: Omit<TagProps, 'children'> };
@@ -31,12 +31,12 @@ const MemberListCard = ({ tagStyle, ...props }: MemberListCardProps) => {
{collaboratorList?.map((member) => {
return (
<Tag
key={member.tmbId || member.groupId}
key={member.tmbId || member.groupId || member.orgId}
type={'fill'}
colorSchema="white"
{...tagStyle}
>
<Avatar src={member.avatar} w="1.25rem" />
<Avatar src={member.avatar} w="1.25rem" rounded={'50%'} />
<Box fontSize={'sm'} ml={1}>
{member.name === DefaultGroupName ? userInfo?.team.teamName : member.name}
</Box>

View File

@@ -1,21 +1,24 @@
import { useDisclosure } from '@chakra-ui/react';
import {
import type {
CollaboratorItemType,
UpdateClbPermissionProps
} from '@fastgpt/global/support/permission/collaborator';
import { PermissionList } from '@fastgpt/global/support/permission/constant';
import { Permission } from '@fastgpt/global/support/permission/controller';
import { PermissionListType, PermissionValueType } from '@fastgpt/global/support/permission/type';
import { ReactNode, useCallback } from 'react';
import type {
PermissionListType,
PermissionValueType
} from '@fastgpt/global/support/permission/type';
import { type ReactNode, useCallback } from 'react';
import { createContext } from 'use-context-selector';
import dynamic from 'next/dynamic';
import MemberListCard, { MemberListCardProps } from './MemberListCard';
import MemberListCard, { type MemberListCardProps } from './MemberListCard';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
const AddMemberModal = dynamic(() => import('./AddMemberModal'));
const ManageModal = dynamic(() => import('./ManageModal'));
@@ -24,7 +27,9 @@ export type MemberManagerInputPropsType = {
onGetCollaboratorList: () => Promise<CollaboratorItemType[]>;
permissionList: PermissionListType;
onUpdateCollaborators: (props: UpdateClbPermissionProps) => Promise<any>;
onDelOneCollaborator: (props: RequireOnlyOne<{ tmbId: string; groupId: string }>) => Promise<any>;
onDelOneCollaborator: (
props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }>
) => Promise<any>;
refreshDeps?: any[];
mode?: 'member' | 'all';
};
@@ -46,19 +51,19 @@ type CollaboratorContextType = MemberManagerPropsType & {};
export const CollaboratorContext = createContext<CollaboratorContextType>({
collaboratorList: [],
permissionList: PermissionList,
onUpdateCollaborators: function () {
onUpdateCollaborators: () => {
throw new Error('Function not implemented.');
},
onDelOneCollaborator: function () {
onDelOneCollaborator: () => {
throw new Error('Function not implemented.');
},
getPerLabelList: function (): string[] {
getPerLabelList: (): string[] => {
throw new Error('Function not implemented.');
},
refetchCollaboratorList: function (): void {
refetchCollaboratorList: (): void => {
throw new Error('Function not implemented.');
},
onGetCollaboratorList: function (): Promise<CollaboratorItemType[]> {
onGetCollaboratorList: (): Promise<CollaboratorItemType[]> => {
throw new Error('Function not implemented.');
},
isFetchingCollaborator: false,
@@ -88,7 +93,7 @@ const CollaboratorContextProvider = ({
refetchCollaboratorList();
};
const onDelOneCollaboratorThen = async (
props: RequireOnlyOne<{ tmbId: string; groupId: string }>
props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }>
) => {
await onDelOneCollaborator(props);
refetchCollaboratorList();

View File

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

View File

@@ -0,0 +1,150 @@
import { compressImgFileAndUpload } from '@/web/common/file/controller';
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 { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { DEFAULT_ORG_AVATAR } from '@fastgpt/global/common/system/constants';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
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 { useEffect } from 'react';
import { useForm } from 'react-hook-form';
export type OrgFormType = {
avatar: string;
description?: string;
name: string;
};
function OrgInfoModal({
editOrg,
createOrgParentId: parentId,
onClose,
onSuccess
}: {
editOrg?: OrgType;
createOrgParentId?: string;
onClose: () => void;
onSuccess?: () => void;
}) {
const { t } = useTranslation();
const { File: AvatarSelect, onOpen: onOpenSelectAvatar } = useSelectFile({
fileType: '.jpg, .jpeg, .png',
multiple: false
});
const { register, handleSubmit, getValues, setValue } = useForm<OrgFormType>({
defaultValues: {
name: '',
avatar: DEFAULT_ORG_AVATAR,
description: undefined
}
});
useEffect(() => {
setValue('name', editOrg?.name ?? '');
setValue('avatar', editOrg?.avatar || DEFAULT_ORG_AVATAR);
setValue('description', editOrg?.description);
}, [editOrg, setValue]);
const { run: onCreate, loading: isLoadingCreate } = useRequest2(
(data: OrgFormType, parentId: string) => {
return postCreateOrg({
name: data.name,
avatar: data.avatar,
parentId,
description: data.description
});
},
{
onSuccess: () => {
onClose();
onSuccess?.();
}
}
);
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
(data: OrgFormType, orgId: string) => {
return putUpdateOrg({
orgId,
name: data.name,
avatar: data.avatar,
description: data.description
});
},
{
onSuccess: () => {
onClose();
onSuccess?.();
}
}
);
const { loading: uploadingAvatar, run: onSelectAvatar } = useRequest2(
async (file: File[]) => {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.groupAvatar,
file: file[0],
maxW: 300,
maxH: 300
});
return src;
},
{
onSuccess: (src: string) => {
setValue('avatar', src);
}
}
);
const isLoading = uploadingAvatar;
return (
<MyModal
isOpen={!!(editOrg || parentId)}
onClose={onClose}
title={editOrg ? t('account_team:edit_org_info') : t('account_team:create_org')}
iconSrc={editOrg?.avatar || DEFAULT_ORG_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') || 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 {...register('description')} placeholder={t('account_team:org_description')} />
</ModalBody>
<ModalFooter alignItems="flex-end">
<Button
isLoading={isLoading}
onClick={handleSubmit((data) => {
if (editOrg) {
onUpdate(data, editOrg._id);
} else if (parentId) {
onCreate(data, parentId);
}
})}
>
{editOrg ? t('common:common.Save') : t('common:new_create')}
</Button>
</ModalFooter>
<AvatarSelect onSelect={onSelectAvatar} />
</MyModal>
);
}
export default OrgInfoModal;

View File

@@ -0,0 +1,202 @@
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 { useEffect, useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '../context';
export type GroupFormType = {
members: {
tmbId: string;
role: `${GroupMemberRole}`;
}[];
};
function CheckboxIcon({
name
}: {
isChecked?: boolean;
isIndeterminate?: boolean;
name: IconNameType;
}) {
return <MyIcon name={name} w="12px" />;
}
function OrgMemberModal({ onClose, editOrgId }: { onClose: () => void; editOrgId?: string }) {
// 1. Owner can not be deleted, toast
// 2. Owner/Admin can manage members
// 3. Owner can add/remove admins
const { t } = useTranslation();
const {
members: allMembers,
orgs,
refetchOrgs,
refetchMembers
} = useContextSelector(TeamContext, (v) => v);
const org = useMemo(() => orgs.find((item) => item._id === editOrgId), [editOrgId, orgs]);
const [members, setMembers] = useState<{ tmbId: string }[]>(org?.members || []);
useEffect(() => {
setMembers(org?.members || []);
}, [org]);
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 (!editOrgId) return;
return putUpdateOrgMembers({
orgId: editOrgId,
members
});
},
{
onSuccess: () => Promise.all([onClose(), refetchOrgs(), refetchMembers()])
}
);
const isSelected = (memberId: string) => {
return members.find((item) => item.tmbId === memberId);
};
const handleToggleSelect = (memberId: string) => {
if (isSelected(memberId)) {
setMembers(members.filter((item) => item.tmbId !== memberId));
} else {
setMembers([...members, { tmbId: memberId }]);
}
};
const isLoading = isLoadingUpdate;
return (
<MyModal
onClose={onClose}
isOpen={!!editOrgId}
title={t('user:team.group.manage_member')}
iconSrc={org?.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={<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')}:${members.length}`}</Box>
<Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'400px'}>
{members.map((member) => {
return (
<HStack
justifyContent="space-between"
py="2"
px={3}
borderRadius={'md'}
key={member.tmbId}
_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>
<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 OrgMemberModal;

View File

@@ -0,0 +1,80 @@
import { putMoveOrg, putMoveOrgMember } 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 type { TeamTmbItemType } from '@fastgpt/global/support/user/team/type';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import OrgTree from './OrgTree';
function OrgMoveModal({
movingOrg,
movingTmb,
orgs,
team,
onClose,
onSuccess
}: {
movingOrg?: OrgType;
movingTmb?: { tmbId: string; orgId: string };
orgs: OrgType[];
team: TeamTmbItemType;
onClose: () => void;
onSuccess: () => void;
}) {
const { t } = useTranslation();
const [selectedOrg, selectOrg] = useState<OrgType>();
const { runAsync: moveOrg, loading: loadingOrg } = useRequest2(putMoveOrg, {
onSuccess: () => {
onClose();
onSuccess();
}
});
const { runAsync: moveTmb, loading: loadingTmb } = useRequest2(putMoveOrgMember, {
onSuccess: () => {
onClose();
onSuccess();
}
});
const handleConfirm = () => {
if (!selectedOrg) return;
if (movingTmb) {
moveTmb({ orgId: movingTmb.orgId, tmbId: movingTmb.tmbId, newOrgId: selectedOrg._id });
} else if (movingOrg) {
moveOrg(movingOrg._id, selectedOrg._id);
}
};
const loading = loadingOrg || loadingTmb;
return (
<MyModal
isOpen={!!movingOrg || !!movingTmb}
onClose={onClose}
title={movingOrg ? t('account_team:move_org') : t('account_team:move_member')}
iconSrc="common/file/move"
iconColor="blue.600"
>
<ModalBody>
<OrgTree
orgs={orgs}
teamName={team.teamName}
teamAvatar={team.avatar}
selectedOrg={selectedOrg}
selectOrg={selectOrg}
/>
</ModalBody>
<ModalFooter>
<Button isDisabled={!selectedOrg} isLoading={loading} onClick={() => handleConfirm()}>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
}
export default OrgMoveModal;

View File

@@ -0,0 +1,115 @@
import { Box, HStack, Text, 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, useState } from 'react';
import IconButton from './IconButton';
function OrgTreeNode({
org,
list,
selectedOrg,
selectOrg,
indent = 0
}: {
org: OrgType;
list: OrgType[];
selectedOrg?: OrgType;
selectOrg?: (org?: OrgType) => void;
indent?: number;
}) {
const children = useMemo(
() => list.filter((item) => item.path === `${org.path}/${org._id}`),
[org, list]
);
const [isExpanded, toggleIsExpanded] = useToggle(false);
return (
<VStack alignItems={'start'} w="full" gap={'8px'}>
<HStack
w="full"
_hover={{ bgColor: selectedOrg === org ? 'blue.200' : 'gray.100' }}
borderRadius="4px"
boxSizing="border-box"
py="4px"
pl={`calc(${indent}rem + 4px)`}
transition={'background 0.1s'}
{...(selectedOrg === org ? { bgColor: 'blue.100' } : {})}
>
{children.length > 0 ? (
<IconButton
name={isExpanded ? 'common/downArrowFill' : 'common/rightArrowFill'}
onClick={() => toggleIsExpanded.toggle()}
/>
) : (
<Box w={'1rem'} h={'1rem'} m="1" />
)}
<HStack onClick={() => selectOrg?.(org)} cursor="pointer">
<Avatar src={org.avatar} w="20px" h="20px" rounded={'50%'} />
<Text>{org.name}</Text>
</HStack>
</HStack>
{isExpanded &&
children.length > 0 &&
children.map((child) => (
<OrgTreeNode
key={child._id}
org={child}
indent={indent + 1}
list={list}
selectedOrg={selectedOrg}
selectOrg={selectOrg}
/>
))}
</VStack>
);
}
function OrgTree({
orgs,
teamName,
teamAvatar,
selectedOrg,
selectOrg
}: {
orgs: OrgType[];
teamAvatar: string;
teamName: string;
selectedOrg?: OrgType;
selectOrg?: (org?: OrgType) => void;
}) {
const root = orgs[0];
if (!root) return null;
const children = useMemo(
() => orgs.filter((item) => item.path === `${root.path}/${root._id}`),
[root, orgs]
);
return (
<VStack alignItems={'start'} gap={'8px'}>
<HStack
w="full"
onClick={() => selectOrg?.(root)}
cursor="pointer"
_hover={{ bgColor: selectedOrg === root ? 'blue.200' : 'gray.100' }}
borderRadius="4px"
p="4px"
transition={'background 0.1s'}
{...(selectedOrg === root ? { bgColor: 'blue.100' } : {})}
>
<Avatar src={teamAvatar} w="20px" h="20px" rounded={'50%'} />
<Text>{teamName}</Text>
</HStack>
{children.map((child) => (
<OrgTreeNode
key={child._id}
org={child}
list={orgs}
selectOrg={selectOrg}
selectedOrg={selectedOrg}
/>
))}
</VStack>
);
}
export default OrgTree;

View File

@@ -0,0 +1,370 @@
import { deleteOrg, deleteOrgMember } from '@/web/support/user/team/org/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import {
Box,
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
Divider,
HStack,
Table,
TableContainer,
Tag,
Tbody,
Td,
Text,
Th,
Thead,
Tr,
VStack,
useDisclosure
} 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 { useEffect, useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import MemberTag from '../../../../../components/support/user/team/Info/MemberTag';
import { TeamContext } from '../context';
import IconButton from './IconButton';
import OrgInfoModal from './OrgInfoModal';
import OrgMemberModal from './OrgMemberModal';
import OrgMoveModal from './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="16px" h="16px" p="1" />
<Text fontSize={'12px'} lineHeight={'16px'}>
{text}
</Text>
</HStack>
);
}
function MemberTable() {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { orgs, refetchOrgs, members, refetchMembers, isLoading } = useContextSelector(
TeamContext,
(v) => v
);
const [currentOrg, setCurrentOrg] = useState<OrgType>();
// Set current org by hash
useEffect(() => {
const hash = window.location.hash.substring(1);
const initialOrg = orgs.find((org) => org._id === hash) || orgs[0];
setCurrentOrg(initialOrg);
}, [orgs, isLoading]);
// Update hash when current org changes
useEffect(() => {
if (currentOrg) {
window.location.hash = currentOrg._id;
}
}, [currentOrg]);
const currentPath = useMemo<{ path: string; parents: OrgType[] }>(
() => ({
path: currentOrg ? `${currentOrg.path}/${currentOrg._id}` : '',
parents: currentOrg
? currentOrg.path
.split('/')
.filter(Boolean)
.map((orgId) => orgs.find((org) => org._id === orgId)!)
: []
}),
[orgs, currentOrg]
);
const orgList = useMemo(
() =>
orgs
.filter((org) => org.path === currentPath.path)
.map((org) => {
// calc org members count
let count = org.members.length;
for (const item of orgs.filter((item) =>
item.path.startsWith(`${org.path}/${org._id}`)
)) {
count += item.members.length;
}
return { ...org, count };
}),
[orgs, currentPath]
);
const [editOrg, setEditOrg] = useState<OrgType | undefined>();
const [editMemberOrgId, setEditMemberOrgId] = useState<string | undefined>();
const [movingOrg, setMovingOrg] = useState<OrgType | undefined>();
const [movingTmb, setMovingTmb] = useState<{ tmbId: string; orgId: string } | undefined>();
const [createOrgParentId, setCreateOrgParentId] = useState<string | undefined>();
const { ConfirmModal: ConfirmDeleteOrgModal, openConfirm: openDeleteOrgModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_org')
});
const { ConfirmModal: ConfirmDeleteMember, openConfirm: openDeleteMemberModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_member')
});
const { runAsync: deleteOrgReq } = useRequest2(deleteOrg, {
onSuccess: () => {
refetchOrgs();
refetchMembers();
}
});
const { runAsync: deleteMemberReq } = useRequest2(deleteOrgMember, {
onSuccess: () => {
refetchOrgs();
refetchMembers();
}
});
const deleteOrgHandler = (orgId: string) => openDeleteOrgModal(() => deleteOrgReq(orgId))();
const deleteMemberHandler = (orgId: string, tmbId: string) =>
openDeleteMemberModal(() => deleteMemberReq(orgId, tmbId))();
return (
<VStack>
<Breadcrumb mr={'auto'}>
{currentPath.parents.map((parent) => (
<BreadcrumbItem key={parent._id}>
<BreadcrumbLink onClick={() => setCurrentOrg(parent)}>
{parent.path === '' ? userInfo?.team.teamName : parent.name}
</BreadcrumbLink>
</BreadcrumbItem>
))}
<BreadcrumbItem isCurrentPage>
<BreadcrumbLink color="myGray.900" fontWeight={500}>
{currentOrg?.path === '' ? userInfo?.team.teamName : currentOrg?.name}
</BreadcrumbLink>
</BreadcrumbItem>
</Breadcrumb>
<HStack w={'100%'} gap={'16px'} alignItems={'start'}>
<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>
{orgList.map((org) => (
<Tr key={org._id} overflow={'unset'}>
<Td>
<HStack w="fit-content" cursor={'pointer'} onClick={() => setCurrentOrg(org)}>
<MemberTag name={org.name} avatar={org.avatar} />
<Tag size="sm">{org.count}</Tag>
<MyIcon
name="core/chat/chevronRight"
w={'12px'}
h={'12px'}
color={'myGray.400'}
/>
</HStack>
</Td>
<Td w={'6rem'}>
<MyMenu
trigger="click"
Button={
<MyIcon name="more" w={'1rem'} cursor={'pointer'} p="1" rounded={'sm'} />
}
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} overflow={'unset'}>
<Td>
<MemberTag name={memberInfo.memberName} avatar={memberInfo.avatar} />
</Td>
<Td w={'6rem'}>
<MyMenu
trigger={'click'}
Button={
<MyIcon name="more" w={'1rem'} cursor={'pointer'} p="1" rounded={'sm'} />
}
menuList={[
{
children: [
// {
// icon: 'edit',
// label: t('account_team:remark'),
// onClick: () => {
// // TODO
// console.log(member.tmbId);
// }
// },
{
icon: 'common/file/move',
label: t('common:Move'),
onClick: () =>
setMovingTmb({ tmbId: member.tmbId, orgId: currentOrg!._id })
},
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () => deleteMemberHandler(currentOrg!._id, member.tmbId)
}
]
}
]}
/>
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
<VStack w={'220px'} alignItems={'start'}>
<HStack gap={'6px'}>
<Avatar
src={currentOrg?.path === '' ? userInfo?.team.avatar : currentOrg?.avatar}
w={'16px'}
h={'16px'}
rounded={'20%'}
/>
<Text fontWeight={500} fontSize={'14px'} color={'myGray.900'} lineHeight={'20px'}>
{currentOrg?.path === '' ? userInfo?.team.teamName : currentOrg?.name}
</Text>
{currentOrg?.path !== '' && (
<IconButton name="edit" onClick={() => setEditOrg(currentOrg)} />
)}
</HStack>
<Text fontSize={12} lineHeight={'16px'} w={'full'}>
{currentOrg?.description ?? t('common:common.no_intro')}
</Text>
<Divider my={'20px'} />
<Text fontWeight={500} mb="13px" fontSize="14px" color="myGray.900" lineHeight="20px">
{t('common:common.Action')}
</Text>
<VStack gap="13px" w="100%">
<ActionButton
icon="common/add2"
text={t('account_team:create_sub_org')}
onClick={() => {
setCreateOrgParentId(currentOrg?._id);
}}
/>
<ActionButton
icon="common/administrator"
text={t('account_team:manage_member')}
onClick={() => setEditMemberOrgId(currentOrg?._id)}
/>
{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>
</HStack>
<OrgInfoModal
editOrg={editOrg}
createOrgParentId={createOrgParentId}
onClose={() => {
setEditOrg(undefined);
setCreateOrgParentId(undefined);
}}
onSuccess={() => {
refetchOrgs();
refetchMembers();
}}
/>
<OrgMoveModal
orgs={orgs}
team={userInfo?.team!}
movingOrg={movingOrg}
movingTmb={movingTmb}
onClose={() => {
setMovingOrg(undefined);
setMovingTmb(undefined);
}}
onSuccess={() => {
refetchOrgs();
refetchMembers();
}}
/>
<OrgMemberModal editOrgId={editMemberOrgId} onClose={() => setEditMemberOrgId(undefined)} />
<ConfirmDeleteOrgModal />
<ConfirmDeleteMember />
</VStack>
);
}
export default MemberTable;

View File

@@ -9,7 +9,9 @@ import type { TeamTmbItemType, TeamMemberItemType } from '@fastgpt/global/suppor
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { getGroupList } from '@/web/support/user/team/group/api';
import { getOrgList } from '@/web/support/user/team/org/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'));
@@ -17,6 +19,7 @@ type TeamModalContextType = {
myTeams: TeamTmbItemType[];
members: TeamMemberItemType[];
groups: MemberGroupListType;
orgs: OrgType[];
isLoading: boolean;
onSwitchTeam: (teamId: string) => void;
setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>;
@@ -24,6 +27,7 @@ type TeamModalContextType = {
refetchMembers: () => void;
refetchTeams: () => void;
refetchGroups: () => void;
refetchOrgs: () => void;
searchKey: string;
setSearchKey: React.Dispatch<React.SetStateAction<string>>;
teamSize: number;
@@ -33,6 +37,7 @@ export const TeamContext = createContext<TeamModalContextType>({
myTeams: [],
groups: [],
members: [],
orgs: [],
isLoading: false,
onSwitchTeam: function (_teamId: string): void {
throw new Error('Function not implemented.');
@@ -49,6 +54,9 @@ export const TeamContext = createContext<TeamModalContextType>({
refetchGroups: function (): void {
throw new Error('Function not implemented.');
},
refetchOrgs: function (): void {
throw new Error('Function not implemented.');
},
searchKey: '',
setSearchKey: function (_value: React.SetStateAction<string>): void {
@@ -107,7 +115,17 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
refreshDeps: [userInfo?.team?.teamId]
});
const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers || isLoadingGroups;
const {
data: orgs = [],
loading: isLoadingOrgs,
refresh: refetchOrgs
} = useRequest2(getOrgList, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const isLoading =
isLoadingTeams || isSwitchingTeam || loadingMembers || isLoadingGroups || isLoadingOrgs;
const contextValue = {
myTeams,
@@ -123,6 +141,8 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
refetchMembers,
groups,
refetchGroups,
orgs,
refetchOrgs,
teamSize: members.length
};

View File

@@ -25,11 +25,13 @@ import MemberTable from './components/MemberTable';
const InviteModal = dynamic(() => import('./components/InviteModal'));
const PermissionManage = dynamic(() => import('./components/PermissionManage/index'));
const GroupManage = dynamic(() => import('./components/GroupManage/index'));
const OrgManage = dynamic(() => import('./components/OrgManage/index'));
const GroupInfoModal = dynamic(() => import('./components/GroupManage/GroupInfoModal'));
const ManageGroupMemberModal = dynamic(() => import('./components/GroupManage/GroupManageMember'));
export enum TeamTabEnum {
member = 'member',
org = 'org',
group = 'group',
permission = 'permission'
}
@@ -172,6 +174,7 @@ const Team = () => {
<FillRowTabs
list={[
{ label: t('account_team:member'), value: TeamTabEnum.member },
{ label: t('account_team:org'), value: TeamTabEnum.org },
{ label: t('account_team:group'), value: TeamTabEnum.group },
{ label: t('account_team:permission'), value: TeamTabEnum.permission }
]}
@@ -274,6 +277,7 @@ const Team = () => {
{teamTab === TeamTabEnum.group && (
<GroupManage onEditGroup={onEditGroup} onManageMember={onManageMember} />
)}
{teamTab === TeamTabEnum.org && <OrgManage />}
{teamTab === TeamTabEnum.permission && <PermissionManage />}
</Box>
</Box>

View File

@@ -1,40 +1,40 @@
import React, { useCallback } from 'react';
import CollaboratorContextProvider from '@/components/support/permission/MemberManager/context';
import ResumeInherit from '@/components/support/permission/ResumeInheritText';
import { AppContext } from '@/pages/app/detail/components/context';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useI18n } from '@/web/context/I18n';
import { resumeInheritPer } from '@/web/core/app/api';
import {
deleteAppCollaborators,
getCollaboratorList,
postUpdateAppCollaborators
} from '@/web/core/app/api/collaborator';
import {
Box,
Flex,
Button,
Flex,
FormControl,
Input,
Textarea,
ModalBody,
ModalFooter,
ModalBody
Textarea
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import CollaboratorContextProvider from '@/components/support/permission/MemberManager/context';
import {
postUpdateAppCollaborators,
deleteAppCollaborators,
getCollaboratorList
} from '@/web/core/app/api/collaborator';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pages/app/detail/components/context';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import type { AppSchema } from '@fastgpt/global/core/app/type.d';
import { AppPermissionList } from '@fastgpt/global/support/permission/app/constant';
import type { PermissionValueType } from '@fastgpt/global/support/permission/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { resumeInheritPer } from '@/web/core/app/api';
import { useI18n } from '@/web/context/I18n';
import ResumeInherit from '@/components/support/permission/ResumeInheritText';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
import React, { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { useContextSelector } from 'use-context-selector';
const InfoModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
@@ -126,20 +126,23 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
const onUpdateCollaborators = ({
members,
groups,
orgs,
permission
}: {
members?: string[];
groups?: string[];
orgs?: string[];
permission: PermissionValueType;
}) =>
postUpdateAppCollaborators({
members,
groups,
permission,
orgs,
appId: appDetail._id
});
const onDelCollaborator = async (props: RequireOnlyOne<{ tmbId: string; groupId: string }>) =>
const onDelCollaborator = async (props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }>) =>
deleteAppCollaborators({
appId: appDetail._id,
...props
@@ -211,7 +214,8 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
onUpdateCollaborators({
permission: props.permission,
members: props.members,
groups: props.groups
groups: props.groups,
orgs: props.orgs
})
}
onDelOneCollaborator={onDelCollaborator}

View File

@@ -258,20 +258,20 @@ const ListItem = () => {
{(AppFolderTypeList.includes(app.type)
? app.permission.hasManagePer
: app.permission.hasWritePer) && (
<Box className="more" display={['', 'none']}>
<MyMenu
size={'xs'}
Button={
<IconButton
size={'xsSquare'}
variant={'transparentBase'}
icon={<MyIcon name={'more'} w={'0.875rem'} color={'myGray.500'} />}
aria-label={''}
/>
}
menuList={[
...([AppTypeEnum.simple, AppTypeEnum.workflow].includes(app.type)
? [
<Box className="more" display={['', 'none']}>
<MyMenu
size={'xs'}
Button={
<IconButton
size={'xsSquare'}
variant={'transparentBase'}
icon={<MyIcon name={'more'} w={'0.875rem'} color={'myGray.500'} />}
aria-label={''}
/>
}
menuList={[
...([AppTypeEnum.simple, AppTypeEnum.workflow].includes(app.type)
? [
{
children: [
{
@@ -285,9 +285,9 @@ const ListItem = () => {
]
}
]
: []),
...([AppTypeEnum.plugin].includes(app.type)
? [
: []),
...([AppTypeEnum.plugin].includes(app.type)
? [
{
children: [
{
@@ -301,9 +301,9 @@ const ListItem = () => {
]
}
]
: []),
...(app.permission.hasManagePer
? [
: []),
...(app.permission.hasManagePer
? [
{
children: [
{
@@ -330,34 +330,34 @@ const ListItem = () => {
}
},
...(folderDetail?.type === AppTypeEnum.httpPlugin &&
!(parentApp ? parentApp.permission : app.permission)
.hasManagePer
!(parentApp ? parentApp.permission : app.permission)
.hasManagePer
? []
: [
{
icon: 'common/file/move',
type: 'grayBg' as MenuItemType,
label: t('common:common.folder.Move to'),
onClick: () => setMoveAppId(app._id)
}
]),
{
icon: 'common/file/move',
type: 'grayBg' as MenuItemType,
label: t('common:common.folder.Move to'),
onClick: () => setMoveAppId(app._id)
}
]),
...(app.permission.hasManagePer
? [
{
icon: 'support/team/key',
type: 'grayBg' as MenuItemType,
label: t('common:permission.Permission'),
onClick: () => setEditPerAppIndex(index)
}
]
{
icon: 'support/team/key',
type: 'grayBg' as MenuItemType,
label: t('common:permission.Permission'),
onClick: () => setEditPerAppIndex(index)
}
]
: [])
]
}
]
: []),
...(AppFolderTypeList.includes(app.type)
? []
: [
: []),
...(AppFolderTypeList.includes(app.type)
? []
: [
{
children: [
{
@@ -370,8 +370,8 @@ const ListItem = () => {
]
}
]),
...(app.permission.isOwner
? [
...(app.permission.isOwner
? [
{
children: [
{
@@ -390,11 +390,11 @@ const ListItem = () => {
]
}
]
: [])
]}
/>
</Box>
)}
: [])
]}
/>
</Box>
)}
</HStack>
</Flex>
</MyBox>
@@ -438,6 +438,7 @@ const ListItem = () => {
onUpdateCollaborators: (props: {
members?: string[];
groups?: string[];
orgs?: string[];
permission: number;
}) =>
postUpdateAppCollaborators({
@@ -448,6 +449,7 @@ const ListItem = () => {
props: RequireOnlyOne<{
tmbId?: string;
groupId?: string;
orgId?: string;
}>
) =>
deleteAppCollaborators({

View File

@@ -324,10 +324,12 @@ const MyApps = () => {
refreshDeps: [folderDetail._id, folderDetail.inheritPermission],
onDelOneCollaborator: async ({
tmbId,
groupId
groupId,
orgId
}: {
tmbId?: string;
groupId?: string;
orgId?: string;
}) => {
if (tmbId) {
return deleteAppCollaborators({
@@ -339,6 +341,11 @@ const MyApps = () => {
appId: folderDetail._id,
groupId
});
} else if (orgId) {
return deleteAppCollaborators({
appId: folderDetail._id,
orgId
});
}
}
}}

View File

@@ -392,7 +392,7 @@ const Info = ({ datasetId }: { datasetId: string }) => {
...body,
datasetId
}),
onDelOneCollaborator: async ({ groupId, tmbId }) => {
onDelOneCollaborator: async ({ groupId, tmbId, orgId }) => {
if (tmbId) {
return deleteDatasetCollaborators({
datasetId,
@@ -403,6 +403,11 @@ const Info = ({ datasetId }: { datasetId: string }) => {
datasetId,
groupId
});
} else if (orgId) {
return deleteDatasetCollaborators({
datasetId,
orgId
});
}
}
}}

View File

@@ -257,7 +257,7 @@ const Dataset = () => {
permission,
datasetId: folderDetail._id
}),
onDelOneCollaborator: async ({ tmbId, groupId }) => {
onDelOneCollaborator: async ({ tmbId, groupId, orgId }) => {
if (tmbId) {
return deleteDatasetCollaborators({
datasetId: folderDetail._id,
@@ -268,6 +268,11 @@ const Dataset = () => {
datasetId: folderDetail._id,
groupId
});
} else if (orgId) {
return deleteDatasetCollaborators({
datasetId: folderDetail._id,
orgId
});
}
},
refreshDeps: [folderDetail._id, folderDetail.inheritPermission]

View File

@@ -0,0 +1,34 @@
import { DELETE, GET, POST, PUT } from '@/web/common/api/request';
import type {
postCreateOrgData,
putUpdateOrgData,
putUpdateOrgMembersData,
putMoveOrgMemberData
} from '@fastgpt/global/support/user/team/org/api';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
export const getOrgList = () => GET<OrgType[]>('/proApi/support/user/team/org/list');
export const postCreateOrg = (data: postCreateOrgData) =>
POST('/proApi/support/user/team/org/create', data);
export const deleteOrg = (orgId: string) =>
DELETE('/proApi/support/user/team/org/delete', { orgId });
export const deleteOrgMember = (orgId: string, tmbId: string) =>
DELETE('/proApi/support/user/team/org/deleteMember', { orgId, tmbId });
export const putMoveOrg = (orgId: string, parentId: string) =>
PUT('/proApi/support/user/team/org/move', { orgId, parentId });
export const putMoveOrgMember = (data: putMoveOrgMemberData) =>
PUT('/proApi/support/user/team/org/moveMember', data);
export const putUpdateOrg = (data: putUpdateOrgData) =>
PUT('/proApi/support/user/team/org/update', data);
export const putUpdateOrgMembers = (data: putUpdateOrgMembersData) =>
PUT('/proApi/support/user/team/org/updateMembers', data);
// export const putChnageOrgOwner = (data: putChnageOrgOwnerData) =>
// PUT('/proApi/support/user/team/org/changeOwner', data);

View File

@@ -1,16 +1,18 @@
import type { UserUpdateParams } from '@/types/user';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { getTokenLogin, putUserInfo } from '@/web/support/user/api';
import { getTeamMembers } from '@/web/support/user/team/api';
import type { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import type { OrgMemberSchemaType, OrgType } from '@fastgpt/global/support/user/team/org/type';
import type { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import type { UserType } from '@fastgpt/global/support/user/type.d';
import type { FeTeamPlanStatusType } from '@fastgpt/global/support/wallet/sub/type';
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import type { UserUpdateParams } from '@/types/user';
import type { UserType } from '@fastgpt/global/support/user/type.d';
import { getTokenLogin, putUserInfo } from '@/web/support/user/api';
import { FeTeamPlanStatusType } from '@fastgpt/global/support/wallet/sub/type';
import { getTeamPlanStatus } from './team/api';
import { getTeamMembers } from '@/web/support/user/team/api';
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import { getGroupList } from './team/group/api';
import { getOrgList } from './team/org/api';
type State = {
systemMsgReadId: string;
@@ -30,6 +32,10 @@ type State = {
teamMemberGroups: MemberGroupListType;
myGroups: MemberGroupListType;
loadAndGetGroups: (init?: boolean) => Promise<MemberGroupListType>;
teamOrgs: OrgType[];
myOrgs: OrgType[];
loadAndGetOrgs: (init?: boolean) => Promise<OrgType[]>;
};
export const useUserStore = create<State>()(
@@ -107,6 +113,7 @@ export const useUserStore = create<State>()(
return res;
},
teamMemberGroups: [],
teamOrgs: [],
myGroups: [],
loadAndGetGroups: async (init = false) => {
if (!useSystemStore.getState()?.feConfigs?.isPlus) return [];
@@ -123,6 +130,23 @@ export const useUserStore = create<State>()(
);
});
return res;
},
myOrgs: [],
loadAndGetOrgs: async (init = false) => {
if (!useSystemStore.getState()?.feConfigs?.isPlus) return [];
const randomRefresh = Math.random() > 0.7;
if (!randomRefresh && !init && get().myOrgs.length) return Promise.resolve(get().myOrgs);
const res = await getOrgList();
set((state) => {
state.teamOrgs = res;
state.myOrgs = res.filter((item) =>
item.members.map((i) => String(i.tmbId)).includes(String(state.userInfo?.team?.tmbId))
);
});
return res;
}
})),