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:
10
projects/app/public/imgs/avatar/defaultOrgAvatar.svg
Normal file
10
projects/app/public/imgs/avatar/defaultOrgAvatar.svg
Normal 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 |
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}>);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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]
|
||||
|
||||
34
projects/app/src/web/support/user/team/org/api.ts
Normal file
34
projects/app/src/web/support/user/team/org/api.ts
Normal 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);
|
||||
@@ -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;
|
||||
}
|
||||
})),
|
||||
|
||||
Reference in New Issue
Block a user