feat: org CRUD (#3380)

* feat: add org schema

* feat: org manage UI

* feat: OrgInfoModal

* feat: org tree view

* feat: org management

* fix: init root org

* feat: org permission for app

* feat: org support for dataset

* fix: disable org role control

* styles: opt type signatures

* fix: remove unused permission

* feat: delete org collaborator
This commit is contained in:
a.e.
2024-12-30 13:49:56 +08:00
committed by archer
parent bb669ca3ff
commit 1fc77a126a
46 changed files with 1934 additions and 191 deletions

View File

@@ -1,5 +1,5 @@
import { ErrType } from '../errorCode';
import { i18nT } from '../../../../web/i18n/utils';
import type { ErrType } from '../errorCode';
/* team: 500000 */
export enum TeamErrEnum {
teamOverSize = 'teamOverSize',
@@ -14,6 +14,13 @@ export enum TeamErrEnum {
groupNameEmpty = 'groupNameEmpty',
groupNameDuplicate = 'groupNameDuplicate',
groupNotExist = 'groupNotExist',
orgMemberNotExist = 'orgMemberNotExist',
orgMemberDuplicated = 'orgMemberDuplicated',
orgNotExist = 'orgNotExist',
orgParentNotExist = 'orgParentNotExist',
cannotMoveToSubPath = 'cannotMoveToSubPath',
cannotModifyRootOrg = 'cannotModifyRootOrg',
cannotDeleteNonEmptyOrg = 'cannotDeleteNonEmptyOrg',
cannotDeleteDefaultGroup = 'cannotDeleteDefaultGroup',
userNotActive = 'userNotActive'
}
@@ -71,6 +78,34 @@ const teamErr = [
{
statusText: TeamErrEnum.userNotActive,
message: i18nT('common:code_error.team_error.user_not_active')
},
{
statusText: TeamErrEnum.orgMemberNotExist,
message: i18nT('common:code_error.team_error.org_member_not_exist')
},
{
statusText: TeamErrEnum.orgMemberDuplicated,
message: i18nT('common:code_error.team_error.org_member_duplicated')
},
{
statusText: TeamErrEnum.orgNotExist,
message: i18nT('common:code_error.team_error.org_not_exist')
},
{
statusText: TeamErrEnum.orgParentNotExist,
message: i18nT('common:code_error.team_error.org_parent_not_exist')
},
{
statusText: TeamErrEnum.cannotMoveToSubPath,
message: i18nT('common:code_error.team_error.cannot_move_to_sub_path')
},
{
statusText: TeamErrEnum.cannotModifyRootOrg,
message: i18nT('common:code_error.team_error.cannot_modify_root_org')
},
{
statusText: TeamErrEnum.cannotDeleteNonEmptyOrg,
message: i18nT('common:code_error.team_error.cannot_delete_non_empty_org')
}
];

View File

@@ -8,6 +8,7 @@ export enum MongoImageTypeEnum {
userAvatar = 'userAvatar',
teamAvatar = 'teamAvatar',
groupAvatar = 'groupAvatar',
orgAvatar = 'orgAvatar',
chatImage = 'chatImage',
collectionImage = 'collectionImage'
@@ -41,6 +42,10 @@ export const mongoImageTypeMap = {
label: 'groupAvatar',
unique: true
},
[MongoImageTypeEnum.orgAvatar]: {
label: 'orgAvatar',
unique: true
},
[MongoImageTypeEnum.chatImage]: {
label: 'chatImage',

View File

@@ -2,5 +2,6 @@ export const HUMAN_ICON = `/icon/human.svg`;
export const LOGO_ICON = `/icon/logo.svg`;
export const HUGGING_FACE_ICON = `/imgs/model/huggingface.svg`;
export const DEFAULT_TEAM_AVATAR = `/imgs/avatar/defaultTeamAvatar.svg`;
export const DEFAULT_ORG_AVATAR = '/imgs/avatar/defaultOrgAvatar.svg';
export const isProduction = process.env.NODE_ENV === 'production';

View File

@@ -1,6 +1,6 @@
import { RequireOnlyOne } from '../../common/type/utils';
import type { RequireOnlyOne } from '../../common/type/utils';
import {
UpdateClbPermissionProps,
type UpdateClbPermissionProps,
UpdatePermissionBody
} from '../../support/permission/collaborator';
import { PermissionValueType } from '../../support/permission/type';
@@ -14,4 +14,5 @@ export type AppCollaboratorDeleteParams = {
} & RequireOnlyOne<{
tmbId: string;
groupId: string;
orgId: string;
}>;

View File

@@ -11,4 +11,5 @@ export type DatasetCollaboratorDeleteParams = {
} & RequireOnlyOne<{
tmbId: string;
groupId: string;
orgId: string;
}>;

View File

@@ -10,17 +10,20 @@ export type CollaboratorItemType = {
} & RequireOnlyOne<{
tmbId: string;
groupId: string;
orgId: string;
}>;
export type UpdateClbPermissionProps = {
members?: string[];
groups?: string[];
orgs?: string[];
permission: PermissionValueType;
};
export type DeleteClbPermissionProps = RequireOnlyOne<{
tmbId: string;
groupId: string;
orgId: string;
}>;
export type UpdatePermissionBody = {
@@ -28,4 +31,5 @@ export type UpdatePermissionBody = {
} & RequireOnlyOne<{
memberId: string;
groupId: string;
orgId: string;
}>;

View File

@@ -1,8 +1,9 @@
import { UserModelSchema } from '../user/type';
import { RequireOnlyOne } from '../../common/type/utils';
import { TeamMemberSchema } from '../user/team/type';
import { AuthUserTypeEnum, PermissionKeyEnum, PerResourceTypeEnum } from './constant';
import { MemberGroupSchemaType } from './memberGroup/type';
import type { TeamMemberWithUserSchema } from '../user/team/type';
import { AuthUserTypeEnum, type PermissionKeyEnum, type PerResourceTypeEnum } from './constant';
// PermissionValueType, the type of permission's value is a number, which is a bit field actually.
// It is spired by the permission system in Linux.
@@ -29,6 +30,7 @@ export type ResourcePermissionType = {
} & RequireOnlyOne<{
tmbId: string;
groupId: string;
orgId: string;
}>;
export type ResourcePerWithTmbWithUser = Omit<ResourcePermissionType, 'tmbId'> & {

View File

@@ -0,0 +1,38 @@
export type postCreateOrgData = {
name: string;
parentId: string;
description?: string;
avatar?: string;
};
export type putUpdateOrgMembersData = {
orgId: string;
members: {
tmbId: string;
// role: `${OrgMemberRole}`;
}[];
};
export type putUpdateOrgData = {
orgId: string;
name?: string;
avatar?: string;
description?: string;
};
export type putMoveOrgData = {
orgId: string;
parentId: string;
};
export type putMoveOrgMemberData = {
orgId: string;
tmbId: string;
newOrgId: string;
};
// type putChnageOrgOwnerData = {
// orgId: string;
// tmbId: string;
// toAdmin?: boolean;
// };

View File

@@ -0,0 +1,8 @@
export const OrgCollectionName = 'team_orgs';
export const OrgMemberCollectionName = 'team_org_members';
// export enum OrgMemberRole {
// owner = 'owner',
// admin = 'admin',
// member = 'member'
// }

View File

@@ -0,0 +1,23 @@
import type { TeamPermission } from 'support/permission/user/controller';
import { ResourcePermissionType } from '../type';
type OrgSchemaType = {
_id: string;
teamId: string;
path: string;
name: string;
avatar?: string;
description?: string;
updateTime: Date;
};
type OrgMemberSchemaType = {
teamId: string;
orgId: string;
tmbId: string;
};
type OrgType = Omit<OrgSchemaType, 'avatar'> & {
avatar: string;
members: OrgMemberSchemaType[];
};

View File

@@ -0,0 +1,58 @@
import { TeamPermission } from '@fastgpt/global/support/permission/user/controller';
import { AuthModeType, AuthResponseType } from '../type';
import { parseHeaderCert } from '../controller';
import { getTmbInfoByTmbId } from '../../user/team/controller';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
export const authOrgMember = async ({
orgIds,
req,
authToken = false,
authRoot = false,
authApiKey = false
}: {
orgIds: string | string[];
} & AuthModeType): Promise<AuthResponseType> => {
const result = await parseHeaderCert({ req, authToken, authApiKey, authRoot });
const { teamId, tmbId, isRoot } = result;
if (isRoot) {
return {
teamId,
tmbId,
userId: result.userId,
appId: result.appId,
apikey: result.apikey,
isRoot,
authType: result.authType,
permission: new TeamPermission({ isOwner: true })
};
}
if (!Array.isArray(orgIds)) {
orgIds = [orgIds];
}
// const promises = orgIds.map((orgId) => getOrgMemberRole({ orgId, tmbId }));
const tmb = await getTmbInfoByTmbId({ tmbId });
if (tmb.permission.hasManagePer) {
return {
...result,
permission: tmb.permission
};
}
return Promise.reject(TeamErrEnum.unAuthTeam);
// const targetRole = OrgMemberRole[role];
// for (const orgRole of orgRoles) {
// if (!orgRole || checkOrgRole(orgRole, targetRole)) {
// return Promise.reject(TeamErrEnum.unAuthTeam);
// }
// }
// return {
// ...result,
// permission: tmb.permission
// };
};

View File

@@ -21,6 +21,7 @@ import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type';
import { TeamMemberSchema } from '@fastgpt/global/support/user/team/type';
import { UserModelSchema } from '@fastgpt/global/support/user/type';
import { OrgSchemaType } from '@fastgpt/global/support/user/team/org/type';
/** get resource permission for a team member
* If there is no permission for the team member, it will return undefined
@@ -186,6 +187,16 @@ export const getClbsAndGroupsWithInfo = async ({
}
})
.populate<{ group: MemberGroupSchemaType }>('group', 'name avatar')
.lean(),
MongoResourcePermission.find({
teamId,
resourceId,
resourceType,
orgId: {
$exists: true
}
})
.populate<{ org: OrgSchemaType }>({ path: 'org', select: 'name avatar' })
.lean()
]);
@@ -196,6 +207,7 @@ export const delResourcePermission = ({
session,
tmbId,
groupId,
orgId,
...props
}: {
resourceType: PerResourceTypeEnum;
@@ -204,15 +216,18 @@ export const delResourcePermission = ({
session?: ClientSession;
tmbId?: string;
groupId?: string;
orgId?: string;
}) => {
// tmbId or groupId only one and not both
if (!!tmbId === !!groupId) {
// either tmbId or groupId or orgId must be provided
if (!tmbId && !groupId && !orgId) {
return Promise.reject(CommonErrEnum.missingParams);
}
return MongoResourcePermission.deleteOne(
{
...(tmbId ? { tmbId } : {}),
...(groupId ? { groupId } : {}),
...(orgId ? { orgId } : {}),
...props
},
{ session }
@@ -250,7 +265,7 @@ export function authJWT(token: string) {
}>((resolve, reject) => {
const key = process.env.TOKEN_KEY as string;
jwt.verify(token, key, function (err, decoded: any) {
jwt.verify(token, key, (err, decoded: any) => {
if (err || !decoded?.userId) {
reject(ERROR_ENUM.unAuthorization);
return;
@@ -436,7 +451,7 @@ export const authFileToken = (token?: string) =>
}
const key = (process.env.FILE_TOKEN_KEY as string) ?? 'filetoken';
jwt.verify(token, key, function (err, decoded: any) {
jwt.verify(token, key, (err, decoded: any) => {
if (err || !decoded.bucketName || !decoded?.teamId || !decoded?.fileId) {
reject(ERROR_ENUM.unAuthFile);
return;

View File

@@ -1,11 +1,11 @@
import { mongoSessionRun } from '../../common/mongo/sessionRun';
import { MongoResourcePermission } from './schema';
import { ClientSession, Model } from 'mongoose';
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import type { ClientSession, Model } from 'mongoose';
import type { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import type { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { getResourceClbsAndGroups } from './controller';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
export type SyncChildrenPermissionResourceType = {
_id: string;
@@ -18,6 +18,7 @@ export type UpdateCollaboratorItem = {
} & RequireOnlyOne<{
tmbId: string;
groupId: string;
orgId: string;
}>;
// sync the permission to all children folders.
@@ -161,7 +162,7 @@ export async function resumeInheritPermission({
}
}
/*
/*
Delete all the collaborators and then insert the new collaborators.
*/
export async function syncCollaborators({

View File

@@ -0,0 +1,174 @@
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import type {
OrgMemberSchemaType,
OrgSchemaType
} from '@fastgpt/global/support/user/team/org/type';
import type { ClientSession } from 'mongoose';
import { MongoOrgModel } from './orgSchema';
import { MongoOrgMemberModel } from './orgMemberSchema';
// if role1 > role2, return 1
// if role1 < role2, return -1
// else return 0
// export const compareRole = (role1: OrgMemberRole, role2: OrgMemberRole) => {
// if (role1 === OrgMemberRole.owner) {
// if (role2 === OrgMemberRole.owner) {
// return 0;
// }
// return 1;
// }
// if (role2 === OrgMemberRole.owner) {
// return -1;
// }
// if (role1 === OrgMemberRole.admin) {
// if (role2 === OrgMemberRole.admin) {
// return 0;
// }
// return 1;
// }
// if (role2 === OrgMemberRole.admin) {
// return -1;
// }
// return 0;
// };
// export const checkOrgRole = (role: OrgMemberRole, targetRole: OrgMemberRole) => {
// return compareRole(role, targetRole) >= 0;
// };
export const getOrgsByTeamId = async (teamId: string) => {
const orgs = await MongoOrgModel.find({
teamId
})
.populate<{ members: OrgMemberSchemaType }>('members')
.lean();
return orgs;
};
export const getOrgsByTmbId = async ({ teamId, tmbId }: { teamId: string; tmbId: string }) =>
MongoOrgMemberModel.find({ teamId, tmbId }, 'orgId').lean();
export const getChildrenByOrg = async ({
org,
teamId,
session
}: {
org: OrgSchemaType;
teamId: string;
session?: ClientSession;
}) => {
const children = await MongoOrgModel.find(
{ teamId, path: { $regex: `^${org.path}/${org._id}` } },
undefined,
{
session
}
).lean();
return children;
};
export const getOrgAndChildren = async ({
orgId,
teamId,
session
}: {
orgId: string;
teamId: string;
session?: ClientSession;
}) => {
const org = await MongoOrgModel.findOne({ _id: orgId, teamId }, undefined, { session }).lean();
if (!org) {
return Promise.reject(TeamErrEnum.orgNotExist);
}
const children = await getChildrenByOrg({ org, teamId, session });
return { org, children };
};
export async function createRootOrg({
teamId,
session
}: {
teamId: string;
session?: ClientSession;
}) {
// Create the root org
const [org] = await MongoOrgModel.create(
[
{
teamId,
name: 'ROOT',
path: ''
}
],
{ session }
);
// Find the team's owner
// const owner = await MongoTeamMember.findOne({ teamId, role: 'owner' }, undefined);
// if (!owner) {
// return Promise.reject(TeamErrEnum.unAuthTeam);
// }
// Set the owner as the org admin
// await MongoOrgMemberModel.create(
// [
// {
// orgId: org._id,
// tmbId: owner._id
// }
// ],
// { session }
// );
}
// export const getOrgMemberRole = async ({
// orgId,
// tmbId
// }: {
// orgId: string;
// tmbId: string;
// }): Promise<OrgMemberRole | undefined> => {
// let role: OrgMemberRole | undefined;
// const orgMember = await MongoOrgMemberModel.findOne({
// orgId,
// tmbId
// })
// .populate('orgId')
// .lean();
// if (orgMember) {
// role = OrgMemberRole[orgMember.role];
// } else {
// return role;
// }
// if (role === OrgMemberRole.owner) {
// return role;
// }
// // Check the parent orgs
// const org = orgMember.orgId as unknown as OrgSchemaType;
// if (!org) {
// return Promise.reject(TeamErrEnum.orgNotExist);
// }
// const parentIds = org.path.split('/').filter((id) => id);
// if (parentIds.length === 0) {
// return role;
// }
// const parentOrgMembers = await MongoOrgMemberModel.find({
// orgId: {
// $in: parentIds
// },
// tmbId
// }).lean();
// // Update the role to the highest role
// for (const parentOrgMember of parentOrgMembers) {
// const parentRole = OrgMemberRole[parentOrgMember.role];
// if (parentRole === OrgMemberRole.owner) {
// role = parentRole;
// break;
// }
// if (parentRole === OrgMemberRole.admin && role === OrgMemberRole.member) {
// role = parentRole;
// }
// }
// return role;
// };

View File

@@ -0,0 +1,58 @@
import { OrgCollectionName } from '@fastgpt/global/support/user/team/org/constant';
import { connectionMongo, getMongoModel } from '../../../common/mongo';
import {
TeamCollectionName,
TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant';
import { OrgMemberSchemaType } from '@fastgpt/global/support/user/team/org/type';
const { Schema } = connectionMongo;
export const OrgMemberCollectionName = 'team_org_members';
export const OrgMemberSchema = new Schema({
teamId: {
type: Schema.Types.ObjectId,
ref: TeamCollectionName,
required: true
},
orgId: {
type: Schema.Types.ObjectId,
ref: OrgCollectionName,
required: true
},
tmbId: {
type: Schema.Types.ObjectId,
ref: TeamMemberCollectionName,
required: true
}
// role: {
// type: String,
// enum: Object.values(OrgMemberRole),
// required: true,
// default: OrgMemberRole.member
// }
});
try {
OrgMemberSchema.index(
{
teamId: 1,
orgId: 1,
tmbId: 1
},
{
unique: true
}
);
OrgMemberSchema.index({
teamId: 1,
tmbId: 1
});
} catch (error) {
console.log(error);
}
export const MongoOrgMemberModel = getMongoModel<OrgMemberSchemaType>(
OrgMemberCollectionName,
OrgMemberSchema
);

View File

@@ -0,0 +1,69 @@
import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant';
import { OrgCollectionName } from '@fastgpt/global/support/user/team/org/constant';
import type { OrgSchemaType } from '@fastgpt/global/support/user/team/org/type';
import { connectionMongo, getMongoModel } from '../../../common/mongo';
import { ResourcePermissionCollectionName } from '../schema';
import { OrgMemberCollectionName } from './orgMemberSchema';
const { Schema } = connectionMongo;
function requiredStringPath(this: OrgSchemaType) {
return typeof this.path !== 'string';
}
export const OrgSchema = new Schema(
{
teamId: {
type: Schema.Types.ObjectId,
ref: TeamCollectionName,
required: true
},
path: {
type: String,
required: requiredStringPath // allow empty string, but not null
},
name: {
type: String,
required: true
},
avatar: String,
description: String,
updateTime: {
type: Date,
default: () => new Date()
}
},
{
// Auto update updateTime
timestamps: {
updatedAt: 'updateTime'
}
}
);
OrgSchema.virtual('members', {
ref: OrgMemberCollectionName,
localField: '_id',
foreignField: 'orgId'
});
OrgSchema.virtual('permission', {
ref: ResourcePermissionCollectionName,
localField: '_id',
foreignField: 'orgId',
justOne: true
});
try {
OrgSchema.index(
{
teamId: 1,
path: 1
},
{
unique: true
}
);
} catch (error) {
console.log(error);
}
export const MongoOrgModel = getMongoModel<OrgSchemaType>(OrgCollectionName, OrgSchema);

View File

@@ -6,6 +6,7 @@ import { connectionMongo, getMongoModel } from '../../common/mongo';
import type { ResourcePermissionType } from '@fastgpt/global/support/permission/type';
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { MemberGroupCollectionName } from './memberGroup/memberGroupSchema';
import { OrgCollectionName } from '@fastgpt/global/support/user/team/org/constant';
const { Schema } = connectionMongo;
export const ResourcePermissionCollectionName = 'resource_permissions';
@@ -23,6 +24,10 @@ export const ResourcePermissionSchema = new Schema({
type: Schema.Types.ObjectId,
ref: MemberGroupCollectionName
},
orgId: {
type: Schema.Types.ObjectId,
ref: OrgCollectionName
},
resourceType: {
type: String,
enum: Object.values(PerResourceTypeEnum),
@@ -51,6 +56,12 @@ ResourcePermissionSchema.virtual('group', {
foreignField: '_id',
justOne: true
});
ResourcePermissionSchema.virtual('org', {
ref: OrgCollectionName,
localField: 'orgId',
foreignField: '_id',
justOne: true
});
try {
ResourcePermissionSchema.index(
@@ -70,6 +81,23 @@ try {
}
);
ResourcePermissionSchema.index(
{
resourceType: 1,
teamId: 1,
resourceId: 1,
orgId: 1
},
{
unique: true,
partialFilterExpression: {
orgId: {
$exists: true
}
}
}
);
ResourcePermissionSchema.index(
{
resourceType: 1,

View File

@@ -16,6 +16,7 @@ import { MongoMemberGroupModel } from '../../permission/memberGroup/memberGroupS
import { mongoSessionRun } from '../../../common/mongo/sessionRun';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { getAIApi, openaiBaseUrl } from '../../../core/ai/config';
import { createRootOrg } from '../../permission/org/controllers';
async function getTeamMember(match: Record<string, any>): Promise<TeamTmbItemType> {
const tmb = await MongoTeamMember.findOne(match).populate<{ team: TeamSchema }>('team').lean();
@@ -132,7 +133,8 @@ export async function createDefaultTeam({
],
{ session }
);
console.log('create default team and group', userId);
await createRootOrg({ teamId: tmb.teamId, session });
console.log('create default team, group and root org', userId);
return tmb;
} else {
console.log('default team exist', userId);

View File

@@ -73,6 +73,7 @@ export const iconPaths = {
'common/resultLight': () => import('./icons/common/resultLight.svg'),
'common/retryLight': () => import('./icons/common/retryLight.svg'),
'common/rightArrowFill': () => import('./icons/common/rightArrowFill.svg'),
'common/downArrowFill': () => import('./icons/common/downArrowFill.svg'),
'common/rightArrowLight': () => import('./icons/common/rightArrowLight.svg'),
'common/routePushLight': () => import('./icons/common/routePushLight.svg'),
'common/saveFill': () => import('./icons/common/saveFill.svg'),

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon/solid/chevron-down">
<path id="Rectangle 3101" d="M11.4695 5.33325C12.6574 5.33325 13.2523 6.76944 12.4123 7.60939L9.01223 11.0095C8.49154 11.5302 7.64732 11.5302 7.12662 11.0095L3.72653 7.60939C2.88657 6.76944 3.48146 5.33325 4.66933 5.33325H11.4695Z" fill="#667085"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 390 B

View File

@@ -2,11 +2,23 @@
"action": "operate",
"confirm_delete_group": "Confirm to delete group?",
"confirm_leave_team": "Confirmed to leave the team? \n \nAfter you log out, all your resources in the team (applications, knowledge bases, folders, managed groups, etc.) will be transferred to the team owner.",
"confirm_delete_org": "Confirm to delete organization?",
"confirm_delete_member": "Confirm to delete member?",
"create_group": "Create group",
"delete": "delete",
"edit_info": "Edit information",
"group": "group",
"group_name": "Group name",
"org": "organization",
"org_name": "Organization name",
"org_description": "Organization description",
"create_org": "Create organization",
"create_sub_org": "Create sub-organization",
"edit_org_info": "Edit organization information",
"move_org": "Move organization",
"move_member": "Move member",
"delete_org": "Delete organization",
"remark": "remark",
"label_sync": "Tag sync",
"leave_team_failed": "Leaving the team exception",
"manage_member": "Managing members",

View File

@@ -85,6 +85,13 @@
"code_error.team_error.un_auth": "Unauthorized to Operate This Team",
"code_error.team_error.user_not_active": "The user did not accept or has left the team",
"code_error.team_error.website_sync_not_enough": "Unauthorized to Use Website Sync",
"code_error.team_error.org_member_not_exist": "Organization member does not exist",
"code_error.team_error.org_member_duplicated": "Duplicate organization member",
"code_error.team_error.org_not_exist": "Organization does not exist",
"code_error.team_error.org_parent_not_exist": "Parent organization does not exist",
"code_error.team_error.cannot_move_to_sub_path": "Cannot move to same or subdirectory",
"code_error.team_error.cannot_modify_root_org": "Cannot modify root organization",
"code_error.team_error.cannot_delete_non_empty_org": "Cannot delete non-empty organization",
"code_error.token_error_code.403": "Invalid Login Status, Please Re-login",
"code_error.user_error.balance_not_enough": "Insufficient Account Balance",
"code_error.user_error.bin_visitor": "Identity Verification Failed",

View File

@@ -1,29 +1,41 @@
{
"total_team_members": "共 {{amount}} 名成员",
"member": "成员",
"group": "群组",
"permission": "权限",
"user_name": "用户名",
"member_group": "所属成员组",
"action": "操作",
"waiting": "待接受",
"remove_tip": "确认将 {{username}} 移出团队?",
"confirm_leave_team": "确认离开该团队? \n 退出后,您在该团队所有的资源( 应用、知识库、文件夹、管理的群组等)均转让给团队所有者。",
"leave_team_failed": "离开团队异常",
"label_sync": "标签同步",
"user_team_invite_member": "邀请成员",
"user_team_leave_team": "离开团队",
"user_team_leave_team_failed": "离开团队失败",
"create_group": "创建群组",
"search_member_group_name": "搜索成员/群组名称",
"confirm_delete_group": "确认删除群组?",
"group_name": "群组名称",
"owner": "所有者",
"manage_member": "管理成员",
"edit_info": "编辑信息",
"transfer_ownership": "转让所有者",
"delete": "删除",
"retain_admin_permissions": "保留管理员权限"
}
{
"total_team_members": "共 {{amount}} 名成员",
"member": "成员",
"group": "群组",
"org": "组织",
"org_name": "组织名称",
"org_description": "介绍",
"permission": "权限",
"user_name": "用户名",
"member_group": "所属成员组",
"action": "操作",
"remark": "备注",
"waiting": "待接受",
"remove_tip": "确认将 {{username}} 移出团队?",
"confirm_leave_team": "确认离开团队 \n 退出后,您在该团队所有的资源( 应用、知识库、文件夹、管理的群组等)均转让给团队所有者。",
"leave_team_failed": "离开团队异常",
"label_sync": "标签同步",
"user_team_invite_member": "邀请成员",
"user_team_leave_team": "离开团队",
"user_team_leave_team_failed": "离开团队失败",
"create_group": "创建群组",
"search_member_group_name": "搜索成员/群组名称",
"confirm_delete_group": "确认删除群组?",
"group_name": "群组名称",
"owner": "所有者",
"manage_member": "管理成员",
"edit_info": "编辑信息",
"create_org": "创建组织",
"create_sub_org": "创建子组织",
"edit_org_info": "编辑组织信息",
"move_org": "移动组织",
"move_member": "移动成员",
"delete_org": "删除组织",
"confirm_delete_org": "确认删除组织?",
"confirm_delete_member": "确认删除成员?",
"transfer_ownership": "转让所有者",
"delete": "删除",
"retain_admin_permissions": "保留管理员权限"
}

View File

@@ -89,6 +89,13 @@
"code_error.team_error.un_auth": "无权操作该团队",
"code_error.team_error.user_not_active": "用户未接受或已离开团队",
"code_error.team_error.website_sync_not_enough": "无权使用Web站点同步~",
"code_error.team_error.org_member_not_exist": "组织成员不存在",
"code_error.team_error.org_member_duplicated": "重复的组织成员",
"code_error.team_error.org_not_exist": "组织不存在",
"code_error.team_error.org_parent_not_exist": "父组织不存在",
"code_error.team_error.cannot_move_to_sub_path": "不能移动到相同或子目录",
"code_error.team_error.cannot_modify_root_org": "不能修改根组织",
"code_error.team_error.cannot_delete_non_empty_org": "不能删除非空组织",
"code_error.token_error_code.403": "登录状态无效,请重新登录",
"code_error.user_error.balance_not_enough": "账号余额不足~",
"code_error.user_error.bin_visitor": "您的身份校验未通过",

View File

@@ -2,11 +2,23 @@
"action": "操作",
"confirm_delete_group": "確認刪除群組?",
"confirm_leave_team": "確認離開該團隊? \n \n退出後您在該團隊所有的資源 應用程式、知識庫、資料夾、管理的群組等)均轉讓給團隊所有者。",
"confirm_delete_org": "確認刪除組織?",
"confirm_delete_member": "確認刪除成員?",
"create_group": "建立群組",
"delete": "刪除",
"edit_info": "編輯訊息",
"group": "群組",
"group_name": "群組名稱",
"org": "組織",
"org_name": "組織名稱",
"org_description": "介紹",
"create_org": "建立組織",
"create_sub_org": "建立子組織",
"edit_org_info": "編輯組織訊息",
"move_org": "移動組織",
"move_member": "移動成員",
"delete_org": "刪除組織",
"remark": "備註",
"label_sync": "標籤同步",
"leave_team_failed": "離開團隊異常",
"manage_member": "管理成員",

View File

@@ -85,6 +85,13 @@
"code_error.team_error.un_auth": "無權操作此團隊",
"code_error.team_error.user_not_active": "使用者未接受或已離開團隊",
"code_error.team_error.website_sync_not_enough": "無權使用網站同步",
"code_error.team_error.org_member_not_exist": "組織成員不存在",
"code_error.team_error.org_member_duplicated": "重複的組織成員",
"code_error.team_error.org_not_exist": "組織不存在",
"code_error.team_error.org_parent_not_exist": "父組織不存在",
"code_error.team_error.cannot_move_to_sub_path": "無法移動到相同或子目錄",
"code_error.team_error.cannot_modify_root_org": "無法修改根組織",
"code_error.team_error.cannot_delete_non_empty_org": "無法刪除非空組織",
"code_error.token_error_code.403": "登入狀態無效,請重新登入",
"code_error.user_error.balance_not_enough": "帳戶餘額不足",
"code_error.user_error.bin_visitor": "身份驗證未通過",

View File

@@ -0,0 +1,10 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="36" height="36" fill="url(#paint0_linear_13996_748)"/>
<path d="M14.904 10.8976C14.904 10.2323 14.904 9.89961 15.0335 9.64549C15.1474 9.42195 15.3291 9.24021 15.5527 9.12631C15.8068 8.99683 16.1395 8.99683 16.8048 8.99683H19.1952C19.8605 8.99683 20.1932 8.99683 20.4473 9.12631C20.6709 9.24021 20.8526 9.42195 20.9665 9.64549C21.096 9.89961 21.096 10.2323 21.096 10.8976V12.598C21.096 13.2633 21.096 13.596 20.9665 13.8501C20.8526 14.0737 20.6709 14.2554 20.4473 14.3693C20.1932 14.4988 19.8605 14.4988 19.1952 14.4988H18.8506V17.1972H23.215C24.1296 17.1972 24.871 17.9386 24.871 18.8532V21.4949H25.0992C25.7645 21.4949 26.0972 21.4949 26.3513 21.6244C26.5749 21.7383 26.7566 21.92 26.8705 22.1435C27 22.3977 27 22.7303 27 23.3957V25.096C27 25.7614 27 26.0941 26.8705 26.3482C26.7566 26.5717 26.5749 26.7535 26.3513 26.8674C26.0972 26.9968 25.7645 26.9968 25.0992 26.9968H22.7088C22.0435 26.9968 21.7108 26.9968 21.4567 26.8674C21.2331 26.7535 21.0514 26.5717 20.9375 26.3482C20.808 26.0941 20.808 25.7614 20.808 25.096V23.3957C20.808 22.7303 20.808 22.3977 20.9375 22.1435C21.0514 21.92 21.2331 21.7383 21.4567 21.6244C21.7108 21.4949 22.0435 21.4949 22.7088 21.4949H22.999V19.0692H13.001V21.4949H13.2912C13.9565 21.4949 14.2892 21.4949 14.5433 21.6244C14.7669 21.7383 14.9486 21.92 15.0625 22.1435C15.192 22.3977 15.192 22.7303 15.192 23.3957V25.1027C15.192 25.768 15.192 26.1007 15.0625 26.3548C14.9486 26.5783 14.7669 26.7601 14.5433 26.874C14.2892 27.0035 13.9565 27.0035 13.2912 27.0035H10.9008C10.2355 27.0035 9.90279 27.0035 9.64866 26.874C9.42512 26.7601 9.24338 26.5783 9.12948 26.3548C9 26.1007 9 25.768 9 25.1027V23.3957C9 22.7303 9 22.3977 9.12948 22.1435C9.24338 21.92 9.42512 21.7383 9.64866 21.6244C9.90279 21.4949 10.2355 21.4949 10.9008 21.4949H11.129V18.8532C11.129 17.9386 11.8704 17.1972 12.785 17.1972H16.9657V14.4988H16.8048C16.1395 14.4988 15.8068 14.4988 15.5527 14.3693C15.3291 14.2554 15.1474 14.0737 15.0335 13.8501C14.904 13.596 14.904 13.2633 14.904 12.598V10.8976Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_13996_748" x1="36" y1="1.07288e-06" x2="-1.07288e-06" y2="36" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFDCF3"/>
<stop offset="1" stop-color="#00C2D8"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,27 +1,27 @@
import { useUserStore } from '@/web/support/user/useUserStore';
import { ChevronDownIcon } from '@chakra-ui/icons';
import {
Flex,
Box,
ModalBody,
Checkbox,
ModalFooter,
Button,
Checkbox,
Flex,
Grid,
HStack
HStack,
ModalBody,
ModalFooter
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import MyAvatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags';
import { CollaboratorContext } from './context';
import { useUserStore } from '@/web/support/user/useUserStore';
import { ChevronDownIcon } from '@chakra-ui/icons';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
export type AddModalPropsType = {
onClose: () => void;
@@ -30,22 +30,28 @@ export type AddModalPropsType = {
function AddMemberModal({ onClose, mode = 'member' }: AddModalPropsType) {
const { t } = useTranslation();
const { userInfo, loadAndGetTeamMembers, loadAndGetGroups, myGroups } = useUserStore();
const { userInfo, loadAndGetTeamMembers, loadAndGetGroups, myGroups, loadAndGetOrgs, myOrgs } =
useUserStore();
const { permissionList, collaboratorList, onUpdateCollaborators, getPerLabelList, permission } =
useContextSelector(CollaboratorContext, (v) => v);
const [searchText, setSearchText] = useState<string>('');
const { data: [members = [], groups = []] = [], loading: loadingMembersAndGroups } = useRequest2(
async () => {
if (!userInfo?.team?.teamId) return [[], []];
return await Promise.all([loadAndGetTeamMembers(true), loadAndGetGroups(true)]);
},
{
manual: false,
refreshDeps: [userInfo?.team?.teamId]
}
);
const { data: [members = [], groups = [], orgs = []] = [], loading: loadingMembersAndGroups } =
useRequest2(
async () => {
if (!userInfo?.team?.teamId) return [[], []];
return Promise.all([
loadAndGetTeamMembers(true),
loadAndGetGroups(true),
loadAndGetOrgs(true)
]);
},
{
manual: false,
refreshDeps: [userInfo?.team?.teamId]
}
);
const filterMembers = useMemo(() => {
return members.filter((item) => {
@@ -65,8 +71,20 @@ function AddMemberModal({ onClose, mode = 'member' }: AddModalPropsType) {
});
}, [groups, searchText, myGroups, mode, permission]);
const filterOrgs = useMemo(() => {
if (mode !== 'all') return [];
return orgs.filter((item) => {
if (item.path === '') return false; // exclude root org
if (!permission.isOwner && myOrgs.find((i) => String(i._id) !== String(item._id)))
return false;
if (!searchText) return true;
return item.name.includes(searchText);
});
}, [orgs, searchText, myOrgs, mode, permission]);
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
const [selectedGroupIdList, setSelectedGroupIdList] = useState<string[]>([]);
const [selectedOrgIdList, setSelectedOrgIdList] = useState<string[]>([]);
const [selectedPermission, setSelectedPermission] = useState(permissionList['read'].value);
const perLabel = useMemo(() => {
return getPerLabelList(selectedPermission).join('、');
@@ -77,6 +95,7 @@ function AddMemberModal({ onClose, mode = 'member' }: AddModalPropsType) {
onUpdateCollaborators({
members: selectedMemberIdList,
groups: selectedGroupIdList,
orgs: selectedOrgIdList,
permission: selectedPermission
}),
{
@@ -115,6 +134,44 @@ function AddMemberModal({ onClose, mode = 'member' }: AddModalPropsType) {
/>
<Flex flexDirection="column" mt="2" overflow={'auto'} maxH="400px">
{filterOrgs.map((org) => {
const onChange = () => {
setSelectedOrgIdList((state) => {
if (state.includes(org._id)) {
return state.filter((v) => v !== org._id);
}
return [...state, org._id];
});
};
const collaborator = collaboratorList.find((v) => v.orgId === org._id);
return (
<HStack
justifyContent="space-between"
key={org._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer',
...(!selectedOrgIdList.includes(org._id)
? { svg: { color: 'myGray.50' } }
: {})
}}
onClick={onChange}
>
<Checkbox isChecked={selectedOrgIdList.includes(org._id)} />
<MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{org.name}
</Box>
{!!collaborator && (
<PermissionTags permission={collaborator.permission.value} />
)}
</HStack>
);
})}
{filterGroups.map((group) => {
const onChange = () => {
setSelectedGroupIdList((state) => {
@@ -198,10 +255,44 @@ function AddMemberModal({ onClose, mode = 'member' }: AddModalPropsType) {
</Flex>
<Flex p="4" flexDirection="column">
<Box>
{t('user:has_chosen') + ': '}{' '}
{selectedMemberIdList.length + selectedGroupIdList.length}
{`${t('user:has_chosen')}: `}
{selectedMemberIdList.length + selectedGroupIdList.length + selectedOrgIdList.length}
</Box>
<Flex flexDirection="column" mt="2" overflow={'auto'} maxH="400px">
{selectedOrgIdList.map((orgId) => {
const org = orgs.find((v) => String(v._id) === orgId);
return (
<HStack
justifyContent="space-between"
key={orgId}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer',
...(!selectedOrgIdList.includes(orgId) ? { svg: { color: 'myGray.50' } } : {})
}}
onClick={() =>
setSelectedOrgIdList(selectedOrgIdList.filter((v) => v !== orgId))
}
>
<MyAvatar src={org?.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{org?.name}
</Box>
<MyIcon
name="common/closeLight"
w="16px"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
/>
</HStack>
);
})}
{selectedGroupIdList.map((groupId) => {
const onChange = () => {
setSelectedGroupIdList((state) => {

View File

@@ -1,19 +1,19 @@
import { ModalBody, Table, TableContainer, Tbody, Th, Thead, Tr, Td, Flex } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { Flex, ModalBody, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import Avatar from '@fastgpt/web/components/common/Avatar';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Loading from '@fastgpt/web/components/common/MyLoading';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { CollaboratorContext } from './context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useUserStore } from '@/web/support/user/useUserStore';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import Loading from '@fastgpt/web/components/common/MyLoading';
import { useTranslation } from 'next-i18next';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
export type ManageModalProps = {
onClose: () => void;
};
@@ -65,7 +65,7 @@ function ManageModal({ onClose }: ManageModalProps) {
>
<Td border="none">
<Flex alignItems="center">
<Avatar src={item.avatar} w="24px" mr={2} />
<Avatar src={item.avatar} rounded={'50%'} w="24px" mr={2} />
{item.name === DefaultGroupName ? userInfo?.team.teamName : item.name}
</Flex>
</Td>
@@ -85,14 +85,20 @@ function ManageModal({ onClose }: ManageModalProps) {
onUpdate({
members: item.tmbId ? [item.tmbId] : undefined,
groups: item.groupId ? [item.groupId] : undefined,
orgs: item.orgId ? [item.orgId] : undefined,
permission
});
}}
onDelete={() => {
onDelete({
tmbId: item.tmbId,
groupId: item.groupId
} as RequireOnlyOne<{ tmbId: string; groupId: string }>);
groupId: item.groupId,
orgId: item.orgId
} as RequireOnlyOne<{
tmbId: string;
groupId: string;
orgId: string;
}>);
}}
/>
)}

View File

@@ -1,13 +1,13 @@
import { Box, BoxProps, Flex } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { Box, type BoxProps, Flex } from '@chakra-ui/react';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyBox from '@fastgpt/web/components/common/MyBox';
import Tag, { type TagProps } from '@fastgpt/web/components/common/Tag';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context';
import Tag, { TagProps } from '@fastgpt/web/components/common/Tag';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useTranslation } from 'next-i18next';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { useUserStore } from '@/web/support/user/useUserStore';
export type MemberListCardProps = BoxProps & { tagStyle?: Omit<TagProps, 'children'> };
@@ -31,12 +31,12 @@ const MemberListCard = ({ tagStyle, ...props }: MemberListCardProps) => {
{collaboratorList?.map((member) => {
return (
<Tag
key={member.tmbId || member.groupId}
key={member.tmbId || member.groupId || member.orgId}
type={'fill'}
colorSchema="white"
{...tagStyle}
>
<Avatar src={member.avatar} w="1.25rem" />
<Avatar src={member.avatar} w="1.25rem" rounded={'50%'} />
<Box fontSize={'sm'} ml={1}>
{member.name === DefaultGroupName ? userInfo?.team.teamName : member.name}
</Box>

View File

@@ -1,21 +1,24 @@
import { useDisclosure } from '@chakra-ui/react';
import {
import type {
CollaboratorItemType,
UpdateClbPermissionProps
} from '@fastgpt/global/support/permission/collaborator';
import { PermissionList } from '@fastgpt/global/support/permission/constant';
import { Permission } from '@fastgpt/global/support/permission/controller';
import { PermissionListType, PermissionValueType } from '@fastgpt/global/support/permission/type';
import { ReactNode, useCallback } from 'react';
import type {
PermissionListType,
PermissionValueType
} from '@fastgpt/global/support/permission/type';
import { type ReactNode, useCallback } from 'react';
import { createContext } from 'use-context-selector';
import dynamic from 'next/dynamic';
import MemberListCard, { MemberListCardProps } from './MemberListCard';
import MemberListCard, { type MemberListCardProps } from './MemberListCard';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
const AddMemberModal = dynamic(() => import('./AddMemberModal'));
const ManageModal = dynamic(() => import('./ManageModal'));
@@ -24,7 +27,9 @@ export type MemberManagerInputPropsType = {
onGetCollaboratorList: () => Promise<CollaboratorItemType[]>;
permissionList: PermissionListType;
onUpdateCollaborators: (props: UpdateClbPermissionProps) => Promise<any>;
onDelOneCollaborator: (props: RequireOnlyOne<{ tmbId: string; groupId: string }>) => Promise<any>;
onDelOneCollaborator: (
props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }>
) => Promise<any>;
refreshDeps?: any[];
mode?: 'member' | 'all';
};
@@ -46,19 +51,19 @@ type CollaboratorContextType = MemberManagerPropsType & {};
export const CollaboratorContext = createContext<CollaboratorContextType>({
collaboratorList: [],
permissionList: PermissionList,
onUpdateCollaborators: function () {
onUpdateCollaborators: () => {
throw new Error('Function not implemented.');
},
onDelOneCollaborator: function () {
onDelOneCollaborator: () => {
throw new Error('Function not implemented.');
},
getPerLabelList: function (): string[] {
getPerLabelList: (): string[] => {
throw new Error('Function not implemented.');
},
refetchCollaboratorList: function (): void {
refetchCollaboratorList: (): void => {
throw new Error('Function not implemented.');
},
onGetCollaboratorList: function (): Promise<CollaboratorItemType[]> {
onGetCollaboratorList: (): Promise<CollaboratorItemType[]> => {
throw new Error('Function not implemented.');
},
isFetchingCollaborator: false,
@@ -88,7 +93,7 @@ const CollaboratorContextProvider = ({
refetchCollaboratorList();
};
const onDelOneCollaboratorThen = async (
props: RequireOnlyOne<{ tmbId: string; groupId: string }>
props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }>
) => {
await onDelOneCollaborator(props);
refetchCollaboratorList();

View File

@@ -0,0 +1,23 @@
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
function IconButton({ name, onClick }: { name: IconNameType; onClick: () => void }) {
return (
<MyIcon
name={name}
w={'1rem'}
h={'1rem'}
transition={'background 0.1s'}
cursor={'pointer'}
p="1"
rounded={'sm'}
_hover={{
bg: 'myGray.05',
color: 'primary.600'
}}
onClick={onClick}
/>
);
}
export default IconButton;

View File

@@ -0,0 +1,150 @@
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { postCreateOrg, putUpdateOrg } from '@/web/support/user/team/org/api';
import { Button, HStack, Input, ModalBody, ModalFooter, Textarea } from '@chakra-ui/react';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { DEFAULT_ORG_AVATAR } from '@fastgpt/global/common/system/constants';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
export type OrgFormType = {
avatar: string;
description?: string;
name: string;
};
function OrgInfoModal({
editOrg,
createOrgParentId: parentId,
onClose,
onSuccess
}: {
editOrg?: OrgType;
createOrgParentId?: string;
onClose: () => void;
onSuccess?: () => void;
}) {
const { t } = useTranslation();
const { File: AvatarSelect, onOpen: onOpenSelectAvatar } = useSelectFile({
fileType: '.jpg, .jpeg, .png',
multiple: false
});
const { register, handleSubmit, getValues, setValue } = useForm<OrgFormType>({
defaultValues: {
name: '',
avatar: DEFAULT_ORG_AVATAR,
description: undefined
}
});
useEffect(() => {
setValue('name', editOrg?.name ?? '');
setValue('avatar', editOrg?.avatar || DEFAULT_ORG_AVATAR);
setValue('description', editOrg?.description);
}, [editOrg, setValue]);
const { run: onCreate, loading: isLoadingCreate } = useRequest2(
(data: OrgFormType, parentId: string) => {
return postCreateOrg({
name: data.name,
avatar: data.avatar,
parentId,
description: data.description
});
},
{
onSuccess: () => {
onClose();
onSuccess?.();
}
}
);
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
(data: OrgFormType, orgId: string) => {
return putUpdateOrg({
orgId,
name: data.name,
avatar: data.avatar,
description: data.description
});
},
{
onSuccess: () => {
onClose();
onSuccess?.();
}
}
);
const { loading: uploadingAvatar, run: onSelectAvatar } = useRequest2(
async (file: File[]) => {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.groupAvatar,
file: file[0],
maxW: 300,
maxH: 300
});
return src;
},
{
onSuccess: (src: string) => {
setValue('avatar', src);
}
}
);
const isLoading = uploadingAvatar;
return (
<MyModal
isOpen={!!(editOrg || parentId)}
onClose={onClose}
title={editOrg ? t('account_team:edit_org_info') : t('account_team:create_org')}
iconSrc={editOrg?.avatar || DEFAULT_ORG_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') || DEFAULT_ORG_AVATAR}
onClick={onOpenSelectAvatar}
cursor={'pointer'}
borderRadius={'md'}
/>
<Input
bgColor="myGray.50"
{...register('name', { required: true })}
placeholder={t('account_team:org_name')}
/>
</HStack>
<FormLabel w="80px">{t('account_team:org_description')}</FormLabel>
<Textarea {...register('description')} placeholder={t('account_team:org_description')} />
</ModalBody>
<ModalFooter alignItems="flex-end">
<Button
isLoading={isLoading}
onClick={handleSubmit((data) => {
if (editOrg) {
onUpdate(data, editOrg._id);
} else if (parentId) {
onCreate(data, parentId);
}
})}
>
{editOrg ? t('common:common.Save') : t('common:new_create')}
</Button>
</ModalFooter>
<AvatarSelect onSelect={onSelectAvatar} />
</MyModal>
);
}
export default OrgInfoModal;

View File

@@ -0,0 +1,202 @@
import { putUpdateOrgMembers } from '@/web/support/user/team/org/api';
import {
Box,
Button,
Checkbox,
Flex,
Grid,
HStack,
ModalBody,
ModalFooter
} from '@chakra-ui/react';
import type { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import type React from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '../context';
export type GroupFormType = {
members: {
tmbId: string;
role: `${GroupMemberRole}`;
}[];
};
function CheckboxIcon({
name
}: {
isChecked?: boolean;
isIndeterminate?: boolean;
name: IconNameType;
}) {
return <MyIcon name={name} w="12px" />;
}
function OrgMemberModal({ onClose, editOrgId }: { onClose: () => void; editOrgId?: string }) {
// 1. Owner can not be deleted, toast
// 2. Owner/Admin can manage members
// 3. Owner can add/remove admins
const { t } = useTranslation();
const {
members: allMembers,
orgs,
refetchOrgs,
refetchMembers
} = useContextSelector(TeamContext, (v) => v);
const org = useMemo(() => orgs.find((item) => item._id === editOrgId), [editOrgId, orgs]);
const [members, setMembers] = useState<{ tmbId: string }[]>(org?.members || []);
useEffect(() => {
setMembers(org?.members || []);
}, [org]);
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 (!editOrgId) return;
return putUpdateOrgMembers({
orgId: editOrgId,
members
});
},
{
onSuccess: () => Promise.all([onClose(), refetchOrgs(), refetchMembers()])
}
);
const isSelected = (memberId: string) => {
return members.find((item) => item.tmbId === memberId);
};
const handleToggleSelect = (memberId: string) => {
if (isSelected(memberId)) {
setMembers(members.filter((item) => item.tmbId !== memberId));
} else {
setMembers([...members, { tmbId: memberId }]);
}
};
const isLoading = isLoadingUpdate;
return (
<MyModal
onClose={onClose}
isOpen={!!editOrgId}
title={t('user:team.group.manage_member')}
iconSrc={org?.avatar}
iconColor="primary.600"
minW="800px"
h={'100%'}
isCentered
>
<ModalBody flex={1}>
<Grid
border="1px solid"
borderColor="myGray.200"
borderRadius="0.5rem"
gridTemplateColumns="1fr 1fr"
h={'100%'}
>
<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={<CheckboxIcon name={'common/check'} />}
pointerEvents="none"
/>
<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
justifyContent="space-between"
py="2"
px={3}
borderRadius={'md'}
key={member.tmbId}
_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>
<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 OrgMemberModal;

View File

@@ -0,0 +1,80 @@
import { putMoveOrg, putMoveOrgMember } from '@/web/support/user/team/org/api';
import { Button, ModalBody, ModalFooter } from '@chakra-ui/react';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import type { TeamTmbItemType } from '@fastgpt/global/support/user/team/type';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import OrgTree from './OrgTree';
function OrgMoveModal({
movingOrg,
movingTmb,
orgs,
team,
onClose,
onSuccess
}: {
movingOrg?: OrgType;
movingTmb?: { tmbId: string; orgId: string };
orgs: OrgType[];
team: TeamTmbItemType;
onClose: () => void;
onSuccess: () => void;
}) {
const { t } = useTranslation();
const [selectedOrg, selectOrg] = useState<OrgType>();
const { runAsync: moveOrg, loading: loadingOrg } = useRequest2(putMoveOrg, {
onSuccess: () => {
onClose();
onSuccess();
}
});
const { runAsync: moveTmb, loading: loadingTmb } = useRequest2(putMoveOrgMember, {
onSuccess: () => {
onClose();
onSuccess();
}
});
const handleConfirm = () => {
if (!selectedOrg) return;
if (movingTmb) {
moveTmb({ orgId: movingTmb.orgId, tmbId: movingTmb.tmbId, newOrgId: selectedOrg._id });
} else if (movingOrg) {
moveOrg(movingOrg._id, selectedOrg._id);
}
};
const loading = loadingOrg || loadingTmb;
return (
<MyModal
isOpen={!!movingOrg || !!movingTmb}
onClose={onClose}
title={movingOrg ? t('account_team:move_org') : t('account_team:move_member')}
iconSrc="common/file/move"
iconColor="blue.600"
>
<ModalBody>
<OrgTree
orgs={orgs}
teamName={team.teamName}
teamAvatar={team.avatar}
selectedOrg={selectedOrg}
selectOrg={selectOrg}
/>
</ModalBody>
<ModalFooter>
<Button isDisabled={!selectedOrg} isLoading={loading} onClick={() => handleConfirm()}>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
}
export default OrgMoveModal;

View File

@@ -0,0 +1,115 @@
import { Box, HStack, Text, VStack } from '@chakra-ui/react';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useToggle } from 'ahooks';
import { useMemo, useState } from 'react';
import IconButton from './IconButton';
function OrgTreeNode({
org,
list,
selectedOrg,
selectOrg,
indent = 0
}: {
org: OrgType;
list: OrgType[];
selectedOrg?: OrgType;
selectOrg?: (org?: OrgType) => void;
indent?: number;
}) {
const children = useMemo(
() => list.filter((item) => item.path === `${org.path}/${org._id}`),
[org, list]
);
const [isExpanded, toggleIsExpanded] = useToggle(false);
return (
<VStack alignItems={'start'} w="full" gap={'8px'}>
<HStack
w="full"
_hover={{ bgColor: selectedOrg === org ? 'blue.200' : 'gray.100' }}
borderRadius="4px"
boxSizing="border-box"
py="4px"
pl={`calc(${indent}rem + 4px)`}
transition={'background 0.1s'}
{...(selectedOrg === org ? { bgColor: 'blue.100' } : {})}
>
{children.length > 0 ? (
<IconButton
name={isExpanded ? 'common/downArrowFill' : 'common/rightArrowFill'}
onClick={() => toggleIsExpanded.toggle()}
/>
) : (
<Box w={'1rem'} h={'1rem'} m="1" />
)}
<HStack onClick={() => selectOrg?.(org)} cursor="pointer">
<Avatar src={org.avatar} w="20px" h="20px" rounded={'50%'} />
<Text>{org.name}</Text>
</HStack>
</HStack>
{isExpanded &&
children.length > 0 &&
children.map((child) => (
<OrgTreeNode
key={child._id}
org={child}
indent={indent + 1}
list={list}
selectedOrg={selectedOrg}
selectOrg={selectOrg}
/>
))}
</VStack>
);
}
function OrgTree({
orgs,
teamName,
teamAvatar,
selectedOrg,
selectOrg
}: {
orgs: OrgType[];
teamAvatar: string;
teamName: string;
selectedOrg?: OrgType;
selectOrg?: (org?: OrgType) => void;
}) {
const root = orgs[0];
if (!root) return null;
const children = useMemo(
() => orgs.filter((item) => item.path === `${root.path}/${root._id}`),
[root, orgs]
);
return (
<VStack alignItems={'start'} gap={'8px'}>
<HStack
w="full"
onClick={() => selectOrg?.(root)}
cursor="pointer"
_hover={{ bgColor: selectedOrg === root ? 'blue.200' : 'gray.100' }}
borderRadius="4px"
p="4px"
transition={'background 0.1s'}
{...(selectedOrg === root ? { bgColor: 'blue.100' } : {})}
>
<Avatar src={teamAvatar} w="20px" h="20px" rounded={'50%'} />
<Text>{teamName}</Text>
</HStack>
{children.map((child) => (
<OrgTreeNode
key={child._id}
org={child}
list={orgs}
selectOrg={selectOrg}
selectedOrg={selectedOrg}
/>
))}
</VStack>
);
}
export default OrgTree;

View File

@@ -0,0 +1,370 @@
import { deleteOrg, deleteOrgMember } from '@/web/support/user/team/org/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import {
Box,
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
Divider,
HStack,
Table,
TableContainer,
Tag,
Tbody,
Td,
Text,
Th,
Thead,
Tr,
VStack,
useDisclosure
} from '@chakra-ui/react';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import MemberTag from '../../../../../components/support/user/team/Info/MemberTag';
import { TeamContext } from '../context';
import IconButton from './IconButton';
import OrgInfoModal from './OrgInfoModal';
import OrgMemberModal from './OrgMemberModal';
import OrgMoveModal from './OrgMoveModal';
function ActionButton({
icon,
text,
onClick
}: {
icon: IconNameType;
text: string;
onClick: () => void;
}) {
return (
<HStack
gap={'8px'}
w="100%"
transition={'background 0.1s'}
cursor={'pointer'}
p="4px"
rounded={'sm'}
_hover={{
bg: 'myGray.05',
color: 'primary.600'
}}
onClick={onClick}
>
<MyIcon name={icon} w="16px" h="16px" p="1" />
<Text fontSize={'12px'} lineHeight={'16px'}>
{text}
</Text>
</HStack>
);
}
function MemberTable() {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { orgs, refetchOrgs, members, refetchMembers, isLoading } = useContextSelector(
TeamContext,
(v) => v
);
const [currentOrg, setCurrentOrg] = useState<OrgType>();
// Set current org by hash
useEffect(() => {
const hash = window.location.hash.substring(1);
const initialOrg = orgs.find((org) => org._id === hash) || orgs[0];
setCurrentOrg(initialOrg);
}, [orgs, isLoading]);
// Update hash when current org changes
useEffect(() => {
if (currentOrg) {
window.location.hash = currentOrg._id;
}
}, [currentOrg]);
const currentPath = useMemo<{ path: string; parents: OrgType[] }>(
() => ({
path: currentOrg ? `${currentOrg.path}/${currentOrg._id}` : '',
parents: currentOrg
? currentOrg.path
.split('/')
.filter(Boolean)
.map((orgId) => orgs.find((org) => org._id === orgId)!)
: []
}),
[orgs, currentOrg]
);
const orgList = useMemo(
() =>
orgs
.filter((org) => org.path === currentPath.path)
.map((org) => {
// calc org members count
let count = org.members.length;
for (const item of orgs.filter((item) =>
item.path.startsWith(`${org.path}/${org._id}`)
)) {
count += item.members.length;
}
return { ...org, count };
}),
[orgs, currentPath]
);
const [editOrg, setEditOrg] = useState<OrgType | undefined>();
const [editMemberOrgId, setEditMemberOrgId] = useState<string | undefined>();
const [movingOrg, setMovingOrg] = useState<OrgType | undefined>();
const [movingTmb, setMovingTmb] = useState<{ tmbId: string; orgId: string } | undefined>();
const [createOrgParentId, setCreateOrgParentId] = useState<string | undefined>();
const { ConfirmModal: ConfirmDeleteOrgModal, openConfirm: openDeleteOrgModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_org')
});
const { ConfirmModal: ConfirmDeleteMember, openConfirm: openDeleteMemberModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_member')
});
const { runAsync: deleteOrgReq } = useRequest2(deleteOrg, {
onSuccess: () => {
refetchOrgs();
refetchMembers();
}
});
const { runAsync: deleteMemberReq } = useRequest2(deleteOrgMember, {
onSuccess: () => {
refetchOrgs();
refetchMembers();
}
});
const deleteOrgHandler = (orgId: string) => openDeleteOrgModal(() => deleteOrgReq(orgId))();
const deleteMemberHandler = (orgId: string, tmbId: string) =>
openDeleteMemberModal(() => deleteMemberReq(orgId, tmbId))();
return (
<VStack>
<Breadcrumb mr={'auto'}>
{currentPath.parents.map((parent) => (
<BreadcrumbItem key={parent._id}>
<BreadcrumbLink onClick={() => setCurrentOrg(parent)}>
{parent.path === '' ? userInfo?.team.teamName : parent.name}
</BreadcrumbLink>
</BreadcrumbItem>
))}
<BreadcrumbItem isCurrentPage>
<BreadcrumbLink color="myGray.900" fontWeight={500}>
{currentOrg?.path === '' ? userInfo?.team.teamName : currentOrg?.name}
</BreadcrumbLink>
</BreadcrumbItem>
</Breadcrumb>
<HStack w={'100%'} gap={'16px'} alignItems={'start'}>
<TableContainer overflow={'unset'} fontSize={'sm'} flexGrow={1}>
<Table overflow={'unset'}>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="6px">
{t('common:Name')}
</Th>
<Th bg="myGray.100" borderRightRadius="6px">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
<Tbody>
{orgList.map((org) => (
<Tr key={org._id} overflow={'unset'}>
<Td>
<HStack w="fit-content" cursor={'pointer'} onClick={() => setCurrentOrg(org)}>
<MemberTag name={org.name} avatar={org.avatar} />
<Tag size="sm">{org.count}</Tag>
<MyIcon
name="core/chat/chevronRight"
w={'12px'}
h={'12px'}
color={'myGray.400'}
/>
</HStack>
</Td>
<Td w={'6rem'}>
<MyMenu
trigger="click"
Button={
<MyIcon name="more" w={'1rem'} cursor={'pointer'} p="1" rounded={'sm'} />
}
menuList={[
{
children: [
{
icon: 'edit',
label: t('account_team:edit_info'),
onClick: () => setEditOrg(org)
},
{
icon: 'common/file/move',
label: t('common:Move'),
onClick: () => setMovingOrg(org)
},
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () => deleteOrgHandler(org._id)
}
]
}
]}
/>
</Td>
</Tr>
))}
{currentOrg?.members.map((member) => {
const memberInfo = members.find((m) => m.tmbId === member.tmbId);
if (!memberInfo) return null;
return (
<Tr key={member.tmbId} overflow={'unset'}>
<Td>
<MemberTag name={memberInfo.memberName} avatar={memberInfo.avatar} />
</Td>
<Td w={'6rem'}>
<MyMenu
trigger={'click'}
Button={
<MyIcon name="more" w={'1rem'} cursor={'pointer'} p="1" rounded={'sm'} />
}
menuList={[
{
children: [
// {
// icon: 'edit',
// label: t('account_team:remark'),
// onClick: () => {
// // TODO
// console.log(member.tmbId);
// }
// },
{
icon: 'common/file/move',
label: t('common:Move'),
onClick: () =>
setMovingTmb({ tmbId: member.tmbId, orgId: currentOrg!._id })
},
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () => deleteMemberHandler(currentOrg!._id, member.tmbId)
}
]
}
]}
/>
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
<VStack w={'220px'} alignItems={'start'}>
<HStack gap={'6px'}>
<Avatar
src={currentOrg?.path === '' ? userInfo?.team.avatar : currentOrg?.avatar}
w={'16px'}
h={'16px'}
rounded={'20%'}
/>
<Text fontWeight={500} fontSize={'14px'} color={'myGray.900'} lineHeight={'20px'}>
{currentOrg?.path === '' ? userInfo?.team.teamName : currentOrg?.name}
</Text>
{currentOrg?.path !== '' && (
<IconButton name="edit" onClick={() => setEditOrg(currentOrg)} />
)}
</HStack>
<Text fontSize={12} lineHeight={'16px'} w={'full'}>
{currentOrg?.description ?? t('common:common.no_intro')}
</Text>
<Divider my={'20px'} />
<Text fontWeight={500} mb="13px" fontSize="14px" color="myGray.900" lineHeight="20px">
{t('common:common.Action')}
</Text>
<VStack gap="13px" w="100%">
<ActionButton
icon="common/add2"
text={t('account_team:create_sub_org')}
onClick={() => {
setCreateOrgParentId(currentOrg?._id);
}}
/>
<ActionButton
icon="common/administrator"
text={t('account_team:manage_member')}
onClick={() => setEditMemberOrgId(currentOrg?._id)}
/>
{currentOrg?.path !== '' && (
<>
<ActionButton
icon="common/file/move"
text={t('account_team:move_org')}
onClick={() => setMovingOrg(currentOrg)}
/>
<ActionButton
icon="delete"
text={t('account_team:delete_org')}
onClick={() => deleteOrgHandler(currentOrg?._id ?? '')}
/>
</>
)}
</VStack>
</VStack>
</HStack>
<OrgInfoModal
editOrg={editOrg}
createOrgParentId={createOrgParentId}
onClose={() => {
setEditOrg(undefined);
setCreateOrgParentId(undefined);
}}
onSuccess={() => {
refetchOrgs();
refetchMembers();
}}
/>
<OrgMoveModal
orgs={orgs}
team={userInfo?.team!}
movingOrg={movingOrg}
movingTmb={movingTmb}
onClose={() => {
setMovingOrg(undefined);
setMovingTmb(undefined);
}}
onSuccess={() => {
refetchOrgs();
refetchMembers();
}}
/>
<OrgMemberModal editOrgId={editMemberOrgId} onClose={() => setEditMemberOrgId(undefined)} />
<ConfirmDeleteOrgModal />
<ConfirmDeleteMember />
</VStack>
);
}
export default MemberTable;

View File

@@ -9,7 +9,9 @@ import type { TeamTmbItemType, TeamMemberItemType } from '@fastgpt/global/suppor
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { getGroupList } from '@/web/support/user/team/group/api';
import { getOrgList } from '@/web/support/user/team/org/api';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import { OrgType } from '@fastgpt/global/support/user/team/org/type';
const EditInfoModal = dynamic(() => import('./EditInfoModal'));
@@ -17,6 +19,7 @@ type TeamModalContextType = {
myTeams: TeamTmbItemType[];
members: TeamMemberItemType[];
groups: MemberGroupListType;
orgs: OrgType[];
isLoading: boolean;
onSwitchTeam: (teamId: string) => void;
setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>;
@@ -24,6 +27,7 @@ type TeamModalContextType = {
refetchMembers: () => void;
refetchTeams: () => void;
refetchGroups: () => void;
refetchOrgs: () => void;
searchKey: string;
setSearchKey: React.Dispatch<React.SetStateAction<string>>;
teamSize: number;
@@ -33,6 +37,7 @@ export const TeamContext = createContext<TeamModalContextType>({
myTeams: [],
groups: [],
members: [],
orgs: [],
isLoading: false,
onSwitchTeam: function (_teamId: string): void {
throw new Error('Function not implemented.');
@@ -49,6 +54,9 @@ export const TeamContext = createContext<TeamModalContextType>({
refetchGroups: function (): void {
throw new Error('Function not implemented.');
},
refetchOrgs: function (): void {
throw new Error('Function not implemented.');
},
searchKey: '',
setSearchKey: function (_value: React.SetStateAction<string>): void {
@@ -107,7 +115,17 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
refreshDeps: [userInfo?.team?.teamId]
});
const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers || isLoadingGroups;
const {
data: orgs = [],
loading: isLoadingOrgs,
refresh: refetchOrgs
} = useRequest2(getOrgList, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const isLoading =
isLoadingTeams || isSwitchingTeam || loadingMembers || isLoadingGroups || isLoadingOrgs;
const contextValue = {
myTeams,
@@ -123,6 +141,8 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
refetchMembers,
groups,
refetchGroups,
orgs,
refetchOrgs,
teamSize: members.length
};

View File

@@ -25,11 +25,13 @@ import MemberTable from './components/MemberTable';
const InviteModal = dynamic(() => import('./components/InviteModal'));
const PermissionManage = dynamic(() => import('./components/PermissionManage/index'));
const GroupManage = dynamic(() => import('./components/GroupManage/index'));
const OrgManage = dynamic(() => import('./components/OrgManage/index'));
const GroupInfoModal = dynamic(() => import('./components/GroupManage/GroupInfoModal'));
const ManageGroupMemberModal = dynamic(() => import('./components/GroupManage/GroupManageMember'));
export enum TeamTabEnum {
member = 'member',
org = 'org',
group = 'group',
permission = 'permission'
}
@@ -172,6 +174,7 @@ const Team = () => {
<FillRowTabs
list={[
{ label: t('account_team:member'), value: TeamTabEnum.member },
{ label: t('account_team:org'), value: TeamTabEnum.org },
{ label: t('account_team:group'), value: TeamTabEnum.group },
{ label: t('account_team:permission'), value: TeamTabEnum.permission }
]}
@@ -274,6 +277,7 @@ const Team = () => {
{teamTab === TeamTabEnum.group && (
<GroupManage onEditGroup={onEditGroup} onManageMember={onManageMember} />
)}
{teamTab === TeamTabEnum.org && <OrgManage />}
{teamTab === TeamTabEnum.permission && <PermissionManage />}
</Box>
</Box>

View File

@@ -1,40 +1,40 @@
import React, { useCallback } from 'react';
import CollaboratorContextProvider from '@/components/support/permission/MemberManager/context';
import ResumeInherit from '@/components/support/permission/ResumeInheritText';
import { AppContext } from '@/pages/app/detail/components/context';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useI18n } from '@/web/context/I18n';
import { resumeInheritPer } from '@/web/core/app/api';
import {
deleteAppCollaborators,
getCollaboratorList,
postUpdateAppCollaborators
} from '@/web/core/app/api/collaborator';
import {
Box,
Flex,
Button,
Flex,
FormControl,
Input,
Textarea,
ModalBody,
ModalFooter,
ModalBody
Textarea
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import CollaboratorContextProvider from '@/components/support/permission/MemberManager/context';
import {
postUpdateAppCollaborators,
deleteAppCollaborators,
getCollaboratorList
} from '@/web/core/app/api/collaborator';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pages/app/detail/components/context';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import type { AppSchema } from '@fastgpt/global/core/app/type.d';
import { AppPermissionList } from '@fastgpt/global/support/permission/app/constant';
import type { PermissionValueType } from '@fastgpt/global/support/permission/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
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';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
import React, { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { useContextSelector } from 'use-context-selector';
const InfoModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
@@ -126,20 +126,23 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
const onUpdateCollaborators = ({
members,
groups,
orgs,
permission
}: {
members?: string[];
groups?: string[];
orgs?: string[];
permission: PermissionValueType;
}) =>
postUpdateAppCollaborators({
members,
groups,
permission,
orgs,
appId: appDetail._id
});
const onDelCollaborator = async (props: RequireOnlyOne<{ tmbId: string; groupId: string }>) =>
const onDelCollaborator = async (props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }>) =>
deleteAppCollaborators({
appId: appDetail._id,
...props
@@ -211,7 +214,8 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
onUpdateCollaborators({
permission: props.permission,
members: props.members,
groups: props.groups
groups: props.groups,
orgs: props.orgs
})
}
onDelOneCollaborator={onDelCollaborator}

View File

@@ -258,20 +258,20 @@ const ListItem = () => {
{(AppFolderTypeList.includes(app.type)
? app.permission.hasManagePer
: app.permission.hasWritePer) && (
<Box className="more" display={['', 'none']}>
<MyMenu
size={'xs'}
Button={
<IconButton
size={'xsSquare'}
variant={'transparentBase'}
icon={<MyIcon name={'more'} w={'0.875rem'} color={'myGray.500'} />}
aria-label={''}
/>
}
menuList={[
...([AppTypeEnum.simple, AppTypeEnum.workflow].includes(app.type)
? [
<Box className="more" display={['', 'none']}>
<MyMenu
size={'xs'}
Button={
<IconButton
size={'xsSquare'}
variant={'transparentBase'}
icon={<MyIcon name={'more'} w={'0.875rem'} color={'myGray.500'} />}
aria-label={''}
/>
}
menuList={[
...([AppTypeEnum.simple, AppTypeEnum.workflow].includes(app.type)
? [
{
children: [
{
@@ -285,9 +285,9 @@ const ListItem = () => {
]
}
]
: []),
...([AppTypeEnum.plugin].includes(app.type)
? [
: []),
...([AppTypeEnum.plugin].includes(app.type)
? [
{
children: [
{
@@ -301,9 +301,9 @@ const ListItem = () => {
]
}
]
: []),
...(app.permission.hasManagePer
? [
: []),
...(app.permission.hasManagePer
? [
{
children: [
{
@@ -330,34 +330,34 @@ const ListItem = () => {
}
},
...(folderDetail?.type === AppTypeEnum.httpPlugin &&
!(parentApp ? parentApp.permission : app.permission)
.hasManagePer
!(parentApp ? parentApp.permission : app.permission)
.hasManagePer
? []
: [
{
icon: 'common/file/move',
type: 'grayBg' as MenuItemType,
label: t('common:common.folder.Move to'),
onClick: () => setMoveAppId(app._id)
}
]),
{
icon: 'common/file/move',
type: 'grayBg' as MenuItemType,
label: t('common:common.folder.Move to'),
onClick: () => setMoveAppId(app._id)
}
]),
...(app.permission.hasManagePer
? [
{
icon: 'support/team/key',
type: 'grayBg' as MenuItemType,
label: t('common:permission.Permission'),
onClick: () => setEditPerAppIndex(index)
}
]
{
icon: 'support/team/key',
type: 'grayBg' as MenuItemType,
label: t('common:permission.Permission'),
onClick: () => setEditPerAppIndex(index)
}
]
: [])
]
}
]
: []),
...(AppFolderTypeList.includes(app.type)
? []
: [
: []),
...(AppFolderTypeList.includes(app.type)
? []
: [
{
children: [
{
@@ -370,8 +370,8 @@ const ListItem = () => {
]
}
]),
...(app.permission.isOwner
? [
...(app.permission.isOwner
? [
{
children: [
{
@@ -390,11 +390,11 @@ const ListItem = () => {
]
}
]
: [])
]}
/>
</Box>
)}
: [])
]}
/>
</Box>
)}
</HStack>
</Flex>
</MyBox>
@@ -438,6 +438,7 @@ const ListItem = () => {
onUpdateCollaborators: (props: {
members?: string[];
groups?: string[];
orgs?: string[];
permission: number;
}) =>
postUpdateAppCollaborators({
@@ -448,6 +449,7 @@ const ListItem = () => {
props: RequireOnlyOne<{
tmbId?: string;
groupId?: string;
orgId?: string;
}>
) =>
deleteAppCollaborators({

View File

@@ -324,10 +324,12 @@ const MyApps = () => {
refreshDeps: [folderDetail._id, folderDetail.inheritPermission],
onDelOneCollaborator: async ({
tmbId,
groupId
groupId,
orgId
}: {
tmbId?: string;
groupId?: string;
orgId?: string;
}) => {
if (tmbId) {
return deleteAppCollaborators({
@@ -339,6 +341,11 @@ const MyApps = () => {
appId: folderDetail._id,
groupId
});
} else if (orgId) {
return deleteAppCollaborators({
appId: folderDetail._id,
orgId
});
}
}
}}

View File

@@ -392,7 +392,7 @@ const Info = ({ datasetId }: { datasetId: string }) => {
...body,
datasetId
}),
onDelOneCollaborator: async ({ groupId, tmbId }) => {
onDelOneCollaborator: async ({ groupId, tmbId, orgId }) => {
if (tmbId) {
return deleteDatasetCollaborators({
datasetId,
@@ -403,6 +403,11 @@ const Info = ({ datasetId }: { datasetId: string }) => {
datasetId,
groupId
});
} else if (orgId) {
return deleteDatasetCollaborators({
datasetId,
orgId
});
}
}
}}

View File

@@ -257,7 +257,7 @@ const Dataset = () => {
permission,
datasetId: folderDetail._id
}),
onDelOneCollaborator: async ({ tmbId, groupId }) => {
onDelOneCollaborator: async ({ tmbId, groupId, orgId }) => {
if (tmbId) {
return deleteDatasetCollaborators({
datasetId: folderDetail._id,
@@ -268,6 +268,11 @@ const Dataset = () => {
datasetId: folderDetail._id,
groupId
});
} else if (orgId) {
return deleteDatasetCollaborators({
datasetId: folderDetail._id,
orgId
});
}
},
refreshDeps: [folderDetail._id, folderDetail.inheritPermission]

View File

@@ -0,0 +1,34 @@
import { DELETE, GET, POST, PUT } from '@/web/common/api/request';
import type {
postCreateOrgData,
putUpdateOrgData,
putUpdateOrgMembersData,
putMoveOrgMemberData
} from '@fastgpt/global/support/user/team/org/api';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
export const getOrgList = () => GET<OrgType[]>('/proApi/support/user/team/org/list');
export const postCreateOrg = (data: postCreateOrgData) =>
POST('/proApi/support/user/team/org/create', data);
export const deleteOrg = (orgId: string) =>
DELETE('/proApi/support/user/team/org/delete', { orgId });
export const deleteOrgMember = (orgId: string, tmbId: string) =>
DELETE('/proApi/support/user/team/org/deleteMember', { orgId, tmbId });
export const putMoveOrg = (orgId: string, parentId: string) =>
PUT('/proApi/support/user/team/org/move', { orgId, parentId });
export const putMoveOrgMember = (data: putMoveOrgMemberData) =>
PUT('/proApi/support/user/team/org/moveMember', data);
export const putUpdateOrg = (data: putUpdateOrgData) =>
PUT('/proApi/support/user/team/org/update', data);
export const putUpdateOrgMembers = (data: putUpdateOrgMembersData) =>
PUT('/proApi/support/user/team/org/updateMembers', data);
// export const putChnageOrgOwner = (data: putChnageOrgOwnerData) =>
// PUT('/proApi/support/user/team/org/changeOwner', data);

View File

@@ -1,16 +1,18 @@
import type { UserUpdateParams } from '@/types/user';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { getTokenLogin, putUserInfo } from '@/web/support/user/api';
import { getTeamMembers } from '@/web/support/user/team/api';
import type { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import type { OrgMemberSchemaType, OrgType } from '@fastgpt/global/support/user/team/org/type';
import type { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import type { UserType } from '@fastgpt/global/support/user/type.d';
import type { FeTeamPlanStatusType } from '@fastgpt/global/support/wallet/sub/type';
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import type { UserUpdateParams } from '@/types/user';
import type { UserType } from '@fastgpt/global/support/user/type.d';
import { getTokenLogin, putUserInfo } from '@/web/support/user/api';
import { FeTeamPlanStatusType } from '@fastgpt/global/support/wallet/sub/type';
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';
import { getOrgList } from './team/org/api';
type State = {
systemMsgReadId: string;
@@ -30,6 +32,10 @@ type State = {
teamMemberGroups: MemberGroupListType;
myGroups: MemberGroupListType;
loadAndGetGroups: (init?: boolean) => Promise<MemberGroupListType>;
teamOrgs: OrgType[];
myOrgs: OrgType[];
loadAndGetOrgs: (init?: boolean) => Promise<OrgType[]>;
};
export const useUserStore = create<State>()(
@@ -107,6 +113,7 @@ export const useUserStore = create<State>()(
return res;
},
teamMemberGroups: [],
teamOrgs: [],
myGroups: [],
loadAndGetGroups: async (init = false) => {
if (!useSystemStore.getState()?.feConfigs?.isPlus) return [];
@@ -123,6 +130,23 @@ export const useUserStore = create<State>()(
);
});
return res;
},
myOrgs: [],
loadAndGetOrgs: async (init = false) => {
if (!useSystemStore.getState()?.feConfigs?.isPlus) return [];
const randomRefresh = Math.random() > 0.7;
if (!randomRefresh && !init && get().myOrgs.length) return Promise.resolve(get().myOrgs);
const res = await getOrgList();
set((state) => {
state.teamOrgs = res;
state.myOrgs = res.filter((item) =>
item.members.map((i) => String(i.tmbId)).includes(String(state.userInfo?.team?.tmbId))
);
});
return res;
}
})),