From 3a4b4a866b2608ff30450d42b4e5b75c655ccfec Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Wed, 9 Oct 2024 18:32:10 +0800 Subject: [PATCH] Team group (#2864) * feat(member-group): Team (#2616) * feat: member-group schema define * feat(fe): create group * feat: add group edit modal * feat(fe): add avatar group component * feat: edit group fix: permission select menu style * feat: bio-mode support for select-member component * fix: avatar group key unique * feat: group manage * feat: divide member into group and clbs * feat: finish team permission * chore: adjust * fix: get clbs * perf: groups code * pref: member group for team (#2706) * chore: fe adjust fix: remove the member from groups when removing from team feat: change the groups avatar when updating the team's avatar * chore: DefaultGroupName as a constant string '' * fix: create default group when create team for root * feat: comment * feat: 4811 init * pref: member group for team (#2732) * chore: default group name * feat: get default group when get by tmbid * feat(fe): adjust * member ui * fix: delete group (#2736) * perf: init4811 * pref: member group (#2818) * fix: update clb per then refetch clb list * fix: calculate group permission * feat(fe): group tag * refactor(fe): team and group manage * feat: manage group member * feat: add group transfer owner modal * feat: group manage member * chore: adjust the file structure * pref: member group * chore: adjust fe style * fix: ts error * chore: fe adjust * chore: fe adjust * chore: adjust * chore: adjust the code * perf: i18n and schema name * pref: member-group (#2862) * feat: group list ordered by updateTime * fix: transfer ownership of group when deleting member * fix: i18n fix * feat: can not set member as admin/owner when user is not active * fix: GroupInfoModal hover input do not change color * fix(fe): searchinput do not scroll * perf: team group ui * doc * remove enum --------- Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com> --- .../zh-cn/docs/development/upgrading/4811.md | 23 +- packages/global/common/error/code/team.ts | 27 +- .../global/common/file/image/constants.ts | 5 + packages/global/common/system/constants.ts | 1 + .../support/permission/collaborator.d.ts | 16 +- .../global/support/permission/constant.ts | 2 +- .../global/support/permission/controller.ts | 38 +- .../permission/memberGroup/constant.ts | 23 ++ .../support/permission/memberGroup/type.d.ts | 27 ++ packages/global/support/permission/type.d.ts | 7 +- .../support/permission/user/constant.ts | 15 +- .../support/permission/user/controller.ts | 3 +- .../global/support/user/team/controller.d.ts | 1 - .../global/support/user/team/group/api.d.ts | 17 + .../support/user/team/group/constant.ts | 1 + packages/global/support/user/team/type.d.ts | 4 +- .../service/support/permission/app/auth.ts | 2 +- .../service/support/permission/controller.ts | 109 +++++- .../support/permission/dataset/auth.ts | 2 +- .../support/permission/inheritPermission.ts | 7 +- .../permission/memberGroup/controllers.ts | 183 +++++++++ .../memberGroup/groupMemberSchema.ts | 44 +++ .../memberGroup/memberGroupSchema.ts | 51 +++ packages/service/support/permission/schema.ts | 11 +- packages/service/support/permission/type.d.ts | 6 +- .../service/support/user/team/controller.ts | 58 ++- .../service/support/user/team/teamSchema.ts | 5 - .../components/common/Avatar/AvatarGroup.tsx | 55 +++ .../web/components/common/Icon/constants.ts | 1 + .../common/Icon/icons/common/inviteLight.svg | 16 +- .../common/Icon/icons/core/dataset/tag.svg | 8 +- .../common/Icon/icons/modal/changePer.svg | 6 +- .../common/Icon/icons/support/team/group.svg | 3 + .../common/Input/SearchInput/index.tsx | 12 +- .../web/components/common/MyMenu/index.tsx | 13 +- .../web/components/common/MyModal/index.tsx | 7 +- .../components/common/Tabs/LightRowTabs.tsx | 5 +- packages/web/i18n/en/app.json | 2 +- packages/web/i18n/en/common.json | 2 +- packages/web/i18n/en/login.json | 2 +- packages/web/i18n/en/user.json | 1 + packages/web/i18n/en/workflow.json | 2 +- packages/web/i18n/zh/app.json | 2 +- packages/web/i18n/zh/common.json | 12 +- packages/web/i18n/zh/login.json | 2 +- packages/web/i18n/zh/user.json | 32 +- packages/web/i18n/zh/workflow.json | 2 +- .../public/imgs/avatar/defaultTeamAvatar.svg | 10 + .../permission/ChangeOwnerModal/index.tsx | 5 +- .../support/permission/Group/GroupTags.tsx | 59 +++ .../MemberManager/AddMemberModal.tsx | 10 +- .../permission/MemberManager/ManageModal.tsx | 34 +- .../MemberManager/PermissionSelect.tsx | 57 +-- .../permission/MemberManager/context.tsx | 2 +- .../support/user/team/Info/MemberTag.tsx | 21 + .../user/team/TeamManageModal/TeamCard.tsx | 164 ++++++-- .../user/team/TeamManageModal/TeamList.tsx | 4 +- .../components/EditInfoModal.tsx | 6 +- .../components/GroupManage/GroupInfoModal.tsx | 128 +++++++ .../GroupManage/GroupManageMember.tsx | 275 ++++++++++++++ .../GroupManage/GroupTransferOwnerModal.tsx | 200 ++++++++++ .../components/GroupManage/index.tsx | 217 +++++++++++ .../components/InviteModal.tsx | 91 ++--- .../components/MemberTable.tsx | 116 +++--- .../PermissionManage/AddManager.tsx | 170 --------- .../components/PermissionManage/index.tsx | 358 +++++++++++++----- .../components/SelectMember.tsx | 279 ++++++++++++++ .../user/team/TeamManageModal/context.tsx | 139 ++++--- .../user/team/TeamManageModal/index.tsx | 7 +- .../support/user/team/TeamMenu/index.tsx | 2 +- projects/app/src/pages/api/admin/initv4811.ts | 64 ++++ .../pages/app/detail/components/InfoModal.tsx | 45 ++- .../src/pages/app/list/components/List.tsx | 6 +- projects/app/src/pages/app/list/index.tsx | 6 +- .../CollectionCard/TagManageModal.tsx | 1 + .../src/pages/dataset/list/component/List.tsx | 6 +- projects/app/src/pages/dataset/list/index.tsx | 17 +- projects/app/src/web/support/user/team/api.ts | 12 +- .../src/web/support/user/team/group/api.ts | 17 + .../app/src/web/support/user/useUserStore.ts | 20 + 80 files changed, 2670 insertions(+), 751 deletions(-) create mode 100644 packages/global/support/permission/memberGroup/constant.ts create mode 100644 packages/global/support/permission/memberGroup/type.d.ts create mode 100644 packages/global/support/user/team/group/api.d.ts create mode 100644 packages/global/support/user/team/group/constant.ts create mode 100644 packages/service/support/permission/memberGroup/controllers.ts create mode 100644 packages/service/support/permission/memberGroup/groupMemberSchema.ts create mode 100644 packages/service/support/permission/memberGroup/memberGroupSchema.ts create mode 100644 packages/web/components/common/Avatar/AvatarGroup.tsx create mode 100644 packages/web/components/common/Icon/icons/support/team/group.svg create mode 100644 projects/app/public/imgs/avatar/defaultTeamAvatar.svg create mode 100644 projects/app/src/components/support/permission/Group/GroupTags.tsx create mode 100644 projects/app/src/components/support/user/team/Info/MemberTag.tsx create mode 100644 projects/app/src/components/support/user/team/TeamManageModal/components/GroupManage/GroupInfoModal.tsx create mode 100644 projects/app/src/components/support/user/team/TeamManageModal/components/GroupManage/GroupManageMember.tsx create mode 100644 projects/app/src/components/support/user/team/TeamManageModal/components/GroupManage/GroupTransferOwnerModal.tsx create mode 100644 projects/app/src/components/support/user/team/TeamManageModal/components/GroupManage/index.tsx delete mode 100644 projects/app/src/components/support/user/team/TeamManageModal/components/PermissionManage/AddManager.tsx create mode 100644 projects/app/src/components/support/user/team/TeamManageModal/components/SelectMember.tsx create mode 100644 projects/app/src/pages/api/admin/initv4811.ts create mode 100644 projects/app/src/web/support/user/team/group/api.ts diff --git a/docSite/content/zh-cn/docs/development/upgrading/4811.md b/docSite/content/zh-cn/docs/development/upgrading/4811.md index 4a66b9571..67589f1d6 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/4811.md +++ b/docSite/content/zh-cn/docs/development/upgrading/4811.md @@ -101,14 +101,15 @@ weight: 813 13. 新增 - 支持工作流嵌套子应用时,可以设置`非流模式`,同时简易模式也可以选择工作流作为插件了,简易模式调用子应用时,都将强制使用非流模式。 14. 新增 - 调试模式下,子应用调用,支持返回详细运行数据。 15. 新增 - 保留所有模式下子应用嵌套调用的日志。 -16. 优化 - 工作流嵌套层级限制 20 层,避免因编排不合理导致的无限死循环。 -17. 优化 - 工作流 handler 性能优化。 -18. 优化 - 工作流快捷键,避免调试测试时也会触发复制和回退。 -19. 修复 - 工作流工具调用中修改全局变量后,无法传递到后续流程。 -20. 优化 - 流输出,切换浏览器 Tab 后仍可以继续输出。 -21. 优化 - 完善外部文件知识库相关 API -22. 修复 - 知识库选择权限问题。 -23. 修复 - 空 chatId 发起对话,首轮携带用户选择时会异常。 -24. 修复 - createDataset 接口,intro 为赋值。 -25. 修复 - 对话框渲染性能问题。 -26. 修复 - 工具调用历史记录存储不正确。 +16. 新增 - 商业版支持团队成员组,后续将逐渐覆盖工作台和知识库权限。 +17. 优化 - 工作流嵌套层级限制 20 层,避免因编排不合理导致的无限死循环。 +18. 优化 - 工作流 handler 性能优化。 +19. 优化 - 工作流快捷键,避免调试测试时也会触发复制和回退。 +20. 修复 - 工作流工具调用中修改全局变量后,无法传递到后续流程。 +21. 优化 - 流输出,切换浏览器 Tab 后仍可以继续输出。 +22. 优化 - 完善外部文件知识库相关 API +23. 修复 - 知识库选择权限问题。 +24. 修复 - 空 chatId 发起对话,首轮携带用户选择时会异常。 +25. 修复 - createDataset 接口,intro 为赋值。 +26. 修复 - 对话框渲染性能问题。 +27. 修复 - 工具调用历史记录存储不正确。 diff --git a/packages/global/common/error/code/team.ts b/packages/global/common/error/code/team.ts index f404afe1a..8ad9abe02 100644 --- a/packages/global/common/error/code/team.ts +++ b/packages/global/common/error/code/team.ts @@ -10,7 +10,12 @@ export enum TeamErrEnum { appAmountNotEnough = 'appAmountNotEnough', pluginAmountNotEnough = 'pluginAmountNotEnough', websiteSyncNotEnough = 'websiteSyncNotEnough', - reRankNotEnough = 'reRankNotEnough' + reRankNotEnough = 'reRankNotEnough', + groupNameEmpty = 'groupNameEmpty', + groupNameDuplicate = 'groupNameDuplicate', + groupNotExist = 'groupNotExist', + cannotDeleteDefaultGroup = 'cannotDeleteDefaultGroup', + userNotActive = 'userNotActive' } const teamErr = [ @@ -46,6 +51,26 @@ const teamErr = [ { statusText: TeamErrEnum.reRankNotEnough, message: i18nT('common:code_error.team_error.re_rank_not_enough') + }, + { + statusText: TeamErrEnum.groupNameEmpty, + message: i18nT('common:code_error.team_error.group_name_empty') + }, + { + statusText: TeamErrEnum.groupNotExist, + message: i18nT('common:code_error.team_error.group_not_exist') + }, + { + statusText: TeamErrEnum.cannotDeleteDefaultGroup, + message: i18nT('common:code_error.team_error.cannot_delete_default_group') + }, + { + statusText: TeamErrEnum.groupNameDuplicate, + message: i18nT('common:code_error.team_error.group_name_duplicate') + }, + { + statusText: TeamErrEnum.userNotActive, + message: i18nT('common:code_error.team_error.user_not_active') } ]; diff --git a/packages/global/common/file/image/constants.ts b/packages/global/common/file/image/constants.ts index 8e810919d..6a178e2af 100644 --- a/packages/global/common/file/image/constants.ts +++ b/packages/global/common/file/image/constants.ts @@ -7,6 +7,7 @@ export enum MongoImageTypeEnum { datasetAvatar = 'datasetAvatar', userAvatar = 'userAvatar', teamAvatar = 'teamAvatar', + groupAvatar = 'groupAvatar', chatImage = 'chatImage', collectionImage = 'collectionImage' @@ -36,6 +37,10 @@ export const mongoImageTypeMap = { label: 'teamAvatar', unique: true }, + [MongoImageTypeEnum.groupAvatar]: { + label: 'groupAvatar', + unique: true + }, [MongoImageTypeEnum.chatImage]: { label: 'chatImage', diff --git a/packages/global/common/system/constants.ts b/packages/global/common/system/constants.ts index 04ff3ea1d..5c6b192d6 100644 --- a/packages/global/common/system/constants.ts +++ b/packages/global/common/system/constants.ts @@ -1,3 +1,4 @@ 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`; diff --git a/packages/global/support/permission/collaborator.d.ts b/packages/global/support/permission/collaborator.d.ts index 1d4a957de..ea7801446 100644 --- a/packages/global/support/permission/collaborator.d.ts +++ b/packages/global/support/permission/collaborator.d.ts @@ -1,3 +1,4 @@ +import { RequireAtLeastOne, RequireOnlyOne } from '../../common/type/utils'; import { Permission } from './controller'; import { PermissionValueType } from './type'; @@ -10,6 +11,19 @@ export type CollaboratorItemType = { }; export type UpdateClbPermissionProps = { - tmbIds: string[]; + members?: string[]; + groups?: string[]; permission: PermissionValueType; }; + +export type DeleteClbPermissionProps = RequireOnlyOne<{ + tmbId: string; + groupId: string; +}>; + +export type UpdatePermissionBody = { + permission: PermissionValueType; +} & RequireOnlyOne<{ + memberId: string; + groupId: string; +}>; diff --git a/packages/global/support/permission/constant.ts b/packages/global/support/permission/constant.ts index ff4e8454d..6e0e2c1b0 100644 --- a/packages/global/support/permission/constant.ts +++ b/packages/global/support/permission/constant.ts @@ -61,7 +61,7 @@ export const PermissionList: PermissionListType = { [PermissionKeyEnum.write]: { name: i18nT('common:permission.write'), description: '', - value: 0b110, // 如果某个资源有特殊要求,再重写这个值 + value: 0b110, checkBoxType: 'single' }, [PermissionKeyEnum.manage]: { diff --git a/packages/global/support/permission/controller.ts b/packages/global/support/permission/controller.ts index 0666612af..695b9327c 100644 --- a/packages/global/support/permission/controller.ts +++ b/packages/global/support/permission/controller.ts @@ -1,9 +1,10 @@ -import { PermissionValueType } from './type'; +import { PermissionListType, PermissionValueType } from './type'; import { PermissionList, NullPermission, OwnerPermissionVal } from './constant'; export type PerConstructPros = { per?: PermissionValueType; isOwner?: boolean; + permissionList?: PermissionListType; }; // the Permission helper class @@ -13,9 +14,10 @@ export class Permission { hasManagePer: boolean; hasWritePer: boolean; hasReadPer: boolean; + _permissionList: PermissionListType; constructor(props?: PerConstructPros) { - const { per = NullPermission, isOwner = false } = props || {}; + const { per = NullPermission, isOwner = false, permissionList = PermissionList } = props || {}; if (isOwner) { this.value = OwnerPermissionVal; } else { @@ -23,9 +25,10 @@ export class Permission { } this.isOwner = isOwner; - this.hasManagePer = this.checkPer(PermissionList['manage'].value); - this.hasWritePer = this.checkPer(PermissionList['write'].value); - this.hasReadPer = this.checkPer(PermissionList['read'].value); + this._permissionList = permissionList; + this.hasManagePer = this.checkPer(this._permissionList['manage'].value); + this.hasWritePer = this.checkPer(this._permissionList['write'].value); + this.hasReadPer = this.checkPer(this._permissionList['read'].value); } // add permission(s) @@ -36,36 +39,39 @@ export class Permission { // perm.add(PermissionList['read'], PermissionList['write']) // perm.add(PermissionList['read']).add(PermissionList['write']) addPer(...perList: PermissionValueType[]) { - for (let oer of perList) { - this.value = this.value | oer; + if (this.isOwner) { + return this; + } + for (const per of perList) { + this.value = this.value | per; } this.updatePermissions(); - return this.value; + return this; } removePer(...perList: PermissionValueType[]) { - for (let per of perList) { + if (this.isOwner) { + return this.value; + } + for (const per of perList) { this.value = this.value & ~per; } this.updatePermissions(); - return this.value; + return this; } checkPer(perm: PermissionValueType): boolean { // if the permission is owner permission, only owner has this permission. if (perm === OwnerPermissionVal) { return this.value === OwnerPermissionVal; - } else if (this.hasManagePer) { - // The manager has all permissions except the owner permission - return true; } return (this.value & perm) === perm; } private updatePermissions() { this.isOwner = this.value === OwnerPermissionVal; - this.hasManagePer = this.checkPer(PermissionList['manage'].value); - this.hasWritePer = this.checkPer(PermissionList['write'].value); - this.hasReadPer = this.checkPer(PermissionList['read'].value); + this.hasManagePer = this.checkPer(this._permissionList['manage'].value); + this.hasWritePer = this.checkPer(this._permissionList['write'].value); + this.hasReadPer = this.checkPer(this._permissionList['read'].value); } } diff --git a/packages/global/support/permission/memberGroup/constant.ts b/packages/global/support/permission/memberGroup/constant.ts new file mode 100644 index 000000000..db12c05b4 --- /dev/null +++ b/packages/global/support/permission/memberGroup/constant.ts @@ -0,0 +1,23 @@ +import { PermissionKeyEnum, PermissionList } from '../constant'; +import { PermissionListType } from '../type'; + +export enum GroupMemberRole { + owner = 'owner', + admin = 'admin', + member = 'member' +} + +export const memberGroupPermissionList: PermissionListType = { + [PermissionKeyEnum.read]: { + ...PermissionList[PermissionKeyEnum.read], + value: 0b100 + }, + [PermissionKeyEnum.write]: { + ...PermissionList[PermissionKeyEnum.write], + value: 0b010 + }, + [PermissionKeyEnum.manage]: { + ...PermissionList[PermissionKeyEnum.manage], + value: 0b001 + } +}; diff --git a/packages/global/support/permission/memberGroup/type.d.ts b/packages/global/support/permission/memberGroup/type.d.ts new file mode 100644 index 000000000..7045cf534 --- /dev/null +++ b/packages/global/support/permission/memberGroup/type.d.ts @@ -0,0 +1,27 @@ +import { TeamMemberItemType } from 'support/user/team/type'; +import { TeamPermission } from '../user/controller'; +import { GroupMemberRole } from './constant'; + +type MemberGroupSchemaType = { + _id: string; + teamId: string; + name: string; + avatar: string; + updateTime: Date; +}; + +type GroupMemberSchemaType = { + groupId: string; + tmbId: string; + role: `${GroupMemberRole}`; +}; + +type MemberGroupType = MemberGroupSchemaType & { + members: { + tmbId: string; + role: `${GroupMemberRole}`; + }[]; // we can get tmb's info from other api. there is no need but only need to get tmb's id + permission: TeamPermission; +}; + +type MemberGroupListType = MemberGroupType[]; diff --git a/packages/global/support/permission/type.d.ts b/packages/global/support/permission/type.d.ts index 8a6ea45a8..a97b43c14 100644 --- a/packages/global/support/permission/type.d.ts +++ b/packages/global/support/permission/type.d.ts @@ -1,3 +1,4 @@ +import { RequireOnlyOne } from '../../common/type/utils'; import { TeamMemberWithUserSchema } from '../user/team/type'; import { AuthUserTypeEnum, PermissionKeyEnum, PerResourceTypeEnum } from './constant'; @@ -20,11 +21,13 @@ export type PermissionListType = Record< export type ResourcePermissionType = { teamId: string; - tmbId: string; resourceType: ResourceType; permission: PermissionValueType; resourceId: string; -}; +} & RequireOnlyOne<{ + tmbId: string; + groupId: string; +}>; export type ResourcePerWithTmbWithUser = Omit & { tmbId: TeamMemberWithUserSchema; diff --git a/packages/global/support/permission/user/constant.ts b/packages/global/support/permission/user/constant.ts index cef0671e6..743753cbd 100644 --- a/packages/global/support/permission/user/constant.ts +++ b/packages/global/support/permission/user/constant.ts @@ -1,19 +1,22 @@ -import { PermissionKeyEnum, PermissionList, ReadPermissionVal } from '../constant'; +import { PermissionKeyEnum } from '../constant'; import { PermissionListType } from '../type'; -import { i18nT } from '../../../../web/i18n/utils'; +import { PermissionList } from '../constant'; export const TeamPermissionList: PermissionListType = { [PermissionKeyEnum.read]: { ...PermissionList[PermissionKeyEnum.read], - description: i18nT('user:permission_des.read') + value: 0b100 }, [PermissionKeyEnum.write]: { ...PermissionList[PermissionKeyEnum.write], - description: i18nT('user:permission_des.write') + value: 0b010 }, [PermissionKeyEnum.manage]: { ...PermissionList[PermissionKeyEnum.manage], - description: i18nT('user:permission_des.manage') + value: 0b001 } }; -export const TeamDefaultPermissionVal = ReadPermissionVal; +export const TeamReadPermissionVal = TeamPermissionList['read'].value; +export const TeamWritePermissionVal = TeamPermissionList['write'].value; +export const TeamManagePermissionVal = TeamPermissionList['manage'].value; +export const TeamDefaultPermissionVal = TeamReadPermissionVal; diff --git a/packages/global/support/permission/user/controller.ts b/packages/global/support/permission/user/controller.ts index ec1c09386..18129e6dd 100644 --- a/packages/global/support/permission/user/controller.ts +++ b/packages/global/support/permission/user/controller.ts @@ -1,5 +1,5 @@ import { PerConstructPros, Permission } from '../controller'; -import { TeamDefaultPermissionVal } from './constant'; +import { TeamDefaultPermissionVal, TeamPermissionList } from './constant'; export class TeamPermission extends Permission { constructor(props?: PerConstructPros) { @@ -10,6 +10,7 @@ export class TeamPermission extends Permission { } else if (!props?.per) { props.per = TeamDefaultPermissionVal; } + props.permissionList = TeamPermissionList; super(props); } } diff --git a/packages/global/support/user/team/controller.d.ts b/packages/global/support/user/team/controller.d.ts index 6ac334f89..d485b39ae 100644 --- a/packages/global/support/user/team/controller.d.ts +++ b/packages/global/support/user/team/controller.d.ts @@ -33,7 +33,6 @@ export type UpdateTeamMemberProps = { export type InviteMemberProps = { teamId: string; usernames: string[]; - permission: PermissionValueType; }; export type UpdateInviteProps = { tmbId: string; diff --git a/packages/global/support/user/team/group/api.d.ts b/packages/global/support/user/team/group/api.d.ts new file mode 100644 index 000000000..1f2fb04f8 --- /dev/null +++ b/packages/global/support/user/team/group/api.d.ts @@ -0,0 +1,17 @@ +import { GroupMemberRole } from '../../../../support/permission/memberGroup/constant'; + +export type postCreateGroupData = { + name: string; + avatar?: string; + memberIdList?: string[]; +}; + +export type putUpdateGroupData = { + groupId: string; + name?: string; + avatar?: string; + memberList?: { + tmbId: string; + role: `${GroupMemberRole}`; + }[]; +}; diff --git a/packages/global/support/user/team/group/constant.ts b/packages/global/support/user/team/group/constant.ts new file mode 100644 index 000000000..381cfffbb --- /dev/null +++ b/packages/global/support/user/team/group/constant.ts @@ -0,0 +1 @@ +export const DefaultGroupName = 'DEFAULT_GROUP'; diff --git a/packages/global/support/user/team/type.d.ts b/packages/global/support/user/team/type.d.ts index f359797a1..61e518257 100644 --- a/packages/global/support/user/team/type.d.ts +++ b/packages/global/support/user/team/type.d.ts @@ -17,7 +17,6 @@ export type TeamSchema = { lastWebsiteSyncTime: Date; }; lafAccount: LafAccountType; - defaultPermission: PermissionValueType; notificationAccount?: string; }; @@ -25,6 +24,7 @@ export type tagsType = { label: string; key: string; }; + export type TeamTagSchema = TeamTagItemType & { _id: string; teamId: string; @@ -45,9 +45,11 @@ export type TeamMemberSchema = { export type TeamMemberWithUserSchema = Omit & { userId: UserModelSchema; }; + export type TeamMemberWithTeamSchema = Omit & { teamId: TeamSchema; }; + export type TeamMemberWithTeamAndUserSchema = Omit & { userId: UserModelSchema; }; diff --git a/packages/service/support/permission/app/auth.ts b/packages/service/support/permission/app/auth.ts index 0257405ab..9a545b05f 100644 --- a/packages/service/support/permission/app/auth.ts +++ b/packages/service/support/permission/app/auth.ts @@ -86,7 +86,7 @@ export const authAppByTmbId = async ({ resourceId: appId, resourceType: PerResourceTypeEnum.app }); - const Per = new AppPermission({ per: rp?.permission ?? app.defaultPermission, isOwner }); + const Per = new AppPermission({ per: rp ?? app.defaultPermission, isOwner }); return { Per, defaultPermission: app.defaultPermission diff --git a/packages/service/support/permission/controller.ts b/packages/service/support/permission/controller.ts index c3d689597..bb5a048a6 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -8,51 +8,113 @@ import { authOpenApiKey } from '../openapi/auth'; import { FileTokenQuery } from '@fastgpt/global/common/file/type'; import { MongoResourcePermission } from './schema'; import { ClientSession } from 'mongoose'; -import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; -import { ResourcePermissionType } from '@fastgpt/global/support/permission/type'; +import { + PermissionValueType, + ResourcePermissionType +} from '@fastgpt/global/support/permission/type'; import { bucketNameMap } from '@fastgpt/global/common/file/constants'; import { addMinutes } from 'date-fns'; +import { getGroupsByTmbId } from './memberGroup/controllers'; +import { Permission } from '@fastgpt/global/support/permission/controller'; +/** get resource permission for a team member + * If there is no permission for the team member, it will return undefined + * @param resourceType: PerResourceTypeEnum + * @param teamId + * @param tmbId + * @param resourceId + * @returns PermissionValueType | undefined + */ export const getResourcePermission = async ({ resourceType, teamId, tmbId, resourceId }: { - resourceType: PerResourceTypeEnum; teamId: string; tmbId: string; - resourceId?: string; -}) => { - const per = await MongoResourcePermission.findOne({ - tmbId, - teamId, - resourceType, - resourceId - }); +} & ( + | { + resourceType: 'team'; + resourceId?: undefined; + } + | { + resourceType: Omit; + resourceId: string; + } +)): Promise => { + // Personal permission has the highest priority + const tmbPer = ( + await MongoResourcePermission.findOne( + { + tmbId, + teamId, + resourceType, + resourceId + }, + 'permission' + ).lean() + )?.permission; - if (!per) { - return null; + // could be 0 + if (tmbPer !== undefined) { + return tmbPer; } - return per; + + // If there is no personal permission, get the group permission + const groupIdList = (await getGroupsByTmbId({ tmbId, teamId })).map((item) => item._id); + + if (groupIdList.length === 0) { + return undefined; + } + + // get the maximum permission of the group + const pers = ( + await MongoResourcePermission.find( + { + teamId, + resourceType, + groupId: { + $in: groupIdList + }, + resourceId + }, + 'permission' + ).lean() + ).map((item) => item.permission); + + const groupPer = getGroupPer(pers); + + return groupPer; }; + +/* 仅取 members 不取 groups */ export async function getResourceAllClbs({ resourceId, teamId, resourceType, session }: { - resourceId: ParentIdType; teamId: string; - resourceType: PerResourceTypeEnum; session?: ClientSession; -}): Promise { - if (!resourceId) return []; +} & ( + | { + resourceType: 'team'; + resourceId?: undefined; + } + | { + resourceType: Omit; + resourceId?: string | null; + } +)): Promise { return MongoResourcePermission.find( { resourceId, resourceType: resourceType, - teamId: teamId + teamId: teamId, + groupId: { + $exists: false + } }, null, { @@ -60,6 +122,7 @@ export async function getResourceAllClbs({ } ).lean(); } + export const delResourcePermissionById = (id: string) => { return MongoResourcePermission.findByIdAndRemove(id); }; @@ -301,3 +364,11 @@ export const authFileToken = (token?: string) => }); }); }); + +export const getGroupPer = (groups: PermissionValueType[] = []) => { + if (groups.length === 0) { + return undefined; + } + + return new Permission().addPer(...groups).value; +}; diff --git a/packages/service/support/permission/dataset/auth.ts b/packages/service/support/permission/dataset/auth.ts index 52bd44628..2592b02c8 100644 --- a/packages/service/support/permission/dataset/auth.ts +++ b/packages/service/support/permission/dataset/auth.ts @@ -78,7 +78,7 @@ export const authDatasetByTmbId = async ({ resourceType: PerResourceTypeEnum.dataset }); const Per = new DatasetPermission({ - per: rp?.permission ?? dataset.defaultPermission, + per: rp ?? dataset.defaultPermission, isOwner }); return { diff --git a/packages/service/support/permission/inheritPermission.ts b/packages/service/support/permission/inheritPermission.ts index b0422c73c..a30dfb8b9 100644 --- a/packages/service/support/permission/inheritPermission.ts +++ b/packages/service/support/permission/inheritPermission.ts @@ -3,8 +3,9 @@ import { MongoResourcePermission } from './schema'; import { ClientSession, Model } from 'mongoose'; import { NullPermission, PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { PermissionValueType } from '@fastgpt/global/support/permission/type'; -import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { getResourceAllClbs } from './controller'; +import { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; +import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; export type SyncChildrenPermissionResourceType = { _id: string; @@ -14,8 +15,10 @@ export type SyncChildrenPermissionResourceType = { }; export type UpdateCollaboratorItem = { permission: PermissionValueType; +} & RequireOnlyOne<{ tmbId: string; -}; + groupId: string; +}>; // sync the permission to all children folders. export async function syncChildrenPermission({ diff --git a/packages/service/support/permission/memberGroup/controllers.ts b/packages/service/support/permission/memberGroup/controllers.ts new file mode 100644 index 000000000..7203f4b2d --- /dev/null +++ b/packages/service/support/permission/memberGroup/controllers.ts @@ -0,0 +1,183 @@ +import { MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type'; +import { MongoGroupMemberModel } from './groupMemberSchema'; +import { TeamMemberSchema } from '@fastgpt/global/support/user/team/type'; +import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; +import { MongoResourcePermission } from '../schema'; +import { getGroupPer, parseHeaderCert } from '../controller'; +import { MongoMemberGroupModel } from './memberGroupSchema'; +import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; +import { ClientSession } from 'mongoose'; +import { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant'; +import { AuthModeType, AuthResponseType } from '../type'; +import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; +import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; +import { getTmbInfoByTmbId } from '../../user/team/controller'; + +/** + * Get the default group of a team + * @param{Object} obj + * @param{string} obj.teamId + * @param{ClientSession} obj.session + */ +export const getTeamDefaultGroup = async ({ + teamId, + session +}: { + teamId: string; + session?: ClientSession; +}) => { + const group = await MongoMemberGroupModel.findOne({ teamId, name: DefaultGroupName }, undefined, { + session + }).lean(); + + // Create the default group if it does not exist + if (!group) { + const [group] = await MongoMemberGroupModel.create( + [ + { + teamId, + name: DefaultGroupName + } + ], + { session } + ); + + return group; + } + return group; +}; + +export const getGroupsByTmbId = async ({ + tmbId, + teamId, + role +}: { + tmbId: string; + teamId: string; + role?: `${GroupMemberRole}`[]; +}) => + ( + await Promise.all([ + ( + await MongoGroupMemberModel.find({ + tmbId, + groupId: { + $exists: true + }, + role: role ? { $in: role } : undefined + }) + .populate('groupId') + .lean() + ).map((item) => { + return { + ...(item.groupId as any as MemberGroupSchemaType) + }; + }), + + role ? [] : getTeamDefaultGroup({ teamId }) + ]) + ).flat(); + +export const getTmbByGroupId = async (groupId: string) => { + return ( + await MongoGroupMemberModel.find({ + groupId + }) + .populate('tmbId') + .lean() + ).map((item) => { + return { + ...(item.tmbId as any as MemberGroupSchemaType) + }; + }); +}; + +export const getGroupMembersByGroupId = async (groupId: string) => { + return await MongoGroupMemberModel.find({ + groupId + }).lean(); +}; + +export const getGroupMembersWithInfoByGroupId = async (groupId: string) => { + return ( + await MongoGroupMemberModel.find({ + groupId + }) + .populate('tmbId') + .lean() + ).map((item) => item.tmbId) as any as TeamMemberSchema[]; // HACK: type casting +}; + +/** + * Get tmb's group permission: the maximum permission of the group + * @param tmbId + * @param resourceId + * @param resourceType + * @returns the maximum permission of the group + */ +export const getGroupPermission = async ({ + tmbId, + resourceId, + teamId, + resourceType +}: { + tmbId: string; + teamId: string; +} & ( + | { + resourceId?: undefined; + resourceType: 'team'; + } + | { + resourceId: string; + resourceType: Omit; + } +)) => { + const groupIds = (await getGroupsByTmbId({ tmbId, teamId })).map((item) => item._id); + const groupPermissions = ( + await MongoResourcePermission.find({ + groupId: { + $in: groupIds + }, + resourceType, + resourceId, + teamId + }) + ).map((item) => item.permission); + + return getGroupPer(groupPermissions); +}; + +// auth group member role +export const authGroupMemberRole = async ({ + groupId, + role, + ...props +}: { + groupId: string; + role: `${GroupMemberRole}`[]; +} & AuthModeType): Promise => { + const result = await parseHeaderCert(props); + const { teamId, tmbId, isRoot } = result; + if (isRoot) { + return { + ...result, + permission: new TeamPermission({ + isOwner: true + }), + teamId, + tmbId + }; + } + const groupMember = await MongoGroupMemberModel.findOne({ groupId, tmbId }); + const tmb = await getTmbInfoByTmbId({ tmbId }); + if (tmb.permission.hasManagePer || (groupMember && role.includes(groupMember.role))) { + return { + ...result, + permission: tmb.permission, + teamId, + tmbId + }; + } + return Promise.reject(TeamErrEnum.unAuthTeam); +}; diff --git a/packages/service/support/permission/memberGroup/groupMemberSchema.ts b/packages/service/support/permission/memberGroup/groupMemberSchema.ts new file mode 100644 index 000000000..bfd0a8af3 --- /dev/null +++ b/packages/service/support/permission/memberGroup/groupMemberSchema.ts @@ -0,0 +1,44 @@ +import { TeamMemberCollectionName } from '@fastgpt/global/support/user/team/constant'; +import { connectionMongo, getMongoModel } from '../../../common/mongo'; +import { MemberGroupCollectionName } from './memberGroupSchema'; +import { GroupMemberSchemaType } from '@fastgpt/global/support/permission/memberGroup/type'; +import { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant'; +const { Schema } = connectionMongo; + +export const GroupMemberCollectionName = 'team_group_members'; + +export const GroupMemberSchema = new Schema({ + groupId: { + type: Schema.Types.ObjectId, + ref: MemberGroupCollectionName, + required: true + }, + tmbId: { + type: Schema.Types.ObjectId, + ref: TeamMemberCollectionName, + required: true + }, + role: { + type: String, + enum: Object.values(GroupMemberRole), + required: true, + default: GroupMemberRole.member + } +}); + +try { + GroupMemberSchema.index({ + groupId: 1 + }); + + GroupMemberSchema.index({ + tmbId: 1 + }); +} catch (error) { + console.log(error); +} + +export const MongoGroupMemberModel = getMongoModel( + GroupMemberCollectionName, + GroupMemberSchema +); diff --git a/packages/service/support/permission/memberGroup/memberGroupSchema.ts b/packages/service/support/permission/memberGroup/memberGroupSchema.ts new file mode 100644 index 000000000..c962a0968 --- /dev/null +++ b/packages/service/support/permission/memberGroup/memberGroupSchema.ts @@ -0,0 +1,51 @@ +import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant'; +import { connectionMongo, getMongoModel } from '../../../common/mongo'; +import { MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type'; +const { Schema } = connectionMongo; + +export const MemberGroupCollectionName = 'team_member_groups'; + +export const MemberGroupSchema = new Schema( + { + teamId: { + type: Schema.Types.ObjectId, + ref: TeamCollectionName, + required: true + }, + name: { + type: String, + required: true + }, + avatar: { + type: String + }, + updateTime: { + type: Date, + default: () => new Date() + } + }, + { + timestamps: { + updatedAt: 'updateTime' + } + } +); + +try { + MemberGroupSchema.index( + { + teamId: 1, + name: 1 + }, + { + unique: true + } + ); +} catch (error) { + console.log(error); +} + +export const MongoMemberGroupModel = getMongoModel( + MemberGroupCollectionName, + MemberGroupSchema +); diff --git a/packages/service/support/permission/schema.ts b/packages/service/support/permission/schema.ts index 3a6a2ab9a..e381ba2ea 100644 --- a/packages/service/support/permission/schema.ts +++ b/packages/service/support/permission/schema.ts @@ -5,9 +5,10 @@ import { 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'; const { Schema } = connectionMongo; -export const ResourcePermissionCollectionName = 'resource_permission'; +export const ResourcePermissionCollectionName = 'resource_permissions'; export const ResourcePermissionSchema = new Schema({ teamId: { @@ -18,6 +19,10 @@ export const ResourcePermissionSchema = new Schema({ type: Schema.Types.ObjectId, ref: TeamMemberCollectionName }, + groupId: { + type: Schema.Types.ObjectId, + ref: MemberGroupCollectionName + }, resourceType: { type: String, enum: Object.values(PerResourceTypeEnum), @@ -40,12 +45,14 @@ try { resourceType: 1, teamId: 1, tmbId: 1, - resourceId: 1 + resourceId: 1, + groupId: 1 }, { unique: true } ); + ResourcePermissionSchema.index({ resourceType: 1, teamId: 1, diff --git a/packages/service/support/permission/type.d.ts b/packages/service/support/permission/type.d.ts index 7826981cf..176903aa1 100644 --- a/packages/service/support/permission/type.d.ts +++ b/packages/service/support/permission/type.d.ts @@ -1,6 +1,7 @@ import { Permission } from '@fastgpt/global/support/permission/controller'; import { ApiRequestProps } from '../../type/next'; import type { PermissionValueType } from '@fastgpt/global/support/permission/type'; +import { RequireAtLeastOne } from '@fastgpt/global/common/type/utils'; export type ReqHeaderAuthType = { cookie?: string; @@ -11,11 +12,6 @@ export type ReqHeaderAuthType = { authorization?: string; }; -type RequireAtLeastOne = Omit & - { - [K in Keys]-?: Required> & Partial>; - }[Keys]; - type authModeType = { req: ApiRequestProps; authToken?: boolean; diff --git a/packages/service/support/user/team/controller.ts b/packages/service/support/user/team/controller.ts index cc3332e93..bc72a9089 100644 --- a/packages/service/support/user/team/controller.ts +++ b/packages/service/support/user/team/controller.ts @@ -11,6 +11,10 @@ import { UpdateTeamProps } from '@fastgpt/global/support/user/team/controller'; import { getResourcePermission } from '../../permission/controller'; import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; +import { TeamDefaultPermissionVal } from '@fastgpt/global/support/permission/user/constant'; +import { MongoMemberGroupModel } from '../../permission/memberGroup/memberGroupSchema'; +import { mongoSessionRun } from '../../../common/mongo/sessionRun'; +import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; async function getTeamMember(match: Record): Promise { const tmb = (await MongoTeamMember.findOne(match).populate('teamId')) as TeamMemberWithTeamSchema; @@ -18,7 +22,7 @@ async function getTeamMember(match: Record): Promise): Promise { + await MongoTeam.findByIdAndUpdate( + teamId, + { + name, + avatar, + teamDomain, + lafAccount + }, + { session } + ); + + // update default group + if (avatar) { + await MongoMemberGroupModel.updateOne( + { + teamId: teamId, + name: DefaultGroupName + }, + { + avatar + }, + { session } + ); + } }); } diff --git a/packages/service/support/user/team/teamSchema.ts b/packages/service/support/user/team/teamSchema.ts index 596fa9da2..361a076b1 100644 --- a/packages/service/support/user/team/teamSchema.ts +++ b/packages/service/support/user/team/teamSchema.ts @@ -3,7 +3,6 @@ const { Schema } = connectionMongo; import { TeamSchema as TeamType } from '@fastgpt/global/support/user/team/type.d'; import { userCollectionName } from '../../user/schema'; import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant'; -import { TeamDefaultPermissionVal } from '@fastgpt/global/support/permission/user/constant'; const TeamSchema = new Schema({ name: { @@ -14,10 +13,6 @@ const TeamSchema = new Schema({ type: Schema.Types.ObjectId, ref: userCollectionName }, - defaultPermission: { - type: Number, - default: TeamDefaultPermissionVal - }, avatar: { type: String, default: '/icon/logo.svg' diff --git a/packages/web/components/common/Avatar/AvatarGroup.tsx b/packages/web/components/common/Avatar/AvatarGroup.tsx new file mode 100644 index 000000000..2c26b9a9d --- /dev/null +++ b/packages/web/components/common/Avatar/AvatarGroup.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import Avatar from '.'; +import { Box, Flex } from '@chakra-ui/react'; + +/** + * AvatarGroup + * + * @param avatars - avatars array + * @param max - max avatars to show + * @param [groupId] - group id to make the key unique + * @returns + */ +function AvatarGroup({ + avatars, + max = 3, + groupId +}: { + max?: number; + avatars: string[]; + groupId?: string; +}) { + return ( + + {avatars.slice(0, max).map((avatar, index) => ( + 0 ? 'absolute' : 'relative'} + left={index > 0 ? `${index * 15}px` : 0} + zIndex={index > 0 ? index + 1 : 0} + w={'24px'} + borderRadius={'50%'} + /> + ))} + {avatars.length > max && ( + + +{avatars.length - max} + + )} + + ); +} + +export default AvatarGroup; diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index dbe302e5f..74f3e4fa5 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -342,6 +342,7 @@ export const iconPaths = { 'support/permission/collaborator': () => import('./icons/support/permission/collaborator.svg'), 'support/permission/privateLight': () => import('./icons/support/permission/privateLight.svg'), 'support/permission/publicLight': () => import('./icons/support/permission/publicLight.svg'), + 'support/team/group': () => import('./icons/support/team/group.svg'), 'support/team/key': () => import('./icons/support/team/key.svg'), 'support/team/memberLight': () => import('./icons/support/team/memberLight.svg'), 'support/usage/usageRecordLight': () => import('./icons/support/usage/usageRecordLight.svg'), diff --git a/packages/web/components/common/Icon/icons/common/inviteLight.svg b/packages/web/components/common/Icon/icons/common/inviteLight.svg index 6d71ceb6b..665252a18 100644 --- a/packages/web/components/common/Icon/icons/common/inviteLight.svg +++ b/packages/web/components/common/Icon/icons/common/inviteLight.svg @@ -1,11 +1,5 @@ - - - - - \ No newline at end of file + + + + + diff --git a/packages/web/components/common/Icon/icons/core/dataset/tag.svg b/packages/web/components/common/Icon/icons/core/dataset/tag.svg index c8bd2d35a..401a6d881 100644 --- a/packages/web/components/common/Icon/icons/core/dataset/tag.svg +++ b/packages/web/components/common/Icon/icons/core/dataset/tag.svg @@ -1,4 +1,4 @@ - - - - \ No newline at end of file + + + + diff --git a/packages/web/components/common/Icon/icons/modal/changePer.svg b/packages/web/components/common/Icon/icons/modal/changePer.svg index 5f4c06109..8e5ad1ef2 100644 --- a/packages/web/components/common/Icon/icons/modal/changePer.svg +++ b/packages/web/components/common/Icon/icons/modal/changePer.svg @@ -1,4 +1,4 @@ - - - \ No newline at end of file + + + diff --git a/packages/web/components/common/Icon/icons/support/team/group.svg b/packages/web/components/common/Icon/icons/support/team/group.svg new file mode 100644 index 000000000..353e29d58 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/team/group.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/web/components/common/Input/SearchInput/index.tsx b/packages/web/components/common/Input/SearchInput/index.tsx index af7502476..313f9b354 100644 --- a/packages/web/components/common/Input/SearchInput/index.tsx +++ b/packages/web/components/common/Input/SearchInput/index.tsx @@ -1,13 +1,15 @@ import React from 'react'; -import { InputGroup, Input, InputProps, Flex } from '@chakra-ui/react'; +import { Input, InputProps, InputGroup, InputLeftElement } from '@chakra-ui/react'; import MyIcon from '../../Icon'; const SearchInput = (props: InputProps) => { return ( - - - - + + + + + + ); }; diff --git a/packages/web/components/common/MyMenu/index.tsx b/packages/web/components/common/MyMenu/index.tsx index 565a75d0b..98d1442ce 100644 --- a/packages/web/components/common/MyMenu/index.tsx +++ b/packages/web/components/common/MyMenu/index.tsx @@ -143,10 +143,21 @@ const MyMenu = ({ bottom={0} left={0} /> - {Button} + + {Button} + )} diff --git a/packages/web/components/common/Tabs/LightRowTabs.tsx b/packages/web/components/common/Tabs/LightRowTabs.tsx index 45f7859e1..ab743c1c7 100644 --- a/packages/web/components/common/Tabs/LightRowTabs.tsx +++ b/packages/web/components/common/Tabs/LightRowTabs.tsx @@ -67,8 +67,11 @@ const LightRowTabs = ({ justifyContent={'center'} borderBottom={'2px solid'} borderColor={defaultColor} - px={3} + px={1} whiteSpace={'nowrap'} + _hover={{ + color: activeColor + }} {...(value === item.value ? { color: activeColor, diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index e611213b0..769606200 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -159,4 +159,4 @@ "workflow.user_file_input_desc": "Links to documents and images uploaded by users.", "workflow.user_select": "User Selection", "workflow.user_select_tip": "This module can configure multiple options for selection during the dialogue. Different options can lead to different workflow branches." -} +} \ No newline at end of file diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 8effd585e..76cd5d03b 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -1197,4 +1197,4 @@ "verification": "Verification", "xx_search_result": "{{key}} Search Results", "yes": "Yes" -} +} \ No newline at end of file diff --git a/packages/web/i18n/en/login.json b/packages/web/i18n/en/login.json index 7959b85b1..049f20113 100644 --- a/packages/web/i18n/en/login.json +++ b/packages/web/i18n/en/login.json @@ -13,4 +13,4 @@ "root_password_placeholder": "The root user password is the value of the environment variable DEFAULT_ROOT_PSW", "terms": "Terms", "use_root_login": "Log in as root user" -} +} \ No newline at end of file diff --git a/packages/web/i18n/en/user.json b/packages/web/i18n/en/user.json index df4f96b85..0af708fd7 100644 --- a/packages/web/i18n/en/user.json +++ b/packages/web/i18n/en/user.json @@ -78,6 +78,7 @@ "team.add_collaborator": "Add Collaborator", "team.manage_collaborators": "Manage Collaborators", "team.no_collaborators": "No Collaborators", + "team.write_role_member": "", "usage.feishu": "Feishu", "usage.official_account": "Official Account", "usage.share": "Share Link", diff --git a/packages/web/i18n/en/workflow.json b/packages/web/i18n/en/workflow.json index fb0c1dddc..857fd9e50 100644 --- a/packages/web/i18n/en/workflow.json +++ b/packages/web/i18n/en/workflow.json @@ -186,4 +186,4 @@ "workflow.Switch_success": "Switch Successful", "workflow.Team cloud": "Team Cloud", "workflow.exit_tips": "Your changes have not been saved. 'Exit directly' will not save your edits." -} +} \ No newline at end of file diff --git a/packages/web/i18n/zh/app.json b/packages/web/i18n/zh/app.json index 8e11b8a85..fdae5aeba 100644 --- a/packages/web/i18n/zh/app.json +++ b/packages/web/i18n/zh/app.json @@ -159,4 +159,4 @@ "workflow.user_file_input_desc": "用户上传的文档和图片链接", "workflow.user_select": "用户选择", "workflow.user_select_tip": "该模块可配置多个选项,以供对话时选择。不同选项可导向不同工作流支线" -} +} \ No newline at end of file diff --git a/packages/web/i18n/zh/common.json b/packages/web/i18n/zh/common.json index 518710adf..246df7957 100644 --- a/packages/web/i18n/zh/common.json +++ b/packages/web/i18n/zh/common.json @@ -65,7 +65,7 @@ "code_error.outlink_error.un_auth_user": "身份校验失败", "code_error.plugin_error.not_exist": "插件不存在", "code_error.plugin_error.un_auth": "无权操作该插件", - "code_error.system_error.community_version_num_limit": "超出开源版数量限制,请升级商业版: https://tryfastgpt.ai", + "code_error.system_error.community_version_num_limit": "超出开源版数量限制,请升级商业版: https://fastgpt.in", "code_error.team_error.ai_points_not_enough": "", "code_error.team_error.app_amount_not_enough": "应用数量已达上限~", "code_error.team_error.dataset_amount_not_enough": "知识库数量已达上限~", @@ -75,6 +75,8 @@ "code_error.team_error.re_rank_not_enough": "无权使用检索重排~", "code_error.team_error.un_auth": "无权操作该团队", "code_error.team_error.website_sync_not_enough": "无权使用Web站点同步~", + "code_error.team_error.group_name_duplicate": "群组名称重复", + "code_error.team_error.user_not_active": "用户未接受或已离开团队", "code_error.token_error_code.403": "登录状态无效,请重新登录", "code_error.user_error.balance_not_enough": "账号余额不足~", "code_error.user_error.bin_visitor": "您的身份校验未通过", @@ -226,7 +228,7 @@ "common.submit_success": "提交成功", "common.submitted": "已提交", "common.support": "支持", - "common.system.Commercial version function": "请升级商业版后使用该功能:https://tryfastgpt.ai", + "common.system.Commercial version function": "请升级商业版后使用该功能:https://fastgpt.in", "common.system.Help Chatbot": "机器人助手", "common.system.Use Helper": "使用帮助", "common.ui.textarea.Magnifying": "放大", @@ -240,6 +242,10 @@ "comon.Continue_Adding": "继续添加", "compliance.chat": "内容由第三方 AI 生成,无法确保真实准确,仅供参考", "compliance.dataset": "请确保您的内容严格遵守相关法律法规,避免包含任何违法或侵权的内容。请谨慎上传可能涉及敏感信息的资料。", + "code_error.team_error.group_name_empty": "群组名称不能为空", + "code_error.team_error.group_not_exist": "群组不存在", + "code_error.team_error.cannot_delete_default_group": "不能删除默认群组", + "common.Permission_tip": "个人权限大于群组权限", "confirm_choice": "确认选择", "contribute_app_template": "贡献模板", "core.Chat": "对话", @@ -1193,6 +1199,8 @@ "user.team.role.Admin": "管理员", "user.team.role.Owner": "创建者", "user.type": "类型", + "user.team.role.writer": "可写成员", + "user.team.role.Visitor": "访客", "verification": "验证", "xx_search_result": "{{key}} 的搜索结果", "yes": "是" diff --git a/packages/web/i18n/zh/login.json b/packages/web/i18n/zh/login.json index 4b4e36ff2..3c0bc87e6 100644 --- a/packages/web/i18n/zh/login.json +++ b/packages/web/i18n/zh/login.json @@ -13,4 +13,4 @@ "redirect": "跳转", "no_remind": "不再提醒", "Chinese_ip_tip": "检测到您是中国大陆 IP,点击跳转访问中国大陆版。" -} +} \ No newline at end of file diff --git a/packages/web/i18n/zh/user.json b/packages/web/i18n/zh/user.json index 3e11a2e05..3e74e5328 100644 --- a/packages/web/i18n/zh/user.json +++ b/packages/web/i18n/zh/user.json @@ -22,6 +22,8 @@ "bind_inform_account_success": "绑定通知账号成功", "delete.admin_failed": "删除管理员失败", "delete.admin_success": "删除管理员成功", + "delete.failed": "删除失败", + "delete.success": "删除成功", "has_chosen": "已选择", "individuation": "个性化", "login.error": "登录异常", @@ -76,11 +78,39 @@ "synchronization.title": "填写标签同步链接,点击同步按钮即可同步", "team.Add manager": "添加管理员", "team.add_collaborator": "添加协作者", + "team.add_writer": "添加可写成员", + "team.avatar_and_name": "头像 & 名称", + "team.group.avatar": "群头像", + "team.group.create": "创建群组", + "team.group.create_failed": "创建群组失败", + "team.group.default_group": "默认群组", + "team.group.delete_confirm": "确认删除群组?", + "team.group.edit": "编辑群组", + "team.group.edit_info": "编辑信息", + "team.group.manage_member": "管理成员", + "team.group.transfer_owner": "转让所有者", + "team.group.group": "群组", + "team.group.members": "成员", + "team.group.name": "群组名称", + "team.group.role.admin": "管理员", + "team.group.role.member": "成员", + "team.group.role.owner": "所有者", + "team.group.toast.can_not_delete_owner": "不能删除所有者, 请先转让", + "team.group.set_as_admin": "设为管理员", + "team.group.keep_admin": "保留管理员权限", + "team.group.permission_tip": "单独配置权限的成员,将遵循个人权限配置,不再受群组权限影响。\n若成员在多个权限组,则该成员的权限取并集。", + "team.group.permission.write": "工作台/知识库创建", + "team.group.permission.manage": "管理员", + "team.group.manage_tip": "可以邀请成员、删除成员、创建群组、管理所有群组、为群组和成员分配权限", + "team.group.search_placeholder": "搜索成员/群组名称", "team.manage_collaborators": "管理协作者", "team.no_collaborators": "暂无协作者", + "team.belong_to_group": "所属成员组", + "team.write_role_member": "可写权限", "usage.feishu": "飞书", "usage.official_account": "公众号", "usage.share": "分享链接", "usage.wecom": "企业微信", - "usage_record": "使用记录" + "usage_record": "使用记录", + "owner": "所有者" } \ No newline at end of file diff --git a/packages/web/i18n/zh/workflow.json b/packages/web/i18n/zh/workflow.json index c18173018..9e1b18f9f 100644 --- a/packages/web/i18n/zh/workflow.json +++ b/packages/web/i18n/zh/workflow.json @@ -187,4 +187,4 @@ "workflow.Switch_success": "切换成功", "workflow.Team cloud": "团队云端", "workflow.exit_tips": "您的更改尚未保存,「直接退出」将不会保存您的编辑记录。" -} +} \ No newline at end of file diff --git a/projects/app/public/imgs/avatar/defaultTeamAvatar.svg b/projects/app/public/imgs/avatar/defaultTeamAvatar.svg new file mode 100644 index 000000000..7c874f796 --- /dev/null +++ b/projects/app/public/imgs/avatar/defaultTeamAvatar.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/projects/app/src/components/support/permission/ChangeOwnerModal/index.tsx b/projects/app/src/components/support/permission/ChangeOwnerModal/index.tsx index edff52374..501f441c4 100644 --- a/projects/app/src/components/support/permission/ChangeOwnerModal/index.tsx +++ b/projects/app/src/components/support/permission/ChangeOwnerModal/index.tsx @@ -66,6 +66,7 @@ export function ChangeOwnerModal({ {name} - + {t('common:permission.change_owner_to')} @@ -162,3 +163,5 @@ export function ChangeOwnerModal({ ); } + +export default ChangeOwnerModal; diff --git a/projects/app/src/components/support/permission/Group/GroupTags.tsx b/projects/app/src/components/support/permission/Group/GroupTags.tsx new file mode 100644 index 000000000..bbf93ddc3 --- /dev/null +++ b/projects/app/src/components/support/permission/Group/GroupTags.tsx @@ -0,0 +1,59 @@ +import { + Box, + Flex, + Popover, + PopoverContent, + PopoverTrigger, + useDisclosure +} from '@chakra-ui/react'; +import Tag from '@fastgpt/web/components/common/Tag'; +import React from 'react'; + +type Props = { + max: number; + names?: string[]; +}; + +function GroupTags({ max, names }: Props) { + const length = names?.length || 0; + const { isOpen, onToggle, onClose } = useDisclosure(); + + return ( + + {names?.slice(0, max).map((name, index) => ( + + {name.length > 10 ? name.slice(0, 10) + '...' : name} + + ))} + + + + + {length > max && ( + + {'+' + (length - max)} + + )} + + + + + {names?.slice(max)?.map((name, index) => ( + + {name} + + ))} + + + + + ); +} + +export default GroupTags; diff --git a/projects/app/src/components/support/permission/MemberManager/AddMemberModal.tsx b/projects/app/src/components/support/permission/MemberManager/AddMemberModal.tsx index b63ee01e6..439cd2e14 100644 --- a/projects/app/src/components/support/permission/MemberManager/AddMemberModal.tsx +++ b/projects/app/src/components/support/permission/MemberManager/AddMemberModal.tsx @@ -1,15 +1,13 @@ import { Flex, Box, - Grid, ModalBody, InputGroup, InputLeftElement, Input, Checkbox, ModalFooter, - Button, - useToast + Button } from '@chakra-ui/react'; import MyModal from '@fastgpt/web/components/common/MyModal'; import MyIcon from '@fastgpt/web/components/common/Icon'; @@ -67,7 +65,7 @@ function AddMemberModal({ onClose }: AddModalPropsType) { const { mutate: onConfirm, isLoading: isUpdating } = useRequest({ mutationFn: () => { return onUpdateCollaborators({ - tmbIds: selectedMemberIdList, + members: selectedMemberIdList, permission: selectedPermission }); }, @@ -184,7 +182,9 @@ function AddMemberModal({ onClose }: AddModalPropsType) { _notLast={{ mb: 2 }} > - {member.memberName} + + {member.memberName} + { - return onUpdateCollaborators({ - tmbIds: [tmbId], + const { runAsync: onUpdate, loading: isUpdating } = useRequest2( + ({ tmbId, per }: { tmbId: string; per: PermissionValueType }) => + onUpdateCollaborators({ + members: [tmbId], permission: per - }); - }, - successToast: t('common.Update Success'), - errorToast: 'Error' - }); + }), + { + successToast: t('common.Update Success'), + errorToast: 'Error' + } + ); const loading = isDeleting || isUpdating; diff --git a/projects/app/src/components/support/permission/MemberManager/PermissionSelect.tsx b/projects/app/src/components/support/permission/MemberManager/PermissionSelect.tsx index 3430e9033..5b8562b65 100644 --- a/projects/app/src/components/support/permission/MemberManager/PermissionSelect.tsx +++ b/projects/app/src/components/support/permission/MemberManager/PermissionSelect.tsx @@ -2,12 +2,12 @@ import { ButtonProps, Flex, Menu, - MenuButton, MenuList, Box, Radio, useOutsideClick, - HStack + HStack, + MenuButton } from '@chakra-ui/react'; import React, { useMemo, useRef, useState } from 'react'; import MyIcon from '@fastgpt/web/components/common/Icon'; @@ -46,18 +46,17 @@ function PermissionSelect({ offset = [0, 5], Button, width = 'auto', - onDelete, - ...props + onDelete }: PermissionSelectProps) { const { t } = useTranslation(); const { permission, permissionList } = useContextSelector(CollaboratorContext, (v) => v); - const ref = useRef(null); - const closeTimer = useRef(); + const ref = useRef(null); + const closeTimer = useRef(); const [isOpen, setIsOpen] = useState(false); const permissionSelectList = useMemo(() => { - const list = Object.entries(permissionList).map(([key, value]) => { + const list = Object.entries(permissionList).map(([_, value]) => { return { name: value.name, value: value.value, @@ -85,15 +84,15 @@ function PermissionSelect({ return permissionList['read'].value; }, [permissionList, value]); - const selectedMultipleValues = useMemo(() => { - const per = new Permission({ per: value }); - - return permissionSelectList.multipleCheckBoxList - .filter((item) => { - return per.checkPer(item.value); - }) - .map((item) => item.value); - }, [permissionSelectList.multipleCheckBoxList, value]); + // const selectedMultipleValues = useMemo(() => { + // const per = new Permission({ per: value }); + // + // return permissionSelectList.multipleCheckBoxList + // .filter((item) => { + // return per.checkPer(item.value); + // }) + // .map((item) => item.value); + // }, [permissionSelectList.multipleCheckBoxList, value]); const onSelectPer = (per: PermissionValueType) => { if (per === value) return; @@ -111,7 +110,7 @@ function PermissionSelect({ return ( { if (trigger === 'hover') { setIsOpen(true); @@ -126,7 +125,8 @@ function PermissionSelect({ } }} > - { if (trigger === 'click') { @@ -134,25 +134,8 @@ function PermissionSelect({ } }} > - - - {Button} - - + {Button} + Promise; permissionList: PermissionListType; - onUpdateCollaborators: (props: UpdateClbPermissionProps) => any; + onUpdateCollaborators: (props: any) => any; // TODO: type. should be UpdatePermissionBody after app and dataset permission refactored onDelOneCollaborator: (tmbId: string) => any; refreshDeps?: any[]; }; diff --git a/projects/app/src/components/support/user/team/Info/MemberTag.tsx b/projects/app/src/components/support/user/team/Info/MemberTag.tsx new file mode 100644 index 000000000..c62f0f1cc --- /dev/null +++ b/projects/app/src/components/support/user/team/Info/MemberTag.tsx @@ -0,0 +1,21 @@ +import { Box, HStack } from '@chakra-ui/react'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import React from 'react'; + +type Props = { + name: string; + avatar: string; +}; + +function MemberTag({ name, avatar }: Props) { + return ( + + + + {name} + + + ); +} + +export default MemberTag; diff --git a/projects/app/src/components/support/user/team/TeamManageModal/TeamCard.tsx b/projects/app/src/components/support/user/team/TeamManageModal/TeamCard.tsx index a67948454..d78dd4477 100644 --- a/projects/app/src/components/support/user/team/TeamManageModal/TeamCard.tsx +++ b/projects/app/src/components/support/user/team/TeamManageModal/TeamCard.tsx @@ -7,29 +7,41 @@ import { useMemo, useState } from 'react'; import { useTranslation } from 'next-i18next'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useToast } from '@fastgpt/web/hooks/useToast'; -import { DragHandleIcon } from '@chakra-ui/icons'; import MemberTable from './components/MemberTable'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { TeamModalContext } from './context'; -import { useRequest } from '@fastgpt/web/hooks/useRequest'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { delLeaveTeam } from '@/web/support/user/team/api'; import dynamic from 'next/dynamic'; import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs'; +import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; enum TabListEnum { member = 'member', - permission = 'permission' + permission = 'permission', + group = 'group' } const TeamTagModal = dynamic(() => import('../TeamTagModal')); const InviteModal = dynamic(() => import('./components/InviteModal')); const PermissionManage = dynamic(() => import('./components/PermissionManage/index')); +const GroupManage = dynamic(() => import('./components/GroupManage/index')); +const GroupInfoModal = dynamic(() => import('./components/GroupManage/GroupInfoModal')); +const ManageGroupMemberModal = dynamic(() => import('./components/GroupManage/GroupManageMember')); function TeamCard() { const { toast } = useToast(); const { t } = useTranslation(); - const { myTeams, refetchTeams, members, refetchMembers, setEditTeamData, onSwitchTeam } = - useContextSelector(TeamModalContext, (v) => v); + const { + myTeams, + refetchTeams, + members, + refetchMembers, + setEditTeamData, + onSwitchTeam, + searchKey, + setSearchKey + } = useContextSelector(TeamModalContext, (v) => v); const { userInfo, teamPlanStatus } = useUserStore(); const { feConfigs } = useSystemStore(); @@ -37,8 +49,9 @@ function TeamCard() { const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({ content: t('common:user.team.member.Confirm Leave') }); - const { mutate: onLeaveTeam, isLoading: isLoadingLeaveTeam } = useRequest({ - mutationFn: async (teamId?: string) => { + + const { runAsync: onLeaveTeam, loading: isLoadingLeaveTeam } = useRequest2( + async (teamId?: string) => { if (!teamId) return; const defaultTeam = myTeams.find((item) => item.defaultTeam) || myTeams[0]; // change to personal team @@ -46,19 +59,45 @@ function TeamCard() { onSwitchTeam(defaultTeam.teamId); return delLeaveTeam(teamId); }, - onSuccess() { - refetchTeams(); - }, - errorToast: t('common:user.team.Leave Team Failed') - }); + { + onSuccess() { + refetchTeams(); + }, + errorToast: t('common:user.team.Leave Team Failed') + } + ); const { isOpen: isOpenTeamTagsAsync, onOpen: onOpenTeamTagsAsync, onClose: onCloseTeamTagsAsync } = useDisclosure(); + + const { + isOpen: isOpenGroupInfo, + onOpen: onOpenGroupInfo, + onClose: onCloseGroupInfo + } = useDisclosure(); + + const { + isOpen: isOpenManageGroupMember, + onOpen: onOpenManageGroupMember, + onClose: onCloseManageGroupMember + } = useDisclosure(); + const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure(); + const [editGroupId, setEditGroupId] = useState(); + const onEditGroup = (groupId: string) => { + setEditGroupId(groupId); + onOpenGroupInfo(); + }; + + const onManageMember = (groupId: string) => { + setEditGroupId(groupId); + onOpenManageGroupMember(); + }; + const Tablist = useMemo( () => [ { @@ -73,6 +112,11 @@ function TeamCard() { ), value: TabListEnum.member }, + { + icon: 'support/team/group', + label: t('user:team.group.group'), + value: TabListEnum.group + }, { icon: 'support/team/key', label: t('common:common.Role'), @@ -99,15 +143,15 @@ function TeamCard() { borderBottomColor={'myGray.100'} mb={2} > - + {userInfo?.team.teamName} {userInfo?.team.role === TeamMemberRoleEnum.owner && ( - - overflow={'auto'} - list={Tablist} - value={tab} - onChange={setTab} - > + overflow={'auto'} list={Tablist} value={tab} onChange={setTab} /> {/* ctrl buttons */} + {tab === TabListEnum.member && + userInfo?.team.permission.hasManagePer && + feConfigs?.show_team_chat && ( + + )} {tab === TabListEnum.member && userInfo?.team.permission.hasManagePer && ( )} - {userInfo?.team.permission.hasManagePer && feConfigs?.show_team_chat && ( + {tab === TabListEnum.member && !userInfo?.team.permission.isOwner && ( - )} - {!userInfo?.team.permission.isOwner && ( - )} + {tab === TabListEnum.group && userInfo?.team.permission.hasManagePer && ( + + )} + {tab === TabListEnum.permission && ( + + setSearchKey(e.target.value)} + /> + + )} {tab === TabListEnum.member && } + {tab === TabListEnum.group && ( + + )} {tab === TabListEnum.permission && } @@ -203,6 +269,24 @@ function TeamCard() { /> )} {isOpenTeamTagsAsync && } + {isOpenGroupInfo && ( + { + onCloseGroupInfo(); + setEditGroupId(undefined); + }} + editGroupId={editGroupId} + /> + )} + {isOpenManageGroupMember && ( + { + onCloseManageGroupMember(); + setEditGroupId(undefined); + }} + editGroupId={editGroupId} + /> + )} ); diff --git a/projects/app/src/components/support/user/team/TeamManageModal/TeamList.tsx b/projects/app/src/components/support/user/team/TeamManageModal/TeamList.tsx index c9b020af8..95997c0b5 100644 --- a/projects/app/src/components/support/user/team/TeamManageModal/TeamList.tsx +++ b/projects/app/src/components/support/user/team/TeamManageModal/TeamList.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Flex, IconButton, Text } from '@chakra-ui/react'; +import { Box, Button, Flex, IconButton } from '@chakra-ui/react'; import Avatar from '@fastgpt/web/components/common/Avatar'; import { useTranslation } from 'next-i18next'; import MyIcon from '@fastgpt/web/components/common/Icon'; @@ -28,7 +28,7 @@ function TeamList() { h={'40px'} borderBottom={'1.5px solid rgba(0, 0, 0, 0.05)'} > - + {t('common:common.Team')} {/* if there is no team */} diff --git a/projects/app/src/components/support/user/team/TeamManageModal/components/EditInfoModal.tsx b/projects/app/src/components/support/user/team/TeamManageModal/components/EditInfoModal.tsx index 416c120cb..5a7948313 100644 --- a/projects/app/src/components/support/user/team/TeamManageModal/components/EditInfoModal.tsx +++ b/projects/app/src/components/support/user/team/TeamManageModal/components/EditInfoModal.tsx @@ -13,6 +13,7 @@ import Avatar from '@fastgpt/web/components/common/Avatar'; import { postCreateTeam, putUpdateTeam } from '@/web/support/user/team/api'; import { CreateTeamProps } from '@fastgpt/global/support/user/team/controller.d'; import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; +import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants'; export type EditTeamFormDataType = CreateTeamProps & { id?: string; @@ -20,7 +21,7 @@ export type EditTeamFormDataType = CreateTeamProps & { export const defaultForm = { name: '', - avatar: '/icon/logo.svg' + avatar: DEFAULT_TEAM_AVATAR }; function EditModal({ @@ -98,7 +99,8 @@ function EditModal({ diff --git a/projects/app/src/components/support/user/team/TeamManageModal/components/GroupManage/GroupInfoModal.tsx b/projects/app/src/components/support/user/team/TeamManageModal/components/GroupManage/GroupInfoModal.tsx new file mode 100644 index 000000000..61f2d401b --- /dev/null +++ b/projects/app/src/components/support/user/team/TeamManageModal/components/GroupManage/GroupInfoModal.tsx @@ -0,0 +1,128 @@ +import { Input, HStack, ModalBody, Button, ModalFooter } from '@chakra-ui/react'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; + +import { useTranslation } from 'next-i18next'; +import React, { useMemo } from 'react'; +import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; +import { compressImgFileAndUpload } from '@/web/common/file/controller'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; +import { useForm } from 'react-hook-form'; +import { useContextSelector } from 'use-context-selector'; +import { TeamModalContext } from '../../context'; +import { postCreateGroup, putUpdateGroup } from '@/web/support/user/team/group/api'; +import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants'; + +export type GroupFormType = { + avatar: string; + name: string; +}; + +function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) { + const { refetchGroups, groups, refetchMembers } = useContextSelector(TeamModalContext, (v) => v); + const { t } = useTranslation(); + const { File: AvatarSelect, onOpen: onOpenSelectAvatar } = useSelectFile({ + fileType: '.jpg, .jpeg, .png', + multiple: false + }); + + const group = useMemo(() => { + return groups.find((item) => item._id === editGroupId); + }, [editGroupId, groups]); + + const { register, handleSubmit, getValues, setValue } = useForm({ + defaultValues: { + name: group?.name || '', + avatar: group?.avatar || DEFAULT_TEAM_AVATAR + } + }); + + 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 { run: onCreate, loading: isLoadingCreate } = useRequest2( + (data: GroupFormType) => { + return postCreateGroup({ + name: data.name, + avatar: data.avatar + }); + }, + { + onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()]) + } + ); + + const { run: onUpdate, loading: isLoadingUpdate } = useRequest2( + async (data: GroupFormType) => { + if (!editGroupId) return; + return putUpdateGroup({ + groupId: editGroupId, + name: data.name, + avatar: data.avatar + }); + }, + { + onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()]) + } + ); + + const isLoading = isLoadingUpdate || isLoadingCreate || uploadingAvatar; + + return ( + + + {t('user:team.avatar_and_name')} + + + + + + + + + + + ); +} + +export default GroupInfoModal; diff --git a/projects/app/src/components/support/user/team/TeamManageModal/components/GroupManage/GroupManageMember.tsx b/projects/app/src/components/support/user/team/TeamManageModal/components/GroupManage/GroupManageMember.tsx new file mode 100644 index 000000000..3100dd9ef --- /dev/null +++ b/projects/app/src/components/support/user/team/TeamManageModal/components/GroupManage/GroupManageMember.tsx @@ -0,0 +1,275 @@ +import { + Box, + ModalBody, + Flex, + Button, + ModalFooter, + Checkbox, + Grid, + HStack +} from '@chakra-ui/react'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import Tag from '@fastgpt/web/components/common/Tag'; + +import { useTranslation } from 'next-i18next'; +import React, { useMemo, useState } from 'react'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { useContextSelector } from 'use-context-selector'; +import { TeamModalContext } from '../../context'; +import { putUpdateGroup } from '@/web/support/user/team/group/api'; +import { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { useToast } from '@fastgpt/web/hooks/useToast'; +import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants'; +import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; + +export type GroupFormType = { + members: { + tmbId: string; + role: `${GroupMemberRole}`; + }[]; +}; + +function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) { + // 1. Owner can not be deleted, toast + // 2. Owner/Admin can manage members + // 3. Owner can add/remove admins + const { t } = useTranslation(); + const { userInfo } = useUserStore(); + const { toast } = useToast(); + const [hoveredMemberId, setHoveredMemberId] = useState(undefined); + const { + members: allMembers, + refetchGroups, + groups, + refetchMembers + } = useContextSelector(TeamModalContext, (v) => v); + + const group = useMemo(() => { + return groups.find((item) => item._id === editGroupId); + }, [editGroupId, groups]); + + const [members, setMembers] = useState(group?.members || []); + const [searchKey, setSearchKey] = useState(''); + const filtered = useMemo(() => { + return [ + ...allMembers.filter((member) => { + if (member.memberName.toLowerCase().includes(searchKey.toLowerCase())) return true; + return false; + }) + ]; + }, [searchKey, allMembers]); + + const { run: onUpdate, loading: isLoadingUpdate } = useRequest2( + async () => { + if (!editGroupId || !members.length) return; + return putUpdateGroup({ + groupId: editGroupId, + memberList: members + }); + }, + { + onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()]) + } + ); + + const isSelected = (memberId: string) => { + return members.find((item) => item.tmbId === memberId); + }; + + const myRole = useMemo(() => { + if (userInfo?.team.permission.hasManagePer) { + return 'owner'; + } + return members.find((item) => item.tmbId === userInfo?.team.tmbId)?.role ?? 'member'; + }, [members, userInfo]); + + const handleToggleSelect = (memberId: string) => { + if ( + myRole === 'owner' && + memberId === group?.members.find((item) => item.role === 'owner')?.tmbId + ) { + toast({ + title: t('user:team.group.toast.can_not_delete_owner'), + status: 'error' + }); + return; + } + + if ( + myRole === 'admin' && + group?.members.find((item) => String(item.tmbId) === memberId)?.role !== 'member' + ) { + return; + } + + if (isSelected(memberId)) { + setMembers(members.filter((item) => item.tmbId !== memberId)); + } else { + setMembers([...members, { tmbId: memberId, role: 'member' }]); + } + }; + + const handleToggleAdmin = (memberId: string) => { + if (myRole === 'owner' && isSelected(memberId)) { + const oldRole = members.find((item) => item.tmbId === memberId)?.role; + if (oldRole === 'admin') { + setMembers( + members.map((item) => (item.tmbId === memberId ? { ...item, role: 'member' } : item)) + ); + } else { + setMembers( + members.map((item) => (item.tmbId === memberId ? { ...item, role: 'admin' } : item)) + ); + } + } + }; + + const isLoading = isLoadingUpdate; + return ( + + + + + { + setSearchKey(e.target.value); + }} + /> + + {filtered.map((member) => { + return ( + handleToggleSelect(member.tmbId)} + > + } + /> + + {member.memberName} + + ); + })} + + + + {t('common:chosen') + ': ' + members.length} + + {members.map((member) => { + return ( + setHoveredMemberId(member.tmbId)} + onMouseLeave={() => setHoveredMemberId(undefined)} + justifyContent="space-between" + py="2" + px={3} + borderRadius={'md'} + key={member.tmbId + member.role} + _hover={{ bg: 'myGray.50' }} + _notLast={{ mb: 2 }} + > + + item.tmbId === member.tmbId)?.avatar} + w="1.5rem" + borderRadius={'md'} + /> + + {allMembers.find((item) => item.tmbId === member.tmbId)?.memberName} + + + + {(() => { + if (member.role === 'owner') { + return ( + + {t('user:team.group.role.owner')} + + ); + } else if (member.role === 'admin') { + return ( + + {t('user:team.group.role.admin')} + {myRole === 'owner' && ( + handleToggleAdmin(member.tmbId)} + /> + )} + + ); + } else if (member.role === 'member') { + return ( + myRole === 'owner' && + hoveredMemberId === member.tmbId && ( + handleToggleAdmin(member.tmbId)} + > + {t('user:team.group.set_as_admin')} + + ) + ); + } + })()} + + {(myRole === 'owner' || (myRole === 'admin' && member.role === 'member')) && ( + handleToggleSelect(member.tmbId)} + /> + )} + + ); + })} + + + + + + + + + ); +} + +export default GroupEditModal; diff --git a/projects/app/src/components/support/user/team/TeamManageModal/components/GroupManage/GroupTransferOwnerModal.tsx b/projects/app/src/components/support/user/team/TeamManageModal/components/GroupManage/GroupTransferOwnerModal.tsx new file mode 100644 index 000000000..53bfe395d --- /dev/null +++ b/projects/app/src/components/support/user/team/TeamManageModal/components/GroupManage/GroupTransferOwnerModal.tsx @@ -0,0 +1,200 @@ +import { putUpdateGroup } from '@/web/support/user/team/group/api'; +import { + Box, + Flex, + HStack, + Input, + ModalBody, + ModalFooter, + Button, + useDisclosure, + Checkbox +} from '@chakra-ui/react'; +import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { useTranslation } from 'next-i18next'; +import React, { useMemo, useState } from 'react'; +import { TeamModalContext } from '../../context'; +import { useContextSelector } from 'use-context-selector'; + +export type ChangeOwnerModalProps = { + groupId: string; +}; + +export function ChangeOwnerModal({ + onClose, + groupId +}: ChangeOwnerModalProps & { onClose: () => void }) { + const { t } = useTranslation(); + const [inputValue, setInputValue] = React.useState(''); + const { + members: allMembers, + groups, + refetchGroups + } = useContextSelector(TeamModalContext, (v) => v); + const group = useMemo(() => { + return groups.find((item) => item._id === groupId); + }, [groupId, groups]); + + const memberList = allMembers.filter((item) => { + return item.memberName.toLowerCase().includes(inputValue.toLowerCase()); + }); + + const OldOwnerId = useMemo(() => { + return group?.members.find((item) => item.role === 'owner')?.tmbId; + }, [group]); + + const [keepAdmin, setKeepAdmin] = useState(true); + + const { + isOpen: isOpenMemberListMenu, + onClose: onCloseMemberListMenu, + onOpen: onOpenMemberListMenu + } = useDisclosure(); + + const [selectedMember, setSelectedMember] = useState(null); + + const onChangeOwner = async (tmbId: string) => { + if (!group) { + return; + } + + const newMemberList = group.members + .map((item) => { + if (item.tmbId === OldOwnerId) { + if (keepAdmin) { + return { tmbId: OldOwnerId, role: 'admin' }; + } + return { tmbId: OldOwnerId, role: 'member' }; + } + return item; + }) + .filter((item) => item.tmbId !== tmbId) as any; + + newMemberList.push({ tmbId, role: 'owner' }); + + return putUpdateGroup({ + groupId, + memberList: newMemberList + }); + }; + + const { runAsync, loading } = useRequest2(onChangeOwner, { + onSuccess: () => Promise.all([onClose(), refetchGroups()]), + successToast: t('common:permission.change_owner_success'), + errorToast: t('common:permission.change_owner_failed') + }); + + const onConfirm = async () => { + if (!selectedMember) { + return; + } + await runAsync(selectedMember.tmbId); + }; + + return ( + + + + + {group?.name} + + + + {t('common:permission.change_owner_to')} + + + {selectedMember && ( + + )} + { + setInputValue(e.target.value); + setSelectedMember(null); + }} + onFocus={() => { + onOpenMemberListMenu(); + setSelectedMember(null); + }} + {...(selectedMember && { pl: '10' })} + /> + + {isOpenMemberListMenu && memberList.length > 0 && ( + + {memberList.map((item) => ( + { + setInputValue(item.memberName); + setSelectedMember(item); + onCloseMemberListMenu(); + }} + > + + + {item.memberName} + + + ))} + + )} + + + { + setKeepAdmin(e.target.checked); + }} + > + {t('user:team.group.keep_admin')} + + + + + + + + + + + + ); +} + +export default ChangeOwnerModal; diff --git a/projects/app/src/components/support/user/team/TeamManageModal/components/GroupManage/index.tsx b/projects/app/src/components/support/user/team/TeamManageModal/components/GroupManage/index.tsx new file mode 100644 index 000000000..4ded0d411 --- /dev/null +++ b/projects/app/src/components/support/user/team/TeamManageModal/components/GroupManage/index.tsx @@ -0,0 +1,217 @@ +import AvatarGroup from '@fastgpt/web/components/common/Avatar/AvatarGroup'; +import { + Box, + HStack, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr, + useDisclosure +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import { useContextSelector } from 'use-context-selector'; +import { TeamModalContext } from '../../context'; +import MyMenu, { MenuItemType } from '@fastgpt/web/components/common/MyMenu'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { deleteGroup } from '@/web/support/user/team/group/api'; +import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; +import MemberTag from '../../../Info/MemberTag'; +import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; +import dynamic from 'next/dynamic'; +import { useState } from 'react'; + +const ChangeOwnerModal = dynamic(() => import('./GroupTransferOwnerModal')); + +function MemberTable({ + onEditGroup, + onManageMember +}: { + onEditGroup: (groupId: string) => void; + onManageMember: (groupId: string) => void; +}) { + const { t } = useTranslation(); + const { userInfo } = useUserStore(); + const [editGroupId, setEditGroupId] = useState(); + + const { ConfirmModal: ConfirmDeleteGroupModal, openConfirm: openDeleteGroupModal } = useConfirm({ + type: 'delete', + content: t('user:team.group.delete_confirm') + }); + + const { groups, refetchGroups, members, refetchMembers } = useContextSelector( + TeamModalContext, + (v) => v + ); + + const { runAsync: delDeleteGroup } = useRequest2(deleteGroup, { + onSuccess: () => { + refetchGroups(); + refetchMembers(); + } + }); + + const hasGroupManagePer = (group: (typeof groups)[0]) => + userInfo?.team.permission.hasManagePer || + ['admin', 'owner'].includes( + group.members.find((item) => item.tmbId === userInfo?.team.tmbId)?.role ?? '' + ); + + const isGroupOwner = (group: (typeof groups)[0]) => + userInfo?.team.permission.hasManagePer || + group.members.find((item) => item.role === 'owner')?.tmbId === userInfo?.team.tmbId; + + const { + isOpen: isOpenChangeOwner, + onOpen: onOpenChangeOwner, + onClose: onCloseChangeOwner + } = useDisclosure(); + + const onChangeOwner = (groupId: string) => { + setEditGroupId(groupId); + onOpenChangeOwner(); + }; + + return ( + + + + + + + + + + + + + {groups?.map((group) => ( + + + + + + + ))} + +
+ {t('user:team.group.name')} + {t('user:owner')}{t('user:team.group.members')} + {t('common:common.Action')} +
+ + + + ({group.name === DefaultGroupName ? members.length : group.members.length}) + + + + item.role === 'owner')?.memberName ?? '' + : members.find( + (item) => + item.tmbId === + group.members.find((item) => item.role === 'owner')?.tmbId + )?.memberName ?? '' + } + avatar={ + group.name === DefaultGroupName + ? members.find((item) => item.role === 'owner')?.avatar ?? '' + : members.find( + (i) => + i.tmbId === group.members.find((item) => item.role === 'owner')?.tmbId + )?.avatar ?? '' + } + /> + + {group.name === DefaultGroupName ? ( + v.avatar)} groupId={group._id} /> + ) : hasGroupManagePer(group) ? ( + + onManageMember(group._id)}> + members.find((m) => m.tmbId === v.tmbId)?.avatar ?? '' + )} + groupId={group._id} + /> + + + ) : ( + members.find((m) => m.tmbId === v.tmbId)?.avatar ?? '' + )} + groupId={group._id} + /> + )} + + {hasGroupManagePer(group) && group.name !== DefaultGroupName && ( + } + menuList={[ + { + children: [ + { + label: t('user:team.group.edit_info'), + icon: 'edit', + onClick: () => { + onEditGroup(group._id); + } + }, + { + label: t('user:team.group.manage_member'), + icon: 'support/team/group', + onClick: () => { + onManageMember(group._id); + } + }, + ...(isGroupOwner(group) + ? [ + { + label: t('user:team.group.transfer_owner'), + icon: 'modal/changePer', + onClick: () => { + onChangeOwner(group._id); + }, + type: 'primary' as MenuItemType + }, + { + label: t('common:common.Delete'), + icon: 'delete', + onClick: () => { + openDeleteGroupModal(() => delDeleteGroup(group._id))(); + }, + type: 'danger' as MenuItemType + } + ] + : []) + ] + } + ]} + /> + )} +
+
+ + {isOpenChangeOwner && editGroupId && ( + + )} +
+ ); +} + +export default MemberTable; diff --git a/projects/app/src/components/support/user/team/TeamManageModal/components/InviteModal.tsx b/projects/app/src/components/support/user/team/TeamManageModal/components/InviteModal.tsx index 366d00fc0..8df0e1514 100644 --- a/projects/app/src/components/support/user/team/TeamManageModal/components/InviteModal.tsx +++ b/projects/app/src/components/support/user/team/TeamManageModal/components/InviteModal.tsx @@ -1,20 +1,12 @@ -import React, { useMemo, useState } from 'react'; +import React, { useState } from 'react'; import MyModal from '@fastgpt/web/components/common/MyModal'; import { useTranslation } from 'next-i18next'; import { ModalCloseButton, ModalBody, Box, ModalFooter, Button } from '@chakra-ui/react'; import TagTextarea from '@/components/common/Textarea/TagTextarea'; -import { useRequest } from '@fastgpt/web/hooks/useRequest'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { postInviteTeamMember } from '@/web/support/user/team/api'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import type { InviteMemberResponse } from '@fastgpt/global/support/user/team/controller.d'; -import MySelect from '@fastgpt/web/components/common/MySelect'; -import { - ManagePermissionVal, - ReadPermissionVal, - WritePermissionVal -} from '@fastgpt/global/support/permission/constant'; -import { useI18n } from '@/web/context/I18n'; -import { useUserStore } from '@/web/support/user/useUserStore'; const InviteModal = ({ teamId, @@ -26,69 +18,43 @@ const InviteModal = ({ onSuccess: () => void; }) => { const { t } = useTranslation(); - const { userT } = useI18n(); const { ConfirmModal, openConfirm } = useConfirm({ title: t('common:user.team.Invite Member Result Tip'), showCancel: false }); - const { userInfo } = useUserStore(); const [inviteUsernames, setInviteUsernames] = useState([]); - const inviteTypes = useMemo( - () => [ - { - label: userT('permission.Read'), - description: userT('permission.Read desc'), - value: ReadPermissionVal - }, - { - label: userT('permission.Write'), - description: userT('permission.Write tip'), - value: WritePermissionVal - }, - ...(userInfo?.team?.permission.isOwner - ? [ - { - label: userT('permission.Manage'), - description: userT('permission.Manage tip'), - value: ManagePermissionVal - } - ] - : []) - ], - [userInfo?.team?.permission.isOwner, userT] - ); - const [selectedInviteType, setSelectInviteType] = useState(inviteTypes[0].value); - const { mutate: onInvite, isLoading } = useRequest({ - mutationFn: () => { - return postInviteTeamMember({ + const { runAsync: onInvite, loading: isLoading } = useRequest2( + () => + postInviteTeamMember({ teamId, - usernames: inviteUsernames, - permission: selectedInviteType - }); - }, - onSuccess(res: InviteMemberResponse) { - onSuccess(); - openConfirm( - () => onClose(), - undefined, - - {t('user.team.Invite Member Success Tip', { - success: res.invite.length, - inValid: res.inValid.map((item) => item.username).join(', '), - inTeam: res.inTeam.map((item) => item.username).join(', ') - })} - - )(); - }, - errorToast: t('common:user.team.Invite Member Failed Tip') - }); + usernames: inviteUsernames + }), + { + onSuccess(res: InviteMemberResponse) { + onSuccess(); + openConfirm( + () => onClose(), + undefined, + + {t('user.team.Invite Member Success Tip', { + success: res.invite.length, + inValid: res.inValid.map((item) => item.username).join(', '), + inTeam: res.inTeam.map((item) => item.username).join(', ') + })} + + )(); + }, + errorToast: t('common:user.team.Invite Member Failed Tip') + } + ); return ( {t('common:user.team.Invite Member')} @@ -104,9 +70,6 @@ const InviteModal = ({ {t('common:user.Account')} - - -