diff --git a/.vscode/i18n-ally-custom-framework.yml b/.vscode/i18n-ally-custom-framework.yml index ad841db50..ef78f3cf0 100644 --- a/.vscode/i18n-ally-custom-framework.yml +++ b/.vscode/i18n-ally-custom-framework.yml @@ -24,6 +24,7 @@ usageMatchRegex: - "[^\\w\\d]fileT\\(['\"`]({key})['\"`]" - "[^\\w\\d]publishT\\(['\"`]({key})['\"`]" - "[^\\w\\d]workflowT\\(['\"`]({key})['\"`]" + - "[^\\w\\d]userT\\(['\"`]({key})['\"`]" # A RegEx to set a custom scope range. This scope will be used as a prefix when detecting keys # and works like how the i18next framework identifies the namespace scope from the diff --git a/packages/global/support/permission/constant.ts b/packages/global/support/permission/constant.ts index 5a9851fb5..000fa8991 100644 --- a/packages/global/support/permission/constant.ts +++ b/packages/global/support/permission/constant.ts @@ -20,3 +20,9 @@ export const PermissionTypeMap = { label: 'permission.Public' } }; + +export enum ResourceTypeEnum { + team = 'team', + app = 'app', + dataset = 'dataset' +} diff --git a/packages/global/support/permission/type.d.ts b/packages/global/support/permission/type.d.ts index f0ad8f48e..3e182acd7 100644 --- a/packages/global/support/permission/type.d.ts +++ b/packages/global/support/permission/type.d.ts @@ -1,5 +1,7 @@ import { AuthUserTypeEnum } from './constant'; +export type PermissionValueType = number; + export type AuthResponseType = { teamId: string; tmbId: string; @@ -9,3 +11,10 @@ export type AuthResponseType = { appId?: string; apikey?: string; }; + +export type ResourcePermissionType = { + teamId: string; + tmbId: string; + resourceType: ResourceType; + permission: PermissionValueType; +}; diff --git a/packages/global/support/user/team/controller.d.ts b/packages/global/support/user/team/controller.d.ts index d0396030d..316bacd58 100644 --- a/packages/global/support/user/team/controller.d.ts +++ b/packages/global/support/user/team/controller.d.ts @@ -1,3 +1,4 @@ +import { PermissionValueType } from 'support/permission/type'; import { TeamMemberRoleEnum } from './constant'; import { LafAccountType, TeamMemberSchema } from './type'; @@ -44,3 +45,9 @@ export type InviteMemberResponse = Record< 'invite' | 'inValid' | 'inTeam', { username: string; userId: string }[] >; + +export type UpdateTeamMemberPermissionProps = { + teamId: string; + memberIds: string[]; + permission: PermissionValueType; +}; diff --git a/packages/global/support/user/team/type.d.ts b/packages/global/support/user/team/type.d.ts index d043b6be2..93f0c653d 100644 --- a/packages/global/support/user/team/type.d.ts +++ b/packages/global/support/user/team/type.d.ts @@ -1,6 +1,7 @@ import type { UserModelSchema } from '../type'; import type { TeamMemberRoleEnum, TeamMemberStatusEnum } from './constant'; import { LafAccountType } from './type'; +import { PermissionValueType, ResourcePermissionType } from '../../permission/type'; export type TeamSchema = { _id: string; @@ -15,6 +16,7 @@ export type TeamSchema = { lastWebsiteSyncTime: Date; }; lafAccount: LafAccountType; + defaultPermission: PermissionValueType; }; export type tagsType = { label: string; @@ -61,6 +63,7 @@ export type TeamItemType = { status: `${TeamMemberStatusEnum}`; canWrite: boolean; lafAccount?: LafAccountType; + defaultPermission: PermissionValueType; }; export type TeamMemberItemType = { @@ -69,8 +72,10 @@ export type TeamMemberItemType = { teamId: string; memberName: string; avatar: string; + // TODO: this should be deprecated. role: `${TeamMemberRoleEnum}`; status: `${TeamMemberStatusEnum}`; + permission: PermissionValueType; }; export type TeamTagItemType = { diff --git a/packages/service/support/permission/resourcePermission/controller.ts b/packages/service/support/permission/resourcePermission/controller.ts new file mode 100644 index 000000000..3b482354a --- /dev/null +++ b/packages/service/support/permission/resourcePermission/controller.ts @@ -0,0 +1,16 @@ +import { ResourcePermissionType } from '@fastgpt/global/support/permission/type'; +import { MongoResourcePermission } from './schema'; +import { ResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; + +export async function getResourcePermission({ + tmbId, + resourceType +}: { + tmbId: string; + resourceType: ResourceTypeEnum; +}) { + return (await MongoResourcePermission.findOne({ + tmbId, + resourceType + })) as ResourcePermissionType; +} diff --git a/packages/service/support/permission/resourcePermission/permisson.ts b/packages/service/support/permission/resourcePermission/permisson.ts new file mode 100644 index 000000000..fb87a3e8b --- /dev/null +++ b/packages/service/support/permission/resourcePermission/permisson.ts @@ -0,0 +1,127 @@ +// 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. +// The lowest 3 bits present the permission of reading, writing and managing. +// The higher bits are advanced permissions or extended permissions, which could be customized. +export type PermissionValueType = number; +export type PermissionListType = { [key: string]: PermissionValueType }; +export const NullPermission: PermissionValueType = 0; + +// the Permission helper class +export class Permission { + value: PermissionValueType; + constructor(value: PermissionValueType) { + this.value = value; + } + + // add permission(s) + // it can be chaining called. + // @example + // const perm = new Permission(permission) + // perm.add(PermissionList['read']) + // perm.add(PermissionList['read'], PermissionList['write']) + // perm.add(PermissionList['read']).add(PermissionList['write']) + add(...perm: PermissionValueType[]): Permission { + for (let p of perm) { + this.value = addPermission(this.value, p); + } + return this; + } + + remove(...perm: PermissionValueType[]): Permission { + for (let p of perm) { + this.value = removePermission(this.value, p); + } + return this; + } + + check(perm: PermissionValueType): Permission | boolean { + if (checkPermission(this.value, perm)) { + return this; + } else { + return false; + } + } +} + +export function constructPermission(permList: PermissionValueType[]) { + return new Permission(NullPermission).add(...permList); +} + +// The base Permissions List +// It can be extended, for example: +// export const UserPermissionList: PermissionListType = { +// ...PermissionList, +// 'Invite': 0b1000 +// } +export const PermissionList: PermissionListType = { + Read: 0b100, + Write: 0b010, + Manage: 0b001 +}; + +// list of permissions. could be customized. +// ! removal of the basic permissions is not recommended. +// const PermList: Array = [ReadPerm, WritePerm, ManagePerm]; + +// return the list of permissions +// @param Perm(optional): the list of permissions to be added +// export function getPermList(Perm?: PermissionType[]): Array { +// if (Perm === undefined) { +// return PermList; +// } else { +// return PermList.concat(Perm); +// } +// } + +// check the permission +// @param [val]: The permission value to be checked +// @parma [perm]: Which Permission value will be checked +// @returns [booean]: if the [val] has the [perm] +// example: +// const perm = user.permission // get this permisiion from db or somewhere else +// const ok = checkPermission(perm, PermissionList['Read']) +export function checkPermission(val: PermissionValueType, perm: PermissionValueType): boolean { + return (val & perm) === perm; +} + +// add the permission +// it can be chaining called. +// return the new permission value based on [val] added with [perm] +// @param val: PermissionValueType +// @param perm: PermissionValueType +// example: +// const basePerm = 0b001; // Manage only +export function addPermission( + val: PermissionValueType, + perm: PermissionValueType +): PermissionValueType { + return val | perm; +} + +// remove the permission +export function removePermission( + val: PermissionValueType, + perm: PermissionValueType +): PermissionValueType { + return val & ~perm; +} + +// export function parsePermission(val: PermissionValueType, list: PermissionValueType[]) { +// const result: [[string, boolean]] = [] as any; +// list.forEach((perm) => { +// result.push([perm[0], checkPermission(val, perm)]); +// }); +// return result; +// } + +export function hasManage(val: PermissionValueType) { + return checkPermission(val, PermissionList['Manage']); +} + +export function hasWrite(val: PermissionValueType) { + return checkPermission(val, PermissionList['Write']); +} + +export function hasRead(val: PermissionValueType) { + return checkPermission(val, PermissionList['Read']); +} diff --git a/packages/service/support/permission/resourcePermission/schema.ts b/packages/service/support/permission/resourcePermission/schema.ts new file mode 100644 index 000000000..98be1fcda --- /dev/null +++ b/packages/service/support/permission/resourcePermission/schema.ts @@ -0,0 +1,48 @@ +import { + TeamCollectionName, + TeamMemberCollectionName +} from '@fastgpt/global/support/user/team/constant'; +import { Model, connectionMongo } from '../../../common/mongo'; +import type { ResourcePermissionType } from '@fastgpt/global/support/permission/type'; +import { ResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; +const { Schema, model, models } = connectionMongo; + +export const ResourcePermissionSchema = new Schema({ + teamId: { + type: Schema.Types.ObjectId, + ref: TeamCollectionName + }, + tmbId: { + type: Schema.Types.ObjectId, + ref: TeamMemberCollectionName + }, + resourceType: { + type: Object.values(ResourceTypeEnum), + required: true + }, + permission: { + type: Number, + required: true + } +}); + +try { + ResourcePermissionSchema.index({ + teamId: 1, + resourceType: 1 + }); + ResourcePermissionSchema.index({ + tmbId: 1, + resourceType: 1 + }); +} catch (error) { + console.log(error); +} + +export const ResourcePermissionCollectionName = 'resource_permission'; + +export const MongoResourcePermission: Model = + models[ResourcePermissionCollectionName] || + model(ResourcePermissionCollectionName, ResourcePermissionSchema); + +MongoResourcePermission.syncIndexes(); diff --git a/packages/service/support/user/team/controller.ts b/packages/service/support/user/team/controller.ts index 4145b3dd4..73592e806 100644 --- a/packages/service/support/user/team/controller.ts +++ b/packages/service/support/user/team/controller.ts @@ -27,7 +27,8 @@ async function getTeamMember(match: Record): Promise status: tmb.status, defaultTeam: tmb.defaultTeam, canWrite: tmb.role !== TeamMemberRoleEnum.visitor, - lafAccount: tmb.teamId.lafAccount + lafAccount: tmb.teamId.lafAccount, + defaultPermission: tmb.teamId.defaultPermission }; } diff --git a/packages/service/support/user/team/teamSchema.ts b/packages/service/support/user/team/teamSchema.ts index 8f0f472eb..870a67ef4 100644 --- a/packages/service/support/user/team/teamSchema.ts +++ b/packages/service/support/user/team/teamSchema.ts @@ -3,6 +3,7 @@ const { Schema, model, models } = 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 { NullPermission } from '../../permission/resourcePermission/permisson'; const TeamSchema = new Schema({ name: { @@ -13,6 +14,10 @@ const TeamSchema = new Schema({ type: Schema.Types.ObjectId, ref: userCollectionName }, + defaultPermission: { + type: Number, + default: NullPermission + }, avatar: { type: String, default: '/icon/logo.svg' diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index 2b597a524..2afda1a54 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -53,6 +53,7 @@ export const iconPaths = { 'common/settingLight': () => import('./icons/common/settingLight.svg'), 'common/text/t': () => import('./icons/common/text/t.svg'), 'common/tickFill': () => import('./icons/common/tickFill.svg'), + 'common/trash': () => import('./icons/common/trash.svg'), 'common/uploadFileFill': () => import('./icons/common/uploadFileFill.svg'), 'common/viewLight': () => import('./icons/common/viewLight.svg'), 'common/voiceLight': () => import('./icons/common/voiceLight.svg'), @@ -207,8 +208,10 @@ export const iconPaths = { 'support/outlink/iframeLight': () => import('./icons/support/outlink/iframeLight.svg'), 'support/outlink/share': () => import('./icons/support/outlink/share.svg'), 'support/outlink/shareLight': () => import('./icons/support/outlink/shareLight.svg'), + '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/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'), 'support/user/individuation': () => import('./icons/support/user/individuation.svg'), diff --git a/packages/web/components/common/Icon/icons/common/trash.svg b/packages/web/components/common/Icon/icons/common/trash.svg new file mode 100644 index 000000000..e3e22df66 --- /dev/null +++ b/packages/web/components/common/Icon/icons/common/trash.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/web/components/common/Icon/icons/support/permission/collaborator.svg b/packages/web/components/common/Icon/icons/support/permission/collaborator.svg new file mode 100644 index 000000000..ed0ad1f81 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/permission/collaborator.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/team/key.svg b/packages/web/components/common/Icon/icons/support/team/key.svg new file mode 100644 index 000000000..8beb9eec6 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/team/key.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/web/components/common/Icon/icons/support/team/memberLight.svg b/packages/web/components/common/Icon/icons/support/team/memberLight.svg index 5b81b3474..46af62ef4 100644 --- a/packages/web/components/common/Icon/icons/support/team/memberLight.svg +++ b/packages/web/components/common/Icon/icons/support/team/memberLight.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/projects/app/i18n/en/common.json b/projects/app/i18n/en/common.json index 14fe00397..41b16bf49 100644 --- a/projects/app/i18n/en/common.json +++ b/projects/app/i18n/en/common.json @@ -93,6 +93,7 @@ "Rename Success": "Rename Success", "Request Error": "Request Error", "Require Input": "Required Input", + "Role": "Role", "Root folder": "Root folder", "Save": "Save", "Save Failed": "Save Failed", @@ -1563,7 +1564,7 @@ "Remove Member Failed": "Failed to remove team member", "Remove Member Success": "Successfully removed team member", "Remove Member Tip": "Remove from Team", - "Role": "Role", + "Role": "Role(Old)", "Select Team": "Select Team", "Set Name": "Name Your Team", "Switch Team Failed": "Failed to switch team", diff --git a/projects/app/i18n/en/user.json b/projects/app/i18n/en/user.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/projects/app/i18n/en/user.json @@ -0,0 +1 @@ +{} diff --git a/projects/app/i18n/zh/common.json b/projects/app/i18n/zh/common.json index ca14b35f8..db7ed9fb6 100644 --- a/projects/app/i18n/zh/common.json +++ b/projects/app/i18n/zh/common.json @@ -82,6 +82,7 @@ "Output": "输出", "Params": "参数", "Password inconsistency": "两次密码不一致", + "Permission": "权限", "Please Input Name": "请输入名称", "Price used": "金额消耗", "Read document": "查看文档", @@ -93,6 +94,7 @@ "Rename Success": "重命名成功", "Request Error": "请求异常", "Require Input": "必填", + "Role": "权限", "Root folder": "根目录", "Save": "保存", "Save Failed": "保存失败", @@ -1569,7 +1571,7 @@ "Remove Member Failed": "移除团队成员异常", "Remove Member Success": "移除团队成员成功", "Remove Member Tip": "移出团队", - "Role": "身份", + "Role": "身份(旧版)", "Select Team": "团队选择", "Set Name": "给团队取个名字", "Switch Team Failed": "切换团队异常", diff --git a/projects/app/i18n/zh/user.json b/projects/app/i18n/zh/user.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/projects/app/i18n/zh/user.json @@ -0,0 +1 @@ +{} diff --git a/projects/app/src/components/common/Rowtabs/index.tsx b/projects/app/src/components/common/Rowtabs/index.tsx new file mode 100644 index 000000000..c4be6631e --- /dev/null +++ b/projects/app/src/components/common/Rowtabs/index.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Flex, Box, BoxProps, border } from '@chakra-ui/react'; +import MyIcon from '@fastgpt/web/components/common/Icon'; + +type Props = BoxProps & { + list: { + icon?: string; + label: string | React.ReactNode; + value: string; + }[]; + value: string; + onChange: (e: string) => void; +}; + +const RowTabs = ({ list, value, onChange, py = '7px', px = '12px', ...props }: Props) => { + return ( + + {list.map((item) => ( + onChange(item.value) + })} + > + {item.icon && } + {item.label} + + ))} + + ); +}; + +export default RowTabs; diff --git a/projects/app/src/components/support/user/team/TeamManageModal/AddManager.tsx b/projects/app/src/components/support/user/team/TeamManageModal/AddManager.tsx new file mode 100644 index 000000000..4fbc69313 --- /dev/null +++ b/projects/app/src/components/support/user/team/TeamManageModal/AddManager.tsx @@ -0,0 +1,174 @@ +import { + Box, + Button, + ModalBody, + ModalCloseButton, + ModalFooter, + Grid, + Input, + Flex, + Checkbox, + CloseButton, + InputGroup, + InputLeftElement +} from '@chakra-ui/react'; +import Avatar from '@/components/Avatar'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useContextSelector } from 'use-context-selector'; +import { TeamContext } from '.'; +import { + hasManage, + constructPermission, + PermissionList +} from '@fastgpt/service/support/permission/resourcePermission/permisson'; +import { useRequest } from '@fastgpt/web/hooks/useRequest'; +import { updateMemberPermission } from '@/web/support/user/team/api'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import MyIcon from '@fastgpt/web/components/common/Icon'; + +function AddManagerModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) { + const { t } = useTranslation(); + const { userInfo } = useUserStore(); + const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers); + const members = useContextSelector(TeamContext, (v) => + v.members.filter((member) => { + return member.tmbId != userInfo!.team.tmbId && !hasManage(member.permission); + }) + ); + const [selected, setSelected] = useState([]); + const [search, setSearch] = useState(''); + const [searched, setSearched] = useState(members); + + const { mutate: submit, isLoading } = useRequest({ + mutationFn: async () => { + console.log(selected); + return updateMemberPermission({ + teamId: userInfo!.team.teamId, + permission: constructPermission([ + PermissionList['Read'], + PermissionList['Write'], + PermissionList['Manage'] + ]).value, + memberIds: selected.map((item) => { + return item.tmbId; + }) + }); + }, + onSuccess: () => { + refetchMembers(); + onSuccess(); + }, + successToast: '成功', + errorToast: '失败' + }); + return ( + + 添加管理员 + + } + > + + + + + + + + + { + setSearch(e.target.value); + setSearched( + members.filter((member) => member.memberName.includes(e.target.value)) + ); + }} + /> + + + {searched.map((member) => { + return ( + { + if (selected.indexOf(member) == -1) { + setSelected([...selected, member]); + } else { + setSelected([...selected.filter((item) => item.tmbId != member.tmbId)]); + } + }} + > + + + {member.memberName} + + ); + })} + + + + 已选: {selected.length} 个 + + {selected.map((member) => { + return ( + + + + {member.memberName} + + + setSelected([...selected.filter((item) => item.tmbId != member.tmbId)]) + } + /> + + ); + })} + + + + + + + + + ); +} + +export default AddManagerModal; diff --git a/projects/app/src/components/support/user/team/TeamManageModal/MemberTable.tsx b/projects/app/src/components/support/user/team/TeamManageModal/MemberTable.tsx new file mode 100644 index 000000000..658b85896 --- /dev/null +++ b/projects/app/src/components/support/user/team/TeamManageModal/MemberTable.tsx @@ -0,0 +1,103 @@ +import Avatar from '@/components/Avatar'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { Box, MenuButton, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react'; +import { + TeamMemberRoleEnum, + TeamMemberRoleMap, + TeamMemberStatusMap +} from '@fastgpt/global/support/user/team/constant'; +import MyMenu from '@fastgpt/web/components/common/MyMenu'; +import { useTranslation } from 'react-i18next'; +import { useContextSelector } from 'use-context-selector'; +import { TeamContext } from '.'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { hasManage } from '@fastgpt/service/support/permission/resourcePermission/permisson'; + +function MemberTable() { + const members = useContextSelector(TeamContext, (v) => v.members); + const openRemoveMember = useContextSelector(TeamContext, (v) => v.openRemoveMember); + const onRemoveMember = useContextSelector(TeamContext, (v) => v.onRemoveMember); + + const { userInfo } = useUserStore(); + const { t } = useTranslation(); + + return ( + + + + + + + + + + + + {members.map((item) => ( + + + + + + + ))} + +
{t('common.Username')}{t('user.team.Role')}{t('common.Status')}{t('common.Action')}
+ + + {item.memberName} + + {t(TeamMemberRoleMap[item.role]?.label || '')} + {t(TeamMemberStatusMap[item.status]?.label || '')} + + {hasManage( + members.find((item) => item.tmbId === userInfo?.team.tmbId)?.permission! + ) && + item.role !== TeamMemberRoleEnum.owner && + item.tmbId !== userInfo?.team.tmbId && ( + + + + } + menuList={[ + { + label: t('user.team.Remove Member Tip'), + onClick: () => + openRemoveMember( + () => + onRemoveMember({ + teamId: item.teamId, + memberId: item.tmbId + }), + undefined, + t('user.team.Remove Member Confirm Tip', { + username: item.memberName + }) + )() + } + ]} + /> + )} +
+
+ ); +} + +export default MemberTable; diff --git a/projects/app/src/components/support/user/team/TeamManageModal/PermissionManage.tsx b/projects/app/src/components/support/user/team/TeamManageModal/PermissionManage.tsx new file mode 100644 index 000000000..96db5d51b --- /dev/null +++ b/projects/app/src/components/support/user/team/TeamManageModal/PermissionManage.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { Box, Button, Flex, Tag, TagLabel, useDisclosure } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { TeamContext } from '.'; +import { useContextSelector } from 'use-context-selector'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import Avatar from '@/components/Avatar'; +import { useRequest } from '@fastgpt/web/hooks/useRequest'; +import AddManagerModal from './AddManager'; +import { updateMemberPermission } from '@/web/support/user/team/api'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { + constructPermission, + hasManage, + PermissionList +} from '@fastgpt/service/support/permission/resourcePermission/permisson'; +import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant'; + +function PermissionManage() { + const { t } = useTranslation(); + const members = useContextSelector(TeamContext, (v) => v.members); + const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers); + const { userInfo } = useUserStore(); + + const { + isOpen: isOpenAddManager, + onOpen: onOpenAddManager, + onClose: onCloseAddManager + } = useDisclosure(); + + const { mutate: removeManager } = useRequest({ + mutationFn: async (memberId: string) => { + return updateMemberPermission({ + teamId: userInfo!.team.teamId, + permission: constructPermission([PermissionList['Read'], PermissionList['Write']]).value, + memberIds: [memberId] + }); + }, + successToast: 'Success', + errorToast: 'Error', + onSuccess: () => { + refetchMembers(); + }, + onError: () => { + refetchMembers(); + } + }); + + return ( + + {isOpenAddManager && ( + + )} + + + + + {t('user.team.role.Admin')} + + + 可邀请, 删除成员 + + + {userInfo?.team.role === 'owner' && ( + + )} + + + {members.map((member) => { + if (hasManage(member.permission) && member.role !== TeamMemberRoleEnum.owner) { + return ( + + + + {member.memberName} + + {userInfo?.team.role === 'owner' && ( + { + removeManager(member.tmbId); + }} + /> + )} + + ); + } + })} + + + ); +} + +export default PermissionManage; diff --git a/projects/app/src/components/support/user/team/TeamManageModal/TeamCard.tsx b/projects/app/src/components/support/user/team/TeamManageModal/TeamCard.tsx new file mode 100644 index 000000000..db3291847 --- /dev/null +++ b/projects/app/src/components/support/user/team/TeamManageModal/TeamCard.tsx @@ -0,0 +1,183 @@ +import { Box, Button, Flex } from '@chakra-ui/react'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { TeamContext } from '.'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { useContextSelector } from 'use-context-selector'; +import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant'; +import RowTabs from '../../../../common/Rowtabs'; +import { 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 './MemberTable'; +import PermissionManage from './PermissionManage'; +import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; +import { hasManage } from '@fastgpt/service/support/permission/resourcePermission/permisson'; +import { useI18n } from '@/web/context/I18n'; +type TabListType = Pick, 'list'>['list']; +enum TabListEnum { + member = 'member', + permission = 'permission' +} + +function TeamCard() { + const { toast } = useToast(); + const { t } = useTranslation(); + const { userT } = useI18n(); + const members = useContextSelector(TeamContext, (v) => v.members); + const onOpenInvite = useContextSelector(TeamContext, (v) => v.onOpenInvite); + const onOpenTeamTagsAsync = useContextSelector(TeamContext, (v) => v.onOpenTeamTagsAsync); + const setEditTeamData = useContextSelector(TeamContext, (v) => v.setEditTeamData); + const onLeaveTeam = useContextSelector(TeamContext, (v) => v.onLeaveTeam); + + const { userInfo, teamPlanStatus } = useUserStore(); + + const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({ + content: t('user.team.member.Confirm Leave') + }); + + const { feConfigs } = useSystemStore(); + + const Tablist: TabListType = [ + { + icon: 'support/team/memberLight', + label: ( + + {t('user.team.Member')} + + {members.length} + + + ), + value: TabListEnum.member + }, + { + icon: 'support/team/key', + label: t('common.Role'), + value: TabListEnum.permission + } + ]; + + const [tab, setTab] = useState(Tablist[0].value); + return ( + + + + {userInfo?.team.teamName} + + {userInfo?.team.role === TeamMemberRoleEnum.owner && ( + { + if (!userInfo?.team) return; + setEditTeamData({ + id: userInfo.team.teamId, + name: userInfo.team.teamName, + avatar: userInfo.team.avatar + }); + }} + /> + )} + + + + { + setTab(v as string); + }} + > + + {hasManage( + members.find((item) => item.tmbId.toString() === userInfo?.team.tmbId.toString()) + ?.permission! + ) && + tab === 'member' && ( + + )} + {userInfo?.team.role === TeamMemberRoleEnum.owner && feConfigs?.show_team_chat && ( + + )} + {userInfo?.team.role !== TeamMemberRoleEnum.owner && ( + + )} + + + + + {tab === 'member' ? : } + + + + ); +} + +export default TeamCard; diff --git a/projects/app/src/components/support/user/team/TeamManageModal/TeamList.tsx b/projects/app/src/components/support/user/team/TeamManageModal/TeamList.tsx new file mode 100644 index 000000000..755efdcd6 --- /dev/null +++ b/projects/app/src/components/support/user/team/TeamManageModal/TeamList.tsx @@ -0,0 +1,117 @@ +import { Box, Button, Flex, IconButton } from '@chakra-ui/react'; +import Avatar from '@/components/Avatar'; +import { useTranslation } from 'next-i18next'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant'; +import EditModal, { defaultForm } from './EditModal'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { useContextSelector } from 'use-context-selector'; +import { TeamContext } from '.'; + +function TeamList() { + const { t } = useTranslation(); + const { userInfo, initUserInfo } = useUserStore(); + const editTeamData = useContextSelector(TeamContext, (v) => v.editTeamData); + const setEditTeamData = useContextSelector(TeamContext, (v) => v.setEditTeamData); + const myTeams = useContextSelector(TeamContext, (v) => v.myTeams); + const refetchTeam = useContextSelector(TeamContext, (v) => v.refetchTeam); + const onSwitchTeam = useContextSelector(TeamContext, (v) => v.onSwitchTeam); + // get the list of teams + + return ( + + + + {t('common.Team')} + + {/* if there is no team */} + {myTeams.length < 1 && ( + + } + aria-label={''} + onClick={() => setEditTeamData(defaultForm)} + /> + )} + + + {myTeams.map((team) => ( + + + + {team.teamName} + + {userInfo?.team?.teamId === team.teamId ? ( + + ) : ( + + )} + + ))} + + {!!editTeamData && ( + setEditTeamData(undefined)} + onSuccess={() => { + refetchTeam(); + initUserInfo(); + }} + /> + )} + + ); +} + +export default TeamList; diff --git a/projects/app/src/components/support/user/team/TeamManageModal/index.tsx b/projects/app/src/components/support/user/team/TeamManageModal/index.tsx index 329ceb89e..838d28844 100644 --- a/projects/app/src/components/support/user/team/TeamManageModal/index.tsx +++ b/projects/app/src/components/support/user/team/TeamManageModal/index.tsx @@ -2,66 +2,59 @@ import React, { useMemo, useState } from 'react'; import MyModal from '@fastgpt/web/components/common/MyModal'; import { useTranslation } from 'next-i18next'; import { useQuery } from '@tanstack/react-query'; -import { DragHandleIcon } from '@chakra-ui/icons'; import { - getTeamList, getTeamMembers, - putSwitchTeam, putUpdateMember, delRemoveMember, - delLeaveTeam + getTeamList, + delLeaveTeam, + putSwitchTeam } from '@/web/support/user/team/api'; -import { - Box, - Button, - Flex, - IconButton, - Table, - Thead, - Tbody, - Tr, - Th, - Td, - TableContainer, - useDisclosure, - MenuButton -} from '@chakra-ui/react'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import Avatar from '@/components/Avatar'; +import { Box, useDisclosure } from '@chakra-ui/react'; import { useUserStore } from '@/web/support/user/useUserStore'; -import { - TeamMemberRoleEnum, - TeamMemberRoleMap, - TeamMemberStatusEnum, - TeamMemberStatusMap -} from '@fastgpt/global/support/user/team/constant'; import dynamic from 'next/dynamic'; import { useRequest } from '@fastgpt/web/hooks/useRequest'; -import { setToken } from '@/web/support/user/auth'; import { useLoading } from '@fastgpt/web/hooks/useLoading'; -import { FormDataType, defaultForm } from './EditModal'; -import MyMenu from '@fastgpt/web/components/common/MyMenu'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; -import { useToast } from '@fastgpt/web/hooks/useToast'; -import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { createContext } from 'use-context-selector'; +import TeamList from './TeamList'; +import TeamCard from './TeamCard'; +import { FormDataType } from './EditModal'; +import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant'; +import { setToken } from '@/web/support/user/auth'; -const EditModal = dynamic(() => import('./EditModal')); const InviteModal = dynamic(() => import('./InviteModal')); const TeamTagModal = dynamic(() => import('../TeamTagModal')); +export const TeamContext = createContext<{ + editTeamData?: FormDataType; + setEditTeamData: React.Dispatch>; + members: Awaited>; + myTeams: Awaited>; + refetchTeam: ReturnType['refetch']; + onSwitchTeam: ReturnType['mutate']; + refetchMembers: ReturnType['refetch']; + openRemoveMember: ReturnType['openConfirm']; + onOpenInvite: ReturnType['onOpen']; + onOpenTeamTagsAsync: ReturnType['onOpen']; + onRemoveMember: ReturnType['mutate']; + onLeaveTeam: ReturnType['mutate']; +}>({} as any); + const TeamManageModal = ({ onClose }: { onClose: () => void }) => { const { t } = useTranslation(); const { Loading } = useLoading(); - const { toast } = useToast(); - const { teamPlanStatus } = useUserStore(); - const { feConfigs } = useSystemStore(); const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm(); - const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({ - content: t('user.team.member.Confirm Leave') - }); const { userInfo, initUserInfo } = useUserStore(); + + const { + data: myTeams = [], + isFetching: isLoadingTeams, + refetch: refetchTeam + } = useQuery(['getTeams', userInfo?._id], () => getTeamList(TeamMemberStatusEnum.active)); + const [editTeamData, setEditTeamData] = useState(); const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure(); const { @@ -70,25 +63,6 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => { onClose: onCloseTeamTagsAsync } = useDisclosure(); - const { - data: myTeams = [], - isFetching: isLoadingTeams, - refetch: refetchTeam - } = useQuery(['getTeams', userInfo?._id], () => getTeamList(TeamMemberStatusEnum.active)); - const defaultTeam = useMemo( - () => myTeams.find((item) => item.defaultTeam) || myTeams[0], - [myTeams] - ); - - const { mutate: onSwitchTeam, isLoading: isSwitchTeam } = useRequest({ - mutationFn: async (teamId: string) => { - const token = await putSwitchTeam(teamId); - token && setToken(token); - return initUserInfo(); - }, - errorToast: t('user.team.Switch Team Failed') - }); - // member action const { data: members = [], refetch: refetchMembers } = useQuery( ['getMembers', userInfo?.team?.teamId], @@ -104,6 +78,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => { refetchMembers(); } }); + const { mutate: onRemoveMember, isLoading: isLoadingRemoveMember } = useRequest({ mutationFn: delRemoveMember, onSuccess() { @@ -112,13 +87,27 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => { successToast: t('user.team.Remove Member Success'), errorToast: t('user.team.Remove Member Failed') }); + + const defaultTeam = useMemo( + () => myTeams.find((item) => item.defaultTeam) || myTeams[0], + [myTeams] + ); + + const { mutate: onSwitchTeam, isLoading: isSwitchTeam } = useRequest({ + mutationFn: async (teamId: string) => { + const token = await putSwitchTeam(teamId); + token && setToken(token); + return initUserInfo(); + }, + errorToast: t('user.team.Switch Team Failed') + }); + const { mutate: onLeaveTeam, isLoading: isLoadingLeaveTeam } = useRequest({ mutationFn: async (teamId?: string) => { if (!teamId) return; // change to personal team // get members - await onSwitchTeam(defaultTeam.teamId); - + onSwitchTeam(defaultTeam.teamId); return delLeaveTeam(teamId); }, onSuccess() { @@ -128,7 +117,22 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => { }); return !!userInfo?.team ? ( - <> + void }) => { overflow={'hidden'} > - {/* teams */} - - - - {t('common.Team')} - - {myTeams.length < 1 && ( - - } - aria-label={''} - onClick={() => setEditTeamData(defaultForm)} - /> - )} - - - {myTeams.map((team) => ( - - - - {team.teamName} - - {userInfo?.team?.teamId === team.teamId ? ( - - ) : ( - - )} - - ))} - - - {/* team card */} - - - - {userInfo.team.teamName} - - {userInfo.team.role === TeamMemberRoleEnum.owner && ( - { - if (!userInfo?.team) return; - setEditTeamData({ - id: userInfo.team.teamId, - name: userInfo.team.teamName, - avatar: userInfo.team.avatar - }); - }} - /> - )} - - - - {t('user.team.Member')} - - {members.length} - - {userInfo.team.role === TeamMemberRoleEnum.owner && ( - - )} - {userInfo.team.role === TeamMemberRoleEnum.owner && feConfigs?.show_team_chat && ( - - )} - - {userInfo.team.role !== TeamMemberRoleEnum.owner && ( - - )} - - - - - - - - - - - - - - {members.map((item) => ( - - - - - - - ))} - -
{t('common.Username')}{t('user.team.Role')}{t('common.Status')}
- - - {item.memberName} - - {t(TeamMemberRoleMap[item.role]?.label || '')} - {t(TeamMemberStatusMap[item.status]?.label || '')} - - {userInfo?.team?.role === TeamMemberRoleEnum.owner && - item.role !== TeamMemberRoleEnum.owner && ( - - - - } - menuList={[ - { - isActive: item.role === TeamMemberRoleEnum.visitor, - label: t('user.team.Invite Role Visitor Tip'), - onClick: () => { - onUpdateMember({ - teamId: item.teamId, - memberId: item.tmbId, - role: TeamMemberRoleEnum.visitor - }); - } - }, - { - isActive: item.role === TeamMemberRoleEnum.admin, - label: t('user.team.Invite Role Admin Tip'), - onClick: () => { - onUpdateMember({ - teamId: item.teamId, - memberId: item.tmbId, - role: TeamMemberRoleEnum.admin - }); - } - }, - ...(item.status === TeamMemberStatusEnum.reject - ? [ - { - label: t('user.team.Reinvite'), - onClick: () => { - onUpdateMember({ - teamId: item.teamId, - memberId: item.tmbId, - status: TeamMemberStatusEnum.waiting - }); - } - } - ] - : []), - { - label: t('user.team.Remove Member Tip'), - onClick: () => - openRemoveMember( - () => - onRemoveMember({ - teamId: item.teamId, - memberId: item.tmbId - }), - undefined, - t('user.team.Remove Member Confirm Tip', { - username: item.memberName - }) - )() - } - ]} - /> - )} -
-
-
-
+ +
- {!!editTeamData && ( - setEditTeamData(undefined)} - onSuccess={() => { - refetchTeam(); - initUserInfo(); - }} - /> - )} {isOpenInvite && userInfo?.team?.teamId && ( void }) => { )} {isOpenTeamTagsAsync && } - - +
) : null; }; diff --git a/projects/app/src/pages/account/index.tsx b/projects/app/src/pages/account/index.tsx index 4f4d08434..5929d9260 100644 --- a/projects/app/src/pages/account/index.tsx +++ b/projects/app/src/pages/account/index.tsx @@ -190,7 +190,7 @@ export async function getServerSideProps(content: any) { return { props: { currentTab: content?.query?.currentTab || TabEnum.info, - ...(await serviceSideProps(content, ['publish'])) + ...(await serviceSideProps(content, ['publish', 'user'])) } }; } diff --git a/projects/app/src/types/i18n.d.ts b/projects/app/src/types/i18n.d.ts index 7f9ae3954..c68a3b077 100644 --- a/projects/app/src/types/i18n.d.ts +++ b/projects/app/src/types/i18n.d.ts @@ -5,6 +5,7 @@ import app from '../../i18n/zh/app.json'; import file from '../../i18n/zh/file.json'; import publish from '../../i18n/zh/publish.json'; import workflow from '../../i18n/zh/workflow.json'; +import user from '../../i18n/zh/user.json'; export interface I18nNamespaces { common: typeof common; @@ -13,6 +14,7 @@ export interface I18nNamespaces { file: typeof file; publish: typeof publish; workflow: typeof workflow; + user: typeof user; } export type I18nNsType = (keyof I18nNamespaces)[]; diff --git a/projects/app/src/web/context/I18n.tsx b/projects/app/src/web/context/I18n.tsx index 5cbc715f0..ca73ba828 100644 --- a/projects/app/src/web/context/I18n.tsx +++ b/projects/app/src/web/context/I18n.tsx @@ -9,6 +9,7 @@ type I18nContextType = { fileT: TFunction<['file'], undefined>; publishT: TFunction<['publish'], undefined>; workflowT: TFunction<['workflow'], undefined>; + userT: TFunction<['user'], undefined>; }; export const I18nContext = createContext({ @@ -23,6 +24,7 @@ const I18nContextProvider = ({ children }: { children: React.ReactNode }) => { const { t: fileT } = useTranslation('file'); const { t: publishT } = useTranslation('publish'); const { t: workflowT } = useTranslation('workflow'); + const { t: userT } = useTranslation('user'); return ( { datasetT, fileT, publishT, - workflowT + workflowT, + userT }} > {children} diff --git a/projects/app/src/web/support/user/team/api.ts b/projects/app/src/web/support/user/team/api.ts index 372df3ca7..457823224 100644 --- a/projects/app/src/web/support/user/team/api.ts +++ b/projects/app/src/web/support/user/team/api.ts @@ -5,6 +5,7 @@ import { InviteMemberProps, InviteMemberResponse, UpdateInviteProps, + UpdateTeamMemberPermissionProps, UpdateTeamMemberProps, UpdateTeamProps } from '@fastgpt/global/support/user/team/controller.d'; @@ -41,6 +42,8 @@ export const updateInviteResult = (data: UpdateInviteProps) => PUT('/proApi/support/user/team/member/updateInvite', data); export const delLeaveTeam = (teamId: string) => DELETE('/proApi/support/user/team/member/leave', { teamId }); +export const updateMemberPermission = (data: UpdateTeamMemberPermissionProps) => + PUT('/proApi/support/user/team/member/updatePermission', data); /* --------------- team tags ---------------- */ export const getTeamsTags = () => GET(`/proApi/support/user/team/tag/list`);