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>
|
|
@ -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. 修复 - 工具调用历史记录存储不正确。
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}>;
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export const PermissionList: PermissionListType = {
|
|||
[PermissionKeyEnum.write]: {
|
||||
name: i18nT('common:permission.write'),
|
||||
description: '',
|
||||
value: 0b110, // 如果某个资源有特殊要求,再重写这个值
|
||||
value: 0b110,
|
||||
checkBoxType: 'single'
|
||||
},
|
||||
[PermissionKeyEnum.manage]: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
|
@ -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[];
|
||||
|
|
@ -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<T = {}> = Record<
|
|||
|
||||
export type ResourcePermissionType = {
|
||||
teamId: string;
|
||||
tmbId: string;
|
||||
resourceType: ResourceType;
|
||||
permission: PermissionValueType;
|
||||
resourceId: string;
|
||||
};
|
||||
} & RequireOnlyOne<{
|
||||
tmbId: string;
|
||||
groupId: string;
|
||||
}>;
|
||||
|
||||
export type ResourcePerWithTmbWithUser = Omit<ResourcePermissionType, 'tmbId'> & {
|
||||
tmbId: TeamMemberWithUserSchema;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ export type UpdateTeamMemberProps = {
|
|||
export type InviteMemberProps = {
|
||||
teamId: string;
|
||||
usernames: string[];
|
||||
permission: PermissionValueType;
|
||||
};
|
||||
export type UpdateInviteProps = {
|
||||
tmbId: string;
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
}[];
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export const DefaultGroupName = 'DEFAULT_GROUP';
|
||||
|
|
@ -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<TeamMemberSchema, 'userId'> & {
|
||||
userId: UserModelSchema;
|
||||
};
|
||||
|
||||
export type TeamMemberWithTeamSchema = Omit<TeamMemberSchema, 'teamId'> & {
|
||||
teamId: TeamSchema;
|
||||
};
|
||||
|
||||
export type TeamMemberWithTeamAndUserSchema = Omit<TeamMemberWithTeamSchema, 'userId'> & {
|
||||
userId: UserModelSchema;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<PerResourceTypeEnum, 'team'>;
|
||||
resourceId: string;
|
||||
}
|
||||
)): Promise<PermissionValueType | undefined> => {
|
||||
// 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<ResourcePermissionType[]> {
|
||||
if (!resourceId) return [];
|
||||
} & (
|
||||
| {
|
||||
resourceType: 'team';
|
||||
resourceId?: undefined;
|
||||
}
|
||||
| {
|
||||
resourceType: Omit<PerResourceTypeEnum, 'team'>;
|
||||
resourceId?: string | null;
|
||||
}
|
||||
)): Promise<ResourcePermissionType[]> {
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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<PerResourceTypeEnum, 'team'>;
|
||||
}
|
||||
)) => {
|
||||
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<AuthResponseType> => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<GroupMemberSchemaType>(
|
||||
GroupMemberCollectionName,
|
||||
GroupMemberSchema
|
||||
);
|
||||
|
|
@ -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<MemberGroupSchemaType>(
|
||||
MemberGroupCollectionName,
|
||||
MemberGroupSchema
|
||||
);
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<T, Keys extends keyof T = keyof T> = Omit<T, Keys> &
|
||||
{
|
||||
[K in Keys]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>;
|
||||
}[Keys];
|
||||
|
||||
type authModeType = {
|
||||
req: ApiRequestProps;
|
||||
authToken?: boolean;
|
||||
|
|
|
|||
|
|
@ -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<string, any>): Promise<TeamTmbItemType> {
|
||||
const tmb = (await MongoTeamMember.findOne(match).populate('teamId')) as TeamMemberWithTeamSchema;
|
||||
|
|
@ -18,7 +22,7 @@ async function getTeamMember(match: Record<string, any>): Promise<TeamTmbItemTyp
|
|||
return Promise.reject('member not exist');
|
||||
}
|
||||
|
||||
const tmbPer = await getResourcePermission({
|
||||
const Per = await getResourcePermission({
|
||||
resourceType: PerResourceTypeEnum.team,
|
||||
teamId: tmb.teamId._id,
|
||||
tmbId: tmb._id
|
||||
|
|
@ -38,7 +42,7 @@ async function getTeamMember(match: Record<string, any>): Promise<TeamTmbItemTyp
|
|||
defaultTeam: tmb.defaultTeam,
|
||||
lafAccount: tmb.teamId.lafAccount,
|
||||
permission: new TeamPermission({
|
||||
per: tmbPer?.permission ?? tmb.teamId.defaultPermission,
|
||||
per: Per ?? TeamDefaultPermissionVal,
|
||||
isOwner: tmb.role === TeamMemberRoleEnum.owner
|
||||
}),
|
||||
notificationAccount: tmb.teamId.notificationAccount
|
||||
|
|
@ -64,6 +68,7 @@ export async function getUserDefaultTeam({ userId }: { userId: string }) {
|
|||
defaultTeam: true
|
||||
});
|
||||
}
|
||||
|
||||
export async function createDefaultTeam({
|
||||
userId,
|
||||
teamName = 'My Team',
|
||||
|
|
@ -84,7 +89,7 @@ export async function createDefaultTeam({
|
|||
});
|
||||
|
||||
if (!tmb) {
|
||||
// create
|
||||
// create team
|
||||
const [{ _id: insertedId }] = await MongoTeam.create(
|
||||
[
|
||||
{
|
||||
|
|
@ -97,7 +102,8 @@ export async function createDefaultTeam({
|
|||
],
|
||||
{ session }
|
||||
);
|
||||
await MongoTeamMember.create(
|
||||
// create team member
|
||||
const [tmb] = await MongoTeamMember.create(
|
||||
[
|
||||
{
|
||||
teamId: insertedId,
|
||||
|
|
@ -111,7 +117,19 @@ export async function createDefaultTeam({
|
|||
],
|
||||
{ session }
|
||||
);
|
||||
console.log('create default team', userId);
|
||||
// create default group
|
||||
await MongoMemberGroupModel.create(
|
||||
[
|
||||
{
|
||||
teamId: tmb.teamId,
|
||||
name: DefaultGroupName,
|
||||
avatar
|
||||
}
|
||||
],
|
||||
{ session }
|
||||
);
|
||||
console.log('create default team and group', userId);
|
||||
return tmb;
|
||||
} else {
|
||||
console.log('default team exist', userId);
|
||||
await MongoTeam.findByIdAndUpdate(tmb.teamId, {
|
||||
|
|
@ -129,10 +147,30 @@ export async function updateTeam({
|
|||
teamDomain,
|
||||
lafAccount
|
||||
}: UpdateTeamProps & { teamId: string }) {
|
||||
await MongoTeam.findByIdAndUpdate(teamId, {
|
||||
name,
|
||||
avatar,
|
||||
teamDomain,
|
||||
lafAccount
|
||||
return mongoSessionRun(async (session) => {
|
||||
await MongoTeam.findByIdAndUpdate(
|
||||
teamId,
|
||||
{
|
||||
name,
|
||||
avatar,
|
||||
teamDomain,
|
||||
lafAccount
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
|
||||
// update default group
|
||||
if (avatar) {
|
||||
await MongoMemberGroupModel.updateOne(
|
||||
{
|
||||
teamId: teamId,
|
||||
name: DefaultGroupName
|
||||
},
|
||||
{
|
||||
avatar
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Flex position="relative">
|
||||
{avatars.slice(0, max).map((avatar, index) => (
|
||||
<Avatar
|
||||
key={avatar + groupId}
|
||||
src={avatar}
|
||||
position={index > 0 ? 'absolute' : 'relative'}
|
||||
left={index > 0 ? `${index * 15}px` : 0}
|
||||
zIndex={index > 0 ? index + 1 : 0}
|
||||
w={'24px'}
|
||||
borderRadius={'50%'}
|
||||
/>
|
||||
))}
|
||||
{avatars.length > max && (
|
||||
<Box
|
||||
position="relative"
|
||||
left={`${(max - 1) * 15}px`}
|
||||
w={'24px'}
|
||||
h={'24px'}
|
||||
borderRadius="50%"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
fontSize="sm"
|
||||
color="myGray.500"
|
||||
>
|
||||
+{avatars.length - max}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default AvatarGroup;
|
||||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1698673802976"
|
||||
class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2726"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128">
|
||||
<path
|
||||
d="M496.68187246 465.60374835c121.95586149 0 220.9345317-98.9786702 220.9345317-220.9345317s-98.9786702-220.9345317-220.9345317-220.93453167-220.9345317 98.9786702-220.93453167 220.93453167 98.9786702 220.9345317 220.93453167 220.9345317z m-125.04894492-345.98347662c33.43475913-33.28746944 77.76895517-51.69868042 125.04894492-51.69868042 47.27998978 0 91.61418581 18.41121097 125.04894495 51.69868042 33.43475913 33.43475913 51.69868042 77.76895517 51.69868041 125.04894492 0 47.27998978-18.41121097 91.61418581-51.69868041 125.04894495-33.43475913 33.43475913-77.76895517 51.69868042-125.04894495 51.69868042-47.27998978 0-91.61418581-18.41121097-125.04894492-51.69868042-33.43475913-33.43475913-51.69868042-77.76895517-51.69868041-125.04894495 0-47.27998978 18.41121097-91.61418581 51.69868041-125.04894492zM511.41084125 892.59655326h-213.5700473c-81.15661796 0-147.2896878-66.13306981-147.28968778-147.2896878 0-39.32634663 15.31812754-76.29605827 43.1558785-104.13380927 27.83775099-27.83775099 64.80746262-43.15587853 104.13380928-43.15587853h397.68215704c52.14054948 0 100.89343614 27.98504069 127.25829025 73.05568516 6.1861669 10.60485751 19.73681815 14.13981003 30.19438599 7.95364313 10.60485751-6.1861669 14.13981003-19.73681815 7.95364314-30.19438599-34.31849726-58.47400606-97.65306301-94.85455893-165.40631938-94.85455893h-397.68215704c-51.10952167 0-99.12595989 19.88410785-135.35922308 56.11737104-36.2332632 36.0859735-56.11737105 84.24970142-56.11737105 135.35922308 0 105.60670615 85.86988799 191.47659413 191.47659413 191.47659413h213.5700473c12.22504409 0 22.09345317-10.01569877 22.09345316-22.24074285s-9.86840908-22.09345317-22.09345316-22.09345317z"
|
||||
p-id="2727"></path>
|
||||
<path
|
||||
d="M873.74347321 830.88217408h-103.10278144v-103.10278145c0-12.22504409-9.86840908-22.09345317-22.09345318-22.09345318s-22.09345317 9.86840908-22.09345316 22.09345318v103.10278145h-103.10278146c-12.22504409 0-22.09345317 9.86840908-22.09345318 22.09345315s9.86840908 22.09345317 22.09345318 22.09345318h103.10278146v103.10278146c0 12.22504409 9.86840908 22.09345317 22.09345316 22.09345315s22.09345317-9.86840908 22.09345318-22.09345315v-103.10278146h103.10278144c12.22504409 0 22.09345317-9.86840908 22.09345318-22.09345318s-9.86840908-22.09345317-22.09345318-22.09345315z"
|
||||
p-id="2728"></path>
|
||||
</svg>
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.2292 4.81275C11.2292 6.59622 9.78346 8.04201 7.99999 8.04201C6.21652 8.04201 4.77073 6.59622 4.77073 4.81275C4.77073 3.02928 6.21652 1.5835 7.99999 1.5835C9.78346 1.5835 11.2292 3.02928 11.2292 4.81275ZM9.89591 4.81275C9.89591 5.85984 9.04708 6.70868 7.99999 6.70868C6.9529 6.70868 6.10406 5.85984 6.10406 4.81275C6.10406 3.76566 6.9529 2.91683 7.99999 2.91683C9.04708 2.91683 9.89591 3.76566 9.89591 4.81275Z" fill=""/>
|
||||
<path d="M2.40472 13.2218C2.40472 11.0126 4.19559 9.22177 6.40472 9.22177H8.84072C9.20891 9.22177 9.50738 9.52025 9.50738 9.88844C9.50738 10.2566 9.20891 10.5551 8.84072 10.5551H6.40472C5.01236 10.5551 3.86924 11.6222 3.74858 12.9832H8.84072C9.20891 12.9832 9.50738 13.2817 9.50738 13.6499C9.50738 14.0181 9.20891 14.3165 8.84072 14.3165H3.13806C2.73305 14.3165 2.40472 13.9882 2.40472 13.5832V13.2218Z"/>
|
||||
<path d="M13.1385 9.91359C13.1385 9.5454 12.84 9.24693 12.4718 9.24693C12.1036 9.24693 11.8052 9.54541 11.8052 9.91359V11.165H10.5537C10.1855 11.165 9.88702 11.4635 9.88702 11.8317C9.88702 12.1999 10.1855 12.4984 10.5537 12.4984H11.8052V13.7498C11.8052 14.118 12.1036 14.4165 12.4718 14.4165C12.84 14.4165 13.1385 14.118 13.1385 13.7498V12.4984H14.3899C14.7581 12.4984 15.0566 12.1999 15.0566 11.8317C15.0566 11.4635 14.7581 11.165 14.3899 11.165H13.1385V9.91359Z"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.4 KiB |
|
|
@ -1,4 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M6.99509 8.73514C7.91556 8.73514 8.66175 7.98895 8.66175 7.06848C8.66175 6.148 7.91556 5.40181 6.99509 5.40181C6.07461 5.40181 5.32842 6.148 5.32842 7.06848C5.32842 7.98895 6.07461 8.73514 6.99509 8.73514Z" fill="#3370FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.99565 1.36786C2.54416 1.36786 1.36749 2.54452 1.36749 3.99602V8.73989C1.36749 8.78421 1.36927 8.82834 1.37282 8.87218C1.40208 9.61777 1.70199 10.357 2.27163 10.9267L9.07586 17.7309C10.2775 18.9325 12.2256 18.9325 13.4272 17.7309L17.7314 13.4268C18.933 12.2252 18.933 10.277 17.7314 9.07542L10.9271 2.27119C10.3517 1.69578 9.60329 1.39563 8.85009 1.37157C8.8134 1.3691 8.77653 1.36786 8.73952 1.36786H3.99565ZM3.03416 3.99602C3.03416 3.465 3.46463 3.03453 3.99565 3.03453H8.73865L8.7619 3.03649L8.78665 3.0371C9.13683 3.04575 9.48192 3.183 9.74862 3.4497L16.5528 10.2539C17.1036 10.8046 17.1036 11.6975 16.5528 12.2483L12.2487 16.5524C11.698 17.1031 10.8051 17.1031 10.2544 16.5524L3.45015 9.74818C3.1856 9.48364 3.04846 9.14202 3.03778 8.79481L3.03689 8.76573L3.03416 8.73871V3.99602Z" fill="#3370FF"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M6.99509 8.73514C7.91556 8.73514 8.66175 7.98895 8.66175 7.06848C8.66175 6.148 7.91556 5.40181 6.99509 5.40181C6.07461 5.40181 5.32842 6.148 5.32842 7.06848C5.32842 7.98895 6.07461 8.73514 6.99509 8.73514Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.99565 1.36786C2.54416 1.36786 1.36749 2.54452 1.36749 3.99602V8.73989C1.36749 8.78421 1.36927 8.82834 1.37282 8.87218C1.40208 9.61777 1.70199 10.357 2.27163 10.9267L9.07586 17.7309C10.2775 18.9325 12.2256 18.9325 13.4272 17.7309L17.7314 13.4268C18.933 12.2252 18.933 10.277 17.7314 9.07542L10.9271 2.27119C10.3517 1.69578 9.60329 1.39563 8.85009 1.37157C8.8134 1.3691 8.77653 1.36786 8.73952 1.36786H3.99565ZM3.03416 3.99602C3.03416 3.465 3.46463 3.03453 3.99565 3.03453H8.73865L8.7619 3.03649L8.78665 3.0371C9.13683 3.04575 9.48192 3.183 9.74862 3.4497L16.5528 10.2539C17.1036 10.8046 17.1036 11.6975 16.5528 12.2483L12.2487 16.5524C11.698 17.1031 10.8051 17.1031 10.2544 16.5524L3.45015 9.74818C3.1856 9.48364 3.04846 9.14202 3.03778 8.79481L3.03689 8.76573L3.03416 8.73871V3.99602Z"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
|
|
@ -1,4 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M13.6688 3.97738C13.9942 3.65195 14.5219 3.65195 14.8473 3.97738L18.1446 7.27471C18.1531 7.28305 18.1613 7.29157 18.1693 7.30026C18.2353 7.37136 18.2865 7.45116 18.3231 7.53579C18.3669 7.63717 18.3912 7.74898 18.3912 7.86647C18.3912 7.98306 18.3673 8.09406 18.3241 8.19481C18.2868 8.28189 18.234 8.3639 18.1656 8.43664C18.1537 8.44941 18.1413 8.46179 18.1285 8.47378C18.0538 8.54406 17.9693 8.59796 17.8796 8.63546C17.7843 8.67534 17.6801 8.69805 17.5708 8.69971L17.5565 8.6998L5.39124 8.6998C4.931 8.6998 4.55791 8.32671 4.55791 7.86647C4.55791 7.40623 4.931 7.03314 5.39124 7.03314L15.5461 7.03314L13.6688 5.1559C13.3434 4.83046 13.3434 4.30282 13.6688 3.97738Z" fill="#3370FF"/>
|
||||
<path d="M1.96866 11.5443C2.04865 11.4643 2.14086 11.404 2.23928 11.3633C2.33521 11.3236 2.44016 11.3012 2.55022 11.3002L2.55791 11.3002H14.7246C15.1848 11.3002 15.5579 11.6733 15.5579 12.1335C15.5579 12.5938 15.1848 12.9669 14.7246 12.9669H4.56976L6.447 14.8441C6.77244 15.1696 6.77244 15.6972 6.447 16.0226C6.12156 16.3481 5.59392 16.3481 5.26849 16.0226L1.96953 12.7237L1.96146 12.7155C1.88513 12.6373 1.82725 12.5478 1.78781 12.4525C1.74707 12.3543 1.72458 12.2465 1.72458 12.1335C1.72458 12.0207 1.74701 11.9131 1.78767 11.8149C1.82833 11.7165 1.88866 11.6243 1.96866 11.5443Z" fill="#3370FF"/>
|
||||
</svg>
|
||||
<path d="M13.6688 3.97738C13.9942 3.65195 14.5219 3.65195 14.8473 3.97738L18.1446 7.27471C18.1531 7.28305 18.1613 7.29157 18.1693 7.30026C18.2353 7.37136 18.2865 7.45116 18.3231 7.53579C18.3669 7.63717 18.3912 7.74898 18.3912 7.86647C18.3912 7.98306 18.3673 8.09406 18.3241 8.19481C18.2868 8.28189 18.234 8.3639 18.1656 8.43664C18.1537 8.44941 18.1413 8.46179 18.1285 8.47378C18.0538 8.54406 17.9693 8.59796 17.8796 8.63546C17.7843 8.67534 17.6801 8.69805 17.5708 8.69971L17.5565 8.6998L5.39124 8.6998C4.931 8.6998 4.55791 8.32671 4.55791 7.86647C4.55791 7.40623 4.931 7.03314 5.39124 7.03314L15.5461 7.03314L13.6688 5.1559C13.3434 4.83046 13.3434 4.30282 13.6688 3.97738Z"/>
|
||||
<path d="M1.96866 11.5443C2.04865 11.4643 2.14086 11.404 2.23928 11.3633C2.33521 11.3236 2.44016 11.3012 2.55022 11.3002L2.55791 11.3002H14.7246C15.1848 11.3002 15.5579 11.6733 15.5579 12.1335C15.5579 12.5938 15.1848 12.9669 14.7246 12.9669H4.56976L6.447 14.8441C6.77244 15.1696 6.77244 15.6972 6.447 16.0226C6.12156 16.3481 5.59392 16.3481 5.26849 16.0226L1.96953 12.7237L1.96146 12.7155C1.88513 12.6373 1.82725 12.5478 1.78781 12.4525C1.74707 12.3543 1.72458 12.2465 1.72458 12.1335C1.72458 12.0207 1.74701 11.9131 1.78767 11.8149C1.82833 11.7165 1.88866 11.6243 1.96866 11.5443Z"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.4119 2.94954C5.37676 2.94954 4.53761 3.78869 4.53761 4.82383C4.53761 5.85898 5.37676 6.69812 6.4119 6.69812C7.44704 6.69812 8.28619 5.85898 8.28619 4.82383C8.28619 3.78869 7.44704 2.94954 6.4119 2.94954ZM3.20428 4.82383C3.20428 3.05231 4.64038 1.61621 6.4119 1.61621C8.18342 1.61621 9.61952 3.05231 9.61952 4.82383C9.61952 6.59536 8.18342 8.03146 6.4119 8.03146C4.64038 8.03146 3.20428 6.59536 3.20428 4.82383ZM9.60538 2.21744C9.74353 1.87615 10.1322 1.71147 10.4735 1.84962C11.6478 2.32496 12.4781 3.47663 12.4781 4.82383C12.4781 6.17103 11.6478 7.3227 10.4735 7.79805C10.1322 7.9362 9.74353 7.77152 9.60538 7.43023C9.46723 7.08894 9.6319 6.70028 9.97319 6.56213C10.6613 6.28359 11.1448 5.60937 11.1448 4.82383C11.1448 4.0383 10.6613 3.36408 9.97319 3.08554C9.6319 2.94739 9.46723 2.55872 9.60538 2.21744ZM5.43553 9.23908H7.38828C7.96028 9.23908 8.42329 9.23907 8.80044 9.2648C9.18874 9.2913 9.5345 9.34728 9.86274 9.48325C10.6487 9.8088 11.2731 10.4332 11.5987 11.2192C11.7347 11.5474 11.7906 11.8932 11.8171 12.2815C11.8429 12.6586 11.8429 13.1216 11.8429 13.6936V13.7172C11.8429 14.0854 11.5444 14.3839 11.1762 14.3839C10.808 14.3839 10.5095 14.0854 10.5095 13.7172C10.5095 13.1161 10.5092 12.6987 10.4869 12.3723C10.465 12.0519 10.4243 11.8682 10.3669 11.7294C10.1766 11.2702 9.81175 10.9053 9.3525 10.7151C9.21379 10.6576 9.03009 10.6169 8.70968 10.595C8.38328 10.5728 7.96585 10.5724 7.36476 10.5724H5.45904C4.85795 10.5724 4.44052 10.5728 4.11413 10.595C3.79372 10.6169 3.61001 10.6576 3.47131 10.7151C3.01205 10.9053 2.64718 11.2702 2.45695 11.7294C2.39949 11.8682 2.35877 12.0519 2.33691 12.3723C2.31464 12.6987 2.31428 13.1161 2.31428 13.7172C2.31428 14.0854 2.0158 14.3839 1.64761 14.3839C1.27942 14.3839 0.980942 14.0854 0.980942 13.7172L0.980942 13.6936C0.980937 13.1216 0.980933 12.6587 1.00667 12.2815C1.03316 11.8932 1.08914 11.5474 1.22511 11.2192C1.55066 10.4332 2.1751 9.8088 2.96106 9.48325C3.28931 9.34728 3.63507 9.2913 4.02336 9.2648C4.40052 9.23907 4.86352 9.23908 5.43553 9.23908ZM11.8011 9.81963C11.8928 9.46306 12.2563 9.2484 12.6128 9.34018C13.9962 9.69622 15.0191 10.9512 15.0191 12.4467V13.7172C15.0191 14.0854 14.7206 14.3839 14.3524 14.3839C13.9842 14.3839 13.6857 14.0854 13.6857 13.7172V12.4467C13.6857 11.5743 13.089 10.8395 12.2805 10.6314C11.9239 10.5396 11.7093 10.1762 11.8011 9.81963Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -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 (
|
||||
<Flex alignItems={'center'} position={'relative'}>
|
||||
<Input {...props} />
|
||||
<MyIcon name={'common/searchLight'} w={'1rem'} position={'absolute'} left={2} zIndex={10} />
|
||||
</Flex>
|
||||
<InputGroup alignItems="center" size={'sm'}>
|
||||
<InputLeftElement>
|
||||
<MyIcon name="common/searchLight" w="16px" color={'myGray.500'} />
|
||||
</InputLeftElement>
|
||||
<Input fontSize="sm" bg={'myGray.50'} {...props} />
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -143,10 +143,21 @@ const MyMenu = ({
|
|||
bottom={0}
|
||||
left={0}
|
||||
/>
|
||||
<Box position={'relative'}>{Button}</Box>
|
||||
<Box
|
||||
position={'relative'}
|
||||
color={isOpen ? 'primary.600' : ''}
|
||||
w="fit-content"
|
||||
p="1"
|
||||
bgColor={isOpen ? 'myGray.50' : ''}
|
||||
h="fit-content"
|
||||
borderRadius="sm"
|
||||
>
|
||||
{Button}
|
||||
</Box>
|
||||
</Box>
|
||||
<MenuList
|
||||
minW={isOpen ? `${width}px !important` : '80px'}
|
||||
zIndex={100}
|
||||
maxW={'300px'}
|
||||
p={'6px'}
|
||||
border={'1px solid #fff'}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import {
|
|||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalContentProps,
|
||||
Box
|
||||
Box,
|
||||
ImageProps
|
||||
} from '@chakra-ui/react';
|
||||
import MyBox from '../MyBox';
|
||||
import { useSystem } from '../../../hooks/useSystem';
|
||||
|
|
@ -14,7 +15,7 @@ import Avatar from '../Avatar';
|
|||
|
||||
export interface MyModalProps extends ModalContentProps {
|
||||
iconSrc?: string;
|
||||
iconColor?: string;
|
||||
iconColor?: ImageProps['color'];
|
||||
title?: any;
|
||||
isCentered?: boolean;
|
||||
isLoading?: boolean;
|
||||
|
|
@ -81,7 +82,7 @@ const MyModal = ({
|
|||
alt=""
|
||||
src={iconSrc}
|
||||
w={'1.5rem'}
|
||||
borderRadius={'md'}
|
||||
borderRadius={'sm'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -67,8 +67,11 @@ const LightRowTabs = <ValueType = string,>({
|
|||
justifyContent={'center'}
|
||||
borderBottom={'2px solid'}
|
||||
borderColor={defaultColor}
|
||||
px={3}
|
||||
px={1}
|
||||
whiteSpace={'nowrap'}
|
||||
_hover={{
|
||||
color: activeColor
|
||||
}}
|
||||
{...(value === item.value
|
||||
? {
|
||||
color: activeColor,
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
@ -1197,4 +1197,4 @@
|
|||
"verification": "Verification",
|
||||
"xx_search_result": "{{key}} Search Results",
|
||||
"yes": "Yes"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
@ -159,4 +159,4 @@
|
|||
"workflow.user_file_input_desc": "用户上传的文档和图片链接",
|
||||
"workflow.user_select": "用户选择",
|
||||
"workflow.user_select_tip": "该模块可配置多个选项,以供对话时选择。不同选项可导向不同工作流支线"
|
||||
}
|
||||
}
|
||||
|
|
@ -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": "是"
|
||||
|
|
|
|||
|
|
@ -13,4 +13,4 @@
|
|||
"redirect": "跳转",
|
||||
"no_remind": "不再提醒",
|
||||
"Chinese_ip_tip": "检测到您是中国大陆 IP,点击跳转访问中国大陆版。"
|
||||
}
|
||||
}
|
||||
|
|
@ -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": "所有者"
|
||||
}
|
||||
|
|
@ -187,4 +187,4 @@
|
|||
"workflow.Switch_success": "切换成功",
|
||||
"workflow.Team cloud": "团队云端",
|
||||
"workflow.exit_tips": "您的更改尚未保存,「直接退出」将不会保存您的编辑记录。"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="20" height="20" fill="url(#paint0_linear_12282_71879)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.61043 5.58085C7.70468 5.58085 6.97042 6.31511 6.97042 7.22086C6.97042 8.1266 7.70468 8.86086 8.61043 8.86086C9.51618 8.86086 10.2504 8.1266 10.2504 7.22086C10.2504 6.31511 9.51618 5.58085 8.61043 5.58085ZM5.80376 7.22086C5.80376 5.67077 7.06035 4.41418 8.61043 4.41418C10.1605 4.41418 11.4171 5.67077 11.4171 7.22086C11.4171 8.77094 10.1605 10.0275 8.61043 10.0275C7.06035 10.0275 5.80376 8.77094 5.80376 7.22086ZM11.4047 4.94026C11.5256 4.64163 11.8657 4.49754 12.1643 4.61842C13.1918 5.03434 13.9184 6.04205 13.9184 7.22086C13.9184 8.39966 13.1918 9.40737 12.1643 9.82329C11.8657 9.94417 11.5256 9.80008 11.4047 9.50145C11.2838 9.20283 11.4279 8.86275 11.7266 8.74187C12.3287 8.49814 12.7517 7.9082 12.7517 7.22086C12.7517 6.53351 12.3287 5.94357 11.7266 5.69985C11.4279 5.57896 11.2838 5.23888 11.4047 4.94026ZM7.7561 11.0842H9.46476C9.96526 11.0842 10.3704 11.0842 10.7004 11.1067C11.0402 11.1299 11.3427 11.1789 11.6299 11.2978C12.3176 11.5827 12.864 12.1291 13.1489 12.8168C13.2678 13.104 13.3168 13.4066 13.34 13.7463C13.3625 14.0763 13.3625 14.4814 13.3625 14.9819V15.0025C13.3625 15.3247 13.1014 15.5859 12.7792 15.5859C12.457 15.5859 12.1959 15.3247 12.1959 15.0025C12.1959 14.4766 12.1955 14.1113 12.1761 13.8257C12.1569 13.5454 12.1213 13.3846 12.071 13.2633C11.9046 12.8614 11.5853 12.5422 11.1835 12.3757C11.0621 12.3254 10.9013 12.2898 10.621 12.2707C10.3354 12.2512 9.97013 12.2509 9.44418 12.2509H7.77668C7.25072 12.2509 6.88547 12.2512 6.59987 12.2707C6.31951 12.2898 6.15877 12.3254 6.03741 12.3757C5.63556 12.5422 5.31629 12.8614 5.14984 13.2633C5.09957 13.3846 5.06394 13.5454 5.04481 13.8257C5.02532 14.1113 5.025 14.4766 5.025 15.0025C5.025 15.3247 4.76384 15.5859 4.44167 15.5859C4.1195 15.5859 3.85834 15.3247 3.85834 15.0025L3.85834 14.9819C3.85833 14.4814 3.85833 14.0763 3.88085 13.7463C3.90403 13.4066 3.95301 13.104 4.07198 12.8168C4.35684 12.1291 4.90323 11.5827 5.59094 11.2978C5.87816 11.1789 6.1807 11.1299 6.52046 11.1067C6.85047 11.0842 7.2556 11.0842 7.7561 11.0842ZM13.3259 11.5922C13.4062 11.2802 13.7243 11.0924 14.0363 11.1727C15.2467 11.4842 16.1417 12.5823 16.1417 13.8909V15.0025C16.1417 15.3247 15.8805 15.5859 15.5584 15.5859C15.2362 15.5859 14.975 15.3247 14.975 15.0025V13.8909C14.975 13.1275 14.4529 12.4846 13.7455 12.3025C13.4335 12.2222 13.2456 11.9042 13.3259 11.5922Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_12282_71879" x1="1.5" y1="20" x2="20" y2="1.6056e-06" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#53A3FF"/>
|
||||
<stop offset="1" stop-color="#68BFFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
|
@ -66,6 +66,7 @@ export function ChangeOwnerModal({
|
|||
<MyModal
|
||||
isOpen
|
||||
iconSrc="modal/changePer"
|
||||
iconColor="primary.600"
|
||||
onClose={onClose}
|
||||
title={t('common:permission.change_owner')}
|
||||
isLoading={loading}
|
||||
|
|
@ -76,7 +77,7 @@ export function ChangeOwnerModal({
|
|||
<Box>{name}</Box>
|
||||
</HStack>
|
||||
<Flex mt={4} justify="start" flexDirection="column">
|
||||
<Box fontSize="14px" fontWeight="500" color="myGray.900">
|
||||
<Box fontSize="14px" fontWeight="500">
|
||||
{t('common:permission.change_owner_to')}
|
||||
</Box>
|
||||
<Flex mt="4" alignItems="center" position={'relative'}>
|
||||
|
|
@ -162,3 +163,5 @@ export function ChangeOwnerModal({
|
|||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangeOwnerModal;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Flex flexWrap="wrap" rowGap={2}>
|
||||
{names?.slice(0, max).map((name, index) => (
|
||||
<Tag key={index} colorSchema={'gray'} ml={2}>
|
||||
{name.length > 10 ? name.slice(0, 10) + '...' : name}
|
||||
</Tag>
|
||||
))}
|
||||
|
||||
<Popover
|
||||
isOpen={isOpen}
|
||||
trigger={'hover'}
|
||||
onOpen={onToggle}
|
||||
onClose={onClose}
|
||||
placement="bottom"
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<Box>
|
||||
{length > max && (
|
||||
<Tag colorSchema={'gray'} ml={2} cursor={'pointer'}>
|
||||
{'+' + (length - max)}
|
||||
</Tag>
|
||||
)}
|
||||
</Box>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w={'fit-content'} bg={'white'} px={4} py={2}>
|
||||
<Flex rowGap={2} flexWrap={'wrap'} columnGap={2}>
|
||||
{names?.slice(max)?.map((name, index) => (
|
||||
<Tag key={index + length} colorSchema={'gray'}>
|
||||
{name}
|
||||
</Tag>
|
||||
))}
|
||||
</Flex>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default GroupTags;
|
||||
|
|
@ -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 }}
|
||||
>
|
||||
<Avatar src={member.avatar} w="24px" />
|
||||
<Box w="full">{member.memberName}</Box>
|
||||
<Box w="full" ml={2}>
|
||||
{member.memberName}
|
||||
</Box>
|
||||
<MyIcon
|
||||
name="common/closeLight"
|
||||
w="16px"
|
||||
|
|
|
|||
|
|
@ -1,15 +1,4 @@
|
|||
import {
|
||||
ModalBody,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
Td,
|
||||
Box,
|
||||
Flex
|
||||
} from '@chakra-ui/react';
|
||||
import { ModalBody, Table, TableContainer, Tbody, Th, Thead, Tr, Td, Flex } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import React from 'react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
|
|
@ -18,7 +7,7 @@ import PermissionTags from './PermissionTags';
|
|||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { CollaboratorContext } from './context';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
|
|
@ -38,16 +27,17 @@ function ManageModal({ onClose }: ManageModalProps) {
|
|||
onDelOneCollaborator(tmbId)
|
||||
);
|
||||
|
||||
const { mutate: onUpdate, isLoading: isUpdating } = useRequest({
|
||||
mutationFn: ({ tmbId, per }: { tmbId: string; per: PermissionValueType }) => {
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>(null);
|
||||
const closeTimer = useRef<any>();
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
const closeTimer = useRef<NodeJS.Timeout>();
|
||||
|
||||
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 (
|
||||
<Menu offset={offset} isOpen={isOpen} autoSelect={false} direction={'ltr'}>
|
||||
<Box
|
||||
ref={ref}
|
||||
w="fit-content"
|
||||
onMouseEnter={() => {
|
||||
if (trigger === 'hover') {
|
||||
setIsOpen(true);
|
||||
|
|
@ -126,7 +125,8 @@ function PermissionSelect({
|
|||
}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
<MenuButton
|
||||
ref={ref}
|
||||
position={'relative'}
|
||||
onClickCapture={() => {
|
||||
if (trigger === 'click') {
|
||||
|
|
@ -134,25 +134,8 @@ function PermissionSelect({
|
|||
}
|
||||
}}
|
||||
>
|
||||
<MenuButton
|
||||
w={'100%'}
|
||||
h={'100%'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
/>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
position={'relative'}
|
||||
cursor={'pointer'}
|
||||
userSelect={'none'}
|
||||
>
|
||||
{Button}
|
||||
</Flex>
|
||||
</Box>
|
||||
{Button}
|
||||
</MenuButton>
|
||||
<MenuList
|
||||
minW={isOpen ? `${width}px !important` : 0}
|
||||
p="3"
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export type MemberManagerInputPropsType = {
|
|||
permission: Permission;
|
||||
onGetCollaboratorList: () => Promise<CollaboratorItemType[]>;
|
||||
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[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<HStack>
|
||||
<Avatar src={avatar} w={['18px', '22px']} rounded="50%" />
|
||||
<Box maxW={'150px'} className={'textEllipsis'}>
|
||||
{name}
|
||||
</Box>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
export default MemberTag;
|
||||
|
|
@ -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<string>();
|
||||
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}
|
||||
>
|
||||
<Box fontSize={['sm', 'md']} fontWeight={'bold'} alignItems={'center'}>
|
||||
<Box fontSize={['sm', 'md']} fontWeight={'bold'} alignItems={'center'} color={'myGray.900'}>
|
||||
{userInfo?.team.teamName}
|
||||
</Box>
|
||||
{userInfo?.team.role === TeamMemberRoleEnum.owner && (
|
||||
<MyIcon
|
||||
name="edit"
|
||||
w={'14px'}
|
||||
w="14px"
|
||||
ml={2}
|
||||
cursor={'pointer'}
|
||||
cursor="pointer"
|
||||
_hover={{
|
||||
color: 'primary.500'
|
||||
}}
|
||||
|
|
@ -124,21 +168,32 @@ function TeamCard() {
|
|||
</Flex>
|
||||
|
||||
<Flex px={5} alignItems={'center'} justifyContent={'space-between'}>
|
||||
<LightRowTabs<TabListEnum>
|
||||
overflow={'auto'}
|
||||
list={Tablist}
|
||||
value={tab}
|
||||
onChange={setTab}
|
||||
></LightRowTabs>
|
||||
<LightRowTabs<TabListEnum> overflow={'auto'} list={Tablist} value={tab} onChange={setTab} />
|
||||
{/* ctrl buttons */}
|
||||
<Flex alignItems={'center'}>
|
||||
{tab === TabListEnum.member &&
|
||||
userInfo?.team.permission.hasManagePer &&
|
||||
feConfigs?.show_team_chat && (
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
size="md"
|
||||
borderRadius={'md'}
|
||||
ml={3}
|
||||
leftIcon={<MyIcon name="core/dataset/tag" w={'16px'} />}
|
||||
onClick={() => {
|
||||
onOpenTeamTagsAsync();
|
||||
}}
|
||||
>
|
||||
{t('common:user.team.Team Tags Async')}
|
||||
</Button>
|
||||
)}
|
||||
{tab === TabListEnum.member && userInfo?.team.permission.hasManagePer && (
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
size="sm"
|
||||
variant={'primary'}
|
||||
size="md"
|
||||
borderRadius={'md'}
|
||||
ml={3}
|
||||
leftIcon={<MyIcon name="common/inviteLight" w={'14px'} color={'primary.500'} />}
|
||||
leftIcon={<MyIcon name="common/inviteLight" w={'16px'} color={'white'} />}
|
||||
onClick={() => {
|
||||
if (
|
||||
teamPlanStatus?.standardConstants?.maxTeamMember &&
|
||||
|
|
@ -158,24 +213,10 @@ function TeamCard() {
|
|||
{t('common:user.team.Invite Member')}
|
||||
</Button>
|
||||
)}
|
||||
{userInfo?.team.permission.hasManagePer && feConfigs?.show_team_chat && (
|
||||
{tab === TabListEnum.member && !userInfo?.team.permission.isOwner && (
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
size="sm"
|
||||
borderRadius={'md'}
|
||||
ml={3}
|
||||
leftIcon={<DragHandleIcon w={'14px'} color={'primary.500'} />}
|
||||
onClick={() => {
|
||||
onOpenTeamTagsAsync();
|
||||
}}
|
||||
>
|
||||
{t('common:user.team.Team Tags Async')}
|
||||
</Button>
|
||||
)}
|
||||
{!userInfo?.team.permission.isOwner && (
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
size="sm"
|
||||
size="md"
|
||||
borderRadius={'md'}
|
||||
ml={3}
|
||||
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
|
||||
|
|
@ -187,11 +228,36 @@ function TeamCard() {
|
|||
{t('common:user.team.Leave Team')}
|
||||
</Button>
|
||||
)}
|
||||
{tab === TabListEnum.group && userInfo?.team.permission.hasManagePer && (
|
||||
<Button
|
||||
variant={'primary'}
|
||||
size="md"
|
||||
borderRadius={'md'}
|
||||
ml={3}
|
||||
leftIcon={<MyIcon name="support/permission/collaborator" w={'14px'} />}
|
||||
onClick={onOpenGroupInfo}
|
||||
>
|
||||
{t('user:team.group.create')}
|
||||
</Button>
|
||||
)}
|
||||
{tab === TabListEnum.permission && (
|
||||
<Box ml="auto">
|
||||
<SearchInput
|
||||
placeholder={t('user:team.group.search_placeholder')}
|
||||
w="200px"
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Box mt={3} flex={'1 0 0'} overflow={'auto'}>
|
||||
{tab === TabListEnum.member && <MemberTable />}
|
||||
{tab === TabListEnum.group && (
|
||||
<GroupManage onEditGroup={onEditGroup} onManageMember={onManageMember} />
|
||||
)}
|
||||
{tab === TabListEnum.permission && <PermissionManage />}
|
||||
</Box>
|
||||
|
||||
|
|
@ -203,6 +269,24 @@ function TeamCard() {
|
|||
/>
|
||||
)}
|
||||
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
|
||||
{isOpenGroupInfo && (
|
||||
<GroupInfoModal
|
||||
onClose={() => {
|
||||
onCloseGroupInfo();
|
||||
setEditGroupId(undefined);
|
||||
}}
|
||||
editGroupId={editGroupId}
|
||||
/>
|
||||
)}
|
||||
{isOpenManageGroupMember && (
|
||||
<ManageGroupMemberModal
|
||||
onClose={() => {
|
||||
onCloseManageGroupMember();
|
||||
setEditGroupId(undefined);
|
||||
}}
|
||||
editGroupId={editGroupId}
|
||||
/>
|
||||
)}
|
||||
<ConfirmLeaveTeamModal />
|
||||
</Flex>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)'}
|
||||
>
|
||||
<Box flex={['0 0 auto', 1]} fontSize={['sm', 'md']}>
|
||||
<Box flex={['0 0 auto', 1]} fontSize={['sm', 'md']} fontWeight={'bold'}>
|
||||
{t('common:common.Team')}
|
||||
</Box>
|
||||
{/* if there is no team */}
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
<MyModal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
iconSrc="/imgs/modal/team.svg"
|
||||
iconSrc="support/team/group"
|
||||
iconColor="primary.600"
|
||||
title={defaultData.id ? t('common:user.team.Update Team') : t('common:user.team.Create Team')}
|
||||
>
|
||||
<ModalBody>
|
||||
|
|
|
|||
|
|
@ -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<GroupFormType>({
|
||||
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 (
|
||||
<MyModal
|
||||
onClose={onClose}
|
||||
title={editGroupId ? t('user:team.group.edit') : t('user:team.group.create')}
|
||||
iconSrc={group?.avatar ?? DEFAULT_TEAM_AVATAR}
|
||||
>
|
||||
<ModalBody flex={1} overflow={'auto'} display={'flex'} flexDirection={'column'} gap={4}>
|
||||
<FormLabel w="80px">{t('user:team.avatar_and_name')}</FormLabel>
|
||||
<HStack>
|
||||
<Avatar
|
||||
src={getValues('avatar')}
|
||||
onClick={onOpenSelectAvatar}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
/>
|
||||
<Input
|
||||
bgColor="myGray.50"
|
||||
{...register('name', { required: true })}
|
||||
placeholder={t('user:team.group.name')}
|
||||
/>
|
||||
</HStack>
|
||||
</ModalBody>
|
||||
<ModalFooter alignItems="flex-end">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
onClick={handleSubmit((data) => {
|
||||
if (editGroupId) {
|
||||
onUpdate(data);
|
||||
} else {
|
||||
onCreate(data);
|
||||
}
|
||||
})}
|
||||
>
|
||||
{editGroupId ? t('common:common.Save') : t('common:new_create')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
<AvatarSelect onSelect={onSelectAvatar} />
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default GroupInfoModal;
|
||||
|
|
@ -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<string | undefined>(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 (
|
||||
<MyModal
|
||||
onClose={onClose}
|
||||
title={t('user:team.group.manage_member')}
|
||||
iconSrc={group?.avatar ?? DEFAULT_TEAM_AVATAR}
|
||||
iconColor="primary.600"
|
||||
minW={['70vw', '800px']}
|
||||
>
|
||||
<ModalBody flex={1} display={'flex'} flexDirection={'column'} gap={4}>
|
||||
<Grid
|
||||
templateColumns="1fr 1fr"
|
||||
borderRadius="8px"
|
||||
border="1px solid"
|
||||
borderColor="myGray.200"
|
||||
>
|
||||
<Flex flexDirection="column" p="4">
|
||||
<SearchInput
|
||||
placeholder={t('user:search_user')}
|
||||
fontSize="sm"
|
||||
bg={'myGray.50'}
|
||||
onChange={(e) => {
|
||||
setSearchKey(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Flex flexDirection="column" mt={3} flexGrow="1" overflow={'auto'} maxH={'400px'}>
|
||||
{filtered.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
alignItems="center"
|
||||
key={member.tmbId}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'myGray.50',
|
||||
...(!isSelected(member.tmbId) ? { svg: { color: 'myGray.50' } } : {})
|
||||
}}
|
||||
_notLast={{ mb: 2 }}
|
||||
onClick={() => handleToggleSelect(member.tmbId)}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={!!isSelected(member.tmbId)}
|
||||
icon={<MyIcon name={'common/check'} w={'12px'} />}
|
||||
/>
|
||||
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
|
||||
<Box>{member.memberName}</Box>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}>
|
||||
<Box mt={2}>{t('common:chosen') + ': ' + members.length}</Box>
|
||||
<Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'400px'}>
|
||||
{members.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
onMouseEnter={() => 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 }}
|
||||
>
|
||||
<HStack>
|
||||
<Avatar
|
||||
src={allMembers.find((item) => item.tmbId === member.tmbId)?.avatar}
|
||||
w="1.5rem"
|
||||
borderRadius={'md'}
|
||||
/>
|
||||
<Box>
|
||||
{allMembers.find((item) => item.tmbId === member.tmbId)?.memberName}
|
||||
</Box>
|
||||
</HStack>
|
||||
<Box mr="auto">
|
||||
{(() => {
|
||||
if (member.role === 'owner') {
|
||||
return (
|
||||
<Tag ml={2} colorSchema="gray">
|
||||
{t('user:team.group.role.owner')}
|
||||
</Tag>
|
||||
);
|
||||
} else if (member.role === 'admin') {
|
||||
return (
|
||||
<Tag ml={2} mr="auto">
|
||||
{t('user:team.group.role.admin')}
|
||||
{myRole === 'owner' && (
|
||||
<MyIcon
|
||||
ml={1}
|
||||
name={'common/closeLight'}
|
||||
w={'1rem'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => handleToggleAdmin(member.tmbId)}
|
||||
/>
|
||||
)}
|
||||
</Tag>
|
||||
);
|
||||
} else if (member.role === 'member') {
|
||||
return (
|
||||
myRole === 'owner' &&
|
||||
hoveredMemberId === member.tmbId && (
|
||||
<Tag
|
||||
ml={2}
|
||||
colorSchema="yellow"
|
||||
cursor={'pointer'}
|
||||
onClick={() => handleToggleAdmin(member.tmbId)}
|
||||
>
|
||||
{t('user:team.group.set_as_admin')}
|
||||
</Tag>
|
||||
)
|
||||
);
|
||||
}
|
||||
})()}
|
||||
</Box>
|
||||
{(myRole === 'owner' || (myRole === 'admin' && member.role === 'member')) && (
|
||||
<MyIcon
|
||||
name={'common/closeLight'}
|
||||
w={'1rem'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => handleToggleSelect(member.tmbId)}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</ModalBody>
|
||||
<ModalFooter alignItems="flex-end">
|
||||
<Button isLoading={isLoading} onClick={onUpdate}>
|
||||
{t('common:common.Save')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default GroupEditModal;
|
||||
|
|
@ -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<TeamMemberItemType | null>(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 (
|
||||
<MyModal
|
||||
isOpen
|
||||
iconSrc="modal/changePer"
|
||||
iconColor="primary.600"
|
||||
onClose={onClose}
|
||||
title={t('common:permission.change_owner')}
|
||||
isLoading={loading}
|
||||
>
|
||||
<ModalBody>
|
||||
<HStack>
|
||||
<Avatar src={group?.avatar} w={'1.75rem'} borderRadius={'md'} />
|
||||
<Box>{group?.name}</Box>
|
||||
</HStack>
|
||||
<Flex mt={4} justify="start" flexDirection="column">
|
||||
<Box fontSize="14px" fontWeight="500" color="myGray.900">
|
||||
{t('common:permission.change_owner_to')}
|
||||
</Box>
|
||||
<Flex mt="4" alignItems="center" position={'relative'}>
|
||||
{selectedMember && (
|
||||
<Avatar
|
||||
src={selectedMember.avatar}
|
||||
w={'20px'}
|
||||
borderRadius={'md'}
|
||||
position="absolute"
|
||||
left={3}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
placeholder={t('common:permission.change_owner_placeholder')}
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value);
|
||||
setSelectedMember(null);
|
||||
}}
|
||||
onFocus={() => {
|
||||
onOpenMemberListMenu();
|
||||
setSelectedMember(null);
|
||||
}}
|
||||
{...(selectedMember && { pl: '10' })}
|
||||
/>
|
||||
</Flex>
|
||||
{isOpenMemberListMenu && memberList.length > 0 && (
|
||||
<Flex
|
||||
mt={2}
|
||||
w={'100%'}
|
||||
flexDirection={'column'}
|
||||
gap={2}
|
||||
p={1}
|
||||
boxShadow="lg"
|
||||
bg="white"
|
||||
borderRadius="md"
|
||||
zIndex={10}
|
||||
maxH={'300px'}
|
||||
overflow={'auto'}
|
||||
>
|
||||
{memberList.map((item) => (
|
||||
<Box
|
||||
key={item.tmbId}
|
||||
p="2"
|
||||
_hover={{ bg: 'myGray.100' }}
|
||||
mx="1"
|
||||
borderRadius="md"
|
||||
cursor={'pointer'}
|
||||
onClickCapture={() => {
|
||||
setInputValue(item.memberName);
|
||||
setSelectedMember(item);
|
||||
onCloseMemberListMenu();
|
||||
}}
|
||||
>
|
||||
<Flex align="center">
|
||||
<Avatar src={item.avatar} w="1.25rem" />
|
||||
<Box ml="2">{item.memberName}</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Box mt="4">
|
||||
<Checkbox
|
||||
isChecked={keepAdmin}
|
||||
onChange={(e) => {
|
||||
setKeepAdmin(e.target.checked);
|
||||
}}
|
||||
>
|
||||
{t('user:team.group.keep_admin')}
|
||||
</Checkbox>
|
||||
</Box>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<HStack>
|
||||
<Button onClick={onClose} variant={'whiteBase'}>
|
||||
{t('common:common.Cancel')}
|
||||
</Button>
|
||||
<Button onClick={onConfirm}>{t('common:common.Confirm')}</Button>
|
||||
</HStack>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangeOwnerModal;
|
||||
|
|
@ -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<string>();
|
||||
|
||||
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 (
|
||||
<MyBox>
|
||||
<TableContainer overflow={'unset'} fontSize={'sm'} mx="6">
|
||||
<Table overflow={'unset'}>
|
||||
<Thead>
|
||||
<Tr bg={'white !important'}>
|
||||
<Th bg="myGray.100" borderLeftRadius="6px">
|
||||
{t('user:team.group.name')}
|
||||
</Th>
|
||||
<Th bg="myGray.100">{t('user:owner')}</Th>
|
||||
<Th bg="myGray.100">{t('user:team.group.members')}</Th>
|
||||
<Th bg="myGray.100" borderRightRadius="6px">
|
||||
{t('common:common.Action')}
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{groups?.map((group) => (
|
||||
<Tr key={group._id} overflow={'unset'}>
|
||||
<Td>
|
||||
<HStack>
|
||||
<MemberTag
|
||||
name={
|
||||
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
|
||||
}
|
||||
avatar={group.avatar}
|
||||
/>
|
||||
<Box>
|
||||
({group.name === DefaultGroupName ? members.length : group.members.length})
|
||||
</Box>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td>
|
||||
<MemberTag
|
||||
name={
|
||||
group.name === DefaultGroupName
|
||||
? members.find((item) => 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 ?? ''
|
||||
}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
{group.name === DefaultGroupName ? (
|
||||
<AvatarGroup avatars={members.map((v) => v.avatar)} groupId={group._id} />
|
||||
) : hasGroupManagePer(group) ? (
|
||||
<MyTooltip label={t('user:team.group.manage_member')}>
|
||||
<Box cursor="pointer" onClick={() => onManageMember(group._id)}>
|
||||
<AvatarGroup
|
||||
avatars={group.members.map(
|
||||
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
|
||||
)}
|
||||
groupId={group._id}
|
||||
/>
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
) : (
|
||||
<AvatarGroup
|
||||
avatars={group.members.map(
|
||||
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
|
||||
)}
|
||||
groupId={group._id}
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{hasGroupManagePer(group) && group.name !== DefaultGroupName && (
|
||||
<MyMenu
|
||||
Button={<MyIcon name={'edit'} cursor={'pointer'} w="1rem" />}
|
||||
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
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<ConfirmDeleteGroupModal />
|
||||
{isOpenChangeOwner && editGroupId && (
|
||||
<ChangeOwnerModal groupId={editGroupId} onClose={onCloseChangeOwner} />
|
||||
)}
|
||||
</MyBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default MemberTable;
|
||||
|
|
@ -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<string[]>([]);
|
||||
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,
|
||||
<Box whiteSpace={'pre-wrap'}>
|
||||
{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(', ')
|
||||
})}
|
||||
</Box>
|
||||
)();
|
||||
},
|
||||
errorToast: t('common:user.team.Invite Member Failed Tip')
|
||||
});
|
||||
usernames: inviteUsernames
|
||||
}),
|
||||
{
|
||||
onSuccess(res: InviteMemberResponse) {
|
||||
onSuccess();
|
||||
openConfirm(
|
||||
() => onClose(),
|
||||
undefined,
|
||||
<Box whiteSpace={'pre-wrap'}>
|
||||
{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(', ')
|
||||
})}
|
||||
</Box>
|
||||
)();
|
||||
},
|
||||
errorToast: t('common:user.team.Invite Member Failed Tip')
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
iconSrc="/imgs/modal/team.svg"
|
||||
iconSrc="common/inviteLight"
|
||||
iconColor="primary.600"
|
||||
title={
|
||||
<Box>
|
||||
<Box>{t('common:user.team.Invite Member')}</Box>
|
||||
|
|
@ -104,9 +70,6 @@ const InviteModal = ({
|
|||
<ModalBody>
|
||||
<Box mb={2}>{t('common:user.Account')}</Box>
|
||||
<TagTextarea defaultValues={inviteUsernames} onUpdate={setInviteUsernames} />
|
||||
<Box mt={4}>
|
||||
<MySelect list={inviteTypes} value={selectedInviteType} onchange={setSelectInviteType} />
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -1,105 +1,91 @@
|
|||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import {
|
||||
Box,
|
||||
HStack,
|
||||
MenuButton,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
TeamMemberRoleEnum,
|
||||
TeamMemberStatusMap
|
||||
} from '@fastgpt/global/support/user/team/constant';
|
||||
import { Box, HStack, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { TeamModalContext } from '../context';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import PermissionTags from '@/components/support/permission/PermissionTags';
|
||||
import { TeamPermissionList } from '@fastgpt/global/support/permission/user/constant';
|
||||
import PermissionSelect from '@/components/support/permission/MemberManager/PermissionSelect';
|
||||
import { CollaboratorContext } from '@/components/support/permission/MemberManager/context';
|
||||
import { delRemoveMember } from '@/web/support/user/team/api';
|
||||
import Tag from '@fastgpt/web/components/common/Tag';
|
||||
import Icon from '@fastgpt/web/components/common/Icon';
|
||||
import GroupTags from '@/components/support/permission/Group/GroupTags';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { TeamModalContext } from '../context';
|
||||
|
||||
function MemberTable() {
|
||||
const { userInfo } = useUserStore();
|
||||
const { t } = useTranslation();
|
||||
const { members, refetchMembers } = useContextSelector(TeamModalContext, (v) => v);
|
||||
const { onUpdateCollaborators } = useContextSelector(CollaboratorContext, (v) => v);
|
||||
|
||||
const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm({
|
||||
type: 'delete'
|
||||
});
|
||||
|
||||
const { members, groups, refetchMembers, refetchGroups } = useContextSelector(
|
||||
TeamModalContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
return (
|
||||
<MyBox>
|
||||
<TableContainer overflow={'unset'} fontSize={'sm'}>
|
||||
<TableContainer overflow={'unset'} fontSize={'sm'} mx="6">
|
||||
<Table overflow={'unset'}>
|
||||
<Thead bg={'myWhite.400'}>
|
||||
<Tr>
|
||||
<Th borderRadius={'none !important'}>{t('common:common.Username')}</Th>
|
||||
<Th>{t('common:common.Permission')}</Th>
|
||||
<Th>{t('common:common.Status')}</Th>
|
||||
<Th borderRadius={'none !important'}>{t('common:common.Action')}</Th>
|
||||
<Thead>
|
||||
<Tr bgColor={'white !important'}>
|
||||
<Th borderLeftRadius="6px" bgColor="myGray.100">
|
||||
{t('common:common.Username')}
|
||||
</Th>
|
||||
<Th bgColor="myGray.100">{t('user:team.belong_to_group')}</Th>
|
||||
<Th borderRightRadius="6px" bgColor="myGray.100">
|
||||
{t('common:common.Action')}
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{members.map((item) => (
|
||||
{members?.map((item) => (
|
||||
<Tr key={item.userId} overflow={'unset'}>
|
||||
<Td>
|
||||
<HStack>
|
||||
<Avatar src={item.avatar} w={['18px', '22px']} />
|
||||
<Box maxW={'150px'} className={'textEllipsis'}>
|
||||
<Avatar src={item.avatar} w={['18px', '22px']} borderRadius={'50%'} />
|
||||
<Box className={'textEllipsis'}>
|
||||
{item.memberName}
|
||||
{item.status === 'waiting' && (
|
||||
<Tag ml="2" colorSchema="yellow">
|
||||
{t('common:user.team.member.waiting')}
|
||||
</Tag>
|
||||
)}
|
||||
</Box>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td>
|
||||
<PermissionTags
|
||||
permission={item.permission}
|
||||
permissionList={TeamPermissionList}
|
||||
<Td maxW={'300px'}>
|
||||
<GroupTags
|
||||
names={groups
|
||||
?.filter((group) => group.members.map((m) => m.tmbId).includes(item.tmbId))
|
||||
.map((g) => g.name)}
|
||||
max={3}
|
||||
/>
|
||||
</Td>
|
||||
<Td color={TeamMemberStatusMap[item.status].color}>
|
||||
{t(TeamMemberStatusMap[item.status]?.label || ('' as any))}
|
||||
</Td>
|
||||
<Td>
|
||||
{userInfo?.team.permission.hasManagePer &&
|
||||
item.role !== TeamMemberRoleEnum.owner &&
|
||||
item.tmbId !== userInfo?.team.tmbId && (
|
||||
<PermissionSelect
|
||||
value={item.permission.value}
|
||||
Button={
|
||||
<MenuButton
|
||||
_hover={{
|
||||
color: 'primary.600'
|
||||
}}
|
||||
borderRadius={'md'}
|
||||
px={2}
|
||||
py={1}
|
||||
lineHeight={1}
|
||||
>
|
||||
<MyIcon name={'edit'} cursor={'pointer'} w="1rem" />
|
||||
</MenuButton>
|
||||
}
|
||||
onChange={(permission) => {
|
||||
onUpdateCollaborators({
|
||||
tmbIds: [item.tmbId],
|
||||
permission
|
||||
});
|
||||
<Icon
|
||||
name={'common/trash'}
|
||||
cursor={'pointer'}
|
||||
w="1rem"
|
||||
p="1"
|
||||
borderRadius="sm"
|
||||
_hover={{
|
||||
color: 'red.600',
|
||||
bgColor: 'myGray.100'
|
||||
}}
|
||||
onDelete={() => {
|
||||
onClick={() => {
|
||||
openRemoveMember(
|
||||
() => delRemoveMember(item.tmbId).then(refetchMembers),
|
||||
() =>
|
||||
delRemoveMember(item.tmbId).then(() =>
|
||||
Promise.all([refetchGroups(), refetchMembers()])
|
||||
),
|
||||
undefined,
|
||||
t('user.team.Remove Member Confirm Tip', {
|
||||
t('common:user.team.Remove Member Confirm Tip', {
|
||||
username: item.memberName
|
||||
})
|
||||
)();
|
||||
|
|
|
|||
|
|
@ -1,170 +0,0 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalFooter,
|
||||
Grid,
|
||||
Input,
|
||||
Flex,
|
||||
Checkbox,
|
||||
CloseButton,
|
||||
InputGroup,
|
||||
InputLeftElement
|
||||
} from '@chakra-ui/react';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
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';
|
||||
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { TeamModalContext } from '../../context';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
|
||||
function AddManagerModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const { userT } = useI18n();
|
||||
const { userInfo } = useUserStore();
|
||||
const { members, refetchMembers } = useContextSelector(TeamModalContext, (v) => v);
|
||||
|
||||
const [selected, setSelected] = useState<typeof members>([]);
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
|
||||
const { mutate: submit, isLoading } = useRequest({
|
||||
mutationFn: async () => {
|
||||
return updateMemberPermission({
|
||||
permission: ManagePermissionVal,
|
||||
tmbIds: selected.map((item) => {
|
||||
return item.tmbId;
|
||||
})
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
refetchMembers();
|
||||
onSuccess();
|
||||
},
|
||||
successToast: t('common:common.Success'),
|
||||
errorToast: t('common:common.failed')
|
||||
});
|
||||
|
||||
const filterMembers = useMemo(() => {
|
||||
return members.filter((member) => {
|
||||
if (member.permission.isOwner) return false;
|
||||
if (!searchKey) return true;
|
||||
return !!member.memberName.includes(searchKey);
|
||||
});
|
||||
}, [members, searchKey]);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
iconSrc={'modal/AddClb'}
|
||||
maxW={['90vw']}
|
||||
minW={['900px']}
|
||||
overflow={'unset'}
|
||||
title={userT('team.Add manager')}
|
||||
>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
<ModalBody py={6} px={10}>
|
||||
<Grid
|
||||
templateColumns="1fr 1fr"
|
||||
h="448px"
|
||||
borderRadius="8px"
|
||||
border="1px solid"
|
||||
borderColor="myGray.200"
|
||||
>
|
||||
<Flex flexDirection="column" p="4">
|
||||
<InputGroup alignItems="center" size={'sm'}>
|
||||
<InputLeftElement>
|
||||
<MyIcon name="common/searchLight" w="16px" color={'myGray.500'} />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
placeholder={t('user:search_user')}
|
||||
fontSize="sm"
|
||||
bg={'myGray.50'}
|
||||
onChange={(e) => {
|
||||
setSearchKey(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
<Flex flexDirection="column" mt={3}>
|
||||
{filterMembers.map((member) => {
|
||||
return (
|
||||
<Flex
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
alignItems="center"
|
||||
key={member.tmbId}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'myGray.50',
|
||||
...(!selected.includes(member) ? { svg: { color: 'myGray.50' } } : {})
|
||||
}}
|
||||
_notLast={{ mb: 2 }}
|
||||
onClick={() => {
|
||||
if (selected.indexOf(member) == -1) {
|
||||
setSelected([...selected, member]);
|
||||
} else {
|
||||
setSelected([...selected.filter((item) => item.tmbId != member.tmbId)]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={selected.includes(member)}
|
||||
icon={<MyIcon name={'common/check'} w={'12px'} />}
|
||||
/>
|
||||
<Avatar ml={2} src={member.avatar} w="1.5rem" />
|
||||
{member.memberName}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4">
|
||||
<Box mt={3}>{t('common:chosen') + ': ' + selected.length} </Box>
|
||||
<Box mt={5}>
|
||||
{selected.map((member) => {
|
||||
return (
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
key={member.tmbId}
|
||||
_hover={{ bg: 'myGray.50' }}
|
||||
_notLast={{ mb: 2 }}
|
||||
>
|
||||
<Avatar src={member.avatar} w="1.5rem" />
|
||||
<Box w="full">{member.memberName}</Box>
|
||||
<MyIcon
|
||||
name={'common/closeLight'}
|
||||
w={'1rem'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() =>
|
||||
setSelected([...selected.filter((item) => item.tmbId != member.tmbId)])
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</ModalBody>
|
||||
<ModalFooter alignItems="flex-end">
|
||||
<Button h={'30px'} isLoading={isLoading} onClick={submit}>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddManagerModal;
|
||||
|
|
@ -1,116 +1,286 @@
|
|||
import React from 'react';
|
||||
import { Box, Button, Flex, Tag, TagLabel, useDisclosure } from '@chakra-ui/react';
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
HStack,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { delMemberPermission } from '@/web/support/user/team/api';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getTeamClbs, updateMemberPermission } from '@/web/support/user/team/api';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
|
||||
import { TeamModalContext } from '../../context';
|
||||
import { TeamPermissionList } from '@fastgpt/global/support/permission/user/constant';
|
||||
import dynamic from 'next/dynamic';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
|
||||
const AddManagerModal = dynamic(() => import('./AddManager'));
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MemberTag from '../../../Info/MemberTag';
|
||||
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
|
||||
import {
|
||||
TeamManagePermissionVal,
|
||||
TeamWritePermissionVal
|
||||
} from '@fastgpt/global/support/permission/user/constant';
|
||||
import { TeamPermission } from '@fastgpt/global/support/permission/user/controller';
|
||||
import { useCreation } from 'ahooks';
|
||||
|
||||
function PermissionManage() {
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
const { members, refetchMembers } = useContextSelector(TeamModalContext, (v) => v);
|
||||
const { groups, refetchMembers, refetchGroups, members, searchKey } = useContextSelector(
|
||||
TeamModalContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
const {
|
||||
isOpen: isOpenAddManager,
|
||||
onOpen: onOpenAddManager,
|
||||
onClose: onCloseAddManager
|
||||
} = useDisclosure();
|
||||
const { runAsync: refetchClbs, data: clbs = [] } = useRequest2(getTeamClbs, {
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?.team?.teamId]
|
||||
});
|
||||
|
||||
const { mutate: removeManager, isLoading: isRemovingManager } = useRequest({
|
||||
mutationFn: async (memberId: string) => {
|
||||
return delMemberPermission(memberId);
|
||||
},
|
||||
successToast: t('user:delete.admin_success'),
|
||||
errorToast: t('user:delete.admin_failed'),
|
||||
const filteredGroups = useCreation(
|
||||
() => groups?.filter((group) => group.name.toLowerCase().includes(searchKey.toLowerCase())),
|
||||
[groups, searchKey]
|
||||
);
|
||||
const filteredMembers = useCreation(
|
||||
() =>
|
||||
members
|
||||
?.filter((member) => member.memberName.toLowerCase().includes(searchKey.toLowerCase()))
|
||||
.map((member) => {
|
||||
const clb = clbs?.find((clb) => String(clb.tmbId) === String(member.tmbId));
|
||||
const permission =
|
||||
member.role === 'owner'
|
||||
? new TeamPermission({ isOwner: true })
|
||||
: new TeamPermission({ per: clb?.permission });
|
||||
|
||||
return { ...member, permission };
|
||||
}),
|
||||
[clbs, members, searchKey]
|
||||
);
|
||||
|
||||
const { runAsync: onUpdateMemberPermission } = useRequest2(updateMemberPermission, {
|
||||
onSuccess: () => {
|
||||
refetchGroups();
|
||||
refetchMembers();
|
||||
refetchClbs();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<MyBox h={'100%'} isLoading={isRemovingManager} bg={'white'}>
|
||||
<Flex
|
||||
mx={'5'}
|
||||
flexDirection={'row'}
|
||||
alignItems={'center'}
|
||||
rowGap={'8'}
|
||||
justifyContent={'space-between'}
|
||||
>
|
||||
<Flex>
|
||||
<Box fontSize={['sm', 'md']} fontWeight={'bold'} alignItems={'center'}>
|
||||
{t('common:user.team.role.Admin')}
|
||||
</Box>
|
||||
<Box
|
||||
fontSize={['xs']}
|
||||
color={'myGray.500'}
|
||||
bgColor={'myGray.100'}
|
||||
alignItems={'center'}
|
||||
alignContent={'center'}
|
||||
px={'3'}
|
||||
ml={3}
|
||||
borderRadius={'sm'}
|
||||
>
|
||||
{t(TeamPermissionList['manage'].description as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
{userInfo?.team.role === 'owner' && (
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
size="sm"
|
||||
borderRadius={'md'}
|
||||
ml={3}
|
||||
leftIcon={<MyIcon name={'common/inviteLight'} w={'14px'} color={'primary.500'} />}
|
||||
onClick={() => {
|
||||
onOpenAddManager();
|
||||
}}
|
||||
>
|
||||
{t('user:team.Add manager')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex mt="4" mx="4" flexWrap={'wrap'} gap={3}>
|
||||
{members.map((member) => {
|
||||
if (member.permission.hasManagePer && !member.permission.isOwner) {
|
||||
return (
|
||||
<MyTag key={member.tmbId} px="4" py="2" type="fill" colorSchema="gray">
|
||||
<Avatar src={member.avatar} w="1.25rem" />
|
||||
<Box fontSize={'sm'} ml={1}>
|
||||
{member.memberName}
|
||||
</Box>
|
||||
{userInfo?.team.role === 'owner' && (
|
||||
<MyIcon
|
||||
ml={4}
|
||||
name="common/trash"
|
||||
w="1rem"
|
||||
color="myGray.500"
|
||||
cursor="pointer"
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => {
|
||||
removeManager(member.tmbId);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</MyTag>
|
||||
);
|
||||
const { runAsync: onAddPermission, loading: addLoading } = useRequest2(
|
||||
async ({
|
||||
groupId,
|
||||
memberId,
|
||||
per
|
||||
}: {
|
||||
groupId?: string;
|
||||
memberId?: string;
|
||||
per: 'write' | 'manage';
|
||||
}) => {
|
||||
if (groupId) {
|
||||
const group = groups?.find((group) => group._id === groupId);
|
||||
if (group) {
|
||||
const permission = new TeamPermission({ per: group.permission.value });
|
||||
switch (per) {
|
||||
case 'write':
|
||||
permission.addPer(TeamWritePermissionVal);
|
||||
return onUpdateMemberPermission({
|
||||
groupId: group._id,
|
||||
permission: permission.value
|
||||
});
|
||||
case 'manage':
|
||||
permission.addPer(TeamManagePermissionVal);
|
||||
return onUpdateMemberPermission({
|
||||
groupId: group._id,
|
||||
permission: permission.value
|
||||
});
|
||||
}
|
||||
})}
|
||||
</Flex>
|
||||
}
|
||||
}
|
||||
if (memberId) {
|
||||
const member = filteredMembers?.find((member) => String(member.tmbId) === memberId);
|
||||
if (member) {
|
||||
const permission = new TeamPermission({ per: member.permission.value });
|
||||
switch (per) {
|
||||
case 'write':
|
||||
permission.addPer(TeamWritePermissionVal);
|
||||
return onUpdateMemberPermission({
|
||||
memberId: String(member.tmbId),
|
||||
permission: permission.value
|
||||
});
|
||||
case 'manage':
|
||||
permission.addPer(TeamManagePermissionVal);
|
||||
return onUpdateMemberPermission({
|
||||
memberId: String(member.tmbId),
|
||||
permission: permission.value
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
{isOpenAddManager && (
|
||||
<AddManagerModal onClose={onCloseAddManager} onSuccess={onCloseAddManager} />
|
||||
)}
|
||||
</MyBox>
|
||||
const { runAsync: onRemovePermission, loading: removeLoading } = useRequest2(
|
||||
async ({
|
||||
groupId,
|
||||
memberId,
|
||||
per
|
||||
}: {
|
||||
groupId?: string;
|
||||
memberId?: string;
|
||||
per: 'write' | 'manage';
|
||||
}) => {
|
||||
if (groupId) {
|
||||
const group = groups?.find((group) => group._id === groupId);
|
||||
if (group) {
|
||||
const permission = new TeamPermission({ per: group.permission.value });
|
||||
switch (per) {
|
||||
case 'write':
|
||||
permission.removePer(TeamWritePermissionVal);
|
||||
return onUpdateMemberPermission({
|
||||
groupId: group._id,
|
||||
permission: permission.value
|
||||
});
|
||||
case 'manage':
|
||||
permission.removePer(TeamManagePermissionVal);
|
||||
return onUpdateMemberPermission({
|
||||
groupId: group._id,
|
||||
permission: permission.value
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (memberId) {
|
||||
const member = members?.find((member) => String(member.tmbId) === memberId);
|
||||
if (member) {
|
||||
const permission = new TeamPermission({ per: member.permission.value }); // Hint: member.permission is read-only
|
||||
switch (per) {
|
||||
case 'write':
|
||||
permission.removePer(TeamWritePermissionVal);
|
||||
return onUpdateMemberPermission({
|
||||
memberId: String(member.tmbId),
|
||||
permission: permission.value
|
||||
});
|
||||
case 'manage':
|
||||
permission.removePer(TeamManagePermissionVal);
|
||||
return onUpdateMemberPermission({
|
||||
memberId: String(member.tmbId),
|
||||
permission: permission.value
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const userManage = userInfo?.permission.hasManagePer;
|
||||
|
||||
return (
|
||||
<TableContainer fontSize={'sm'} mx="6">
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr bg={'white !important'}>
|
||||
<Th bg="myGray.100" borderLeftRadius="md" maxW={'150px'}>
|
||||
{t('user:team.group.group')} / {t('user:team.group.members')}
|
||||
<QuestionTip ml="1" label={t('user:team.group.permission_tip')} />
|
||||
</Th>
|
||||
<Th bg="myGray.100">
|
||||
<Box mx="auto" w="fit-content">
|
||||
{t('user:team.group.permission.write')}
|
||||
</Box>
|
||||
</Th>
|
||||
<Th bg="myGray.100" borderRightRadius="md">
|
||||
<Box mx="auto" w="fit-content">
|
||||
{t('user:team.group.permission.manage')}
|
||||
<QuestionTip ml="1" label={t('user:team.group.manage_tip')} />
|
||||
</Box>
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{filteredGroups?.map((group) => (
|
||||
<Tr key={group._id} overflow={'unset'} border="none">
|
||||
<Td border="none">
|
||||
<MemberTag
|
||||
name={
|
||||
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
|
||||
}
|
||||
avatar={group.avatar}
|
||||
/>
|
||||
</Td>
|
||||
<Td border="none">
|
||||
<Box mx="auto" w="fit-content">
|
||||
<Checkbox
|
||||
isDisabled={!userManage}
|
||||
isChecked={group.permission.hasWritePer}
|
||||
onChange={(e) =>
|
||||
e.target.checked
|
||||
? onAddPermission({ groupId: group._id, per: 'write' })
|
||||
: onRemovePermission({ groupId: group._id, per: 'write' })
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Td>
|
||||
<Td border="none">
|
||||
<Box mx="auto" w="fit-content">
|
||||
<Checkbox
|
||||
isDisabled={!userInfo?.permission.isOwner}
|
||||
isChecked={group.permission.hasManagePer}
|
||||
onChange={(e) =>
|
||||
e.target.checked
|
||||
? onAddPermission({ groupId: group._id, per: 'manage' })
|
||||
: onRemovePermission({ groupId: group._id, per: 'manage' })
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
{filteredGroups?.length > 0 && filteredMembers?.length > 0 && (
|
||||
<Tr borderBottom={'1px solid'} borderColor={'myGray.300'} />
|
||||
)}
|
||||
{filteredMembers?.map((member) => (
|
||||
<Tr key={member.tmbId} overflow={'unset'} border="none">
|
||||
<Td border="none">
|
||||
<HStack>
|
||||
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
|
||||
<Box>{member.memberName}</Box>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td border="none">
|
||||
<Box mx="auto" w="fit-content">
|
||||
<Checkbox
|
||||
isDisabled={member.permission.isOwner || !userManage}
|
||||
isChecked={member.permission.hasWritePer}
|
||||
onChange={(e) =>
|
||||
e.target.checked
|
||||
? onAddPermission({ memberId: String(member.tmbId), per: 'write' })
|
||||
: onRemovePermission({ memberId: String(member.tmbId), per: 'write' })
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Td>
|
||||
<Td border="none">
|
||||
<Box mx="auto" w="fit-content">
|
||||
<Checkbox
|
||||
isDisabled={member.permission.isOwner || !userInfo?.permission.isOwner}
|
||||
isChecked={member.permission.hasManagePer}
|
||||
onChange={(e) =>
|
||||
e.target.checked
|
||||
? onAddPermission({ memberId: String(member.tmbId), per: 'manage' })
|
||||
: onRemovePermission({ memberId: String(member.tmbId), per: 'manage' })
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,279 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
Flex,
|
||||
Grid,
|
||||
HStack,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputLeftElement
|
||||
} from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
import { RequireAtLeastOne } from '@fastgpt/global/common/type/utils';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
|
||||
|
||||
type memberType = {
|
||||
type: 'member';
|
||||
tmbId: string;
|
||||
memberName: string;
|
||||
avatar: string;
|
||||
};
|
||||
|
||||
type groupType = {
|
||||
type: 'group';
|
||||
_id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
};
|
||||
|
||||
type selectedType = {
|
||||
member: string[];
|
||||
group: string[];
|
||||
};
|
||||
|
||||
function SelectMember({
|
||||
allMembers,
|
||||
selected = { member: [], group: [] },
|
||||
setSelected
|
||||
// mode = 'both'
|
||||
}: {
|
||||
allMembers: {
|
||||
member: memberType[];
|
||||
group: groupType[];
|
||||
};
|
||||
selected?: selectedType;
|
||||
setSelected: React.Dispatch<React.SetStateAction<selectedType>>;
|
||||
mode?: 'member' | 'group' | 'both';
|
||||
}) {
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return [
|
||||
...allMembers.member.filter((member) => {
|
||||
if (member.memberName.toLowerCase().includes(searchKey.toLowerCase())) return true;
|
||||
return false;
|
||||
}),
|
||||
...allMembers.group.filter((member) => {
|
||||
if (member.name.toLowerCase().includes(searchKey.toLowerCase())) return true;
|
||||
return false;
|
||||
})
|
||||
];
|
||||
}, [searchKey, allMembers]);
|
||||
|
||||
const selectedFlated = useMemo(() => {
|
||||
return [
|
||||
...allMembers.member.filter((member) => {
|
||||
return selected.member?.includes(member.tmbId);
|
||||
}),
|
||||
...allMembers.group.filter((member) => {
|
||||
return selected.group?.includes(member._id);
|
||||
})
|
||||
];
|
||||
}, [selected, allMembers]);
|
||||
|
||||
const handleToggleSelect = (member: memberType | groupType) => {
|
||||
if (member.type == 'member') {
|
||||
if (selected.member?.indexOf(member.tmbId) == -1) {
|
||||
setSelected({
|
||||
member: [...selected.member, member.tmbId],
|
||||
group: [...selected.group]
|
||||
});
|
||||
} else {
|
||||
setSelected({
|
||||
member: [...selected.member.filter((item) => item != member.tmbId)],
|
||||
group: [...selected.group]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (selected.group?.indexOf(member._id) == -1) {
|
||||
setSelected({ member: [...selected.member], group: [...selected.group, member._id] });
|
||||
} else {
|
||||
setSelected({
|
||||
member: [...selected.member],
|
||||
group: [...selected.group.filter((item) => item != member._id)]
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isSelected = (member: memberType | groupType) => {
|
||||
if (member.type == 'member') {
|
||||
return selected.member?.includes(member.tmbId);
|
||||
} else {
|
||||
return selected.group?.includes(member._id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid
|
||||
templateColumns="1fr 1fr"
|
||||
borderRadius="8px"
|
||||
border="1px solid"
|
||||
borderColor="myGray.200"
|
||||
h={'100%'}
|
||||
>
|
||||
<Flex flexDirection="column" p="4" h={'100%'} overflow={'auto'}>
|
||||
<InputGroup alignItems="center" size={'sm'}>
|
||||
<InputLeftElement>
|
||||
<MyIcon name="common/searchLight" w="16px" color={'myGray.500'} />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
placeholder={t('user:search_user')}
|
||||
fontSize="sm"
|
||||
bg={'myGray.50'}
|
||||
onChange={(e) => {
|
||||
setSearchKey(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
<Flex flexDirection="column" mt={3}>
|
||||
{filtered.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
alignItems="center"
|
||||
key={member.type == 'member' ? member.tmbId : member._id}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'myGray.50',
|
||||
...(!isSelected(member) ? { svg: { color: 'myGray.50' } } : {})
|
||||
}}
|
||||
_notLast={{ mb: 2 }}
|
||||
onClick={() => handleToggleSelect(member)}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={!!isSelected(member)}
|
||||
icon={<MyIcon name={'common/check'} w={'12px'} />}
|
||||
/>
|
||||
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
|
||||
<Box>
|
||||
{member.type == 'member'
|
||||
? member.memberName
|
||||
: member.name === DefaultGroupName
|
||||
? userInfo?.team.teamName
|
||||
: member.name}
|
||||
</Box>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex
|
||||
borderLeft="1px"
|
||||
borderColor="myGray.200"
|
||||
flexDirection="column"
|
||||
p="4"
|
||||
h={'100%'}
|
||||
overflow={'auto'}
|
||||
>
|
||||
<Box mt={3}>
|
||||
{t('common:chosen') + ': ' + Number(selected.member.length + selected.group.length)}{' '}
|
||||
</Box>
|
||||
<Box mt={5}>
|
||||
{selectedFlated.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
justifyContent="space-between"
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
key={member.type == 'member' ? member.tmbId : member._id}
|
||||
_hover={{ bg: 'myGray.50' }}
|
||||
_notLast={{ mb: 2 }}
|
||||
>
|
||||
<Avatar src={member.avatar} w="1.5rem" borderRadius={'md'} />
|
||||
<Box w="full">
|
||||
{member.type == 'member'
|
||||
? member.memberName
|
||||
: member.name === DefaultGroupName
|
||||
? userInfo?.team.teamName
|
||||
: member.name}
|
||||
</Box>
|
||||
<MyIcon
|
||||
name={'common/closeLight'}
|
||||
w={'1rem'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => handleToggleSelect(member)}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
// This function is for using with react-hook-form
|
||||
function ControllerWrapper({
|
||||
control,
|
||||
allMembers,
|
||||
mode = 'both',
|
||||
name = 'members'
|
||||
}: {
|
||||
control: Control;
|
||||
allMembers: RequireAtLeastOne<{ member?: memberType[]; group?: groupType[] }>;
|
||||
mode?: 'member' | 'group' | 'both';
|
||||
name?: string;
|
||||
}) {
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field: { value: selected, onChange } }) => (
|
||||
<SelectMember
|
||||
mode={mode}
|
||||
allMembers={
|
||||
(() => {
|
||||
switch (mode) {
|
||||
case 'member':
|
||||
return { member: allMembers.member, group: [] };
|
||||
case 'group':
|
||||
return { member: [], group: allMembers.group };
|
||||
case 'both':
|
||||
return { member: allMembers.member, group: allMembers.group };
|
||||
}
|
||||
})() as Required<typeof allMembers>
|
||||
}
|
||||
selected={(() => {
|
||||
switch (mode) {
|
||||
case 'member':
|
||||
return { member: selected, group: [] };
|
||||
case 'group':
|
||||
return { member: [], group: selected };
|
||||
case 'both':
|
||||
return { member: selected.member, group: selected.group };
|
||||
}
|
||||
})()}
|
||||
setSelected={
|
||||
(({ member, group }: selectedType, _prevState: selectedType) => {
|
||||
switch (mode) {
|
||||
case 'member':
|
||||
onChange(member);
|
||||
return;
|
||||
case 'group':
|
||||
onChange(group);
|
||||
return;
|
||||
case 'both':
|
||||
onChange({ member, group });
|
||||
return;
|
||||
}
|
||||
}) as any // hack: we do not need to handle prevState
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const UnControlledSelectMember = SelectMember;
|
||||
export default ControllerWrapper;
|
||||
|
|
@ -1,54 +1,57 @@
|
|||
import React, { ReactNode, useCallback, useState } from 'react';
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { createContext } from 'use-context-selector';
|
||||
import type { EditTeamFormDataType } from './components/EditInfoModal';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
delMemberPermission,
|
||||
getTeamList,
|
||||
putSwitchTeam,
|
||||
updateMemberPermission
|
||||
} from '@/web/support/user/team/api';
|
||||
import { getTeamList, putSwitchTeam } from '@/web/support/user/team/api';
|
||||
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import type { TeamTmbItemType, TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
|
||||
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import CollaboratorContextProvider from '@/components/support/permission/MemberManager/context';
|
||||
import { TeamPermissionList } from '@fastgpt/global/support/permission/user/constant';
|
||||
import {
|
||||
CollaboratorItemType,
|
||||
UpdateClbPermissionProps
|
||||
} from '@fastgpt/global/support/permission/collaborator';
|
||||
import { getGroupList } from '@/web/support/user/team/group/api';
|
||||
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
|
||||
const EditInfoModal = dynamic(() => import('./components/EditInfoModal'));
|
||||
|
||||
type TeamModalContextType = {
|
||||
myTeams: TeamTmbItemType[];
|
||||
refetchTeams: () => void;
|
||||
members: TeamMemberItemType[];
|
||||
groups: MemberGroupListType;
|
||||
isLoading: boolean;
|
||||
onSwitchTeam: (teamId: string) => void;
|
||||
|
||||
setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>;
|
||||
members: TeamMemberItemType[];
|
||||
|
||||
refetchMembers: () => void;
|
||||
refetchTeams: () => void;
|
||||
refetchGroups: () => void;
|
||||
searchKey: string;
|
||||
setSearchKey: React.Dispatch<React.SetStateAction<string>>;
|
||||
};
|
||||
|
||||
export const TeamModalContext = createContext<TeamModalContextType>({
|
||||
myTeams: [],
|
||||
isLoading: false,
|
||||
onSwitchTeam: function (teamId: string): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
setEditTeamData: function (value: React.SetStateAction<EditTeamFormDataType | undefined>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
groups: [],
|
||||
members: [],
|
||||
isLoading: false,
|
||||
onSwitchTeam: function (_teamId: string): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
setEditTeamData: function (_value: React.SetStateAction<EditTeamFormDataType | undefined>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
refetchTeams: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
refetchMembers: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
refetchGroups: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
|
||||
searchKey: '',
|
||||
setSearchKey: function (_value: React.SetStateAction<string>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -56,12 +59,16 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
|
|||
const { t } = useTranslation();
|
||||
const [editTeamData, setEditTeamData] = useState<EditTeamFormDataType>();
|
||||
const { userInfo, initUserInfo, loadAndGetTeamMembers } = useUserStore();
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
|
||||
const {
|
||||
data: myTeams = [],
|
||||
isFetching: isLoadingTeams,
|
||||
refetch: refetchTeams
|
||||
} = useQuery(['getTeams', userInfo?._id], () => getTeamList(TeamMemberStatusEnum.active));
|
||||
loading: isLoadingTeams,
|
||||
refresh: refetchTeams
|
||||
} = useRequest2(() => getTeamList(TeamMemberStatusEnum.active), {
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?._id]
|
||||
});
|
||||
|
||||
// member action
|
||||
const {
|
||||
|
|
@ -79,71 +86,59 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
|
|||
}
|
||||
);
|
||||
|
||||
const onGetClbList = useCallback(() => {
|
||||
return refetchMembers().then((res) =>
|
||||
res.map<CollaboratorItemType>((member) => ({
|
||||
teamId: member.teamId,
|
||||
tmbId: member.tmbId,
|
||||
permission: member.permission,
|
||||
name: member.memberName,
|
||||
avatar: member.avatar
|
||||
}))
|
||||
);
|
||||
}, [refetchMembers]);
|
||||
const { runAsync: onUpdatePer, loading: isUpdatingPer } = useRequest2(
|
||||
(props: UpdateClbPermissionProps) => {
|
||||
return updateMemberPermission(props);
|
||||
}
|
||||
);
|
||||
|
||||
const { mutate: onSwitchTeam, isLoading: isSwitchingTeam } = useRequest({
|
||||
mutationFn: async (teamId: string) => {
|
||||
const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2(
|
||||
async (teamId: string) => {
|
||||
await putSwitchTeam(teamId);
|
||||
return initUserInfo();
|
||||
},
|
||||
errorToast: t('common:user.team.Switch Team Failed')
|
||||
{
|
||||
errorToast: t('common:user.team.Switch Team Failed')
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
data: groups = [],
|
||||
loading: isLoadingGroups,
|
||||
refresh: refetchGroups
|
||||
} = useRequest2(getGroupList, {
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?.team?.teamId]
|
||||
});
|
||||
|
||||
const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers || isUpdatingPer;
|
||||
const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers || isLoadingGroups;
|
||||
|
||||
const contextValue = {
|
||||
myTeams,
|
||||
refetchTeams,
|
||||
isLoading,
|
||||
onSwitchTeam,
|
||||
searchKey,
|
||||
setSearchKey,
|
||||
|
||||
// create | update team
|
||||
setEditTeamData,
|
||||
members,
|
||||
refetchMembers
|
||||
refetchMembers,
|
||||
groups,
|
||||
refetchGroups
|
||||
};
|
||||
|
||||
return (
|
||||
<TeamModalContext.Provider value={contextValue}>
|
||||
{userInfo?.team?.permission && (
|
||||
<CollaboratorContextProvider
|
||||
permission={userInfo?.team?.permission}
|
||||
permissionList={TeamPermissionList}
|
||||
onGetCollaboratorList={onGetClbList}
|
||||
onUpdateCollaborators={onUpdatePer}
|
||||
onDelOneCollaborator={delMemberPermission}
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
{children}
|
||||
{!!editTeamData && (
|
||||
<EditInfoModal
|
||||
defaultData={editTeamData}
|
||||
onClose={() => setEditTeamData(undefined)}
|
||||
onSuccess={() => {
|
||||
refetchTeams();
|
||||
initUserInfo();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<>
|
||||
{children}
|
||||
{!!editTeamData && (
|
||||
<EditInfoModal
|
||||
defaultData={editTeamData}
|
||||
onClose={() => setEditTeamData(undefined)}
|
||||
onSuccess={() => {
|
||||
refetchTeams();
|
||||
initUserInfo();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</CollaboratorContextProvider>
|
||||
</>
|
||||
)}
|
||||
</TeamModalContext.Provider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import React from 'react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useLoading } from '@fastgpt/web/hooks/useLoading';
|
||||
import { createContext, useContextSelector } from 'use-context-selector';
|
||||
import TeamList from './TeamList';
|
||||
import TeamCard from './TeamCard';
|
||||
|
|
@ -14,7 +12,6 @@ export const TeamContext = createContext<{}>({} as any);
|
|||
type Props = { onClose: () => void };
|
||||
|
||||
const TeamManageModal = ({ onClose }: Props) => {
|
||||
const { Loading } = useLoading();
|
||||
const { isLoading } = useContextSelector(TeamModalContext, (v) => v);
|
||||
|
||||
return (
|
||||
|
|
@ -26,15 +23,15 @@ const TeamManageModal = ({ onClose }: Props) => {
|
|||
w={'100%'}
|
||||
h={'550px'}
|
||||
isCentered
|
||||
bg={'myWhite.600'}
|
||||
bg={'myGray.50'}
|
||||
overflow={'hidden'}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<Box display={['block', 'flex']} flex={1} position={'relative'} overflow={'auto'}>
|
||||
<TeamList />
|
||||
<Box h={'100%'} flex={'1 0 0'}>
|
||||
<TeamCard />
|
||||
</Box>
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
</Box>
|
||||
</MyModal>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Box, Button, Flex, Image, useDisclosure } from '@chakra-ui/react';
|
||||
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
|
||||
import { FastGPTProUrl } from '@fastgpt/service/common/system/constants';
|
||||
import { POST } from '@fastgpt/service/common/api/plusRequest';
|
||||
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
|
||||
import { MongoMemberGroupModel } from '@fastgpt/service/support/permission/memberGroup/memberGroupSchema';
|
||||
import { delay } from '@fastgpt/global/common/system/utils';
|
||||
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
|
||||
|
||||
/*
|
||||
1. 给每个 team 创建一个默认的 group
|
||||
*/
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
await authCert({ req, authRoot: true });
|
||||
|
||||
const teamList = await MongoTeam.find({}, '_id');
|
||||
console.log('Total team', teamList.length);
|
||||
let success = 0;
|
||||
|
||||
async function createGroup(teamId: string) {
|
||||
try {
|
||||
await MongoMemberGroupModel.updateOne(
|
||||
{
|
||||
teamId,
|
||||
name: DefaultGroupName
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
teamId: teamId,
|
||||
name: DefaultGroupName
|
||||
}
|
||||
},
|
||||
{
|
||||
upsert: true
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await delay(500);
|
||||
return createGroup(teamId);
|
||||
}
|
||||
}
|
||||
for await (const team of teamList) {
|
||||
await createGroup(team._id);
|
||||
console.log(++success);
|
||||
}
|
||||
|
||||
jsonRes(res, {
|
||||
message: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,7 @@ import {
|
|||
Input,
|
||||
Textarea,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
useDisclosure
|
||||
ModalBody
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type.d';
|
||||
|
|
@ -35,10 +34,10 @@ import {
|
|||
} from '@fastgpt/global/support/permission/app/constant';
|
||||
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator';
|
||||
import { resumeInheritPer } from '@/web/core/app/api';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import ResumeInherit from '@/components/support/permission/ResumeInheritText';
|
||||
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
|
||||
|
||||
const InfoModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -61,7 +60,6 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
|
|||
defaultValues: appDetail
|
||||
});
|
||||
const avatar = getValues('avatar');
|
||||
const name = getValues('name');
|
||||
|
||||
// submit config
|
||||
const { runAsync: saveSubmitSuccess, loading: btnLoading } = useRequest2(
|
||||
|
|
@ -129,31 +127,33 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
|
|||
[setValue, t, toast]
|
||||
);
|
||||
|
||||
const onUpdateCollaborators = async ({ tmbIds, permission }: UpdateClbPermissionProps) => {
|
||||
await postUpdateAppCollaborators({
|
||||
tmbIds,
|
||||
const onUpdateCollaborators = ({
|
||||
members,
|
||||
permission
|
||||
}: {
|
||||
members: string[];
|
||||
permission: PermissionValueType;
|
||||
}) => {
|
||||
return postUpdateAppCollaborators({
|
||||
members,
|
||||
permission,
|
||||
appId: appDetail._id
|
||||
});
|
||||
};
|
||||
|
||||
const onDelCollaborator = async (tmbId: string) => {
|
||||
await deleteAppCollaborators({
|
||||
const onDelCollaborator = (tmbId: string) => {
|
||||
return deleteAppCollaborators({
|
||||
appId: appDetail._id,
|
||||
tmbId
|
||||
});
|
||||
};
|
||||
|
||||
const { runAsync: resumeInheritPermission } = useRequest2(
|
||||
() => resumeInheritPer(appDetail._id),
|
||||
// () => putAppById(appDetail._id, { inheritPermission: true }),
|
||||
{
|
||||
errorToast: t('common:resume_failed'),
|
||||
onSuccess: () => {
|
||||
reloadApp();
|
||||
}
|
||||
const { runAsync: resumeInheritPermission } = useRequest2(() => resumeInheritPer(appDetail._id), {
|
||||
errorToast: t('common:resume_failed'),
|
||||
onSuccess: () => {
|
||||
reloadApp();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
|
|
@ -223,7 +223,14 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
|
|||
permission={appDetail.permission}
|
||||
onGetCollaboratorList={() => getCollaboratorList(appDetail._id)}
|
||||
permissionList={AppPermissionList}
|
||||
onUpdateCollaborators={onUpdateCollaborators}
|
||||
onUpdateCollaborators={(props) => {
|
||||
if (props.members) {
|
||||
return onUpdateCollaborators({
|
||||
permission: props.permission,
|
||||
members: props.members
|
||||
});
|
||||
}
|
||||
}}
|
||||
onDelOneCollaborator={onDelCollaborator}
|
||||
refreshDeps={[appDetail.inheritPermission]}
|
||||
isInheritPermission={appDetail.inheritPermission}
|
||||
|
|
|
|||
|
|
@ -424,14 +424,14 @@ const ListItem = () => {
|
|||
onGetCollaboratorList: () => getCollaboratorList(editPerApp._id),
|
||||
permissionList: AppPermissionList,
|
||||
onUpdateCollaborators: ({
|
||||
tmbIds,
|
||||
members = [], // TODO: remove the default value after group is ready
|
||||
permission
|
||||
}: {
|
||||
tmbIds: string[];
|
||||
members?: string[];
|
||||
permission: number;
|
||||
}) => {
|
||||
return postUpdateAppCollaborators({
|
||||
tmbIds,
|
||||
members,
|
||||
permission,
|
||||
appId: editPerApp._id
|
||||
});
|
||||
|
|
|
|||
|
|
@ -285,14 +285,14 @@ const MyApps = () => {
|
|||
onGetCollaboratorList: () => getCollaboratorList(folderDetail._id),
|
||||
permissionList: AppPermissionList,
|
||||
onUpdateCollaborators: ({
|
||||
tmbIds,
|
||||
members = [], // TODO: remove the default value after group is ready
|
||||
permission
|
||||
}: {
|
||||
tmbIds: string[];
|
||||
members?: string[];
|
||||
permission: number;
|
||||
}) => {
|
||||
return postUpdateAppCollaborators({
|
||||
tmbIds,
|
||||
members,
|
||||
permission,
|
||||
appId: folderDetail._id
|
||||
});
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ const TagManageModal = ({ onClose }: { onClose: () => void }) => {
|
|||
isOpen
|
||||
onClose={onClose}
|
||||
iconSrc="core/dataset/tag"
|
||||
iconColor={'primary.600'}
|
||||
title={t('dataset:tag.manage')}
|
||||
w={'580px'}
|
||||
h={'600px'}
|
||||
|
|
|
|||
|
|
@ -441,14 +441,14 @@ function List() {
|
|||
onGetCollaboratorList: () => getCollaboratorList(editPerDataset._id),
|
||||
permissionList: DatasetPermissionList,
|
||||
onUpdateCollaborators: ({
|
||||
tmbIds,
|
||||
members = [], // TODO: remove default value after group is ready
|
||||
permission
|
||||
}: {
|
||||
tmbIds: string[];
|
||||
members?: string[];
|
||||
permission: number;
|
||||
}) => {
|
||||
return postUpdateDatasetCollaborators({
|
||||
tmbIds,
|
||||
members,
|
||||
permission,
|
||||
datasetId: editPerDataset._id
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +1,5 @@
|
|||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Image,
|
||||
Button,
|
||||
useDisclosure,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
Input
|
||||
} from '@chakra-ui/react';
|
||||
import { Box, Flex, Button, InputGroup, InputLeftElement, Input } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { serviceSideProps } from '@/web/common/utils/i18n';
|
||||
|
|
@ -249,14 +240,14 @@ const Dataset = () => {
|
|||
onGetCollaboratorList: () => getCollaboratorList(folderDetail._id),
|
||||
permissionList: DatasetPermissionList,
|
||||
onUpdateCollaborators: ({
|
||||
tmbIds,
|
||||
members = [], // TODO: remove the default value after group is ready
|
||||
permission
|
||||
}: {
|
||||
tmbIds: string[];
|
||||
members?: string[];
|
||||
permission: number;
|
||||
}) => {
|
||||
return postUpdateDatasetCollaborators({
|
||||
tmbIds,
|
||||
members,
|
||||
permission,
|
||||
datasetId: folderDetail._id
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { GET, POST, PUT, DELETE } from '@/web/common/api/request';
|
||||
import { UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator';
|
||||
import { UpdatePermissionBody } from '@fastgpt/global/support/permission/collaborator';
|
||||
import {
|
||||
CreateTeamProps,
|
||||
InviteMemberProps,
|
||||
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from '@fastgpt/global/support/user/team/type.d';
|
||||
import { FeTeamPlanStatusType, TeamSubSchema } from '@fastgpt/global/support/wallet/sub/type';
|
||||
import { TeamInvoiceHeaderType } from '@fastgpt/global/support/user/team/type';
|
||||
import { ResourcePermissionType } from '@fastgpt/global/support/permission/type';
|
||||
|
||||
/* --------------- team ---------------- */
|
||||
export const getTeamList = (status: `${TeamMemberSchema['status']}`) =>
|
||||
|
|
@ -39,11 +40,12 @@ export const updateInviteResult = (data: UpdateInviteProps) =>
|
|||
export const delLeaveTeam = (teamId: string) =>
|
||||
DELETE('/proApi/support/user/team/member/leave', { teamId });
|
||||
|
||||
export const getTeamClbs = () =>
|
||||
GET<ResourcePermissionType[]>(`/proApi/support/user/team/collaborator/list`);
|
||||
|
||||
/* -------------- team collaborator -------------------- */
|
||||
export const updateMemberPermission = (data: UpdateClbPermissionProps) =>
|
||||
PUT('/proApi/support/user/team/collaborator/update', data);
|
||||
export const delMemberPermission = (tmbId: string) =>
|
||||
DELETE('/proApi/support/user/team/collaborator/delete', { tmbId });
|
||||
export const updateMemberPermission = (data: UpdatePermissionBody) =>
|
||||
PUT('/proApi/support/user/team/collaborator/updatePermission', data);
|
||||
|
||||
/* --------------- team tags ---------------- */
|
||||
export const getTeamsTags = () => GET<TeamTagSchema[]>(`/proApi/support/user/team/tag/list`);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import { DELETE, GET, POST, PUT } from '@/web/common/api/request';
|
||||
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
import {
|
||||
postCreateGroupData,
|
||||
putUpdateGroupData
|
||||
} from '@fastgpt/global/support/user/team/group/api';
|
||||
|
||||
export const getGroupList = () => GET<MemberGroupListType>('/proApi/support/user/team/group/list');
|
||||
|
||||
export const postCreateGroup = (data: postCreateGroupData) =>
|
||||
POST('/proApi/support/user/team/group/create', data);
|
||||
|
||||
export const deleteGroup = (groupId: string) =>
|
||||
DELETE('/proApi/support/user/team/group/delete', { groupId });
|
||||
|
||||
export const putUpdateGroup = (data: putUpdateGroupData) =>
|
||||
PUT('/proApi/support/user/team/group/update', data);
|
||||
|
|
@ -9,6 +9,8 @@ import { getTeamPlanStatus } from './team/api';
|
|||
import { getTeamMembers } from '@/web/support/user/team/api';
|
||||
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
import { getGroupList } from './team/group/api';
|
||||
|
||||
type State = {
|
||||
systemMsgReadId: string;
|
||||
|
|
@ -24,6 +26,9 @@ type State = {
|
|||
|
||||
teamMembers: TeamMemberItemType[];
|
||||
loadAndGetTeamMembers: (init?: boolean) => Promise<TeamMemberItemType[]>;
|
||||
|
||||
teamMemberGroups: MemberGroupListType;
|
||||
loadAndGetGroups: (init?: boolean) => Promise<MemberGroupListType>;
|
||||
};
|
||||
|
||||
export const useUserStore = create<State>()(
|
||||
|
|
@ -98,6 +103,21 @@ export const useUserStore = create<State>()(
|
|||
state.teamMembers = res;
|
||||
});
|
||||
|
||||
return res;
|
||||
},
|
||||
teamMemberGroups: [],
|
||||
loadAndGetGroups: async (init = false) => {
|
||||
if (!useSystemStore.getState()?.feConfigs?.isPlus) return [];
|
||||
|
||||
const randomRefresh = Math.random() > 0.7;
|
||||
if (!randomRefresh && !init && get().teamMemberGroups.length)
|
||||
return Promise.resolve(get().teamMemberGroups);
|
||||
|
||||
const res = await getGroupList();
|
||||
set((state) => {
|
||||
state.teamMemberGroups = res;
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
})),
|
||||
|
|
|
|||