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

@@ -0,0 +1,10 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="36" height="36" fill="url(#paint0_linear_13996_748)"/>
<path d="M14.904 10.8976C14.904 10.2323 14.904 9.89961 15.0335 9.64549C15.1474 9.42195 15.3291 9.24021 15.5527 9.12631C15.8068 8.99683 16.1395 8.99683 16.8048 8.99683H19.1952C19.8605 8.99683 20.1932 8.99683 20.4473 9.12631C20.6709 9.24021 20.8526 9.42195 20.9665 9.64549C21.096 9.89961 21.096 10.2323 21.096 10.8976V12.598C21.096 13.2633 21.096 13.596 20.9665 13.8501C20.8526 14.0737 20.6709 14.2554 20.4473 14.3693C20.1932 14.4988 19.8605 14.4988 19.1952 14.4988H18.8506V17.1972H23.215C24.1296 17.1972 24.871 17.9386 24.871 18.8532V21.4949H25.0992C25.7645 21.4949 26.0972 21.4949 26.3513 21.6244C26.5749 21.7383 26.7566 21.92 26.8705 22.1435C27 22.3977 27 22.7303 27 23.3957V25.096C27 25.7614 27 26.0941 26.8705 26.3482C26.7566 26.5717 26.5749 26.7535 26.3513 26.8674C26.0972 26.9968 25.7645 26.9968 25.0992 26.9968H22.7088C22.0435 26.9968 21.7108 26.9968 21.4567 26.8674C21.2331 26.7535 21.0514 26.5717 20.9375 26.3482C20.808 26.0941 20.808 25.7614 20.808 25.096V23.3957C20.808 22.7303 20.808 22.3977 20.9375 22.1435C21.0514 21.92 21.2331 21.7383 21.4567 21.6244C21.7108 21.4949 22.0435 21.4949 22.7088 21.4949H22.999V19.0692H13.001V21.4949H13.2912C13.9565 21.4949 14.2892 21.4949 14.5433 21.6244C14.7669 21.7383 14.9486 21.92 15.0625 22.1435C15.192 22.3977 15.192 22.7303 15.192 23.3957V25.1027C15.192 25.768 15.192 26.1007 15.0625 26.3548C14.9486 26.5783 14.7669 26.7601 14.5433 26.874C14.2892 27.0035 13.9565 27.0035 13.2912 27.0035H10.9008C10.2355 27.0035 9.90279 27.0035 9.64866 26.874C9.42512 26.7601 9.24338 26.5783 9.12948 26.3548C9 26.1007 9 25.768 9 25.1027V23.3957C9 22.7303 9 22.3977 9.12948 22.1435C9.24338 21.92 9.42512 21.7383 9.64866 21.6244C9.90279 21.4949 10.2355 21.4949 10.9008 21.4949H11.129V18.8532C11.129 17.9386 11.8704 17.1972 12.785 17.1972H16.9657V14.4988H16.8048C16.1395 14.4988 15.8068 14.4988 15.5527 14.3693C15.3291 14.2554 15.1474 14.0737 15.0335 13.8501C14.904 13.596 14.904 13.2633 14.904 12.598V10.8976Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_13996_748" x1="36" y1="1.07288e-06" x2="-1.07288e-06" y2="36" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFDCF3"/>
<stop offset="1" stop-color="#00C2D8"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

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;
}
})),