From 1fc77a126a3d9f3abb3cb2d62cc2c39da250d155 Mon Sep 17 00:00:00 2001 From: "a.e." <49438478+I-Info@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:49:56 +0800 Subject: [PATCH 01/30] 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 --- packages/global/common/error/code/team.ts | 37 +- .../global/common/file/image/constants.ts | 5 + packages/global/common/system/constants.ts | 1 + packages/global/core/app/collaborator.d.ts | 5 +- .../global/core/dataset/collaborator.d.ts | 1 + .../support/permission/collaborator.d.ts | 4 + packages/global/support/permission/type.d.ts | 4 +- .../global/support/user/team/org/api.d.ts | 38 ++ .../global/support/user/team/org/constant.ts | 8 + .../global/support/user/team/org/type.d.ts | 23 ++ .../service/support/permission/auth/org.ts | 58 +++ .../service/support/permission/controller.ts | 23 +- .../support/permission/inheritPermission.ts | 13 +- .../support/permission/org/controllers.ts | 174 ++++++++ .../support/permission/org/orgMemberSchema.ts | 58 +++ .../support/permission/org/orgSchema.ts | 69 ++++ packages/service/support/permission/schema.ts | 28 ++ .../service/support/user/team/controller.ts | 4 +- .../web/components/common/Icon/constants.ts | 1 + .../Icon/icons/common/downArrowFill.svg | 5 + packages/web/i18n/en/account_team.json | 12 + packages/web/i18n/en/common.json | 7 + packages/web/i18n/zh-CN/account_team.json | 70 ++-- packages/web/i18n/zh-CN/common.json | 7 + packages/web/i18n/zh-Hant/account_team.json | 12 + packages/web/i18n/zh-Hant/common.json | 7 + .../public/imgs/avatar/defaultOrgAvatar.svg | 10 + .../MemberManager/AddMemberModal.tsx | 145 +++++-- .../permission/MemberManager/ManageModal.tsx | 32 +- .../MemberManager/MemberListCard.tsx | 16 +- .../permission/MemberManager/context.tsx | 29 +- .../team/components/OrgManage/IconButton.tsx | 23 ++ .../components/OrgManage/OrgInfoModal.tsx | 150 +++++++ .../components/OrgManage/OrgMemberModal.tsx | 202 ++++++++++ .../components/OrgManage/OrgMoveModal.tsx | 80 ++++ .../team/components/OrgManage/OrgTree.tsx | 115 ++++++ .../team/components/OrgManage/index.tsx | 370 ++++++++++++++++++ .../pages/account/team/components/context.tsx | 22 +- projects/app/src/pages/account/team/index.tsx | 4 + .../pages/app/detail/components/InfoModal.tsx | 58 +-- .../src/pages/app/list/components/List.tsx | 96 ++--- projects/app/src/pages/app/list/index.tsx | 9 +- .../dataset/detail/components/Info/index.tsx | 7 +- projects/app/src/pages/dataset/list/index.tsx | 7 +- .../app/src/web/support/user/team/org/api.ts | 34 ++ .../app/src/web/support/user/useUserStore.ts | 40 +- 46 files changed, 1933 insertions(+), 190 deletions(-) create mode 100644 packages/global/support/user/team/org/api.d.ts create mode 100644 packages/global/support/user/team/org/constant.ts create mode 100644 packages/global/support/user/team/org/type.d.ts create mode 100644 packages/service/support/permission/auth/org.ts create mode 100644 packages/service/support/permission/org/controllers.ts create mode 100644 packages/service/support/permission/org/orgMemberSchema.ts create mode 100644 packages/service/support/permission/org/orgSchema.ts create mode 100644 packages/web/components/common/Icon/icons/common/downArrowFill.svg create mode 100644 projects/app/public/imgs/avatar/defaultOrgAvatar.svg create mode 100644 projects/app/src/pages/account/team/components/OrgManage/IconButton.tsx create mode 100644 projects/app/src/pages/account/team/components/OrgManage/OrgInfoModal.tsx create mode 100644 projects/app/src/pages/account/team/components/OrgManage/OrgMemberModal.tsx create mode 100644 projects/app/src/pages/account/team/components/OrgManage/OrgMoveModal.tsx create mode 100644 projects/app/src/pages/account/team/components/OrgManage/OrgTree.tsx create mode 100644 projects/app/src/pages/account/team/components/OrgManage/index.tsx create mode 100644 projects/app/src/web/support/user/team/org/api.ts diff --git a/packages/global/common/error/code/team.ts b/packages/global/common/error/code/team.ts index 8ad9abe021a2..e1b7a80d1f64 100644 --- a/packages/global/common/error/code/team.ts +++ b/packages/global/common/error/code/team.ts @@ -1,5 +1,5 @@ -import { ErrType } from '../errorCode'; import { i18nT } from '../../../../web/i18n/utils'; +import type { ErrType } from '../errorCode'; /* team: 500000 */ export enum TeamErrEnum { teamOverSize = 'teamOverSize', @@ -14,6 +14,13 @@ export enum TeamErrEnum { groupNameEmpty = 'groupNameEmpty', groupNameDuplicate = 'groupNameDuplicate', groupNotExist = 'groupNotExist', + orgMemberNotExist = 'orgMemberNotExist', + orgMemberDuplicated = 'orgMemberDuplicated', + orgNotExist = 'orgNotExist', + orgParentNotExist = 'orgParentNotExist', + cannotMoveToSubPath = 'cannotMoveToSubPath', + cannotModifyRootOrg = 'cannotModifyRootOrg', + cannotDeleteNonEmptyOrg = 'cannotDeleteNonEmptyOrg', cannotDeleteDefaultGroup = 'cannotDeleteDefaultGroup', userNotActive = 'userNotActive' } @@ -71,6 +78,34 @@ const teamErr = [ { statusText: TeamErrEnum.userNotActive, message: i18nT('common:code_error.team_error.user_not_active') + }, + { + statusText: TeamErrEnum.orgMemberNotExist, + message: i18nT('common:code_error.team_error.org_member_not_exist') + }, + { + statusText: TeamErrEnum.orgMemberDuplicated, + message: i18nT('common:code_error.team_error.org_member_duplicated') + }, + { + statusText: TeamErrEnum.orgNotExist, + message: i18nT('common:code_error.team_error.org_not_exist') + }, + { + statusText: TeamErrEnum.orgParentNotExist, + message: i18nT('common:code_error.team_error.org_parent_not_exist') + }, + { + statusText: TeamErrEnum.cannotMoveToSubPath, + message: i18nT('common:code_error.team_error.cannot_move_to_sub_path') + }, + { + statusText: TeamErrEnum.cannotModifyRootOrg, + message: i18nT('common:code_error.team_error.cannot_modify_root_org') + }, + { + statusText: TeamErrEnum.cannotDeleteNonEmptyOrg, + message: i18nT('common:code_error.team_error.cannot_delete_non_empty_org') } ]; diff --git a/packages/global/common/file/image/constants.ts b/packages/global/common/file/image/constants.ts index 6a178e2afcf6..ad516f33754a 100644 --- a/packages/global/common/file/image/constants.ts +++ b/packages/global/common/file/image/constants.ts @@ -8,6 +8,7 @@ export enum MongoImageTypeEnum { userAvatar = 'userAvatar', teamAvatar = 'teamAvatar', groupAvatar = 'groupAvatar', + orgAvatar = 'orgAvatar', chatImage = 'chatImage', collectionImage = 'collectionImage' @@ -41,6 +42,10 @@ export const mongoImageTypeMap = { label: 'groupAvatar', unique: true }, + [MongoImageTypeEnum.orgAvatar]: { + label: 'orgAvatar', + unique: true + }, [MongoImageTypeEnum.chatImage]: { label: 'chatImage', diff --git a/packages/global/common/system/constants.ts b/packages/global/common/system/constants.ts index 1ca44a6118d7..6647317fe2f0 100644 --- a/packages/global/common/system/constants.ts +++ b/packages/global/common/system/constants.ts @@ -2,5 +2,6 @@ export const HUMAN_ICON = `/icon/human.svg`; export const LOGO_ICON = `/icon/logo.svg`; export const HUGGING_FACE_ICON = `/imgs/model/huggingface.svg`; export const DEFAULT_TEAM_AVATAR = `/imgs/avatar/defaultTeamAvatar.svg`; +export const DEFAULT_ORG_AVATAR = '/imgs/avatar/defaultOrgAvatar.svg'; export const isProduction = process.env.NODE_ENV === 'production'; diff --git a/packages/global/core/app/collaborator.d.ts b/packages/global/core/app/collaborator.d.ts index ca0fec7217f0..66a456379d55 100644 --- a/packages/global/core/app/collaborator.d.ts +++ b/packages/global/core/app/collaborator.d.ts @@ -1,6 +1,6 @@ -import { RequireOnlyOne } from '../../common/type/utils'; +import type { RequireOnlyOne } from '../../common/type/utils'; import { - UpdateClbPermissionProps, + type UpdateClbPermissionProps, UpdatePermissionBody } from '../../support/permission/collaborator'; import { PermissionValueType } from '../../support/permission/type'; @@ -14,4 +14,5 @@ export type AppCollaboratorDeleteParams = { } & RequireOnlyOne<{ tmbId: string; groupId: string; + orgId: string; }>; diff --git a/packages/global/core/dataset/collaborator.d.ts b/packages/global/core/dataset/collaborator.d.ts index 7f33f4d516be..672430c1de22 100644 --- a/packages/global/core/dataset/collaborator.d.ts +++ b/packages/global/core/dataset/collaborator.d.ts @@ -11,4 +11,5 @@ export type DatasetCollaboratorDeleteParams = { } & RequireOnlyOne<{ tmbId: string; groupId: string; + orgId: string; }>; diff --git a/packages/global/support/permission/collaborator.d.ts b/packages/global/support/permission/collaborator.d.ts index 60a84a9d58e9..d5c9b3060a79 100644 --- a/packages/global/support/permission/collaborator.d.ts +++ b/packages/global/support/permission/collaborator.d.ts @@ -10,17 +10,20 @@ export type CollaboratorItemType = { } & RequireOnlyOne<{ tmbId: string; groupId: string; + orgId: string; }>; export type UpdateClbPermissionProps = { members?: string[]; groups?: string[]; + orgs?: string[]; permission: PermissionValueType; }; export type DeleteClbPermissionProps = RequireOnlyOne<{ tmbId: string; groupId: string; + orgId: string; }>; export type UpdatePermissionBody = { @@ -28,4 +31,5 @@ export type UpdatePermissionBody = { } & RequireOnlyOne<{ memberId: string; groupId: string; + orgId: string; }>; diff --git a/packages/global/support/permission/type.d.ts b/packages/global/support/permission/type.d.ts index 619f8503e67c..f473d2941060 100644 --- a/packages/global/support/permission/type.d.ts +++ b/packages/global/support/permission/type.d.ts @@ -1,8 +1,9 @@ import { UserModelSchema } from '../user/type'; import { RequireOnlyOne } from '../../common/type/utils'; import { TeamMemberSchema } from '../user/team/type'; -import { AuthUserTypeEnum, PermissionKeyEnum, PerResourceTypeEnum } from './constant'; import { MemberGroupSchemaType } from './memberGroup/type'; +import type { TeamMemberWithUserSchema } from '../user/team/type'; +import { AuthUserTypeEnum, type PermissionKeyEnum, type PerResourceTypeEnum } from './constant'; // PermissionValueType, the type of permission's value is a number, which is a bit field actually. // It is spired by the permission system in Linux. @@ -29,6 +30,7 @@ export type ResourcePermissionType = { } & RequireOnlyOne<{ tmbId: string; groupId: string; + orgId: string; }>; export type ResourcePerWithTmbWithUser = Omit & { diff --git a/packages/global/support/user/team/org/api.d.ts b/packages/global/support/user/team/org/api.d.ts new file mode 100644 index 000000000000..605e9940de88 --- /dev/null +++ b/packages/global/support/user/team/org/api.d.ts @@ -0,0 +1,38 @@ +export type postCreateOrgData = { + name: string; + parentId: string; + description?: string; + avatar?: string; +}; + +export type putUpdateOrgMembersData = { + orgId: string; + members: { + tmbId: string; + // role: `${OrgMemberRole}`; + }[]; +}; + +export type putUpdateOrgData = { + orgId: string; + name?: string; + avatar?: string; + description?: string; +}; + +export type putMoveOrgData = { + orgId: string; + parentId: string; +}; + +export type putMoveOrgMemberData = { + orgId: string; + tmbId: string; + newOrgId: string; +}; + +// type putChnageOrgOwnerData = { +// orgId: string; +// tmbId: string; +// toAdmin?: boolean; +// }; diff --git a/packages/global/support/user/team/org/constant.ts b/packages/global/support/user/team/org/constant.ts new file mode 100644 index 000000000000..5b764877b225 --- /dev/null +++ b/packages/global/support/user/team/org/constant.ts @@ -0,0 +1,8 @@ +export const OrgCollectionName = 'team_orgs'; +export const OrgMemberCollectionName = 'team_org_members'; + +// export enum OrgMemberRole { +// owner = 'owner', +// admin = 'admin', +// member = 'member' +// } diff --git a/packages/global/support/user/team/org/type.d.ts b/packages/global/support/user/team/org/type.d.ts new file mode 100644 index 000000000000..92fbf512b611 --- /dev/null +++ b/packages/global/support/user/team/org/type.d.ts @@ -0,0 +1,23 @@ +import type { TeamPermission } from 'support/permission/user/controller'; +import { ResourcePermissionType } from '../type'; + +type OrgSchemaType = { + _id: string; + teamId: string; + path: string; + name: string; + avatar?: string; + description?: string; + updateTime: Date; +}; + +type OrgMemberSchemaType = { + teamId: string; + orgId: string; + tmbId: string; +}; + +type OrgType = Omit & { + avatar: string; + members: OrgMemberSchemaType[]; +}; diff --git a/packages/service/support/permission/auth/org.ts b/packages/service/support/permission/auth/org.ts new file mode 100644 index 000000000000..4262a8565323 --- /dev/null +++ b/packages/service/support/permission/auth/org.ts @@ -0,0 +1,58 @@ +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'; + +export const authOrgMember = async ({ + orgIds, + req, + authToken = false, + authRoot = false, + authApiKey = false +}: { + orgIds: string | string[]; +} & AuthModeType): Promise => { + const result = await parseHeaderCert({ req, authToken, authApiKey, authRoot }); + const { teamId, tmbId, isRoot } = result; + if (isRoot) { + return { + teamId, + tmbId, + userId: result.userId, + appId: result.appId, + apikey: result.apikey, + isRoot, + authType: result.authType, + permission: new TeamPermission({ isOwner: true }) + }; + } + + 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, + permission: tmb.permission + }; + } + + 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/controller.ts b/packages/service/support/permission/controller.ts index 458c05d019f8..92f2e75452cd 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -21,6 +21,7 @@ import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type'; import { TeamMemberSchema } from '@fastgpt/global/support/user/team/type'; import { UserModelSchema } from '@fastgpt/global/support/user/type'; +import { OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; /** get resource permission for a team member * If there is no permission for the team member, it will return undefined @@ -186,6 +187,16 @@ export const getClbsAndGroupsWithInfo = async ({ } }) .populate<{ group: MemberGroupSchemaType }>('group', 'name avatar') + .lean(), + MongoResourcePermission.find({ + teamId, + resourceId, + resourceType, + orgId: { + $exists: true + } + }) + .populate<{ org: OrgSchemaType }>({ path: 'org', select: 'name avatar' }) .lean() ]); @@ -196,6 +207,7 @@ export const delResourcePermission = ({ session, tmbId, groupId, + orgId, ...props }: { resourceType: PerResourceTypeEnum; @@ -204,15 +216,18 @@ export const delResourcePermission = ({ session?: ClientSession; tmbId?: string; groupId?: string; + orgId?: string; }) => { - // tmbId or groupId only one and not both - if (!!tmbId === !!groupId) { + // either tmbId or groupId or orgId must be provided + if (!tmbId && !groupId && !orgId) { return Promise.reject(CommonErrEnum.missingParams); } + return MongoResourcePermission.deleteOne( { ...(tmbId ? { tmbId } : {}), ...(groupId ? { groupId } : {}), + ...(orgId ? { orgId } : {}), ...props }, { session } @@ -250,7 +265,7 @@ export function authJWT(token: string) { }>((resolve, reject) => { const key = process.env.TOKEN_KEY as string; - jwt.verify(token, key, function (err, decoded: any) { + jwt.verify(token, key, (err, decoded: any) => { if (err || !decoded?.userId) { reject(ERROR_ENUM.unAuthorization); return; @@ -436,7 +451,7 @@ export const authFileToken = (token?: string) => } const key = (process.env.FILE_TOKEN_KEY as string) ?? 'filetoken'; - jwt.verify(token, key, function (err, decoded: any) { + jwt.verify(token, key, (err, decoded: any) => { if (err || !decoded.bucketName || !decoded?.teamId || !decoded?.fileId) { reject(ERROR_ENUM.unAuthFile); return; diff --git a/packages/service/support/permission/inheritPermission.ts b/packages/service/support/permission/inheritPermission.ts index c3ac42eb8a3f..4f9993f5a9f3 100644 --- a/packages/service/support/permission/inheritPermission.ts +++ b/packages/service/support/permission/inheritPermission.ts @@ -1,11 +1,11 @@ import { mongoSessionRun } from '../../common/mongo/sessionRun'; import { MongoResourcePermission } from './schema'; -import { ClientSession, Model } from 'mongoose'; -import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; -import { PermissionValueType } from '@fastgpt/global/support/permission/type'; +import type { ClientSession, Model } from 'mongoose'; +import type { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; +import type { PermissionValueType } from '@fastgpt/global/support/permission/type'; import { getResourceClbsAndGroups } from './controller'; -import { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; -import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; +import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; +import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; export type SyncChildrenPermissionResourceType = { _id: string; @@ -18,6 +18,7 @@ export type UpdateCollaboratorItem = { } & RequireOnlyOne<{ tmbId: string; groupId: string; + orgId: string; }>; // sync the permission to all children folders. @@ -161,7 +162,7 @@ export async function resumeInheritPermission({ } } -/* +/* Delete all the collaborators and then insert the new collaborators. */ export async function syncCollaborators({ diff --git a/packages/service/support/permission/org/controllers.ts b/packages/service/support/permission/org/controllers.ts new file mode 100644 index 000000000000..1a0aecef2d29 --- /dev/null +++ b/packages/service/support/permission/org/controllers.ts @@ -0,0 +1,174 @@ +import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; +import type { + OrgMemberSchemaType, + OrgSchemaType +} from '@fastgpt/global/support/user/team/org/type'; +import type { ClientSession } from 'mongoose'; +import { MongoOrgModel } from './orgSchema'; +import { MongoOrgMemberModel } from './orgMemberSchema'; + +// if role1 > role2, return 1 +// if role1 < role2, return -1 +// else return 0 +// export const compareRole = (role1: OrgMemberRole, role2: OrgMemberRole) => { +// if (role1 === OrgMemberRole.owner) { +// if (role2 === OrgMemberRole.owner) { +// return 0; +// } +// return 1; +// } +// if (role2 === OrgMemberRole.owner) { +// return -1; +// } +// if (role1 === OrgMemberRole.admin) { +// if (role2 === OrgMemberRole.admin) { +// return 0; +// } +// return 1; +// } +// if (role2 === OrgMemberRole.admin) { +// return -1; +// } +// return 0; +// }; + +// export const checkOrgRole = (role: OrgMemberRole, targetRole: OrgMemberRole) => { +// 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(); + +export const getChildrenByOrg = async ({ + org, + teamId, + session +}: { + org: OrgSchemaType; + teamId: string; + session?: ClientSession; +}) => { + const children = await MongoOrgModel.find( + { teamId, path: { $regex: `^${org.path}/${org._id}` } }, + undefined, + { + session + } + ).lean(); + return children; +}; + +export const getOrgAndChildren = async ({ + orgId, + teamId, + session +}: { + orgId: string; + teamId: string; + session?: ClientSession; +}) => { + const org = await MongoOrgModel.findOne({ _id: orgId, teamId }, undefined, { session }).lean(); + if (!org) { + return Promise.reject(TeamErrEnum.orgNotExist); + } + const children = await getChildrenByOrg({ org, teamId, session }); + return { org, children }; +}; + +export async function createRootOrg({ + teamId, + session +}: { + teamId: string; + session?: ClientSession; +}) { + // Create the root org + const [org] = await MongoOrgModel.create( + [ + { + teamId, + name: 'ROOT', + path: '' + } + ], + { session } + ); + // Find the team's owner + // const owner = await MongoTeamMember.findOne({ teamId, role: 'owner' }, undefined); + // if (!owner) { + // return Promise.reject(TeamErrEnum.unAuthTeam); + // } + + // Set the owner as the org admin + // await MongoOrgMemberModel.create( + // [ + // { + // orgId: org._id, + // tmbId: owner._id + + // } + // ], + // { session } + // ); +} + +// export const getOrgMemberRole = async ({ +// orgId, +// tmbId +// }: { +// orgId: string; +// tmbId: string; +// }): Promise => { +// let role: OrgMemberRole | undefined; +// const orgMember = await MongoOrgMemberModel.findOne({ +// orgId, +// tmbId +// }) +// .populate('orgId') +// .lean(); +// if (orgMember) { +// role = OrgMemberRole[orgMember.role]; +// } else { +// return role; +// } +// if (role === OrgMemberRole.owner) { +// return role; +// } +// // Check the parent orgs +// const org = orgMember.orgId as unknown as OrgSchemaType; +// if (!org) { +// return Promise.reject(TeamErrEnum.orgNotExist); +// } +// const parentIds = org.path.split('/').filter((id) => id); +// if (parentIds.length === 0) { +// return role; +// } +// const parentOrgMembers = await MongoOrgMemberModel.find({ +// orgId: { +// $in: parentIds +// }, +// tmbId +// }).lean(); +// // Update the role to the highest role +// for (const parentOrgMember of parentOrgMembers) { +// const parentRole = OrgMemberRole[parentOrgMember.role]; +// if (parentRole === OrgMemberRole.owner) { +// role = parentRole; +// break; +// } +// if (parentRole === OrgMemberRole.admin && role === OrgMemberRole.member) { +// role = parentRole; +// } +// } +// return role; +// }; diff --git a/packages/service/support/permission/org/orgMemberSchema.ts b/packages/service/support/permission/org/orgMemberSchema.ts new file mode 100644 index 000000000000..1b70c5556517 --- /dev/null +++ b/packages/service/support/permission/org/orgMemberSchema.ts @@ -0,0 +1,58 @@ +import { OrgCollectionName } from '@fastgpt/global/support/user/team/org/constant'; +import { connectionMongo, getMongoModel } from '../../../common/mongo'; +import { + TeamCollectionName, + TeamMemberCollectionName +} from '@fastgpt/global/support/user/team/constant'; +import { OrgMemberSchemaType } from '@fastgpt/global/support/user/team/org/type'; +const { Schema } = connectionMongo; + +export const OrgMemberCollectionName = 'team_org_members'; + +export const OrgMemberSchema = new Schema({ + teamId: { + type: Schema.Types.ObjectId, + ref: TeamCollectionName, + required: true + }, + orgId: { + type: Schema.Types.ObjectId, + ref: OrgCollectionName, + required: true + }, + tmbId: { + type: Schema.Types.ObjectId, + ref: TeamMemberCollectionName, + required: true + } + // role: { + // type: String, + // enum: Object.values(OrgMemberRole), + // required: true, + // default: OrgMemberRole.member + // } +}); + +try { + OrgMemberSchema.index( + { + teamId: 1, + orgId: 1, + tmbId: 1 + }, + { + unique: true + } + ); + OrgMemberSchema.index({ + teamId: 1, + tmbId: 1 + }); +} catch (error) { + console.log(error); +} + +export const MongoOrgMemberModel = getMongoModel( + OrgMemberCollectionName, + OrgMemberSchema +); diff --git a/packages/service/support/permission/org/orgSchema.ts b/packages/service/support/permission/org/orgSchema.ts new file mode 100644 index 000000000000..58e2e705b863 --- /dev/null +++ b/packages/service/support/permission/org/orgSchema.ts @@ -0,0 +1,69 @@ +import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant'; +import { OrgCollectionName } from '@fastgpt/global/support/user/team/org/constant'; +import type { OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; +import { connectionMongo, getMongoModel } from '../../../common/mongo'; +import { ResourcePermissionCollectionName } from '../schema'; +import { OrgMemberCollectionName } from './orgMemberSchema'; +const { Schema } = connectionMongo; + +function requiredStringPath(this: OrgSchemaType) { + return typeof this.path !== 'string'; +} + +export const OrgSchema = new Schema( + { + teamId: { + type: Schema.Types.ObjectId, + ref: TeamCollectionName, + required: true + }, + path: { + type: String, + required: requiredStringPath // allow empty string, but not null + }, + name: { + type: String, + required: true + }, + avatar: String, + description: String, + updateTime: { + type: Date, + default: () => new Date() + } + }, + { + // Auto update updateTime + timestamps: { + updatedAt: 'updateTime' + } + } +); + +OrgSchema.virtual('members', { + ref: OrgMemberCollectionName, + localField: '_id', + foreignField: 'orgId' +}); +OrgSchema.virtual('permission', { + ref: ResourcePermissionCollectionName, + localField: '_id', + foreignField: 'orgId', + justOne: true +}); + +try { + OrgSchema.index( + { + teamId: 1, + path: 1 + }, + { + unique: true + } + ); +} catch (error) { + console.log(error); +} + +export const MongoOrgModel = getMongoModel(OrgCollectionName, OrgSchema); diff --git a/packages/service/support/permission/schema.ts b/packages/service/support/permission/schema.ts index ff5491d65e31..cca7b7c790db 100644 --- a/packages/service/support/permission/schema.ts +++ b/packages/service/support/permission/schema.ts @@ -6,6 +6,7 @@ import { connectionMongo, getMongoModel } from '../../common/mongo'; import type { ResourcePermissionType } from '@fastgpt/global/support/permission/type'; import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { MemberGroupCollectionName } from './memberGroup/memberGroupSchema'; +import { OrgCollectionName } from '@fastgpt/global/support/user/team/org/constant'; const { Schema } = connectionMongo; export const ResourcePermissionCollectionName = 'resource_permissions'; @@ -23,6 +24,10 @@ export const ResourcePermissionSchema = new Schema({ type: Schema.Types.ObjectId, ref: MemberGroupCollectionName }, + orgId: { + type: Schema.Types.ObjectId, + ref: OrgCollectionName + }, resourceType: { type: String, enum: Object.values(PerResourceTypeEnum), @@ -51,6 +56,12 @@ ResourcePermissionSchema.virtual('group', { foreignField: '_id', justOne: true }); +ResourcePermissionSchema.virtual('org', { + ref: OrgCollectionName, + localField: 'orgId', + foreignField: '_id', + justOne: true +}); try { ResourcePermissionSchema.index( @@ -70,6 +81,23 @@ try { } ); + ResourcePermissionSchema.index( + { + resourceType: 1, + teamId: 1, + resourceId: 1, + orgId: 1 + }, + { + unique: true, + partialFilterExpression: { + orgId: { + $exists: true + } + } + } + ); + ResourcePermissionSchema.index( { resourceType: 1, diff --git a/packages/service/support/user/team/controller.ts b/packages/service/support/user/team/controller.ts index ede448c93b6d..b9b4d25c4b26 100644 --- a/packages/service/support/user/team/controller.ts +++ b/packages/service/support/user/team/controller.ts @@ -16,6 +16,7 @@ import { MongoMemberGroupModel } from '../../permission/memberGroup/memberGroupS import { mongoSessionRun } from '../../../common/mongo/sessionRun'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; import { getAIApi, openaiBaseUrl } from '../../../core/ai/config'; +import { createRootOrg } from '../../permission/org/controllers'; async function getTeamMember(match: Record): Promise { const tmb = await MongoTeamMember.findOne(match).populate<{ team: TeamSchema }>('team').lean(); @@ -132,7 +133,8 @@ export async function createDefaultTeam({ ], { session } ); - console.log('create default team and group', userId); + await createRootOrg({ teamId: tmb.teamId, session }); + console.log('create default team, group and root org', userId); return tmb; } else { console.log('default team exist', userId); diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index 45b38f4febe1..16cabf5538f3 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -73,6 +73,7 @@ export const iconPaths = { 'common/resultLight': () => import('./icons/common/resultLight.svg'), 'common/retryLight': () => import('./icons/common/retryLight.svg'), 'common/rightArrowFill': () => import('./icons/common/rightArrowFill.svg'), + 'common/downArrowFill': () => import('./icons/common/downArrowFill.svg'), 'common/rightArrowLight': () => import('./icons/common/rightArrowLight.svg'), 'common/routePushLight': () => import('./icons/common/routePushLight.svg'), 'common/saveFill': () => import('./icons/common/saveFill.svg'), diff --git a/packages/web/components/common/Icon/icons/common/downArrowFill.svg b/packages/web/components/common/Icon/icons/common/downArrowFill.svg new file mode 100644 index 000000000000..d0f5035e4e49 --- /dev/null +++ b/packages/web/components/common/Icon/icons/common/downArrowFill.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/web/i18n/en/account_team.json b/packages/web/i18n/en/account_team.json index 453b0a14f1ee..65976bba3ad4 100644 --- a/packages/web/i18n/en/account_team.json +++ b/packages/web/i18n/en/account_team.json @@ -2,11 +2,23 @@ "action": "operate", "confirm_delete_group": "Confirm to delete group?", "confirm_leave_team": "Confirmed to leave the team? \n \nAfter you log out, all your resources in the team (applications, knowledge bases, folders, managed groups, etc.) will be transferred to the team owner.", + "confirm_delete_org": "Confirm to delete organization?", + "confirm_delete_member": "Confirm to delete member?", "create_group": "Create group", "delete": "delete", "edit_info": "Edit information", "group": "group", "group_name": "Group name", + "org": "organization", + "org_name": "Organization name", + "org_description": "Organization description", + "create_org": "Create organization", + "create_sub_org": "Create sub-organization", + "edit_org_info": "Edit organization information", + "move_org": "Move organization", + "move_member": "Move member", + "delete_org": "Delete organization", + "remark": "remark", "label_sync": "Tag sync", "leave_team_failed": "Leaving the team exception", "manage_member": "Managing members", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 76e00a859941..3a97d8f6d159 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -85,6 +85,13 @@ "code_error.team_error.un_auth": "Unauthorized to Operate This Team", "code_error.team_error.user_not_active": "The user did not accept or has left the team", "code_error.team_error.website_sync_not_enough": "Unauthorized to Use Website Sync", + "code_error.team_error.org_member_not_exist": "Organization member does not exist", + "code_error.team_error.org_member_duplicated": "Duplicate organization member", + "code_error.team_error.org_not_exist": "Organization does not exist", + "code_error.team_error.org_parent_not_exist": "Parent organization does not exist", + "code_error.team_error.cannot_move_to_sub_path": "Cannot move to same or subdirectory", + "code_error.team_error.cannot_modify_root_org": "Cannot modify root organization", + "code_error.team_error.cannot_delete_non_empty_org": "Cannot delete non-empty organization", "code_error.token_error_code.403": "Invalid Login Status, Please Re-login", "code_error.user_error.balance_not_enough": "Insufficient Account Balance", "code_error.user_error.bin_visitor": "Identity Verification Failed", diff --git a/packages/web/i18n/zh-CN/account_team.json b/packages/web/i18n/zh-CN/account_team.json index 720dff39f1ab..f63470682e26 100644 --- a/packages/web/i18n/zh-CN/account_team.json +++ b/packages/web/i18n/zh-CN/account_team.json @@ -1,29 +1,41 @@ -{ - "total_team_members": "共 {{amount}} 名成员", - "member": "成员", - "group": "群组", - "permission": "权限", - "user_name": "用户名", - "member_group": "所属成员组", - "action": "操作", - "waiting": "待接受", - "remove_tip": "确认将 {{username}} 移出团队?", - - "confirm_leave_team": "确认离开该团队? \n 退出后,您在该团队所有的资源( 应用、知识库、文件夹、管理的群组等)均转让给团队所有者。", - "leave_team_failed": "离开团队异常", - "label_sync": "标签同步", - "user_team_invite_member": "邀请成员", - "user_team_leave_team": "离开团队", - "user_team_leave_team_failed": "离开团队失败", - "create_group": "创建群组", - "search_member_group_name": "搜索成员/群组名称", - "confirm_delete_group": "确认删除群组?", - "group_name": "群组名称", - "owner": "所有者", - "manage_member": "管理成员", - "edit_info": "编辑信息", - - "transfer_ownership": "转让所有者", - "delete": "删除", - "retain_admin_permissions": "保留管理员权限" -} +{ + "total_team_members": "共 {{amount}} 名成员", + "member": "成员", + "group": "群组", + "org": "组织", + "org_name": "组织名称", + "org_description": "介绍", + "permission": "权限", + "user_name": "用户名", + "member_group": "所属成员组", + "action": "操作", + "remark": "备注", + "waiting": "待接受", + "remove_tip": "确认将 {{username}} 移出团队?", + + "confirm_leave_team": "确认离开该团队? \n 退出后,您在该团队所有的资源( 应用、知识库、文件夹、管理的群组等)均转让给团队所有者。", + "leave_team_failed": "离开团队异常", + "label_sync": "标签同步", + "user_team_invite_member": "邀请成员", + "user_team_leave_team": "离开团队", + "user_team_leave_team_failed": "离开团队失败", + "create_group": "创建群组", + "search_member_group_name": "搜索成员/群组名称", + "confirm_delete_group": "确认删除群组?", + "group_name": "群组名称", + "owner": "所有者", + "manage_member": "管理成员", + "edit_info": "编辑信息", + "create_org": "创建组织", + "create_sub_org": "创建子组织", + "edit_org_info": "编辑组织信息", + "move_org": "移动组织", + "move_member": "移动成员", + "delete_org": "删除组织", + "confirm_delete_org": "确认删除组织?", + "confirm_delete_member": "确认删除成员?", + + "transfer_ownership": "转让所有者", + "delete": "删除", + "retain_admin_permissions": "保留管理员权限" +} diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index 5720977c162c..bb22597d7e41 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -89,6 +89,13 @@ "code_error.team_error.un_auth": "无权操作该团队", "code_error.team_error.user_not_active": "用户未接受或已离开团队", "code_error.team_error.website_sync_not_enough": "无权使用Web站点同步~", + "code_error.team_error.org_member_not_exist": "组织成员不存在", + "code_error.team_error.org_member_duplicated": "重复的组织成员", + "code_error.team_error.org_not_exist": "组织不存在", + "code_error.team_error.org_parent_not_exist": "父组织不存在", + "code_error.team_error.cannot_move_to_sub_path": "不能移动到相同或子目录", + "code_error.team_error.cannot_modify_root_org": "不能修改根组织", + "code_error.team_error.cannot_delete_non_empty_org": "不能删除非空组织", "code_error.token_error_code.403": "登录状态无效,请重新登录", "code_error.user_error.balance_not_enough": "账号余额不足~", "code_error.user_error.bin_visitor": "您的身份校验未通过", diff --git a/packages/web/i18n/zh-Hant/account_team.json b/packages/web/i18n/zh-Hant/account_team.json index c02d361e162e..bffc37abd9d5 100644 --- a/packages/web/i18n/zh-Hant/account_team.json +++ b/packages/web/i18n/zh-Hant/account_team.json @@ -2,11 +2,23 @@ "action": "操作", "confirm_delete_group": "確認刪除群組?", "confirm_leave_team": "確認離開該團隊? \n \n退出後,您在該團隊所有的資源( 應用程式、知識庫、資料夾、管理的群組等)均轉讓給團隊所有者。", + "confirm_delete_org": "確認刪除組織?", + "confirm_delete_member": "確認刪除成員?", "create_group": "建立群組", "delete": "刪除", "edit_info": "編輯訊息", "group": "群組", "group_name": "群組名稱", + "org": "組織", + "org_name": "組織名稱", + "org_description": "介紹", + "create_org": "建立組織", + "create_sub_org": "建立子組織", + "edit_org_info": "編輯組織訊息", + "move_org": "移動組織", + "move_member": "移動成員", + "delete_org": "刪除組織", + "remark": "備註", "label_sync": "標籤同步", "leave_team_failed": "離開團隊異常", "manage_member": "管理成員", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index f046d02c5ce4..f5ccd6d67268 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -85,6 +85,13 @@ "code_error.team_error.un_auth": "無權操作此團隊", "code_error.team_error.user_not_active": "使用者未接受或已離開團隊", "code_error.team_error.website_sync_not_enough": "無權使用網站同步", + "code_error.team_error.org_member_not_exist": "組織成員不存在", + "code_error.team_error.org_member_duplicated": "重複的組織成員", + "code_error.team_error.org_not_exist": "組織不存在", + "code_error.team_error.org_parent_not_exist": "父組織不存在", + "code_error.team_error.cannot_move_to_sub_path": "無法移動到相同或子目錄", + "code_error.team_error.cannot_modify_root_org": "無法修改根組織", + "code_error.team_error.cannot_delete_non_empty_org": "無法刪除非空組織", "code_error.token_error_code.403": "登入狀態無效,請重新登入", "code_error.user_error.balance_not_enough": "帳戶餘額不足", "code_error.user_error.bin_visitor": "身份驗證未通過", diff --git a/projects/app/public/imgs/avatar/defaultOrgAvatar.svg b/projects/app/public/imgs/avatar/defaultOrgAvatar.svg new file mode 100644 index 000000000000..38b019594b8e --- /dev/null +++ b/projects/app/public/imgs/avatar/defaultOrgAvatar.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/projects/app/src/components/support/permission/MemberManager/AddMemberModal.tsx b/projects/app/src/components/support/permission/MemberManager/AddMemberModal.tsx index e113fec3dcba..24611a644ba7 100644 --- a/projects/app/src/components/support/permission/MemberManager/AddMemberModal.tsx +++ b/projects/app/src/components/support/permission/MemberManager/AddMemberModal.tsx @@ -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(''); - 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([]); const [selectedGroupIdList, setSelectedGroupIdList] = useState([]); + const [selectedOrgIdList, setSelectedOrgIdList] = useState([]); 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) { /> + {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 ( + + + + + {org.name} + + {!!collaborator && ( + + )} + + ); + })} {filterGroups.map((group) => { const onChange = () => { setSelectedGroupIdList((state) => { @@ -198,10 +255,44 @@ function AddMemberModal({ onClose, mode = 'member' }: AddModalPropsType) { - {t('user:has_chosen') + ': '}{' '} - {selectedMemberIdList.length + selectedGroupIdList.length} + {`${t('user:has_chosen')}: `} + {selectedMemberIdList.length + selectedGroupIdList.length + selectedOrgIdList.length} + {selectedOrgIdList.map((orgId) => { + const org = orgs.find((v) => String(v._id) === orgId); + return ( + + setSelectedOrgIdList(selectedOrgIdList.filter((v) => v !== orgId)) + } + > + + + {org?.name} + + + + ); + })} {selectedGroupIdList.map((groupId) => { const onChange = () => { setSelectedGroupIdList((state) => { diff --git a/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx b/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx index 37dc4260ec02..b7e95d1a2dd4 100644 --- a/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx +++ b/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx @@ -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) { > - + {item.name === DefaultGroupName ? userInfo?.team.teamName : item.name} @@ -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; + }>); }} /> )} diff --git a/projects/app/src/components/support/permission/MemberManager/MemberListCard.tsx b/projects/app/src/components/support/permission/MemberManager/MemberListCard.tsx index 477c777f7f29..fe85f36a3eaa 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberListCard.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberListCard.tsx @@ -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 }; @@ -31,12 +31,12 @@ const MemberListCard = ({ tagStyle, ...props }: MemberListCardProps) => { {collaboratorList?.map((member) => { return ( - + {member.name === DefaultGroupName ? userInfo?.team.teamName : member.name} diff --git a/projects/app/src/components/support/permission/MemberManager/context.tsx b/projects/app/src/components/support/permission/MemberManager/context.tsx index 5ec06bd935af..dd13afafc195 100644 --- a/projects/app/src/components/support/permission/MemberManager/context.tsx +++ b/projects/app/src/components/support/permission/MemberManager/context.tsx @@ -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; permissionList: PermissionListType; onUpdateCollaborators: (props: UpdateClbPermissionProps) => Promise; - onDelOneCollaborator: (props: RequireOnlyOne<{ tmbId: string; groupId: string }>) => Promise; + onDelOneCollaborator: ( + props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }> + ) => Promise; refreshDeps?: any[]; mode?: 'member' | 'all'; }; @@ -46,19 +51,19 @@ type CollaboratorContextType = MemberManagerPropsType & {}; export const CollaboratorContext = createContext({ 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 { + onGetCollaboratorList: (): Promise => { 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(); diff --git a/projects/app/src/pages/account/team/components/OrgManage/IconButton.tsx b/projects/app/src/pages/account/team/components/OrgManage/IconButton.tsx new file mode 100644 index 000000000000..346806da12c0 --- /dev/null +++ b/projects/app/src/pages/account/team/components/OrgManage/IconButton.tsx @@ -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 ( + + ); +} + +export default IconButton; diff --git a/projects/app/src/pages/account/team/components/OrgManage/OrgInfoModal.tsx b/projects/app/src/pages/account/team/components/OrgManage/OrgInfoModal.tsx new file mode 100644 index 000000000000..e8d6799f5382 --- /dev/null +++ b/projects/app/src/pages/account/team/components/OrgManage/OrgInfoModal.tsx @@ -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({ + 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 ( + + + {t('user:team.avatar_and_name')} + + + + + {t('account_team:org_description')} +