Update permission (#1522)

* Permission (#1442)

* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* feat: add permission display in the team manager modal

* feat: add permission i18n

* feat: let team module acquire permission ablity

* feat: add ownerPermission property into metaData

* feat: team premission system

* feat: extract the resourcePermission from resource schemas

* fix: move enum definition to constant

* feat: auth member permission handler, invite user

* feat: permission manage

* feat: adjust the style

* feat: team card style
- add a new icon

* feat: team permission in guest mode

* chore: change the type

* chore: delete useless file

* chore: delete useless code

* feat: do not show owner in PermissionManage view

* chore: fix style

* fix: icon remove fill

* feat: adjust the codes

---------

Co-authored-by: Archer <545436317@qq.com>

* perf: permission modal

* lock

---------

Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
This commit is contained in:
Archer
2024-05-17 17:42:33 +08:00
committed by GitHub
parent 67c52992d7
commit 2f93dedfb6
30 changed files with 1079 additions and 377 deletions

View File

@@ -24,6 +24,7 @@ usageMatchRegex:
- "[^\\w\\d]fileT\\(['\"`]({key})['\"`]"
- "[^\\w\\d]publishT\\(['\"`]({key})['\"`]"
- "[^\\w\\d]workflowT\\(['\"`]({key})['\"`]"
- "[^\\w\\d]userT\\(['\"`]({key})['\"`]"
# A RegEx to set a custom scope range. This scope will be used as a prefix when detecting keys
# and works like how the i18next framework identifies the namespace scope from the

View File

@@ -20,3 +20,9 @@ export const PermissionTypeMap = {
label: 'permission.Public'
}
};
export enum ResourceTypeEnum {
team = 'team',
app = 'app',
dataset = 'dataset'
}

View File

@@ -1,5 +1,7 @@
import { AuthUserTypeEnum } from './constant';
export type PermissionValueType = number;
export type AuthResponseType = {
teamId: string;
tmbId: string;
@@ -9,3 +11,10 @@ export type AuthResponseType = {
appId?: string;
apikey?: string;
};
export type ResourcePermissionType = {
teamId: string;
tmbId: string;
resourceType: ResourceType;
permission: PermissionValueType;
};

View File

@@ -1,3 +1,4 @@
import { PermissionValueType } from 'support/permission/type';
import { TeamMemberRoleEnum } from './constant';
import { LafAccountType, TeamMemberSchema } from './type';
@@ -44,3 +45,9 @@ export type InviteMemberResponse = Record<
'invite' | 'inValid' | 'inTeam',
{ username: string; userId: string }[]
>;
export type UpdateTeamMemberPermissionProps = {
teamId: string;
memberIds: string[];
permission: PermissionValueType;
};

View File

@@ -1,6 +1,7 @@
import type { UserModelSchema } from '../type';
import type { TeamMemberRoleEnum, TeamMemberStatusEnum } from './constant';
import { LafAccountType } from './type';
import { PermissionValueType, ResourcePermissionType } from '../../permission/type';
export type TeamSchema = {
_id: string;
@@ -15,6 +16,7 @@ export type TeamSchema = {
lastWebsiteSyncTime: Date;
};
lafAccount: LafAccountType;
defaultPermission: PermissionValueType;
};
export type tagsType = {
label: string;
@@ -61,6 +63,7 @@ export type TeamItemType = {
status: `${TeamMemberStatusEnum}`;
canWrite: boolean;
lafAccount?: LafAccountType;
defaultPermission: PermissionValueType;
};
export type TeamMemberItemType = {
@@ -69,8 +72,10 @@ export type TeamMemberItemType = {
teamId: string;
memberName: string;
avatar: string;
// TODO: this should be deprecated.
role: `${TeamMemberRoleEnum}`;
status: `${TeamMemberStatusEnum}`;
permission: PermissionValueType;
};
export type TeamTagItemType = {

View File

@@ -0,0 +1,16 @@
import { ResourcePermissionType } from '@fastgpt/global/support/permission/type';
import { MongoResourcePermission } from './schema';
import { ResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
export async function getResourcePermission({
tmbId,
resourceType
}: {
tmbId: string;
resourceType: ResourceTypeEnum;
}) {
return (await MongoResourcePermission.findOne({
tmbId,
resourceType
})) as ResourcePermissionType;
}

View File

@@ -0,0 +1,127 @@
// PermissionValueType, the type of permission's value is a number, which is a bit field actually.
// It is spired by the permission system in Linux.
// The lowest 3 bits present the permission of reading, writing and managing.
// The higher bits are advanced permissions or extended permissions, which could be customized.
export type PermissionValueType = number;
export type PermissionListType = { [key: string]: PermissionValueType };
export const NullPermission: PermissionValueType = 0;
// the Permission helper class
export class Permission {
value: PermissionValueType;
constructor(value: PermissionValueType) {
this.value = value;
}
// add permission(s)
// it can be chaining called.
// @example
// const perm = new Permission(permission)
// perm.add(PermissionList['read'])
// perm.add(PermissionList['read'], PermissionList['write'])
// perm.add(PermissionList['read']).add(PermissionList['write'])
add(...perm: PermissionValueType[]): Permission {
for (let p of perm) {
this.value = addPermission(this.value, p);
}
return this;
}
remove(...perm: PermissionValueType[]): Permission {
for (let p of perm) {
this.value = removePermission(this.value, p);
}
return this;
}
check(perm: PermissionValueType): Permission | boolean {
if (checkPermission(this.value, perm)) {
return this;
} else {
return false;
}
}
}
export function constructPermission(permList: PermissionValueType[]) {
return new Permission(NullPermission).add(...permList);
}
// The base Permissions List
// It can be extended, for example:
// export const UserPermissionList: PermissionListType = {
// ...PermissionList,
// 'Invite': 0b1000
// }
export const PermissionList: PermissionListType = {
Read: 0b100,
Write: 0b010,
Manage: 0b001
};
// list of permissions. could be customized.
// ! removal of the basic permissions is not recommended.
// const PermList: Array<PermissionType> = [ReadPerm, WritePerm, ManagePerm];
// return the list of permissions
// @param Perm(optional): the list of permissions to be added
// export function getPermList(Perm?: PermissionType[]): Array<PermissionType> {
// if (Perm === undefined) {
// return PermList;
// } else {
// return PermList.concat(Perm);
// }
// }
// check the permission
// @param [val]: The permission value to be checked
// @parma [perm]: Which Permission value will be checked
// @returns [booean]: if the [val] has the [perm]
// example:
// const perm = user.permission // get this permisiion from db or somewhere else
// const ok = checkPermission(perm, PermissionList['Read'])
export function checkPermission(val: PermissionValueType, perm: PermissionValueType): boolean {
return (val & perm) === perm;
}
// add the permission
// it can be chaining called.
// return the new permission value based on [val] added with [perm]
// @param val: PermissionValueType
// @param perm: PermissionValueType
// example:
// const basePerm = 0b001; // Manage only
export function addPermission(
val: PermissionValueType,
perm: PermissionValueType
): PermissionValueType {
return val | perm;
}
// remove the permission
export function removePermission(
val: PermissionValueType,
perm: PermissionValueType
): PermissionValueType {
return val & ~perm;
}
// export function parsePermission(val: PermissionValueType, list: PermissionValueType[]) {
// const result: [[string, boolean]] = [] as any;
// list.forEach((perm) => {
// result.push([perm[0], checkPermission(val, perm)]);
// });
// return result;
// }
export function hasManage(val: PermissionValueType) {
return checkPermission(val, PermissionList['Manage']);
}
export function hasWrite(val: PermissionValueType) {
return checkPermission(val, PermissionList['Write']);
}
export function hasRead(val: PermissionValueType) {
return checkPermission(val, PermissionList['Read']);
}

View File

@@ -0,0 +1,48 @@
import {
TeamCollectionName,
TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant';
import { Model, connectionMongo } from '../../../common/mongo';
import type { ResourcePermissionType } from '@fastgpt/global/support/permission/type';
import { ResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
const { Schema, model, models } = connectionMongo;
export const ResourcePermissionSchema = new Schema({
teamId: {
type: Schema.Types.ObjectId,
ref: TeamCollectionName
},
tmbId: {
type: Schema.Types.ObjectId,
ref: TeamMemberCollectionName
},
resourceType: {
type: Object.values(ResourceTypeEnum),
required: true
},
permission: {
type: Number,
required: true
}
});
try {
ResourcePermissionSchema.index({
teamId: 1,
resourceType: 1
});
ResourcePermissionSchema.index({
tmbId: 1,
resourceType: 1
});
} catch (error) {
console.log(error);
}
export const ResourcePermissionCollectionName = 'resource_permission';
export const MongoResourcePermission: Model<ResourcePermissionType> =
models[ResourcePermissionCollectionName] ||
model(ResourcePermissionCollectionName, ResourcePermissionSchema);
MongoResourcePermission.syncIndexes();

View File

@@ -27,7 +27,8 @@ async function getTeamMember(match: Record<string, any>): Promise<TeamItemType>
status: tmb.status,
defaultTeam: tmb.defaultTeam,
canWrite: tmb.role !== TeamMemberRoleEnum.visitor,
lafAccount: tmb.teamId.lafAccount
lafAccount: tmb.teamId.lafAccount,
defaultPermission: tmb.teamId.defaultPermission
};
}

View File

@@ -3,6 +3,7 @@ const { Schema, model, models } = connectionMongo;
import { TeamSchema as TeamType } from '@fastgpt/global/support/user/team/type.d';
import { userCollectionName } from '../../user/schema';
import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant';
import { NullPermission } from '../../permission/resourcePermission/permisson';
const TeamSchema = new Schema({
name: {
@@ -13,6 +14,10 @@ const TeamSchema = new Schema({
type: Schema.Types.ObjectId,
ref: userCollectionName
},
defaultPermission: {
type: Number,
default: NullPermission
},
avatar: {
type: String,
default: '/icon/logo.svg'

View File

@@ -53,6 +53,7 @@ export const iconPaths = {
'common/settingLight': () => import('./icons/common/settingLight.svg'),
'common/text/t': () => import('./icons/common/text/t.svg'),
'common/tickFill': () => import('./icons/common/tickFill.svg'),
'common/trash': () => import('./icons/common/trash.svg'),
'common/uploadFileFill': () => import('./icons/common/uploadFileFill.svg'),
'common/viewLight': () => import('./icons/common/viewLight.svg'),
'common/voiceLight': () => import('./icons/common/voiceLight.svg'),
@@ -207,8 +208,10 @@ export const iconPaths = {
'support/outlink/iframeLight': () => import('./icons/support/outlink/iframeLight.svg'),
'support/outlink/share': () => import('./icons/support/outlink/share.svg'),
'support/outlink/shareLight': () => import('./icons/support/outlink/shareLight.svg'),
'support/permission/collaborator': () => import('./icons/support/permission/collaborator.svg'),
'support/permission/privateLight': () => import('./icons/support/permission/privateLight.svg'),
'support/permission/publicLight': () => import('./icons/support/permission/publicLight.svg'),
'support/team/key': () => import('./icons/support/team/key.svg'),
'support/team/memberLight': () => import('./icons/support/team/memberLight.svg'),
'support/usage/usageRecordLight': () => import('./icons/support/usage/usageRecordLight.svg'),
'support/user/individuation': () => import('./icons/support/user/individuation.svg'),

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.48246 1.18066H8.51749C8.8405 1.18065 9.12217 1.18064 9.35477 1.19965C9.60121 1.21978 9.85067 1.26453 10.0919 1.38746C10.4489 1.56936 10.7392 1.8596 10.9211 2.2166C11.044 2.45787 11.0888 2.70733 11.1089 2.95376C11.1249 3.14983 11.1274 3.38075 11.1278 3.6419H13.5377C13.9059 3.6419 14.2044 3.94037 14.2044 4.30856C14.2044 4.67675 13.9059 4.97523 13.5377 4.97523H12.9738V11.2272C12.9738 11.721 12.9738 12.1316 12.9464 12.4667C12.9179 12.8157 12.8564 13.1412 12.6999 13.4483C12.459 13.921 12.0747 14.3054 11.6019 14.5463C11.2948 14.7028 10.9694 14.7643 10.6203 14.7928C10.2852 14.8202 9.87463 14.8202 9.38088 14.8202H6.61907C6.12531 14.8202 5.71472 14.8202 5.37961 14.7928C5.03057 14.7643 4.70512 14.7028 4.39805 14.5463C3.92527 14.3054 3.5409 13.921 3.3 13.4483C3.14354 13.1412 3.08203 12.8157 3.05351 12.4667C3.02613 12.1316 3.02614 11.721 3.02615 11.2272L3.02615 4.97523H2.4622C2.09401 4.97523 1.79553 4.67675 1.79553 4.30856C1.79553 3.94037 2.09401 3.6419 2.4622 3.6419H4.87214C4.87253 3.38075 4.87503 3.14983 4.89105 2.95376C4.91119 2.70733 4.95593 2.45787 5.07886 2.2166C5.26076 1.8596 5.55101 1.56936 5.90801 1.38746C6.14927 1.26453 6.39874 1.21978 6.64517 1.19965C6.87778 1.18064 7.15945 1.18065 7.48246 1.18066ZM4.35948 4.97523V11.2C4.35948 11.7279 4.36 12.0838 4.38242 12.3581C4.4042 12.6247 4.44328 12.7552 4.48801 12.8429C4.60107 13.0648 4.78148 13.2452 5.00337 13.3583C5.09115 13.403 5.2216 13.4421 5.48818 13.4639C5.76255 13.4863 6.11839 13.4868 6.64629 13.4868H9.35365C9.88156 13.4868 10.2374 13.4863 10.5118 13.4639C10.7783 13.4421 10.9088 13.403 10.9966 13.3583C11.2185 13.2452 11.3989 13.0648 11.5119 12.8429C11.5567 12.7552 11.5957 12.6247 11.6175 12.3581C11.6399 12.0838 11.6405 11.7279 11.6405 11.2V4.97523H4.35948ZM9.79442 3.6419H6.20552C6.206 3.38346 6.20837 3.20419 6.21996 3.06234C6.23336 2.89836 6.25568 2.8439 6.26687 2.82192C6.32094 2.71581 6.40721 2.62953 6.51333 2.57547C6.5353 2.56427 6.58977 2.54195 6.75375 2.52855C6.92551 2.51452 7.15212 2.514 7.50773 2.514H8.49222C8.84782 2.514 9.07443 2.51452 9.2462 2.52855C9.41018 2.54195 9.46464 2.56427 9.48662 2.57547C9.59273 2.62953 9.679 2.71581 9.73307 2.82192C9.74427 2.8439 9.76659 2.89836 9.77999 3.06234C9.79158 3.20419 9.79395 3.38346 9.79442 3.6419ZM6.76936 7.02609C7.13755 7.02609 7.43602 7.32457 7.43602 7.69276V10.7693C7.43602 11.1375 7.13755 11.436 6.76936 11.436C6.40117 11.436 6.10269 11.1375 6.10269 10.7693V7.69276C6.10269 7.32457 6.40117 7.02609 6.76936 7.02609ZM9.23059 7.02609C9.59878 7.02609 9.89726 7.32457 9.89726 7.69276V10.7693C9.89726 11.1375 9.59878 11.436 9.23059 11.436C8.8624 11.436 8.56392 11.1375 8.56392 10.7693V7.69276C8.56392 7.32457 8.8624 7.02609 9.23059 7.02609Z" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M7.00303 2.64414C5.69861 2.64414 4.64117 3.70158 4.64117 5.006C4.64117 6.31042 5.69861 7.36786 7.00303 7.36786C8.30744 7.36786 9.36488 6.31042 9.36488 5.006C9.36488 3.70158 8.30744 2.64414 7.00303 2.64414ZM2.9745 5.006C2.9745 2.78111 4.77813 0.977478 7.00303 0.977478C9.22792 0.977478 11.0315 2.78111 11.0315 5.006C11.0315 7.23089 9.22792 9.03453 7.00303 9.03453C4.77813 9.03453 2.9745 7.23089 2.9745 5.006ZM11.0234 1.73039C11.1961 1.30378 11.6819 1.09793 12.1085 1.27062C13.5833 1.86762 14.6261 3.31403 14.6261 5.006C14.6261 6.69797 13.5833 8.14439 12.1085 8.74138C11.6819 8.91407 11.1961 8.70823 11.0234 8.28161C10.8507 7.855 11.0565 7.36917 11.4831 7.19649C12.3502 6.84549 12.9595 5.9959 12.9595 5.006C12.9595 4.01611 12.3502 3.16651 11.4831 2.81552C11.0565 2.64283 10.8507 2.157 11.0234 1.73039ZM5.77537 10.563H9.00002C9.46026 10.563 9.83335 10.9361 9.83335 11.3964C9.83335 11.8566 9.46026 12.2297 9.00002 12.2297H5.80483C5.04904 12.2297 4.52394 12.2302 4.11329 12.2582C3.71013 12.2857 3.47852 12.337 3.30339 12.4095C2.72467 12.6492 2.26488 13.109 2.02516 13.6877C1.95262 13.8629 1.90135 14.0945 1.87385 14.4976C1.84583 14.9083 1.84538 15.4334 1.84538 16.1892C1.84538 16.6494 1.47228 17.0225 1.01204 17.0225C0.551807 17.0225 0.178711 16.6494 0.178711 16.1892L0.178711 16.1597C0.178705 15.4403 0.1787 14.8583 0.211047 14.3842C0.244344 13.8962 0.314685 13.462 0.485364 13.0499C0.894235 12.0628 1.67848 11.2786 2.66559 10.8697C3.07764 10.699 3.51182 10.6287 3.99984 10.5954C4.47392 10.563 5.05597 10.563 5.77537 10.563ZM14.5916 10.563C15.0518 10.563 15.4249 10.9361 15.4249 11.3964V12.9594H16.988C17.4482 12.9594 17.8213 13.3325 17.8213 13.7928C17.8213 14.253 17.4482 14.6261 16.988 14.6261H15.4249V16.1892C15.4249 16.6494 15.0518 17.0225 14.5916 17.0225C14.1314 17.0225 13.7583 16.6494 13.7583 16.1892V14.6261H12.1952C11.735 14.6261 11.3619 14.253 11.3619 13.7928C11.3619 13.3325 11.735 12.9594 12.1952 12.9594H13.7583V11.3964C13.7583 10.9361 14.1314 10.563 14.5916 10.563Z"
fill="#3370FF" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.76871 9.91581C11.7759 9.91581 13.4 8.29129 13.4 6.29107C13.4 4.29086 11.7759 2.66634 9.76871 2.66634C7.76153 2.66634 6.1374 4.29086 6.1374 6.29107C6.1374 6.62588 6.18253 6.94805 6.26627 7.25285C6.42305 7.82349 6.29244 8.48531 5.82219 8.95692L2.66824 12.12L2.66824 13.333H4.02373L4.05938 13.2974L4.41851 14.6377C4.34416 14.6566 4.26717 14.6663 4.18917 14.6663H2.1349C1.69307 14.6663 1.3349 14.3082 1.3349 13.8663L1.33491 11.9546C1.33491 11.7075 1.43288 11.4705 1.60733 11.2956L4.87803 8.01546C4.98449 7.90869 5.02052 7.75148 4.98058 7.60608C4.86552 7.18729 4.80407 6.74635 4.80407 6.29107C4.80407 3.55281 7.02682 1.33301 9.76871 1.33301C12.5106 1.33301 14.7334 3.55281 14.7334 6.29107C14.7334 9.02934 12.5106 11.2491 9.76871 11.2491C9.32077 11.2491 8.88667 11.1899 8.47387 11.0788C8.329 11.0398 8.1728 11.0761 8.06661 11.1821L7.12673 12.1201L6.18392 11.1773L7.12477 10.2383C7.59373 9.77034 8.25145 9.63822 8.82031 9.79128C9.12116 9.87223 9.4388 9.91581 9.76871 9.91581Z"/>
<path d="M4.91026 13.4301H5.48328C5.81465 13.4301 6.08332 13.1615 6.08332 12.8301L6.08328 12.8229V12.2571H6.74748C7.07885 12.2571 7.34748 11.9884 7.34748 11.6571C7.34748 11.3257 7.07885 11.0571 6.74748 11.0571H5.57442C5.5591 11.0571 5.5439 11.0576 5.52886 11.0588C5.51382 11.0576 5.49861 11.0571 5.48328 11.0571C5.15191 11.0571 4.88328 11.3257 4.88328 11.6571V12.2301H4.31026C3.97889 12.2301 3.71026 12.4987 3.71026 12.8301V14.0554C3.71026 14.3868 3.97889 14.6554 4.31026 14.6554C4.64163 14.6554 4.91026 14.3868 4.91026 14.0554V13.4301Z" />
<path d="M11.5713 5.75203C11.5713 6.48841 10.9744 7.08537 10.238 7.08537C9.50161 7.08537 8.90465 6.48841 8.90465 5.75203C8.90465 5.01565 9.50161 4.4187 10.238 4.4187C10.9744 4.4187 11.5713 5.01565 11.5713 5.75203Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1 +1 @@
<?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="1698724877512" class="icon" viewBox="0 0 1252 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4699" xmlns:xlink="http://www.w3.org/1999/xlink" width="156.5" height="128"><path d="M1164.136052 546.068492h-143.393656a199.609195 199.609195 0 0 1-70.816241-4.544965 36.927843 36.927843 0 0 1-16.305063-19.884224l-1.619144-8.52181a35.166669 35.166669 0 0 1 29.400245-37.495963c16.049409 0 32.070411 0 48.11982-0.284061h159.698719c41.955711 0 74.395401-5.681207 80.729946 31.246637a31.587509 31.587509 0 0 1-4.033657 20.168283c-13.095181 25.56543-44.767908 19.316103-81.780969 19.316103z m60.135572-199.126293a125.838727 125.838727 0 0 1-29.655899 1.136242H878.71223a100.670982 100.670982 0 0 1-33.405495-2.840604 36.586971 36.586971 0 0 1-18.463921-19.600163c-0.710151-2.556543-1.420302-5.397146-2.130453-8.237749a34.996233 34.996233 0 0 1 28.406033-39.200326 131.179061 131.179061 0 0 1 28.065161-0.568121h337.293238a35.393917 35.393917 0 0 1 5.823237 69.310721z m-618.228906 233.213533A437.736971 437.736971 0 0 1 874.905822 982.669222a41.330778 41.330778 0 0 1-82.633151 0 354.223234 354.223234 0 0 0-344.621994-353.371053h-20.395532A354.223234 354.223234 0 0 0 82.633151 982.669222a41.330778 41.330778 0 0 1-82.633151 0 437.736971 437.736971 0 0 1 267.755269-402.51349 314.710442 314.710442 0 1 1 338.287449 0zM434.015781 83.902332a231.622795 231.622795 0 0 0-6.760636 463.018341h20.395532a231.622795 231.622795 0 0 0-13.634896-463.018341zM1040.797056 682.985572a162.596134 162.596134 0 0 1 66.81099-4.829026h66.697365c40.535409 0 69.594781-3.976845 75.645267 31.246637a32.922592 32.922592 0 0 1-4.033657 20.452343c-12.157782 23.292947-37.80843 19.032042-72.435385 19.032043h-79.224426a158.647695 158.647695 0 0 1-50.534333-3.124664 35.59276 35.59276 0 0 1-18.151455-19.316103l-2.158859-8.521809A35.905226 35.905226 0 0 1 1040.797056 682.985572z" fill="#333333" p-id="4700"></path></svg>
<?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="1698724877512" class="icon" viewBox="0 0 1252 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4699" xmlns:xlink="http://www.w3.org/1999/xlink" width="156.5" height="128"><path d="M1164.136052 546.068492h-143.393656a199.609195 199.609195 0 0 1-70.816241-4.544965 36.927843 36.927843 0 0 1-16.305063-19.884224l-1.619144-8.52181a35.166669 35.166669 0 0 1 29.400245-37.495963c16.049409 0 32.070411 0 48.11982-0.284061h159.698719c41.955711 0 74.395401-5.681207 80.729946 31.246637a31.587509 31.587509 0 0 1-4.033657 20.168283c-13.095181 25.56543-44.767908 19.316103-81.780969 19.316103z m60.135572-199.126293a125.838727 125.838727 0 0 1-29.655899 1.136242H878.71223a100.670982 100.670982 0 0 1-33.405495-2.840604 36.586971 36.586971 0 0 1-18.463921-19.600163c-0.710151-2.556543-1.420302-5.397146-2.130453-8.237749a34.996233 34.996233 0 0 1 28.406033-39.200326 131.179061 131.179061 0 0 1 28.065161-0.568121h337.293238a35.393917 35.393917 0 0 1 5.823237 69.310721z m-618.228906 233.213533A437.736971 437.736971 0 0 1 874.905822 982.669222a41.330778 41.330778 0 0 1-82.633151 0 354.223234 354.223234 0 0 0-344.621994-353.371053h-20.395532A354.223234 354.223234 0 0 0 82.633151 982.669222a41.330778 41.330778 0 0 1-82.633151 0 437.736971 437.736971 0 0 1 267.755269-402.51349 314.710442 314.710442 0 1 1 338.287449 0zM434.015781 83.902332a231.622795 231.622795 0 0 0-6.760636 463.018341h20.395532a231.622795 231.622795 0 0 0-13.634896-463.018341zM1040.797056 682.985572a162.596134 162.596134 0 0 1 66.81099-4.829026h66.697365c40.535409 0 69.594781-3.976845 75.645267 31.246637a32.922592 32.922592 0 0 1-4.033657 20.452343c-12.157782 23.292947-37.80843 19.032042-72.435385 19.032043h-79.224426a158.647695 158.647695 0 0 1-50.534333-3.124664 35.59276 35.59276 0 0 1-18.151455-19.316103l-2.158859-8.521809A35.905226 35.905226 0 0 1 1040.797056 682.985572z" p-id="4700"></path></svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -93,6 +93,7 @@
"Rename Success": "Rename Success",
"Request Error": "Request Error",
"Require Input": "Required Input",
"Role": "Role",
"Root folder": "Root folder",
"Save": "Save",
"Save Failed": "Save Failed",
@@ -1563,7 +1564,7 @@
"Remove Member Failed": "Failed to remove team member",
"Remove Member Success": "Successfully removed team member",
"Remove Member Tip": "Remove from Team",
"Role": "Role",
"Role": "Role(Old)",
"Select Team": "Select Team",
"Set Name": "Name Your Team",
"Switch Team Failed": "Failed to switch team",

View File

@@ -0,0 +1 @@
{}

View File

@@ -82,6 +82,7 @@
"Output": "输出",
"Params": "参数",
"Password inconsistency": "两次密码不一致",
"Permission": "权限",
"Please Input Name": "请输入名称",
"Price used": "金额消耗",
"Read document": "查看文档",
@@ -93,6 +94,7 @@
"Rename Success": "重命名成功",
"Request Error": "请求异常",
"Require Input": "必填",
"Role": "权限",
"Root folder": "根目录",
"Save": "保存",
"Save Failed": "保存失败",
@@ -1569,7 +1571,7 @@
"Remove Member Failed": "移除团队成员异常",
"Remove Member Success": "移除团队成员成功",
"Remove Member Tip": "移出团队",
"Role": "身份",
"Role": "身份(旧版)",
"Select Team": "团队选择",
"Set Name": "给团队取个名字",
"Switch Team Failed": "切换团队异常",

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { Flex, Box, BoxProps, border } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
type Props = BoxProps & {
list: {
icon?: string;
label: string | React.ReactNode;
value: string;
}[];
value: string;
onChange: (e: string) => void;
};
const RowTabs = ({ list, value, onChange, py = '7px', px = '12px', ...props }: Props) => {
return (
<Box display={'inline-flex'} px={'3px'} {...props}>
{list.map((item) => (
<Flex
key={item.value}
flex={'1 0 0'}
alignItems={'center'}
cursor={'pointer'}
px={px}
py={py}
userSelect={'none'}
whiteSpace={'noWrap'}
borderBottom={'2px solid'}
{...(value === item.value
? {
bg: 'white',
color: 'primary.600',
borderColor: 'primary.600'
}
: {
borderColor: 'myGray.100',
onClick: () => onChange(item.value)
})}
>
{item.icon && <MyIcon name={item.icon as any} mr={1} w={'14px'} />}
<Box>{item.label}</Box>
</Flex>
))}
</Box>
);
};
export default RowTabs;

View File

@@ -0,0 +1,174 @@
import {
Box,
Button,
ModalBody,
ModalCloseButton,
ModalFooter,
Grid,
Input,
Flex,
Checkbox,
CloseButton,
InputGroup,
InputLeftElement
} from '@chakra-ui/react';
import Avatar from '@/components/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '.';
import {
hasManage,
constructPermission,
PermissionList
} from '@fastgpt/service/support/permission/resourcePermission/permisson';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { updateMemberPermission } from '@/web/support/user/team/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import MyIcon from '@fastgpt/web/components/common/Icon';
function AddManagerModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers);
const members = useContextSelector(TeamContext, (v) =>
v.members.filter((member) => {
return member.tmbId != userInfo!.team.tmbId && !hasManage(member.permission);
})
);
const [selected, setSelected] = useState<typeof members>([]);
const [search, setSearch] = useState<string>('');
const [searched, setSearched] = useState<typeof members>(members);
const { mutate: submit, isLoading } = useRequest({
mutationFn: async () => {
console.log(selected);
return updateMemberPermission({
teamId: userInfo!.team.teamId,
permission: constructPermission([
PermissionList['Read'],
PermissionList['Write'],
PermissionList['Manage']
]).value,
memberIds: selected.map((item) => {
return item.tmbId;
})
});
},
onSuccess: () => {
refetchMembers();
onSuccess();
},
successToast: '成功',
errorToast: '失败'
});
return (
<MyModal
isOpen
iconSrc={'support/permission/collaborator'}
maxW={['90vw']}
minW={['900px']}
overflow={'unset'}
title={
<Box>
<Box></Box>
</Box>
}
>
<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" h="32px" my="2" py="1">
<InputLeftElement>
<MyIcon name="common/searchLight" w="16px" color={'myGray.500'} />
</InputLeftElement>
<Input
placeholder="搜索用户名"
fontSize="lg"
bg={'myGray.50'}
onChange={(e) => {
setSearch(e.target.value);
setSearched(
members.filter((member) => member.memberName.includes(e.target.value))
);
}}
/>
</InputGroup>
<Flex flexDirection="column" mt={3}>
{searched.map((member) => {
return (
<Flex
py="2"
px={3}
borderRadius={'md'}
fontSize="lg"
alignItems="center"
key={member.tmbId}
cursor={'pointer'}
_hover={{ bg: '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)} size="lg" />
<Avatar src={member.avatar} w="24px" />
{member.memberName}
</Flex>
);
})}
</Flex>
</Flex>
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4">
<Box mt={3}>: {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="24px" />
<Box w="full" fontSize="lg">
{member.memberName}
</Box>
<CloseButton
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}>
</Button>
</ModalFooter>
</MyModal>
);
}
export default AddManagerModal;

View File

@@ -0,0 +1,103 @@
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, MenuButton, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import {
TeamMemberRoleEnum,
TeamMemberRoleMap,
TeamMemberStatusMap
} from '@fastgpt/global/support/user/team/constant';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useTranslation } from 'react-i18next';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '.';
import { useUserStore } from '@/web/support/user/useUserStore';
import { hasManage } from '@fastgpt/service/support/permission/resourcePermission/permisson';
function MemberTable() {
const members = useContextSelector(TeamContext, (v) => v.members);
const openRemoveMember = useContextSelector(TeamContext, (v) => v.openRemoveMember);
const onRemoveMember = useContextSelector(TeamContext, (v) => v.onRemoveMember);
const { userInfo } = useUserStore();
const { t } = useTranslation();
return (
<TableContainer overflow={'unset'}>
<Table overflow={'unset'}>
<Thead bg={'myWhite.400'}>
<Tr>
<Th>{t('common.Username')}</Th>
<Th>{t('user.team.Role')}</Th>
<Th>{t('common.Status')}</Th>
<Th>{t('common.Action')}</Th>
</Tr>
</Thead>
<Tbody>
{members.map((item) => (
<Tr key={item.userId} overflow={'unset'}>
<Td display={'flex'} alignItems={'center'}>
<Avatar src={item.avatar} w={['18px', '22px']} />
<Box flex={'1 0 0'} w={0} ml={1} className={'textEllipsis'}>
{item.memberName}
</Box>
</Td>
<Td>{t(TeamMemberRoleMap[item.role]?.label || '')}</Td>
<Td color={TeamMemberStatusMap[item.status].color}>
{t(TeamMemberStatusMap[item.status]?.label || '')}
</Td>
<Td>
{hasManage(
members.find((item) => item.tmbId === userInfo?.team.tmbId)?.permission!
) &&
item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
<MyMenu
width={20}
trigger="hover"
Button={
<MenuButton
_hover={{
bg: 'myWhite.600'
}}
borderRadius={'md'}
px={2}
py={1}
lineHeight={1}
>
<MyIcon
name={'edit'}
cursor={'pointer'}
w="14px"
_hover={{ color: 'primary.500' }}
/>
</MenuButton>
}
menuList={[
{
label: t('user.team.Remove Member Tip'),
onClick: () =>
openRemoveMember(
() =>
onRemoveMember({
teamId: item.teamId,
memberId: item.tmbId
}),
undefined,
t('user.team.Remove Member Confirm Tip', {
username: item.memberName
})
)()
}
]}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
);
}
export default MemberTable;

View File

@@ -0,0 +1,123 @@
import React from 'react';
import { Box, Button, Flex, Tag, TagLabel, useDisclosure } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { TeamContext } from '.';
import { useContextSelector } from 'use-context-selector';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import AddManagerModal from './AddManager';
import { updateMemberPermission } from '@/web/support/user/team/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import {
constructPermission,
hasManage,
PermissionList
} from '@fastgpt/service/support/permission/resourcePermission/permisson';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
function PermissionManage() {
const { t } = useTranslation();
const members = useContextSelector(TeamContext, (v) => v.members);
const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers);
const { userInfo } = useUserStore();
const {
isOpen: isOpenAddManager,
onOpen: onOpenAddManager,
onClose: onCloseAddManager
} = useDisclosure();
const { mutate: removeManager } = useRequest({
mutationFn: async (memberId: string) => {
return updateMemberPermission({
teamId: userInfo!.team.teamId,
permission: constructPermission([PermissionList['Read'], PermissionList['Write']]).value,
memberIds: [memberId]
});
},
successToast: 'Success',
errorToast: 'Error',
onSuccess: () => {
refetchMembers();
},
onError: () => {
refetchMembers();
}
});
return (
<Flex flexDirection={'column'} flex={'1'} h={['auto', '100%']} bg={'white'}>
{isOpenAddManager && (
<AddManagerModal onClose={onCloseAddManager} onSuccess={onCloseAddManager} />
)}
<Flex
mx={'5'}
flexDirection={'row'}
alignItems={'center'}
rowGap={'8'}
justifyContent={'space-between'}
>
<Flex>
<Box fontSize={['md', 'lg']} fontWeight={'bold'} alignItems={'center'}>
{t('user.team.role.Admin')}
</Box>
<Box
fontSize={['xs']}
color={'myGray.500'}
bgColor={'myGray.100'}
alignItems={'center'}
alignContent={'center'}
mx={'6'}
px={'3'}
borderRadius={'sm'}
>
,
</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();
}}
>
</Button>
)}
</Flex>
<Flex mt="4" mx="4">
{members.map((member) => {
if (hasManage(member.permission) && member.role !== TeamMemberRoleEnum.owner) {
return (
<Tag key={member.memberName} mx={'2'} px="4" py="2" bg="myGray.100">
<Avatar src={member.avatar} w="20px" />
<TagLabel fontSize={'md'} alignItems="center" mr="6" ml="2">
{member.memberName}
</TagLabel>
{userInfo?.team.role === 'owner' && (
<MyIcon
name="common/trash"
w="16px"
color="myGray.500"
cursor="pointer"
onClick={() => {
removeManager(member.tmbId);
}}
/>
)}
</Tag>
);
}
})}
</Flex>
</Flex>
);
}
export default PermissionManage;

View File

@@ -0,0 +1,183 @@
import { Box, Button, Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { TeamContext } from '.';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useContextSelector } from 'use-context-selector';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import RowTabs from '../../../../common/Rowtabs';
import { useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { DragHandleIcon } from '@chakra-ui/icons';
import MemberTable from './MemberTable';
import PermissionManage from './PermissionManage';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { hasManage } from '@fastgpt/service/support/permission/resourcePermission/permisson';
import { useI18n } from '@/web/context/I18n';
type TabListType = Pick<React.ComponentProps<typeof RowTabs>, 'list'>['list'];
enum TabListEnum {
member = 'member',
permission = 'permission'
}
function TeamCard() {
const { toast } = useToast();
const { t } = useTranslation();
const { userT } = useI18n();
const members = useContextSelector(TeamContext, (v) => v.members);
const onOpenInvite = useContextSelector(TeamContext, (v) => v.onOpenInvite);
const onOpenTeamTagsAsync = useContextSelector(TeamContext, (v) => v.onOpenTeamTagsAsync);
const setEditTeamData = useContextSelector(TeamContext, (v) => v.setEditTeamData);
const onLeaveTeam = useContextSelector(TeamContext, (v) => v.onLeaveTeam);
const { userInfo, teamPlanStatus } = useUserStore();
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('user.team.member.Confirm Leave')
});
const { feConfigs } = useSystemStore();
const Tablist: TabListType = [
{
icon: 'support/team/memberLight',
label: (
<Flex alignItems={'center'}>
<Box ml={1}>{t('user.team.Member')}</Box>
<Box ml={2} bg={'myGray.100'} borderRadius={'20px'} px={3} fontSize={'xs'}>
{members.length}
</Box>
</Flex>
),
value: TabListEnum.member
},
{
icon: 'support/team/key',
label: t('common.Role'),
value: TabListEnum.permission
}
];
const [tab, setTab] = useState<string>(Tablist[0].value);
return (
<Flex
flexDirection={'column'}
flex={'1'}
h={['auto', '100%']}
bg={'white'}
minH={['50vh', 'auto']}
borderRadius={['8px 8px 0 0', '8px 0 0 8px']}
>
<Flex
alignItems={'center'}
px={5}
py={4}
borderBottom={'1.5px solid'}
borderBottomColor={'myGray.100'}
mb={3}
>
<Box fontSize={['lg', 'xl']} fontWeight={'bold'} alignItems={'center'}>
{userInfo?.team.teamName}
</Box>
{userInfo?.team.role === TeamMemberRoleEnum.owner && (
<MyIcon
name="edit"
w={'14px'}
ml={2}
cursor={'pointer'}
_hover={{
color: 'primary.500'
}}
onClick={() => {
if (!userInfo?.team) return;
setEditTeamData({
id: userInfo.team.teamId,
name: userInfo.team.teamName,
avatar: userInfo.team.avatar
});
}}
/>
)}
</Flex>
<Flex px={5} alignItems={'center'} justifyContent={'space-between'}>
<RowTabs
overflow={'auto'}
list={Tablist}
value={tab}
onChange={(v) => {
setTab(v as string);
}}
></RowTabs>
<Flex alignItems={'center'}>
{hasManage(
members.find((item) => item.tmbId.toString() === userInfo?.team.tmbId.toString())
?.permission!
) &&
tab === 'member' && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/inviteLight" w={'14px'} color={'primary.500'} />}
onClick={() => {
if (
teamPlanStatus?.standardConstants?.maxTeamMember &&
teamPlanStatus.standardConstants.maxTeamMember <= members.length
) {
toast({
status: 'warning',
title: t('user.team.Over Max Member Tip', {
max: teamPlanStatus.standardConstants.maxTeamMember
})
});
} else {
onOpenInvite();
}
}}
>
{t('user.team.Invite Member')}
</Button>
)}
{userInfo?.team.role === TeamMemberRoleEnum.owner && feConfigs?.show_team_chat && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<DragHandleIcon w={'14px'} color={'primary.500'} />}
onClick={() => {
onOpenTeamTagsAsync();
}}
>
{t('user.team.Team Tags Async')}
</Button>
)}
{userInfo?.team.role !== TeamMemberRoleEnum.owner && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
onClick={() => {
openLeaveConfirm(() => onLeaveTeam(userInfo?.team?.teamId))();
}}
>
{t('user.team.Leave Team')}
</Button>
)}
</Flex>
</Flex>
<Box mt={3} flex={'1 0 0'} overflow={'auto'}>
{tab === 'member' ? <MemberTable /> : <PermissionManage />}
</Box>
<ConfirmLeaveTeamModal />
</Flex>
);
}
export default TeamCard;

View File

@@ -0,0 +1,117 @@
import { Box, Button, Flex, IconButton } from '@chakra-ui/react';
import Avatar from '@/components/Avatar';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import EditModal, { defaultForm } from './EditModal';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '.';
function TeamList() {
const { t } = useTranslation();
const { userInfo, initUserInfo } = useUserStore();
const editTeamData = useContextSelector(TeamContext, (v) => v.editTeamData);
const setEditTeamData = useContextSelector(TeamContext, (v) => v.setEditTeamData);
const myTeams = useContextSelector(TeamContext, (v) => v.myTeams);
const refetchTeam = useContextSelector(TeamContext, (v) => v.refetchTeam);
const onSwitchTeam = useContextSelector(TeamContext, (v) => v.onSwitchTeam);
// get the list of teams
return (
<Flex
flexDirection={'column'}
w={['auto', '270px']}
h={['auto', '100%']}
pt={3}
px={5}
mb={[2, 0]}
>
<Flex
alignItems={'center'}
py={2}
h={'40px'}
borderBottom={'1.5px solid rgba(0, 0, 0, 0.05)'}
>
<Box flex={['0 0 auto', 1]} fontWeight={'bold'} fontSize={['md', 'lg']}>
{t('common.Team')}
</Box>
{/* if there is no team */}
{myTeams.length < 1 && (
<IconButton
variant={'ghost'}
border={'none'}
icon={
<MyIcon
name={'common/addCircleLight'}
w={['16px', '18px']}
color={'primary.500'}
cursor={'pointer'}
/>
}
aria-label={''}
onClick={() => setEditTeamData(defaultForm)}
/>
)}
</Flex>
<Box flex={['auto', '1 0 0']} overflow={'auto'}>
{myTeams.map((team) => (
<Flex
key={team.teamId}
alignItems={'center'}
mt={3}
borderRadius={'md'}
p={3}
cursor={'default'}
gap={3}
{...(userInfo?.team?.teamId === team.teamId
? {
bg: 'primary.200'
}
: {
_hover: {
bg: 'myGray.100'
}
})}
>
<Avatar src={team.avatar} w={['18px', '22px']} />
<Box
flex={'1 0 0'}
w={0}
{...(team.role === TeamMemberRoleEnum.owner
? {
fontWeight: 'bold'
}
: {})}
>
{team.teamName}
</Box>
{userInfo?.team?.teamId === team.teamId ? (
<MyIcon name={'common/tickFill'} w={'16px'} color={'primary.500'} />
) : (
<Button
size={'xs'}
variant={'whitePrimary'}
onClick={() => onSwitchTeam(team.teamId)}
>
{t('user.team.Check Team')}
</Button>
)}
</Flex>
))}
</Box>
{!!editTeamData && (
<EditModal
defaultData={editTeamData}
onClose={() => setEditTeamData(undefined)}
onSuccess={() => {
refetchTeam();
initUserInfo();
}}
/>
)}
</Flex>
);
}
export default TeamList;

View File

@@ -2,66 +2,59 @@ import React, { useMemo, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { DragHandleIcon } from '@chakra-ui/icons';
import {
getTeamList,
getTeamMembers,
putSwitchTeam,
putUpdateMember,
delRemoveMember,
delLeaveTeam
getTeamList,
delLeaveTeam,
putSwitchTeam
} from '@/web/support/user/team/api';
import {
Box,
Button,
Flex,
IconButton,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
useDisclosure,
MenuButton
} from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
import { Box, useDisclosure } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import {
TeamMemberRoleEnum,
TeamMemberRoleMap,
TeamMemberStatusEnum,
TeamMemberStatusMap
} from '@fastgpt/global/support/user/team/constant';
import dynamic from 'next/dynamic';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { setToken } from '@/web/support/user/auth';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { FormDataType, defaultForm } from './EditModal';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { createContext } from 'use-context-selector';
import TeamList from './TeamList';
import TeamCard from './TeamCard';
import { FormDataType } from './EditModal';
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
import { setToken } from '@/web/support/user/auth';
const EditModal = dynamic(() => import('./EditModal'));
const InviteModal = dynamic(() => import('./InviteModal'));
const TeamTagModal = dynamic(() => import('../TeamTagModal'));
export const TeamContext = createContext<{
editTeamData?: FormDataType;
setEditTeamData: React.Dispatch<React.SetStateAction<any>>;
members: Awaited<ReturnType<typeof getTeamMembers>>;
myTeams: Awaited<ReturnType<typeof getTeamList>>;
refetchTeam: ReturnType<typeof useQuery>['refetch'];
onSwitchTeam: ReturnType<typeof useRequest>['mutate'];
refetchMembers: ReturnType<typeof useQuery>['refetch'];
openRemoveMember: ReturnType<typeof useConfirm>['openConfirm'];
onOpenInvite: ReturnType<typeof useDisclosure>['onOpen'];
onOpenTeamTagsAsync: ReturnType<typeof useDisclosure>['onOpen'];
onRemoveMember: ReturnType<typeof useRequest>['mutate'];
onLeaveTeam: ReturnType<typeof useRequest>['mutate'];
}>({} as any);
const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { Loading } = useLoading();
const { toast } = useToast();
const { teamPlanStatus } = useUserStore();
const { feConfigs } = useSystemStore();
const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm();
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('user.team.member.Confirm Leave')
});
const { userInfo, initUserInfo } = useUserStore();
const {
data: myTeams = [],
isFetching: isLoadingTeams,
refetch: refetchTeam
} = useQuery(['getTeams', userInfo?._id], () => getTeamList(TeamMemberStatusEnum.active));
const [editTeamData, setEditTeamData] = useState<FormDataType>();
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const {
@@ -70,25 +63,6 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
onClose: onCloseTeamTagsAsync
} = useDisclosure();
const {
data: myTeams = [],
isFetching: isLoadingTeams,
refetch: refetchTeam
} = useQuery(['getTeams', userInfo?._id], () => getTeamList(TeamMemberStatusEnum.active));
const defaultTeam = useMemo(
() => myTeams.find((item) => item.defaultTeam) || myTeams[0],
[myTeams]
);
const { mutate: onSwitchTeam, isLoading: isSwitchTeam } = useRequest({
mutationFn: async (teamId: string) => {
const token = await putSwitchTeam(teamId);
token && setToken(token);
return initUserInfo();
},
errorToast: t('user.team.Switch Team Failed')
});
// member action
const { data: members = [], refetch: refetchMembers } = useQuery(
['getMembers', userInfo?.team?.teamId],
@@ -104,6 +78,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
refetchMembers();
}
});
const { mutate: onRemoveMember, isLoading: isLoadingRemoveMember } = useRequest({
mutationFn: delRemoveMember,
onSuccess() {
@@ -112,13 +87,27 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
successToast: t('user.team.Remove Member Success'),
errorToast: t('user.team.Remove Member Failed')
});
const defaultTeam = useMemo(
() => myTeams.find((item) => item.defaultTeam) || myTeams[0],
[myTeams]
);
const { mutate: onSwitchTeam, isLoading: isSwitchTeam } = useRequest({
mutationFn: async (teamId: string) => {
const token = await putSwitchTeam(teamId);
token && setToken(token);
return initUserInfo();
},
errorToast: t('user.team.Switch Team Failed')
});
const { mutate: onLeaveTeam, isLoading: isLoadingLeaveTeam } = useRequest({
mutationFn: async (teamId?: string) => {
if (!teamId) return;
// change to personal team
// get members
await onSwitchTeam(defaultTeam.teamId);
onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam(teamId);
},
onSuccess() {
@@ -128,7 +117,22 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
});
return !!userInfo?.team ? (
<>
<TeamContext.Provider
value={{
myTeams: myTeams,
refetchTeam: refetchTeam,
onSwitchTeam: onSwitchTeam,
members: members,
refetchMembers: refetchMembers,
openRemoveMember: openRemoveMember,
onOpenInvite: onOpenInvite,
onOpenTeamTagsAsync: onOpenTeamTagsAsync,
onRemoveMember: onRemoveMember,
editTeamData: editTeamData,
setEditTeamData: setEditTeamData,
onLeaveTeam: onLeaveTeam
}}
>
<MyModal
isOpen
onClose={onClose}
@@ -140,323 +144,20 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
overflow={'hidden'}
>
<Box display={['block', 'flex']} flex={1} position={'relative'} overflow={'auto'}>
{/* teams */}
<Flex
flexDirection={'column'}
w={['auto', '270px']}
h={['auto', '100%']}
pt={3}
px={5}
mb={[2, 0]}
>
<Flex
alignItems={'center'}
py={2}
h={'40px'}
borderBottom={'1.5px solid rgba(0, 0, 0, 0.05)'}
>
<Box flex={['0 0 auto', 1]} fontWeight={'bold'} fontSize={['md', 'lg']}>
{t('common.Team')}
</Box>
{myTeams.length < 1 && (
<IconButton
variant={'ghost'}
border={'none'}
icon={
<MyIcon
name={'common/addCircleLight'}
w={['16px', '18px']}
color={'primary.500'}
cursor={'pointer'}
/>
}
aria-label={''}
onClick={() => setEditTeamData(defaultForm)}
/>
)}
</Flex>
<Box flex={['auto', '1 0 0']} overflow={'auto'}>
{myTeams.map((team) => (
<Flex
key={team.teamId}
alignItems={'center'}
mt={3}
borderRadius={'md'}
p={3}
cursor={'default'}
gap={3}
{...(userInfo?.team?.teamId === team.teamId
? {
bg: 'primary.200'
}
: {
_hover: {
bg: 'myGray.100'
}
})}
>
<Avatar src={team.avatar} w={['18px', '22px']} />
<Box
flex={'1 0 0'}
w={0}
{...(team.role === TeamMemberRoleEnum.owner
? {
fontWeight: 'bold'
}
: {})}
>
{team.teamName}
</Box>
{userInfo?.team?.teamId === team.teamId ? (
<MyIcon name={'common/tickFill'} w={'16px'} color={'primary.500'} />
) : (
<Button
size={'xs'}
variant={'whitePrimary'}
onClick={() => onSwitchTeam(team.teamId)}
>
{t('user.team.Check Team')}
</Button>
)}
</Flex>
))}
</Box>
</Flex>
{/* team card */}
<Flex
flexDirection={'column'}
flex={'1'}
h={['auto', '100%']}
bg={'white'}
minH={['50vh', 'auto']}
borderRadius={['8px 8px 0 0', '8px 0 0 8px']}
>
<Flex
alignItems={'center'}
px={5}
py={4}
borderBottom={'1.5px solid'}
borderBottomColor={'myGray.100'}
mb={3}
>
<Box fontSize={['lg', 'xl']} fontWeight={'bold'} alignItems={'center'}>
{userInfo.team.teamName}
</Box>
{userInfo.team.role === TeamMemberRoleEnum.owner && (
<MyIcon
name="edit"
w={'14px'}
ml={2}
cursor={'pointer'}
_hover={{
color: 'primary.500'
}}
onClick={() => {
if (!userInfo?.team) return;
setEditTeamData({
id: userInfo.team.teamId,
name: userInfo.team.teamName,
avatar: userInfo.team.avatar
});
}}
/>
)}
</Flex>
<Flex px={5} alignItems={'center'}>
<MyIcon name="support/team/memberLight" w={'14px'} />
<Box ml={1}>{t('user.team.Member')}</Box>
<Box ml={2} bg={'myGray.100'} borderRadius={'20px'} px={3} fontSize={'xs'}>
{members.length}
</Box>
{userInfo.team.role === TeamMemberRoleEnum.owner && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'common/inviteLight'} w={'14px'} color={'primary.500'} />}
onClick={() => {
if (
teamPlanStatus?.standardConstants?.maxTeamMember &&
teamPlanStatus.standardConstants.maxTeamMember <= members.length
) {
toast({
status: 'warning',
title: t('user.team.Over Max Member Tip', {
max: teamPlanStatus.standardConstants.maxTeamMember
})
});
} else {
onOpenInvite();
}
}}
>
{t('user.team.Invite Member')}
</Button>
)}
{userInfo.team.role === TeamMemberRoleEnum.owner && feConfigs?.show_team_chat && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<DragHandleIcon w={'14px'} color={'primary.500'} />}
onClick={() => {
onOpenTeamTagsAsync();
}}
>
{t('user.team.Team Tags Async')}
</Button>
)}
<Box flex={1} />
{userInfo.team.role !== TeamMemberRoleEnum.owner && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
onClick={() => {
openLeaveConfirm(() => onLeaveTeam(userInfo?.team?.teamId))();
}}
>
{t('user.team.Leave Team')}
</Button>
)}
</Flex>
<Box mt={3} flex={'1 0 0'} overflow={'auto'}>
<TableContainer overflow={'unset'}>
<Table overflow={'unset'}>
<Thead bg={'myWhite.400'}>
<Tr>
<Th>{t('common.Username')}</Th>
<Th>{t('user.team.Role')}</Th>
<Th>{t('common.Status')}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{members.map((item) => (
<Tr key={item.userId} overflow={'unset'}>
<Td display={'flex'} alignItems={'center'}>
<Avatar src={item.avatar} w={['18px', '22px']} />
<Box flex={'1 0 0'} w={0} ml={1} className={'textEllipsis'}>
{item.memberName}
</Box>
</Td>
<Td>{t(TeamMemberRoleMap[item.role]?.label || '')}</Td>
<Td color={TeamMemberStatusMap[item.status].color}>
{t(TeamMemberStatusMap[item.status]?.label || '')}
</Td>
<Td>
{userInfo?.team?.role === TeamMemberRoleEnum.owner &&
item.role !== TeamMemberRoleEnum.owner && (
<MyMenu
width={20}
trigger="click"
Button={
<MenuButton
_hover={{
bg: 'myWhite.600'
}}
borderRadius={'md'}
px={2}
py={1}
lineHeight={1}
>
<MyIcon
name={'edit'}
cursor={'pointer'}
w="14px"
_hover={{ color: 'primary.500' }}
/>
</MenuButton>
}
menuList={[
{
isActive: item.role === TeamMemberRoleEnum.visitor,
label: t('user.team.Invite Role Visitor Tip'),
onClick: () => {
onUpdateMember({
teamId: item.teamId,
memberId: item.tmbId,
role: TeamMemberRoleEnum.visitor
});
}
},
{
isActive: item.role === TeamMemberRoleEnum.admin,
label: t('user.team.Invite Role Admin Tip'),
onClick: () => {
onUpdateMember({
teamId: item.teamId,
memberId: item.tmbId,
role: TeamMemberRoleEnum.admin
});
}
},
...(item.status === TeamMemberStatusEnum.reject
? [
{
label: t('user.team.Reinvite'),
onClick: () => {
onUpdateMember({
teamId: item.teamId,
memberId: item.tmbId,
status: TeamMemberStatusEnum.waiting
});
}
}
]
: []),
{
label: t('user.team.Remove Member Tip'),
onClick: () =>
openRemoveMember(
() =>
onRemoveMember({
teamId: item.teamId,
memberId: item.tmbId
}),
undefined,
t('user.team.Remove Member Confirm Tip', {
username: item.memberName
})
)()
}
]}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
</Flex>
<TeamList />
<TeamCard />
<Loading
loading={
isSwitchTeam ||
isLoadingTeams ||
isLoadingUpdateMember ||
isLoadingRemoveMember ||
isLoadingLeaveTeam
isLoadingTeams ||
isLoadingLeaveTeam ||
isSwitchTeam
}
fixed={false}
/>
</Box>
</MyModal>
{!!editTeamData && (
<EditModal
defaultData={editTeamData}
onClose={() => setEditTeamData(undefined)}
onSuccess={() => {
refetchTeam();
initUserInfo();
}}
/>
)}
{isOpenInvite && userInfo?.team?.teamId && (
<InviteModal
teamId={userInfo.team.teamId}
@@ -466,8 +167,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
)}
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
<ConfirmRemoveMemberModal />
<ConfirmLeaveTeamModal />
</>
</TeamContext.Provider>
) : null;
};

View File

@@ -190,7 +190,7 @@ export async function getServerSideProps(content: any) {
return {
props: {
currentTab: content?.query?.currentTab || TabEnum.info,
...(await serviceSideProps(content, ['publish']))
...(await serviceSideProps(content, ['publish', 'user']))
}
};
}

View File

@@ -5,6 +5,7 @@ import app from '../../i18n/zh/app.json';
import file from '../../i18n/zh/file.json';
import publish from '../../i18n/zh/publish.json';
import workflow from '../../i18n/zh/workflow.json';
import user from '../../i18n/zh/user.json';
export interface I18nNamespaces {
common: typeof common;
@@ -13,6 +14,7 @@ export interface I18nNamespaces {
file: typeof file;
publish: typeof publish;
workflow: typeof workflow;
user: typeof user;
}
export type I18nNsType = (keyof I18nNamespaces)[];

View File

@@ -9,6 +9,7 @@ type I18nContextType = {
fileT: TFunction<['file'], undefined>;
publishT: TFunction<['publish'], undefined>;
workflowT: TFunction<['workflow'], undefined>;
userT: TFunction<['user'], undefined>;
};
export const I18nContext = createContext<I18nContextType>({
@@ -23,6 +24,7 @@ const I18nContextProvider = ({ children }: { children: React.ReactNode }) => {
const { t: fileT } = useTranslation('file');
const { t: publishT } = useTranslation('publish');
const { t: workflowT } = useTranslation('workflow');
const { t: userT } = useTranslation('user');
return (
<I18nContext.Provider
@@ -32,7 +34,8 @@ const I18nContextProvider = ({ children }: { children: React.ReactNode }) => {
datasetT,
fileT,
publishT,
workflowT
workflowT,
userT
}}
>
{children}

View File

@@ -5,6 +5,7 @@ import {
InviteMemberProps,
InviteMemberResponse,
UpdateInviteProps,
UpdateTeamMemberPermissionProps,
UpdateTeamMemberProps,
UpdateTeamProps
} from '@fastgpt/global/support/user/team/controller.d';
@@ -41,6 +42,8 @@ export const updateInviteResult = (data: UpdateInviteProps) =>
PUT('/proApi/support/user/team/member/updateInvite', data);
export const delLeaveTeam = (teamId: string) =>
DELETE('/proApi/support/user/team/member/leave', { teamId });
export const updateMemberPermission = (data: UpdateTeamMemberPermissionProps) =>
PUT('/proApi/support/user/team/member/updatePermission', data);
/* --------------- team tags ---------------- */
export const getTeamsTags = () => GET<TeamTagSchema[]>(`/proApi/support/user/team/tag/list`);