diff --git a/docSite/content/zh-cn/docs/development/upgrading/4818.md b/docSite/content/zh-cn/docs/development/upgrading/4818.md new file mode 100644 index 000000000000..cfdc2454ff41 --- /dev/null +++ b/docSite/content/zh-cn/docs/development/upgrading/4818.md @@ -0,0 +1,13 @@ +--- +title: 'V4.8.18(进行中)' +description: 'FastGPT V4.8.18 更新说明' +icon: 'upgrade' +draft: false +toc: true +weight: 806 +--- + +## 完整更新内容 + +1. +2. 新增 - 支持组织架构权限模式 \ No newline at end of file diff --git a/packages/global/support/user/team/org/api.d.ts b/packages/global/support/user/team/org/api.d.ts index 605e9940de88..69ca09796225 100644 --- a/packages/global/support/user/team/org/api.d.ts +++ b/packages/global/support/user/team/org/api.d.ts @@ -20,15 +20,9 @@ export type putUpdateOrgData = { description?: string; }; -export type putMoveOrgData = { +export type putMoveOrgType = { orgId: string; - parentId: string; -}; - -export type putMoveOrgMemberData = { - orgId: string; - tmbId: string; - newOrgId: string; + targetOrgId: string; }; // type putChnageOrgOwnerData = { diff --git a/packages/global/support/user/team/type.d.ts b/packages/global/support/user/team/type.d.ts index 9914e631a8d1..d618b3130986 100644 --- a/packages/global/support/user/team/type.d.ts +++ b/packages/global/support/user/team/type.d.ts @@ -55,6 +55,7 @@ export type TeamMemberWithTeamAndUserSchema = TeamMemberSchema & { export type TeamTmbItemType = { userId: string; teamId: string; + teamAvatar?: string; teamName: string; memberName: string; avatar: string; diff --git a/packages/service/support/permission/auth/org.ts b/packages/service/support/permission/auth/org.ts index 4262a8565323..d864b1cb5f3d 100644 --- a/packages/service/support/permission/auth/org.ts +++ b/packages/service/support/permission/auth/org.ts @@ -1,20 +1,24 @@ import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; import { AuthModeType, AuthResponseType } from '../type'; -import { parseHeaderCert } from '../controller'; -import { getTmbInfoByTmbId } from '../../user/team/controller'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; +import { authUserPer } from '../user/auth'; +import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant'; +/* + Team manager can control org +*/ export const authOrgMember = async ({ orgIds, - req, - authToken = false, - authRoot = false, - authApiKey = false + ...props }: { orgIds: string | string[]; } & AuthModeType): Promise => { - const result = await parseHeaderCert({ req, authToken, authApiKey, authRoot }); - const { teamId, tmbId, isRoot } = result; + const result = await authUserPer({ + ...props, + per: ManagePermissionVal + }); + const { teamId, tmbId, isRoot, tmb } = result; + if (isRoot) { return { teamId, @@ -28,13 +32,6 @@ export const authOrgMember = async ({ }; } - if (!Array.isArray(orgIds)) { - orgIds = [orgIds]; - } - - // const promises = orgIds.map((orgId) => getOrgMemberRole({ orgId, tmbId })); - - const tmb = await getTmbInfoByTmbId({ tmbId }); if (tmb.permission.hasManagePer) { return { ...result, @@ -43,16 +40,4 @@ export const authOrgMember = async ({ } return Promise.reject(TeamErrEnum.unAuthTeam); - - // const targetRole = OrgMemberRole[role]; - // for (const orgRole of orgRoles) { - // if (!orgRole || checkOrgRole(orgRole, targetRole)) { - // return Promise.reject(TeamErrEnum.unAuthTeam); - // } - // } - - // return { - // ...result, - // permission: tmb.permission - // }; }; diff --git a/packages/service/support/permission/org/controllers.ts b/packages/service/support/permission/org/controllers.ts index 1a0aecef2d29..4c8263ac092e 100644 --- a/packages/service/support/permission/org/controllers.ts +++ b/packages/service/support/permission/org/controllers.ts @@ -1,8 +1,5 @@ import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; -import type { - OrgMemberSchemaType, - OrgSchemaType -} from '@fastgpt/global/support/user/team/org/type'; +import type { OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; import type { ClientSession } from 'mongoose'; import { MongoOrgModel } from './orgSchema'; import { MongoOrgMemberModel } from './orgMemberSchema'; @@ -36,16 +33,6 @@ import { MongoOrgMemberModel } from './orgMemberSchema'; // return compareRole(role, targetRole) >= 0; // }; -export const getOrgsByTeamId = async (teamId: string) => { - const orgs = await MongoOrgModel.find({ - teamId - }) - .populate<{ members: OrgMemberSchemaType }>('members') - .lean(); - - return orgs; -}; - export const getOrgsByTmbId = async ({ teamId, tmbId }: { teamId: string; tmbId: string }) => MongoOrgMemberModel.find({ teamId, tmbId }, 'orgId').lean(); diff --git a/packages/service/support/permission/org/orgSchema.ts b/packages/service/support/permission/org/orgSchema.ts index 58e2e705b863..a883c02b746d 100644 --- a/packages/service/support/permission/org/orgSchema.ts +++ b/packages/service/support/permission/org/orgSchema.ts @@ -53,15 +53,10 @@ OrgSchema.virtual('permission', { }); try { - OrgSchema.index( - { - teamId: 1, - path: 1 - }, - { - unique: true - } - ); + OrgSchema.index({ + teamId: 1, + path: 1 + }); } catch (error) { console.log(error); } diff --git a/packages/service/support/user/team/controller.ts b/packages/service/support/user/team/controller.ts index b9b4d25c4b26..2442b0a7b07a 100644 --- a/packages/service/support/user/team/controller.ts +++ b/packages/service/support/user/team/controller.ts @@ -33,6 +33,7 @@ async function getTeamMember(match: Record): Promise { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && mutation.attributeName === 'data-bot-src') { + const newBotSrc = script.getAttribute('data-bot-src'); + if (newBotSrc) { + iframe.src = newBotSrc; + } + } + }); + }); + observer.observe(script, { + attributes: true, + attributeFilter: ['data-bot-src'] + }); + let chatBtnDragged = false; let chatBtnDown = false; let chatBtnMouseX; @@ -96,3 +111,4 @@ function embedChatbot() { document.body.appendChild(ChatBtn); } window.addEventListener('load', embedChatbot); + diff --git a/projects/app/src/pages/account/team/components/GroupManage/index.tsx b/projects/app/src/pages/account/team/components/GroupManage/index.tsx index 895d9112c70a..dd7b95a4696a 100644 --- a/projects/app/src/pages/account/team/components/GroupManage/index.tsx +++ b/projects/app/src/pages/account/team/components/GroupManage/index.tsx @@ -26,6 +26,7 @@ import MemberTag from '../../../../../components/support/user/team/Info/MemberTa import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import dynamic from 'next/dynamic'; import { useState } from 'react'; +import IconButton from '../OrgManage/IconButton'; const ChangeOwnerModal = dynamic(() => import('./GroupTransferOwnerModal')); @@ -157,7 +158,7 @@ function MemberTable({ {hasGroupManagePer(group) && group.name !== DefaultGroupName && ( } + Button={} menuList={[ { children: [ diff --git a/projects/app/src/pages/account/team/components/OrgManage/IconButton.tsx b/projects/app/src/pages/account/team/components/OrgManage/IconButton.tsx index 346806da12c0..cb543dc1ab36 100644 --- a/projects/app/src/pages/account/team/components/OrgManage/IconButton.tsx +++ b/projects/app/src/pages/account/team/components/OrgManage/IconButton.tsx @@ -1,12 +1,20 @@ +import { IconProps } from '@chakra-ui/react'; import MyIcon from '@fastgpt/web/components/common/Icon'; import type { IconNameType } from '@fastgpt/web/components/common/Icon/type'; -function IconButton({ name, onClick }: { name: IconNameType; onClick: () => void }) { +function IconButton({ + name, + w = '1rem', + h = '1rem', + ...props +}: { + name: IconNameType; +} & IconProps) { return ( void bg: 'myGray.05', color: 'primary.600' }} - onClick={onClick} + {...props} /> ); } diff --git a/projects/app/src/pages/account/team/components/OrgManage/OrgInfoModal.tsx b/projects/app/src/pages/account/team/components/OrgManage/OrgInfoModal.tsx index e8d6799f5382..981cf7e09b76 100644 --- a/projects/app/src/pages/account/team/components/OrgManage/OrgInfoModal.tsx +++ b/projects/app/src/pages/account/team/components/OrgManage/OrgInfoModal.tsx @@ -4,86 +4,95 @@ 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 dynamic from 'next/dynamic'; import { useForm } from 'react-hook-form'; export type OrgFormType = { + _id: string; avatar: string; description?: string; name: string; + path: string; + parentId?: string; +}; + +export const defaultOrgForm: OrgFormType = { + _id: '', + avatar: '', + description: '', + name: '', + path: '' }; function OrgInfoModal({ editOrg, - createOrgParentId: parentId, onClose, onSuccess }: { - editOrg?: OrgType; - createOrgParentId?: string; + editOrg: OrgFormType; onClose: () => void; - onSuccess?: () => void; + onSuccess: () => void; }) { const { t } = useTranslation(); - const { File: AvatarSelect, onOpen: onOpenSelectAvatar } = useSelectFile({ - fileType: '.jpg, .jpeg, .png', - multiple: false - }); - const { register, handleSubmit, getValues, setValue } = useForm({ + const isEdit = !!editOrg._id; + + const { register, handleSubmit, setValue, watch } = useForm({ defaultValues: { - name: '', - avatar: DEFAULT_ORG_AVATAR, - description: undefined + name: editOrg.name, + avatar: editOrg.avatar, + description: editOrg.description } }); - - useEffect(() => { - setValue('name', editOrg?.name ?? ''); - setValue('avatar', editOrg?.avatar || DEFAULT_ORG_AVATAR); - setValue('description', editOrg?.description); - }, [editOrg, setValue]); + const avatar = watch('avatar'); const { run: onCreate, loading: isLoadingCreate } = useRequest2( - (data: OrgFormType, parentId: string) => { + async (data: OrgFormType) => { + if (!editOrg.parentId) return; return postCreateOrg({ name: data.name, avatar: data.avatar, - parentId, + parentId: editOrg.parentId, description: data.description }); }, { + successToast: t('common:common.Create Success'), onSuccess: () => { onClose(); - onSuccess?.(); + onSuccess(); } } ); const { run: onUpdate, loading: isLoadingUpdate } = useRequest2( - (data: OrgFormType, orgId: string) => { + async (data: OrgFormType) => { + if (!editOrg._id) return; return putUpdateOrg({ - orgId, + orgId: editOrg._id, name: data.name, avatar: data.avatar, description: data.description }); }, { + successToast: t('common:common.Update Success'), onSuccess: () => { onClose(); - onSuccess?.(); + onSuccess(); } } ); + const { File: AvatarSelect, onOpen: onOpenSelectAvatar } = useSelectFile({ + fileType: '.jpg, .jpeg, .png', + multiple: false + }); const { loading: uploadingAvatar, run: onSelectAvatar } = useRequest2( async (file: File[]) => { const src = await compressImgFileAndUpload({ @@ -101,20 +110,20 @@ function OrgInfoModal({ } ); - const isLoading = uploadingAvatar; + const isLoading = uploadingAvatar || isLoadingUpdate || isLoadingCreate; return ( {t('user:team.avatar_and_name')} { - if (editOrg) { - onUpdate(data, editOrg._id); - } else if (parentId) { - onCreate(data, parentId); + if (isEdit) { + onUpdate(data); + } else { + onCreate(data); } })} > - {editOrg ? t('common:common.Save') : t('common:new_create')} + {isEdit ? t('common:common.Save') : t('common:new_create')} @@ -147,4 +156,4 @@ function OrgInfoModal({ ); } -export default OrgInfoModal; +export default dynamic(() => Promise.resolve(OrgInfoModal), { ssr: false }); diff --git a/projects/app/src/pages/account/team/components/OrgManage/OrgMemberModal.tsx b/projects/app/src/pages/account/team/components/OrgManage/OrgMemberManageModal.tsx similarity index 69% rename from projects/app/src/pages/account/team/components/OrgManage/OrgMemberModal.tsx rename to projects/app/src/pages/account/team/components/OrgManage/OrgMemberManageModal.tsx index 65ac1ffea10d..7ef0a3787a2d 100644 --- a/projects/app/src/pages/account/team/components/OrgManage/OrgMemberModal.tsx +++ b/projects/app/src/pages/account/team/components/OrgManage/OrgMemberManageModal.tsx @@ -18,9 +18,11 @@ 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 { useMemo, useState } from 'react'; import { useContextSelector } from 'use-context-selector'; import { TeamContext } from '../context'; +import { OrgType } from '@fastgpt/global/support/user/team/org/type'; +import dynamic from 'next/dynamic'; export type GroupFormType = { members: { @@ -39,69 +41,67 @@ function CheckboxIcon({ return ; } -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 +function OrgMemberManageModal({ + currentOrg, + refetchOrgs, + onClose +}: { + currentOrg: OrgType; + refetchOrgs: () => void; + onClose: () => void; +}) { 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 || []); + const allMembers = useContextSelector(TeamContext, (v) => v.members); - useEffect(() => { - setMembers(org?.members || []); - }, [org]); + const [selectedMembers, setSelectedMembers] = useState( + currentOrg.members.map((item) => item.tmbId) + ); const [searchKey, setSearchKey] = useState(''); - const filtered = useMemo(() => { - return [ - ...allMembers.filter((member) => { - if (member.memberName.toLowerCase().includes(searchKey.toLowerCase())) return true; - return false; - }) - ]; + const filterMembers = useMemo(() => { + if (!searchKey) return allMembers; + const regx = new RegExp(searchKey, 'i'); + return allMembers.filter((member) => regx.test(member.memberName)); }, [searchKey, allMembers]); const { run: onUpdate, loading: isLoadingUpdate } = useRequest2( - async () => { - if (!editOrgId) return; + () => { return putUpdateOrgMembers({ - orgId: editOrgId, - members + orgId: currentOrg._id, + members: selectedMembers.map((tmbId) => ({ + tmbId + })) }); }, { - onSuccess: () => Promise.all([onClose(), refetchOrgs(), refetchMembers()]) + successToast: t('common:common.Update Success'), + onSuccess() { + refetchOrgs(); + onClose(); + } } ); const isSelected = (memberId: string) => { - return members.find((item) => item.tmbId === memberId); + return selectedMembers.find((tmbId) => tmbId === memberId); }; const handleToggleSelect = (memberId: string) => { if (isSelected(memberId)) { - setMembers(members.filter((item) => item.tmbId !== memberId)); + setSelectedMembers((state) => state.filter((tmbId) => tmbId !== memberId)); } else { - setMembers([...members, { tmbId: memberId }]); + setSelectedMembers((state) => [...state, memberId]); } }; const isLoading = isLoadingUpdate; + return ( void; editOrgId }} /> - {filtered.map((member) => { + {filterMembers.map((member) => { return ( void; editOrgId - {`${t('common:chosen')}:${members.length}`} + {`${t('common:chosen')}:${selectedMembers.length}`} - {members.map((member) => { + {selectedMembers.map((tmbId) => { + const member = allMembers.find((item) => item.tmbId === tmbId)!; return ( - item.tmbId === member.tmbId)?.avatar} - w="1.5rem" - borderRadius={'md'} - /> - - {allMembers.find((item) => item.tmbId === member.tmbId)?.memberName} - + + {member?.memberName} handleToggleSelect(member.tmbId)} + onClick={() => handleToggleSelect(tmbId)} /> ); @@ -199,4 +194,4 @@ function OrgMemberModal({ onClose, editOrgId }: { onClose: () => void; editOrgId ); } -export default OrgMemberModal; +export default dynamic(() => Promise.resolve(OrgMemberManageModal), { ssr: false }); diff --git a/projects/app/src/pages/account/team/components/OrgManage/OrgMoveModal.tsx b/projects/app/src/pages/account/team/components/OrgManage/OrgMoveModal.tsx index 6fe782d4d233..df4dbf0a3a3c 100644 --- a/projects/app/src/pages/account/team/components/OrgManage/OrgMoveModal.tsx +++ b/projects/app/src/pages/account/team/components/OrgManage/OrgMoveModal.tsx @@ -1,75 +1,69 @@ -import { putMoveOrg, putMoveOrgMember } from '@/web/support/user/team/org/api'; +import { putMoveOrg } from '@/web/support/user/team/org/api'; import { Button, ModalBody, ModalFooter } from '@chakra-ui/react'; import type { OrgType } from '@fastgpt/global/support/user/team/org/type'; -import 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 { useMemo, useState } from 'react'; import OrgTree from './OrgTree'; +import dynamic from 'next/dynamic'; +import { useUserStore } from '@/web/support/user/useUserStore'; function OrgMoveModal({ movingOrg, - movingTmb, orgs, - team, onClose, onSuccess }: { - movingOrg?: OrgType; - movingTmb?: { tmbId: string; orgId: string }; + movingOrg: OrgType; orgs: OrgType[]; - team: TeamTmbItemType; onClose: () => void; onSuccess: () => void; }) { const { t } = useTranslation(); - const [selectedOrg, selectOrg] = useState(); + const [selectedOrg, setSelectedOrg] = useState(); + const { userInfo } = useUserStore(); + const team = userInfo?.team!; - const { runAsync: moveOrg, loading: loadingOrg } = useRequest2(putMoveOrg, { + const { runAsync: onMoveOrg, loading } = 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; + const filterMovingOrgs = useMemo( + () => orgs.filter((org) => org._id !== movingOrg._id), + [movingOrg._id, orgs] + ); return ( - @@ -77,4 +71,4 @@ function OrgMoveModal({ ); } -export default OrgMoveModal; +export default dynamic(() => Promise.resolve(OrgMoveModal), { ssr: false }); diff --git a/projects/app/src/pages/account/team/components/OrgManage/OrgTree.tsx b/projects/app/src/pages/account/team/components/OrgManage/OrgTree.tsx index b08ac1f2a6a0..cda5df41ab3b 100644 --- a/projects/app/src/pages/account/team/components/OrgManage/OrgTree.tsx +++ b/projects/app/src/pages/account/team/components/OrgManage/OrgTree.tsx @@ -1,114 +1,101 @@ -import { Box, HStack, Text, VStack } from '@chakra-ui/react'; +import { Box, HStack, VStack } from '@chakra-ui/react'; import type { OrgType } from '@fastgpt/global/support/user/team/org/type'; import Avatar from '@fastgpt/web/components/common/Avatar'; import { useToggle } from 'ahooks'; -import { useMemo, useState } from 'react'; +import { useMemo } from 'react'; import IconButton from './IconButton'; function OrgTreeNode({ org, list, selectedOrg, - selectOrg, - indent = 0 + setSelectedOrg, + index = 0 }: { org: OrgType; list: OrgType[]; selectedOrg?: OrgType; - selectOrg?: (org?: OrgType) => void; - indent?: number; + setSelectedOrg: (org?: OrgType) => void; + index?: number; }) { const children = useMemo( () => list.filter((item) => item.path === `${org.path}/${org._id}`), [org, list] ); - const [isExpanded, toggleIsExpanded] = useToggle(false); + const [isExpanded, toggleIsExpanded] = useToggle(index === 0); return ( - + setSelectedOrg(undefined) + } + : { + onClick: () => setSelectedOrg(org) + })} > - {children.length > 0 ? ( + {index > 0 && ( toggleIsExpanded.toggle()} + color={'myGray.500'} + p={0} + w={'1.25rem'} + visibility={children.length > 0 ? 'visible' : 'hidden'} + onClick={(e) => { + e.stopPropagation(); + toggleIsExpanded.toggle(); + }} /> - ) : ( - )} - selectOrg?.(org)} cursor="pointer"> - - {org.name} + setSelectedOrg(org)} + cursor={'pointer'} + borderRadius={'xs'} + > + + {org.name} {isExpanded && children.length > 0 && children.map((child) => ( - + + + ))} - + ); } function OrgTree({ orgs, - teamName, - teamAvatar, selectedOrg, - selectOrg + setSelectedOrg }: { orgs: OrgType[]; - teamAvatar: string; - teamName: string; selectedOrg?: OrgType; - selectOrg?: (org?: OrgType) => void; + setSelectedOrg: (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] - ); + if (!root) return; + return ( - - 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' } : {})} - > - - {teamName} - - {children.map((child) => ( - - ))} - + ); } diff --git a/projects/app/src/pages/account/team/components/OrgManage/index.tsx b/projects/app/src/pages/account/team/components/OrgManage/index.tsx index 01756a1a65cb..49f474dc755f 100644 --- a/projects/app/src/pages/account/team/components/OrgManage/index.tsx +++ b/projects/app/src/pages/account/team/components/OrgManage/index.tsx @@ -2,22 +2,18 @@ import { deleteOrg, deleteOrgMember } from '@/web/support/user/team/org/api'; import { useUserStore } from '@/web/support/user/useUserStore'; import { Box, - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, Divider, + Flex, HStack, Table, TableContainer, Tag, Tbody, Td, - Text, Th, Thead, Tr, - VStack, - useDisclosure + VStack } from '@chakra-ui/react'; import type { OrgType } from '@fastgpt/global/support/user/team/org/type'; import Avatar from '@fastgpt/web/components/common/Avatar'; @@ -27,14 +23,21 @@ 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 { useMemo, useState } from 'react'; import { useContextSelector } from 'use-context-selector'; -import MemberTag from '../../../../../components/support/user/team/Info/MemberTag'; +import MemberTag from '@/components/support/user/team/Info/MemberTag'; import { TeamContext } from '../context'; +import { getOrgList } from '@/web/support/user/team/org/api'; + import IconButton from './IconButton'; -import OrgInfoModal from './OrgInfoModal'; -import OrgMemberModal from './OrgMemberModal'; +import OrgInfoModal, { defaultOrgForm, OrgFormType } from './OrgInfoModal'; +import OrgMemberManageModal from './OrgMemberManageModal'; + import OrgMoveModal from './OrgMoveModal'; +import dynamic from 'next/dynamic'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import Path from '@/components/common/folder/Path'; +import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type'; function ActionButton({ icon, @@ -59,118 +62,101 @@ function ActionButton({ }} onClick={onClick} > - - - {text} - + + {text} ); } function MemberTable() { const { t } = useTranslation(); - const { userInfo } = useUserStore(); + const { userInfo, isTeamAdmin } = useUserStore(); - const { orgs, refetchOrgs, members, refetchMembers, isLoading } = useContextSelector( - TeamContext, - (v) => v - ); - const [currentOrg, setCurrentOrg] = useState(); + const { members } = useContextSelector(TeamContext, (v) => v); - // 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; + const [parentPath, setParentPath] = useState(''); + const { + data: orgs = [], + loading: isLoadingOrgs, + refresh: refetchOrgs + } = useRequest2(getOrgList, { + manual: false, + refreshDeps: [userInfo?.team?.teamId] + }); + const currentOrgs = useMemo(() => { + if (orgs.length === 0) return []; + if (parentPath === '') { + setParentPath(`/${orgs[0]._id}`); + return []; } - }, [currentOrg]); + return orgs + .filter((org) => org.path === parentPath) + .map((item) => { + return { + ...item, + count: + item.members.length + + orgs.filter((org) => org.path === `${item.path}/${item._id}`).length + }; + }); + }, [orgs, parentPath]); + const currentOrg = useMemo(() => { + const splitPath = parentPath.split('/'); + const currentOrgId = splitPath[splitPath.length - 1]; + if (!currentOrgId) return; - 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] - ); + return orgs.find((org) => org._id === currentOrgId); + }, [orgs, parentPath]); + const paths = useMemo(() => { + const splitPath = parentPath.split('/').filter(Boolean); + return splitPath + .map((id) => { + const org = orgs.find((org) => org._id === id)!; - 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; - } + if (org.path === '') return; - return { ...org, count }; - }), - [orgs, currentPath] - ); + return { + parentId: `${org.path}/${org._id}`, + parentName: org.name + }; + }) + .filter(Boolean) as ParentTreePathItemType[]; + }, [parentPath, orgs]); - const [editOrg, setEditOrg] = useState(); - const [editMemberOrgId, setEditMemberOrgId] = useState(); - const [movingOrg, setMovingOrg] = useState(); - const [movingTmb, setMovingTmb] = useState<{ tmbId: string; orgId: string } | undefined>(); - const [createOrgParentId, setCreateOrgParentId] = useState(); + const [editOrg, setEditOrg] = useState(); + const [manageMemberOrg, setManageMemberOrg] = useState(); + const [movingOrg, setMovingOrg] = useState(); + // Delete org 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 deleteOrgHandler = (orgId: string) => openDeleteOrgModal(() => deleteOrgReq(orgId))(); const { runAsync: deleteOrgReq } = useRequest2(deleteOrg, { onSuccess: () => { refetchOrgs(); - refetchMembers(); } }); + + // Delete member + const { ConfirmModal: ConfirmDeleteMember, openConfirm: openDeleteMemberModal } = useConfirm({ + type: 'delete', + content: t('account_team:confirm_delete_member') + }); 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 ( - - - {currentPath.parents.map((parent) => ( - - setCurrentOrg(parent)}> - {parent.path === '' ? userInfo?.team.teamName : parent.name} - - - ))} - - - {currentOrg?.path === '' ? userInfo?.team.teamName : currentOrg?.name} - - - - + + + + + + {/* Table */} @@ -184,94 +170,86 @@ function MemberTable() { - {orgList.map((org) => ( + {currentOrgs.map((org) => ( - - - ))} - {currentOrg?.members.map((member) => { - const memberInfo = members.find((m) => m.tmbId === member.tmbId); - if (!memberInfo) return null; - return ( - - - + + ))} + {currentOrg?.members.map((member) => { + const memberInfo = members.find((m) => m.tmbId === member.tmbId); + if (!memberInfo) return null; + + return ( + + + ); @@ -279,92 +257,87 @@ function MemberTable() {
- setCurrentOrg(org)}> + setParentPath(`${org.path}/${org._id}`)} + > {org.count} - - } - 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) - } - ] - } - ]} - /> -
- - + {isTeamAdmin && ( - } + trigger="hover" + Button={} menuList={[ { children: [ - // { - // icon: 'edit', - // label: t('account_team:remark'), - // onClick: () => { - // // TODO - // console.log(member.tmbId); - // } - // }, + { + icon: 'edit', + label: t('account_team:edit_info'), + onClick: () => setEditOrg(org) + }, { icon: 'common/file/move', label: t('common:Move'), - onClick: () => - setMovingTmb({ tmbId: member.tmbId, orgId: currentOrg!._id }) + onClick: () => setMovingOrg(org) }, { icon: 'delete', label: t('account_team:delete'), type: 'danger', - onClick: () => deleteMemberHandler(currentOrg!._id, member.tmbId) + onClick: () => deleteOrgHandler(org._id) } ] } ]} /> + )} +
+ + + {isTeamAdmin && ( + } + menuList={[ + { + children: [ + { + icon: 'delete', + label: t('account_team:delete'), + type: 'danger', + onClick: () => + openDeleteMemberModal(() => + deleteMemberReq(currentOrg._id, member.tmbId) + )() + } + ] + } + ]} + /> + )}
- - + {/* Slider */} + - - - {currentOrg?.path === '' ? userInfo?.team.teamName : currentOrg?.name} - + + + {currentOrg?.name} + {currentOrg?.path !== '' && ( setEditOrg(currentOrg)} /> )} - - {currentOrg?.description ?? t('common:common.no_intro')} - + {currentOrg?.description || t('common:common.no_intro')} - + {t('common:common.Action')} - - - { - setCreateOrgParentId(currentOrg?._id); - }} - /> - setEditMemberOrgId(currentOrg?._id)} - /> - {currentOrg?.path !== '' && ( - <> - setMovingOrg(currentOrg)} - /> - deleteOrgHandler(currentOrg?._id ?? '')} - /> - - )} - + + {currentOrg && isTeamAdmin && ( + + { + setEditOrg({ + ...defaultOrgForm, + parentId: currentOrg?._id + }); + }} + /> + {currentOrg?.path !== '' && ( + <> + setManageMemberOrg(currentOrg)} + /> + setMovingOrg(currentOrg)} + /> + deleteOrgHandler(currentOrg._id)} + /> + + )} + + )} -
- { - setEditOrg(undefined); - setCreateOrgParentId(undefined); - }} - onSuccess={() => { - refetchOrgs(); - refetchMembers(); - }} - /> - { - setMovingOrg(undefined); - setMovingTmb(undefined); - }} - onSuccess={() => { - refetchOrgs(); - refetchMembers(); - }} - /> - setEditMemberOrgId(undefined)} /> + + + {!!editOrg && ( + setEditOrg(undefined)} + onSuccess={refetchOrgs} + /> + )} + {!!movingOrg && ( + setMovingOrg(undefined)} + onSuccess={refetchOrgs} + /> + )} + {!!manageMemberOrg && ( + setManageMemberOrg(undefined)} + /> + )} + -
+ ); } -export default MemberTable; +export default dynamic(() => Promise.resolve(MemberTable), { ssr: false }); diff --git a/projects/app/src/pages/account/team/components/context.tsx b/projects/app/src/pages/account/team/components/context.tsx index 3c2ba29687c0..7ad30ad5a513 100644 --- a/projects/app/src/pages/account/team/components/context.tsx +++ b/projects/app/src/pages/account/team/components/context.tsx @@ -9,7 +9,6 @@ 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'; @@ -19,7 +18,6 @@ type TeamModalContextType = { myTeams: TeamTmbItemType[]; members: TeamMemberItemType[]; groups: MemberGroupListType; - orgs: OrgType[]; isLoading: boolean; onSwitchTeam: (teamId: string) => void; setEditTeamData: React.Dispatch>; @@ -27,7 +25,6 @@ type TeamModalContextType = { refetchMembers: () => void; refetchTeams: () => void; refetchGroups: () => void; - refetchOrgs: () => void; searchKey: string; setSearchKey: React.Dispatch>; teamSize: number; @@ -37,7 +34,6 @@ export const TeamContext = createContext({ myTeams: [], groups: [], members: [], - orgs: [], isLoading: false, onSwitchTeam: function (_teamId: string): void { throw new Error('Function not implemented.'); @@ -54,9 +50,6 @@ export const TeamContext = createContext({ refetchGroups: function (): void { throw new Error('Function not implemented.'); }, - refetchOrgs: function (): void { - throw new Error('Function not implemented.'); - }, searchKey: '', setSearchKey: function (_value: React.SetStateAction): void { @@ -115,17 +108,7 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode }) refreshDeps: [userInfo?.team?.teamId] }); - const { - data: orgs = [], - loading: isLoadingOrgs, - refresh: refetchOrgs - } = useRequest2(getOrgList, { - manual: false, - refreshDeps: [userInfo?.team?.teamId] - }); - - const isLoading = - isLoadingTeams || isSwitchingTeam || loadingMembers || isLoadingGroups || isLoadingOrgs; + const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers || isLoadingGroups; const contextValue = { myTeams, @@ -141,8 +124,6 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode }) refetchMembers, groups, refetchGroups, - orgs, - refetchOrgs, teamSize: members.length }; diff --git a/projects/app/src/pages/account/team/index.tsx b/projects/app/src/pages/account/team/index.tsx index 8efee699eca6..f4e9a3949987 100644 --- a/projects/app/src/pages/account/team/index.tsx +++ b/projects/app/src/pages/account/team/index.tsx @@ -22,10 +22,11 @@ import dynamic from 'next/dynamic'; import TeamTagModal from '@/components/support/user/team/TeamTagModal'; import MemberTable from './components/MemberTable'; +import OrgManage from './components/OrgManage/index'; + 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')); @@ -272,7 +273,7 @@ const Team = () => { )} - + {teamTab === TeamTabEnum.member && } {teamTab === TeamTabEnum.group && ( diff --git a/projects/app/src/pages/app/detail/components/InfoModal.tsx b/projects/app/src/pages/app/detail/components/InfoModal.tsx index 07e2ff1592cf..c9572eab119c 100644 --- a/projects/app/src/pages/app/detail/components/InfoModal.tsx +++ b/projects/app/src/pages/app/detail/components/InfoModal.tsx @@ -142,7 +142,9 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { appId: appDetail._id }); - const onDelCollaborator = async (props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }>) => + const onDelCollaborator = async ( + props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }> + ) => deleteAppCollaborators({ appId: appDetail._id, ...props diff --git a/projects/app/src/pages/app/list/components/List.tsx b/projects/app/src/pages/app/list/components/List.tsx index 241c0c0c9821..72320796b4aa 100644 --- a/projects/app/src/pages/app/list/components/List.tsx +++ b/projects/app/src/pages/app/list/components/List.tsx @@ -258,20 +258,20 @@ const ListItem = () => { {(AppFolderTypeList.includes(app.type) ? app.permission.hasManagePer : app.permission.hasWritePer) && ( - - } - aria-label={''} - /> - } - menuList={[ - ...([AppTypeEnum.simple, AppTypeEnum.workflow].includes(app.type) - ? [ + + } + 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 = () => { ] } ] - : []) - ]} - /> - - )} + : []) + ]} + /> + + )} diff --git a/projects/app/src/web/support/user/team/org/api.ts b/projects/app/src/web/support/user/team/org/api.ts index aeb8d5157862..2febfb9d77b0 100644 --- a/projects/app/src/web/support/user/team/org/api.ts +++ b/projects/app/src/web/support/user/team/org/api.ts @@ -2,10 +2,10 @@ import { DELETE, GET, POST, PUT } from '@/web/common/api/request'; import type { postCreateOrgData, putUpdateOrgData, - putUpdateOrgMembersData, - putMoveOrgMemberData + putUpdateOrgMembersData } from '@fastgpt/global/support/user/team/org/api'; import type { OrgType } from '@fastgpt/global/support/user/team/org/type'; +import { putMoveOrgType } from '@fastgpt/global/support/user/team/org/api'; export const getOrgList = () => GET('/proApi/support/user/team/org/list'); @@ -18,11 +18,7 @@ export const deleteOrg = (orgId: string) => 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 putMoveOrg = (data: putMoveOrgType) => PUT('/proApi/support/user/team/org/move', data); export const putUpdateOrg = (data: putUpdateOrgData) => PUT('/proApi/support/user/team/org/update', data); diff --git a/projects/app/src/web/support/user/useUserStore.ts b/projects/app/src/web/support/user/useUserStore.ts index d18c391e82c8..a6da4d48c63c 100644 --- a/projects/app/src/web/support/user/useUserStore.ts +++ b/projects/app/src/web/support/user/useUserStore.ts @@ -19,6 +19,7 @@ type State = { setSysMsgReadId: (id: string) => void; userInfo: UserType | null; + isTeamAdmin: boolean; initUserInfo: () => Promise; setUserInfo: (user: UserType | null) => void; updateUserInfo: (user: UserUpdateParams) => Promise; @@ -50,6 +51,7 @@ export const useUserStore = create()( }, userInfo: null, + isTeamAdmin: false, async initUserInfo() { get().initTeamPlanStatus(); @@ -67,6 +69,7 @@ export const useUserStore = create()( setUserInfo(user: UserType | null) { set((state) => { state.userInfo = user ? user : null; + state.isTeamAdmin = !!user?.team?.permission?.hasManagePer; }); }, async updateUserInfo(user: UserUpdateParams) {