4.8.18 test (#3543)

* perf: login check

* doc

* perf: llm model config

* perf: team clb config
This commit is contained in:
Archer
2025-01-07 14:21:05 +08:00
committed by GitHub
parent 07cc849877
commit f556fbf0d5
49 changed files with 1644 additions and 1535 deletions

View File

@@ -10,10 +10,13 @@ weight: 806
## 完整更新内容
1.
2. 新增 - 支持部门架构权限模式
3. 优化 - 图片上传安全校验。并增加头像图片唯一存储,确保不会累计存储
4. 优化 - Mongo 全文索引表分离
5. 优化 - 知识库检索查询语句合并,同时减少查库数量
6. 优化 - 文件编码检测,减少 CSV 文件乱码概率
7. 优化 - 异步读取文件内容,减少进程阻塞
8. 修复 - HTML 文件上传base64 图片无法自动转图片链接。
2. 新增 - 支持部门架构权限模式
3. 新增 - 支持配置自定跨域安全策略,默认全开
4. 优化 - 图片上传安全校验。并增加头像图片唯一存储,确保不会累计存储
5. 优化 - Mongo 全文索引表分离
6. 优化 - 知识库检索查询语句合并,同时减少查库数量
7. 优化 - 文件编码检测,减少 CSV 文件乱码概率
8. 优化 - 异步读取文件内容,减少进程阻塞。
9. 优化 - 文件阅读HTML 直接下载,不允许在线阅读。
10. 修复 - HTML 文件上传base64 图片无法自动转图片链接。
11. 修复 - 插件计费错误。

View File

@@ -53,6 +53,7 @@ export type VectorModelItemType = PriceType & {
};
export type ReRankModelItemType = PriceType & {
provider: ModelProviderIdType;
model: string;
name: string;
requestUrl: string;

View File

@@ -20,40 +20,8 @@ export type UpdateClbPermissionProps = {
permission: PermissionValueType;
};
export type DeleteClbPermissionProps = RequireOnlyOne<{
tmbId: string;
groupId: string;
orgId: string;
}>;
export type UpdatePermissionBody = {
permission: PermissionValueType;
} & RequireOnlyOne<{
memberId: string;
groupId: string;
orgId: string;
}>;
export type CreatePermissionBody = {
tmbId: string[];
groupId: string[];
orgId: string[];
};
export type DeletePermissionQuery = RequireOnlyOne<{
tmbId?: string;
groupId?: string;
orgId?: string;
}>;
export type TeamClbsListType = {
permission: number;
name: string;
avatar: string;
};
export type ListPermissionResponse = {
tmb: (TeamClbsListType & { tmbId: string })[];
group: (TeamClbsListType & { groupId: string })[];
org: (TeamClbsListType & { orgId: string })[];
};

View File

@@ -0,0 +1,11 @@
{
"provider": "OpenAI",
"model": "text-embedding-ada-002",
"name": "text-embedding-ada-002",
"defaultToken": 512, // 默认分块 token
"maxToken": 3000, // 最大分块 token
"weight": 0, // 权重
"charsPointsPrice": 0 // 积分/1k token
}

View File

@@ -0,0 +1,33 @@
{
"provider": "OpenAI",
"model": "gpt-4o-mini",
"name": "GPT-4o-mini", // alias
"maxContext": 125000, // 最大上下文
"maxResponse": 16000, // 最大回复
"quoteMaxToken": 60000, // 最大引用
"maxTemperature": 1.2, // 最大温度
"presencePenaltyRange": [-2, 2], // 惩罚系数范围
"frequencyPenaltyRange": [-2, 2], // 频率惩罚系数范围
"responseFormatList": ["text", "json_object", "json_schema"], // 响应格式
"showStopSign": true, // 是否显示停止符号
"vision": true, // 是否支持图片识别
"toolChoice": true, // 是否支持工具调用
"functionCall": false, // 是否支持函数调用(一般都可以 false 了,基本不用了)
"defaultSystemChatPrompt": "", // 默认系统提示
"datasetProcess": true, // 用于知识库文本处理
"usedInClassify": true, // 用于问题分类
"customCQPrompt": "", // 自定义问题分类提示
"usedInExtractFields": true, // 用于提取字段
"customExtractPrompt": "", // 自定义提取提示
"usedInToolCall": true, // 用于工具调用
"usedInQueryExtension": true, // 用于问题优化
"defaultConfig": {}, // 额外的自定义 body
"fieldMap": {}, // body 字段映射
"censor": false, // 是否开启敏感词过滤
"charsPointsPrice": 0 // n 积分/1k token
}

View File

@@ -0,0 +1,6 @@
{
"provider": "BAAI",
"model": "bge-reranker-v2-m3",
"name": "bge-reranker-v2-m3",
"charsPointsPrice": 0
}

View File

@@ -0,0 +1,6 @@
{
"provider": "OpenAI",
"model": "whisper-1",
"name": "whisper-1",
"charsPointsPrice": 0
}

View File

@@ -0,0 +1,32 @@
{
"provider": "OpenAI",
"model": "tts-1",
"name": "TTS1",
"charsPointsPrice": 0,
"voices": [
{
"label": "Alloy",
"value": "alloy"
},
{
"label": "Echo",
"value": "echo"
},
{
"label": "Fable",
"value": "fable"
},
{
"label": "Onyx",
"value": "onyx"
},
{
"label": "Nova",
"value": "nova"
},
{
"label": "Shimmer",
"value": "shimmer"
}
]
}

View File

@@ -5,11 +5,11 @@ import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
/*
Plugin points calculation:
1. 商业版插件:
1. 系统插件/商业版插件:
- 有错误:返回 0
- 无错误:返回 配置的点数 + 子节点点数
2. 其他插件
- 返回 子节点点数
- 无错误:返回 单次积分 + 子流程积分(可配置)
2. 个人插件
- 返回 子流程积分
*/
export const computedPluginUsage = async ({
plugin,
@@ -26,9 +26,9 @@ export const computedPluginUsage = async ({
if (source !== PluginSourceEnum.personal) {
if (error) return 0;
const pluginCurrentCose = plugin.currentCost ?? 0;
const pluginCurrentCost = plugin.currentCost ?? 0;
return plugin.hasTokenFee ? pluginCurrentCose + childrenUsages : pluginCurrentCose;
return plugin.hasTokenFee ? pluginCurrentCost + childrenUsages : pluginCurrentCost;
}
return childrenUsages;

View File

@@ -8,10 +8,7 @@ import { authOpenApiKey } from '../openapi/auth';
import { FileTokenQuery } from '@fastgpt/global/common/file/type';
import { MongoResourcePermission } from './schema';
import { ClientSession } from 'mongoose';
import {
PermissionValueType,
ResourcePermissionType
} from '@fastgpt/global/support/permission/type';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { bucketNameMap } from '@fastgpt/global/common/file/constants';
import { addMinutes } from 'date-fns';
import { getGroupsByTmbId } from './memberGroup/controllers';
@@ -107,44 +104,6 @@ export const getResourcePermission = async ({
return concatPer([...groupPers, ...orgPers]);
};
/* 仅取 members 不取 groups */
export async function getResourceAllClbs({
resourceId,
teamId,
resourceType,
session
}: {
teamId: string;
session?: ClientSession;
} & (
| {
resourceType: 'team';
resourceId?: undefined;
}
| {
resourceType: Omit<PerResourceTypeEnum, 'team'>;
resourceId?: string | null;
}
)): Promise<ResourcePermissionType[]> {
return MongoResourcePermission.find(
{
resourceType: resourceType,
teamId: teamId,
resourceId,
groupId: {
$exists: false
},
orgId: {
$exists: false
}
},
null,
{
session
}
).lean();
}
export async function getResourceClbsAndGroups({
resourceId,
resourceType,
@@ -172,10 +131,17 @@ export const getClbsAndGroupsWithInfo = async ({
resourceType,
teamId
}: {
resourceId: ParentIdType;
resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>;
teamId: string;
}) =>
} & (
| {
resourceId: ParentIdType;
resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>;
}
| {
resourceType: 'team';
resourceId?: undefined;
}
)) =>
Promise.all([
MongoResourcePermission.find({
teamId,

View File

@@ -18,7 +18,6 @@ const MyIconButton = ({
}: Props) => {
return (
<Flex
mr={1}
p={1}
color={'myGray.500'}
rounded={'sm'}

View File

@@ -1,15 +1,15 @@
import React from 'react';
import React, { forwardRef } from 'react';
import { Flex, Box, BoxProps } from '@chakra-ui/react';
import MyIcon from '../Icon';
type Props = Omit<BoxProps, 'onChange'> & {
type Props<T = string> = Omit<BoxProps, 'onChange'> & {
list: {
icon?: string;
label: string | React.ReactNode;
value: string;
value: T;
}[];
value: string;
onChange: (e: string) => void;
value: T;
onChange: (e: T) => void;
};
const FillRowTabs = ({ list, value, onChange, py = '7px', px = '12px', ...props }: Props) => {
@@ -61,4 +61,6 @@ const FillRowTabs = ({ list, value, onChange, py = '7px', px = '12px', ...props
);
};
export default FillRowTabs;
export default forwardRef(FillRowTabs) as <T>(
props: Props<T> & { ref?: React.Ref<HTMLSelectElement> }
) => JSX.Element;

View File

@@ -1,7 +1,14 @@
{
"active_model": "Available models",
"add_default_model": "Add a preset model",
"api_key": "API key",
"bills_and_invoices": "Bills",
"channel": "Channel",
"confirm_logout": "Confirm to log out?",
"create_channel": "Add new channel",
"create_model": "Add new model",
"custom_model": "custom model",
"default_model": "Default model",
"logout": "Sign out",
"model_provider": "Model Provider",
"notifications": "Notify",

View File

@@ -1187,6 +1187,7 @@
"tag_list": "Tag List",
"team_tag": "Team Tag",
"textarea_variable_picker_tip": "Enter \"/\" to select a variable",
"unauth_token": "The certificate has expired, please log in again",
"unit.character": "Character",
"unit.minute": "Minute",
"unit.seconds": "Second",

View File

@@ -49,7 +49,7 @@
"execution_error": "Execution Error",
"extraction_requirements_description": "Extraction Requirements Description",
"extraction_requirements_description_detail": "Provide AI with some background knowledge or requirements to guide it in completing the task better.\\nThis input box can use global variables.",
"extraction_requirements_placeholder": "For example: \\n1. The current time is: {{cTime}}. You are a lab reservation assistant, and your task is to help users reserve a lab by extracting the corresponding reservation information from the text.\\n2. You are a Google search assistant, and you need to extract suitable search terms from the text.",
"extraction_requirements_placeholder": "For example: 1. The current time is: {{cTime}}. \nYou are a laboratory reservation assistant. Your task is to help users make laboratory reservations and obtain the corresponding reservation information from the text.\n\n2. You are the Google Search Assistant and need to extract appropriate search terms from text.",
"feedback_text": "Feedback Text",
"field_description": "Field Description",
"field_description_placeholder": "Describe the function of this input field. If it is a tool call parameter, this description will affect the quality of the model generation.",

View File

@@ -1,7 +1,14 @@
{
"active_model": "可用模型",
"add_default_model": "添加预设模型",
"api_key": "API 密钥",
"bills_and_invoices": "账单与发票",
"channel": "渠道",
"confirm_logout": "确认退出登录?",
"create_channel": "新增渠道",
"create_model": "新增模型",
"custom_model": "自定义模型",
"default_model": "预设模型",
"logout": "登出",
"model_provider": "模型提供商",
"notifications": "通知",

View File

@@ -1190,6 +1190,7 @@
"tag_list": "标签列表",
"team_tag": "团队标签",
"textarea_variable_picker_tip": "输入\"/\"可选择变量",
"unauth_token": "凭证已过期,请重新登录",
"unit.character": "字符",
"unit.minute": "分钟",
"unit.seconds": "秒",

View File

@@ -106,7 +106,7 @@
"team.group.set_as_admin": "设为管理员",
"team.group.toast.can_not_delete_owner": "不能删除所有者, 请先转让",
"team.group.transfer_owner": "转让所有者",
"team.org.org": "组织",
"team.org.org": "部门",
"team.manage_collaborators": "管理协作者",
"team.no_collaborators": "暂无协作者",
"team.write_role_member": "可写权限",

View File

@@ -49,7 +49,7 @@
"execution_error": "运行错误",
"extraction_requirements_description": "提取要求描述",
"extraction_requirements_description_detail": "给AI一些对应的背景知识或要求描述引导AI更好的完成任务。\\n该输入框可使用全局变量。",
"extraction_requirements_placeholder": "例如: \\n1. 当前时间为: {{cTime}}。你是一个实验室预约助手,你的任务是帮助用户预约实验室,从文本中获取对应的预约信息。\\n2. 你是谷歌搜索助手,需要从文本中提取出合适的搜索词。",
"extraction_requirements_placeholder": "例如: 1. 当前时间为: {{cTime}}。你是一个实验室预约助手,你的任务是帮助用户预约实验室,从文本中获取对应的预约信息。\n2. 你是谷歌搜索助手,需要从文本中提取出合适的搜索词。",
"feedback_text": "反馈的文本",
"field_description": "字段描述",
"field_description_placeholder": "描述该输入字段的功能,如果为工具调用参数,则该描述会影响模型生成的质量",

View File

@@ -1,7 +1,14 @@
{
"active_model": "可用模型",
"add_default_model": "新增預設模型",
"api_key": "API 金鑰",
"bills_and_invoices": "帳單與發票",
"channel": "頻道",
"confirm_logout": "確認登出登入?",
"create_channel": "新增頻道",
"create_model": "新增模型",
"custom_model": "自訂模型",
"default_model": "預設模型",
"logout": "登出",
"model_provider": "模型提供者",
"notifications": "通知",

View File

@@ -1187,6 +1187,7 @@
"tag_list": "標籤列表",
"team_tag": "團隊標籤",
"textarea_variable_picker_tip": "輸入「/」以選擇變數",
"unauth_token": "憑證已過期,請重新登入",
"unit.character": "字元",
"unit.minute": "分鐘",
"unit.seconds": "秒",

View File

@@ -49,7 +49,7 @@
"execution_error": "執行錯誤",
"extraction_requirements_description": "擷取需求描述",
"extraction_requirements_description_detail": "提供 AI 相對應的背景知識或需求描述,引導 AI 更好地完成任務。\\n這個輸入框可以使用全域變數。",
"extraction_requirements_placeholder": "例如\\n1. 目前時間為{{cTime}}。您是一位實驗室預約助理,您的任務是助使用者預約實驗室,從文字中取得對應的預約資訊。\\n2. 您是 Google 搜尋助,需要從文字中取出合適的搜尋詞。",
"extraction_requirements_placeholder": "例如: 1. 目前時間為: {{cTime}}。\n你是實驗室預約助手,你的任務是助使用者預約實驗室,從文字中取得對應的預約資訊。\n\n2. 你是Google搜尋助,需要從文字中取出合適的搜尋詞。",
"feedback_text": "回饋文字",
"field_description": "欄位描述",
"field_description_placeholder": "描述這個輸入欄位的功能,如果是工具呼叫參數,這個描述會影響模型產生的品質",

View File

@@ -7,7 +7,6 @@ import MyDivider from '@fastgpt/web/components/common/MyDivider';
import { useTranslation } from 'next-i18next';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
import CollaboratorContextProvider, {
MemberManagerInputPropsType
} from '../../support/permission/MemberManager/context';
@@ -24,7 +23,6 @@ const FolderSlideCard = ({
deleteTip,
onDelete,
defaultPer,
managePer,
isInheritPermission,
resumeInheritPermission,
@@ -39,11 +37,6 @@ const FolderSlideCard = ({
deleteTip: string;
onDelete: () => void;
defaultPer?: {
value: PermissionValueType;
defaultValue: PermissionValueType;
onChange: (v: PermissionValueType) => Promise<any>;
};
managePer: MemberManagerInputPropsType;
isInheritPermission?: boolean;

View File

@@ -1,11 +0,0 @@
import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context';
import { AddModalPropsType } from './MemberModal';
import MemberModal from './MemberModal';
function AddMemberModal({ onClose, mode = 'member' }: AddModalPropsType) {
const context = useContextSelector(CollaboratorContext, (v) => v);
return <MemberModal onClose={onClose} mode={mode} collaboratorContext={context} />;
}
export default AddMemberModal;

View File

@@ -19,35 +19,31 @@ 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 { useMemo, useRef, useState } from 'react';
import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags';
import { MemberManagerPropsType } from './context';
import { DEFAULT_ORG_AVATAR, DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
import Path from '@/components/common/folder/Path';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { OrgType } from '@fastgpt/global/support/user/team/org/type';
import { createMemberPermission } from '@/web/support/user/team/api';
import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context';
export type AddModalPropsType = {
onClose: () => void;
mode?: 'member' | 'all';
const HoverBoxStyle = {
bgColor: 'myGray.50',
cursor: 'pointer'
};
function MemberModal({
onClose,
mode = 'member',
collaboratorContext: context
}: AddModalPropsType & { collaboratorContext?: MemberManagerPropsType }) {
function MemberModal({ onClose }: { onClose: () => void }) {
const { t } = useTranslation();
const { userInfo, loadAndGetTeamMembers, loadAndGetGroups, myGroups, loadAndGetOrgs } =
useUserStore();
const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList);
const [searchText, setSearchText] = useState<string>('');
const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>();
const [parentPath, setParentPath] = useState('');
const { data: [members = [], groups = [], orgs = []] = [], loading: loadingMembersAndGroups } =
useRequest2(
@@ -65,13 +61,7 @@ function MemberModal({
}
);
const currentOrg = useMemo(() => {
const splitPath = parentPath.split('/');
const currentOrgId = splitPath[splitPath.length - 1];
if (!currentOrgId) return;
return orgs.find((org) => org.pathId === currentOrgId);
}, [orgs, parentPath]);
const [parentPath, setParentPath] = useState('');
const paths = useMemo(() => {
const splitPath = parentPath.split('/').filter(Boolean);
return splitPath
@@ -88,30 +78,17 @@ function MemberModal({
.filter(Boolean) as ParentTreePathItemType[];
}, [parentPath, orgs]);
const filterMembers = useMemo(() => {
if (!searchText && filterClass !== 'member' && filterClass !== 'org') return [];
if (searchText) return members.filter((item) => item.memberName.includes(searchText));
if (filterClass === 'org') {
if (!currentOrg) return [];
return members.filter((item) => currentOrg.members.find((v) => v.tmbId === item.tmbId));
}
return members;
}, [members, searchText, filterClass, currentOrg]);
const filterGroups = useMemo(() => {
if (mode !== 'all') return [];
if (!searchText && filterClass !== 'group') return [];
if (searchText) return groups.filter((item) => item.name.includes(searchText));
return groups.filter((item) => {
if (context === undefined || context.permission.isOwner) return true; // owner can see all groups
return !myGroups.find((i) => String(i._id) === String(item._id));
});
}, [groups, searchText, filterClass, myGroups, mode, context]);
const [selectedOrgIdList, setSelectedOrgIdList] = useState<string[]>([]);
const currentOrg = useMemo(() => {
const splitPath = parentPath.split('/');
const currentOrgId = splitPath[splitPath.length - 1];
if (!currentOrgId) return;
return orgs.find((org) => org.pathId === currentOrgId);
}, [orgs, parentPath]);
const filterOrgs: (OrgType & { count?: number })[] = useMemo(() => {
if (mode !== 'all') return [];
if (!searchText && filterClass !== 'org') return [];
if (searchText) return orgs.filter((item) => item.name.includes(searchText));
if (!searchText && filterClass !== 'org') return [];
if (parentPath === '') {
setParentPath(`/${orgs[0].pathId}`);
return [];
@@ -123,44 +100,97 @@ function MemberModal({
count:
item.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(item)).length
}));
}, [orgs, searchText, filterClass, mode, parentPath]);
}, [orgs, searchText, filterClass, parentPath]);
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
const [selectedGroupIdList, setSelectedGroupIdList] = useState<string[]>([]);
const [selectedOrgIdList, setSelectedOrgIdList] = useState<string[]>([]);
const [selectedPermission, setSelectedPermission] = useState(
context?.permissionList['read'].value
);
const perLabel = useMemo(() => {
if (context) return context.getPerLabelList(selectedPermission!).join('、');
}, [context, selectedPermission]);
const filterMembers = useMemo(() => {
if (searchText) return members.filter((item) => item.memberName.includes(searchText));
if (!searchText && filterClass !== 'member' && filterClass !== 'org') return [];
if (filterClass === 'org') {
if (!currentOrg) return [];
return members.filter((item) => currentOrg.members.find((v) => v.tmbId === item.tmbId));
}
return members;
}, [members, searchText, filterClass, currentOrg]);
const [selectedGroupIdList, setSelectedGroupIdList] = useState<string[]>([]);
const filterGroups = useMemo(() => {
if (searchText) return groups.filter((item) => item.name.includes(searchText));
if (!searchText && filterClass !== 'group') return [];
return groups.filter((item) => {
return !myGroups.find((i) => String(i._id) === String(item._id));
});
}, [groups, searchText, filterClass, myGroups]);
const permissionList = useContextSelector(CollaboratorContext, (v) => v.permissionList);
const getPerLabelList = useContextSelector(CollaboratorContext, (v) => v.getPerLabelList);
const [selectedPermission, setSelectedPermission] = useState<number>();
const perLabel = useMemo(() => {
if (selectedPermission === undefined) return '';
return getPerLabelList(selectedPermission!).join('、');
}, [getPerLabelList, selectedPermission]);
const onUpdateCollaborators = useContextSelector(
CollaboratorContext,
(v) => v.onUpdateCollaborators
);
const { runAsync: onConfirm, loading: isUpdating } = useRequest2(
() => {
if (context) {
return context.onUpdateCollaborators({
members: selectedMemberIdList,
groups: selectedGroupIdList,
orgs: selectedOrgIdList,
permission: selectedPermission!
});
} else {
return createMemberPermission({
tmbId: selectedMemberIdList,
groupId: selectedGroupIdList,
orgId: selectedOrgIdList
});
}
},
() =>
onUpdateCollaborators({
members: selectedMemberIdList,
groups: selectedGroupIdList,
orgs: selectedOrgIdList,
permission: selectedPermission!
}),
{
successToast: t('common:common.Add Success'),
errorToast: 'Error',
onSuccess() {
onClose();
}
}
);
const entryList = useRef([
{ label: t('user:team.group.members'), icon: '/imgs/avatar/BlueAvatar.svg', value: 'member' },
{ label: t('user:team.org.org'), icon: DEFAULT_ORG_AVATAR, value: 'org' },
{ label: t('user:team.group.group'), icon: DEFAULT_TEAM_AVATAR, value: 'group' }
]);
const selectedList = useMemo(() => {
const selectedOrgs = orgs.filter((org) => selectedOrgIdList.includes(org._id));
const selectedGroups = groups.filter((group) => selectedGroupIdList.includes(group._id));
const selectedMembers = members.filter((member) => selectedMemberIdList.includes(member.tmbId));
return [
...selectedOrgs.map((item) => ({
id: `org-${item._id}`,
avatar: item.avatar,
name: item.name,
onDelete: () => setSelectedOrgIdList(selectedOrgIdList.filter((v) => v !== item._id))
})),
...selectedGroups.map((item) => ({
id: `group-${item._id}`,
avatar: item.avatar,
name: item.name === DefaultGroupName ? userInfo?.team.teamName : item.name,
onDelete: () => setSelectedGroupIdList(selectedGroupIdList.filter((v) => v !== item._id))
})),
...selectedMembers.map((item) => ({
id: `member-${item.tmbId}`,
avatar: item.avatar,
name: item.memberName,
onDelete: () => setSelectedMembers(selectedMemberIdList.filter((v) => v !== item.tmbId))
}))
];
}, [
orgs,
groups,
members,
selectedOrgIdList,
selectedGroupIdList,
selectedMemberIdList,
userInfo?.team.teamName
]);
return (
<MyModal
isOpen
@@ -169,7 +199,7 @@ function MemberModal({
title={t('user:team.add_collaborator')}
minW="800px"
h={'100%'}
maxH={'800px'}
maxH={'90vh'}
isCentered
isLoading={loadingMembersAndGroups}
>
@@ -181,73 +211,49 @@ function MemberModal({
gridTemplateColumns="1fr 1fr"
h={'100%'}
>
<Flex flexDirection="column" borderRight="1px solid" borderColor="myGray.200" p="4">
<Flex
h={'100%'}
flexDirection="column"
borderRight="1px solid"
borderColor="myGray.200"
p="4"
>
<SearchInput
placeholder={t('user:search_user')}
bgColor="myGray.50"
onChange={(e) => setSearchText(e.target.value)}
/>
<Flex flexDirection="column" mt="2" overflow={'auto'} maxH="400px">
{!searchText &&
(filterClass === undefined ? (
<>
<HStack
justifyContent="space-between"
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer'
}}
onClick={() => setFilterClass('member')}
>
<MyAvatar src="/imgs/avatar/BlueAvatar.svg" w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{t('user:team.group.members')}
</Box>
<MyIcon name="core/chat/chevronRight" w="16px" />
</HStack>
<HStack
justifyContent="space-between"
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer'
}}
onClick={() => setFilterClass('org')}
>
<MyAvatar src={DEFAULT_ORG_AVATAR} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{t('user:team.org.org')}
</Box>
<MyIcon name="core/chat/chevronRight" w="16px" />
</HStack>
<HStack
justifyContent="space-between"
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer'
}}
onClick={() => setFilterClass('group')}
>
<MyAvatar src={DEFAULT_TEAM_AVATAR} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{t('user:team.group.group')}
</Box>
<MyIcon name="core/chat/chevronRight" w="16px" />
</HStack>
</>
) : (
<Flex flexDirection="column" mt="3" overflow={'auto'} flex={'1 0 0'} h={0}>
{!searchText && !filterClass && (
<>
{entryList.current.map((item) => {
return (
<HStack
key={item.value}
justifyContent="space-between"
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
_notLast={{ mb: 1 }}
onClick={() => setFilterClass(item.value as any)}
>
<MyAvatar src={item.icon} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{item.label}
</Box>
<MyIcon name="core/chat/chevronRight" w="16px" />
</HStack>
);
})}
</>
)}
{/* Path */}
{!searchText && filterClass && (
<Box mb={1}>
<Path
paths={[
{
@@ -278,267 +284,173 @@ function MemberModal({
}}
rootName={t('common:common.Team')}
/>
))}
{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 = context?.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'
}}
onClick={onChange}
>
<Checkbox
isChecked={selectedOrgIdList.includes(org._id)}
pointerEvents="none"
/>
<MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} />
<HStack ml="2" w="full" gap="5px">
<Text>{org.name}</Text>
</Box>
)}
<Flex flexDirection={'column'} gap={1} userSelect={'none'}>
{filterMembers.map((member) => {
const onChange = () => {
setSelectedMembers((state) => {
if (state.includes(member.tmbId)) {
return state.filter((v) => v !== member.tmbId);
}
return [...state, member.tmbId];
});
};
const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId);
return (
<HStack
justifyContent="space-between"
key={member.tmbId}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isChecked={selectedMemberIdList.includes(member.tmbId)}
pointerEvents="none"
/>
<MyAvatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{member.memberName}
</Box>
<PermissionTags permission={collaborator?.permission.value} />
</HStack>
);
})}
{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={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isChecked={selectedOrgIdList.includes(org._id)}
pointerEvents="none"
/>
<MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} />
<HStack ml="2" w="full" gap="5px">
<Text>{org.name}</Text>
{org.count && (
<>
<Tag size="sm" my="auto">
{org.count}
</Tag>
</>
)}
</HStack>
<PermissionTags permission={collaborator?.permission.value} />
{org.count && (
<>
<Tag size="sm" my="auto">
{org.count}
</Tag>
</>
<MyIcon
name="core/chat/chevronRight"
w="16px"
p="4px"
rounded={'6px'}
_hover={{
bgColor: 'myGray.200'
}}
onClick={() => {
setParentPath(getOrgChildrenPath(org));
}}
/>
)}
</HStack>
{!!collaborator && (
<PermissionTags permission={collaborator.permission.value} />
)}
{org.count && (
<MyIcon
name="core/chat/chevronRight"
w="16px"
p="4px"
rounded={'6px'}
_hover={{
bgColor: 'myGray.200'
}}
onClick={() => {
setParentPath(getOrgChildrenPath(org));
}}
);
})}
{filterGroups.map((group) => {
const onChange = () => {
setSelectedGroupIdList((state) => {
if (state.includes(group._id)) {
return state.filter((v) => v !== group._id);
}
return [...state, group._id];
});
};
const collaborator = collaboratorList?.find((v) => v.groupId === group._id);
return (
<HStack
justifyContent="space-between"
key={group._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isChecked={selectedGroupIdList.includes(group._id)}
pointerEvents="none"
/>
)}
</HStack>
);
})}
{filterGroups.map((group) => {
const onChange = () => {
setSelectedGroupIdList((state) => {
if (state.includes(group._id)) {
return state.filter((v) => v !== group._id);
}
return [...state, group._id];
});
};
const collaborator = context?.collaboratorList?.find(
(v) => v.groupId === group._id
);
return (
<HStack
justifyContent="space-between"
key={group._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer'
}}
onClick={onChange}
>
<Checkbox
isChecked={selectedGroupIdList.includes(group._id)}
pointerEvents="none"
/>
<MyAvatar src={group.avatar} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{group.name === DefaultGroupName ? userInfo?.team.teamName : group.name}
</Box>
{!!collaborator && (
<PermissionTags permission={collaborator.permission.value} />
)}
</HStack>
);
})}
{filterMembers.map((member) => {
const onChange = () => {
setSelectedMembers((state) => {
if (state.includes(member.tmbId)) {
return state.filter((v) => v !== member.tmbId);
}
return [...state, member.tmbId];
});
};
const collaborator = context?.collaboratorList?.find(
(v) => v.tmbId === member.tmbId
);
return (
<HStack
justifyContent="space-between"
key={member.tmbId}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer'
}}
onClick={userInfo?.team?.tmbId === member.tmbId ? undefined : onChange}
>
<Checkbox
isChecked={selectedMemberIdList.includes(member.tmbId)}
pointerEvents="none"
isDisabled={userInfo?.team?.tmbId === member.tmbId}
/>
<MyAvatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{member.memberName}
</Box>
{!!collaborator && (
<PermissionTags permission={collaborator.permission.value} />
)}
</HStack>
);
})}
<MyAvatar src={group.avatar} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{group.name === DefaultGroupName ? userInfo?.team.teamName : group.name}
</Box>
<PermissionTags permission={collaborator?.permission.value} />
</HStack>
);
})}
</Flex>
</Flex>
</Flex>
<Flex p="4" flexDirection="column">
<Flex h={'100%'} p="4" flexDirection="column">
<Box>
{`${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);
<Flex flexDirection="column" mt="2" gap={1} overflow={'auto'} flex={'1 0 0'} h={0}>
{selectedList.map((item) => {
return (
<HStack
justifyContent="space-between"
key={orgId}
key={item.id}
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))
}
_hover={HoverBoxStyle}
>
<MyAvatar src={org?.avatar} w="1.5rem" borderRadius={'50%'} />
<MyAvatar src={item.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{org?.name}
{item.name}
</Box>
<MyIcon
name="common/closeLight"
w="16px"
w="1rem"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
onClick={item.onDelete}
/>
</HStack>
);
})}
{selectedGroupIdList.map((groupId) => {
const onChange = () => {
setSelectedGroupIdList((state) => {
if (state.includes(groupId)) {
return state.filter((v) => v !== groupId);
}
return [...state, groupId];
});
};
const group = groups.find((v) => String(v._id) === groupId);
return (
<HStack
justifyContent="space-between"
key={groupId}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer',
...(!selectedGroupIdList.includes(groupId)
? { svg: { color: 'myGray.50' } }
: {})
}}
onClick={onChange}
>
<MyAvatar src={group?.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{group?.name === DefaultGroupName ? userInfo?.team.teamName : group?.name}
</Box>
<MyIcon
name="common/closeLight"
w="16px"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
/>
</HStack>
);
})}
{selectedMemberIdList.map((tmbId) => {
const member = members.find((v) => v.tmbId === tmbId);
return member ? (
<HStack
justifyContent="space-between"
key={tmbId}
alignItems="center"
py="2"
px={3}
borderRadius={'md'}
_hover={{ bg: 'myGray.50' }}
onClick={() =>
setSelectedMembers(selectedMemberIdList.filter((v) => v !== tmbId))
}
>
<MyAvatar src={member.avatar} w="1.5rem" borderRadius="50%" />
<Box w="full" ml={2}>
{member.memberName}
</Box>
<MyIcon
name="common/closeLight"
w="16px"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
/>
</HStack>
) : null;
})}
</Flex>
</Flex>
</Grid>
</ModalBody>
<ModalFooter>
{selectedPermission && (
{!!permissionList && (
<PermissionSelect
value={selectedPermission}
Button={

View File

@@ -49,13 +49,16 @@ function PermissionSelect({
onDelete
}: PermissionSelectProps) {
const { t } = useTranslation();
const { permission, permissionList } = useContextSelector(CollaboratorContext, (v) => v);
const ref = useRef<HTMLButtonElement>(null);
const closeTimer = useRef<NodeJS.Timeout>();
const { permission, permissionList } = useContextSelector(CollaboratorContext, (v) => v);
const [isOpen, setIsOpen] = useState(false);
const permissionSelectList = useMemo(() => {
if (!permissionList) return { singleCheckBoxList: [], multipleCheckBoxList: [] };
const list = Object.entries(permissionList).map(([_, value]) => {
return {
name: value.name,
@@ -77,6 +80,8 @@ function PermissionSelect({
};
}, [permission.isOwner, permissionList]);
const selectedSingleValue = useMemo(() => {
if (!permissionList) return undefined;
const per = new Permission({ per: value });
if (per.hasManagePer) return permissionList['manage'].value;
@@ -107,7 +112,7 @@ function PermissionSelect({
}
});
return (
return selectedSingleValue !== undefined ? (
<Menu offset={offset} isOpen={isOpen} autoSelect={false} direction={'ltr'}>
<Box
w="fit-content"
@@ -241,7 +246,7 @@ function PermissionSelect({
</MenuList>
</Box>
</Menu>
);
) : null;
}
export default React.memo(PermissionSelect);

View File

@@ -7,12 +7,15 @@ import { CollaboratorContext } from './context';
import { useTranslation } from 'next-i18next';
export type PermissionTagsProp = {
permission: PermissionValueType;
permission?: PermissionValueType;
};
function PermissionTags({ permission }: PermissionTagsProp) {
const { getPerLabelList } = useContextSelector(CollaboratorContext, (v) => v);
const { t } = useTranslation();
if (permission === undefined) return null;
const perTagList = getPerLabelList(permission);
return (

View File

@@ -19,19 +19,19 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
const AddMemberModal = dynamic(() => import('./AddMemberModal'));
const MemberModal = dynamic(() => import('./MemberModal'));
const ManageModal = dynamic(() => import('./ManageModal'));
export type MemberManagerInputPropsType = {
permission: Permission;
onGetCollaboratorList: () => Promise<CollaboratorItemType[]>;
permissionList: PermissionListType;
permissionList?: PermissionListType;
onUpdateCollaborators: (props: UpdateClbPermissionProps) => Promise<any>;
onDelOneCollaborator: (
props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }>
) => Promise<any>;
refreshDeps?: any[];
mode?: 'member' | 'all';
};
export type MemberManagerPropsType = MemberManagerInputPropsType & {
@@ -80,8 +80,7 @@ const CollaboratorContextProvider = ({
refetchResource,
refreshDeps = [],
isInheritPermission,
hasParent,
mode = 'member'
hasParent
}: MemberManagerInputPropsType & {
children: (props: ChildrenProps) => ReactNode;
refetchResource?: () => void;
@@ -121,6 +120,8 @@ const CollaboratorContextProvider = ({
const getPerLabelList = useCallback(
(per: PermissionValueType) => {
if (!permissionList) return [];
const Per = new Permission({ per });
const labels: string[] = [];
@@ -128,7 +129,7 @@ const CollaboratorContextProvider = ({
labels.push(permissionList['manage'].name);
} else if (Per.hasWritePer) {
labels.push(permissionList['write'].name);
} else {
} else if (Per.hasReadPer) {
labels.push(permissionList['read'].name);
}
@@ -203,12 +204,11 @@ const CollaboratorContextProvider = ({
MemberListCard
})}
{isOpenAddMember && (
<AddMemberModal
<MemberModal
onClose={() => {
onCloseAddMember();
refetchResource?.();
}}
mode={mode}
/>
)}
{isOpenManageModal && (

View File

@@ -0,0 +1,84 @@
import React, { useMemo, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { Box, Flex, ModalBody } from '@chakra-ui/react';
import { MultipleRowArraySelect } from '@fastgpt/web/components/common/MySelect/MultipleRowSelect';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { ModelProviderList } from '@fastgpt/global/core/ai/provider';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { HUGGING_FACE_ICON } from '@fastgpt/global/common/system/constants';
import { getModelFromList } from '@fastgpt/global/core/ai/model';
const DefaultModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { llmModelList, vectorModelList, whisperModel, audioSpeechModelList, reRankModelList } =
useSystemStore();
const [value, setValue] = useState<string[]>([]);
const modelList = useMemo(() => {
return [
...llmModelList,
...vectorModelList,
...audioSpeechModelList,
...reRankModelList,
whisperModel
].map((item) => ({
provider: item.provider,
name: item.name,
model: item.model
}));
}, [llmModelList, vectorModelList, whisperModel, audioSpeechModelList, reRankModelList]);
const selectorList = useMemo(() => {
const renderList = ModelProviderList.map<{
label: React.JSX.Element;
value: string;
children: { label: string | React.ReactNode; value: string }[];
}>((provider) => ({
label: (
<Flex alignItems={'center'} py={1}>
<Avatar
borderRadius={'0'}
mr={2}
src={provider?.avatar || HUGGING_FACE_ICON}
fallbackSrc={HUGGING_FACE_ICON}
w={'1rem'}
/>
<Box>{t(provider.name as any)}</Box>
</Flex>
),
value: provider.id,
children: []
}));
for (const item of modelList) {
const modelData = getModelFromList(modelList, item.model);
const provider =
renderList.find((item) => item.value === (modelData?.provider || 'Other')) ??
renderList[renderList.length - 1];
provider.children.push({
label: modelData.name,
value: modelData.model
});
}
return renderList.filter((item) => item.children.length > 0);
}, [modelList, t]);
console.log(selectorList);
return (
<MyModal
isOpen
title={t('account:add_default_model')}
iconSrc="common/model"
iconColor="primary.600"
onClose={onClose}
>
<ModalBody>11</ModalBody>
</MyModal>
);
};
export default DefaultModal;

View File

@@ -1,15 +1,72 @@
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import React from 'react';
import React, { useState } from 'react';
import AccountContainer from '../components/AccountContainer';
import { Box } from '@chakra-ui/react';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import ModelTable from '@/components/core/ai/ModelTable';
import { useUserStore } from '@/web/support/user/useUserStore';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import { useTranslation } from 'next-i18next';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import dynamic from 'next/dynamic';
const DefaultModal = dynamic(() => import('./components/DefaultModal'), {
ssr: false
});
const ModelProvider = () => {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const isRoot = userInfo?.username === 'root';
const [tab, setTab] = useState<'model' | 'channel'>('model');
const { isOpen: isOpenDefault, onOpen: onOpenDefault, onClose: onCloseDefault } = useDisclosure();
return (
<AccountContainer>
<Box h={'100%'} py={4} px={6}>
<ModelTable />
</Box>
<Flex h={'100%'} flexDirection={'column'} gap={4} py={4} px={6}>
{/* Header */}
{/* <Flex justifyContent={'space-between'}>
<FillRowTabs<'model' | 'channel'>
list={[
{ label: t('account:active_model'), value: 'model' },
{ label: t('account:channel'), value: 'channel' }
]}
value={tab}
px={8}
py={1}
onChange={setTab}
/>
{tab === 'model' && (
<MyMenu
trigger="hover"
size="mini"
Button={<Button>{t('account:create_model')}</Button>}
menuList={[
{
children: [
{
label: t('account:default_model'),
onClick: onOpenDefault
},
{
label: t('account:custom_model')
}
]
}
]}
/>
)}
{tab === 'channel' && <Button>{t('account:create_channel')}</Button>}
</Flex> */}
<Box flex={'1 0 0'}>
{tab === 'model' && <ModelTable />}
{/* {tab === 'channel' && <ChannelTable />} */}
</Box>
</Flex>
{isOpenDefault && <DefaultModal onClose={onCloseDefault} />}
</AccountContainer>
);
};

View File

@@ -1,6 +1,8 @@
import AvatarGroup from '@fastgpt/web/components/common/Avatar/AvatarGroup';
import {
Box,
Button,
Flex,
HStack,
Table,
TableContainer,
@@ -29,17 +31,32 @@ import { useState } from 'react';
import IconButton from '../OrgManage/IconButton';
const ChangeOwnerModal = dynamic(() => import('./GroupTransferOwnerModal'));
const GroupInfoModal = dynamic(() => import('./GroupInfoModal'));
const ManageGroupMemberModal = dynamic(() => import('./GroupManageMember'));
function MemberTable({
onEditGroup,
onManageMember
}: {
onEditGroup: (groupId: string) => void;
onManageMember: (groupId: string) => void;
}) {
function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const [editGroupId, setEditGroupId] = useState<string>();
const {
isOpen: isOpenGroupInfo,
onOpen: onOpenGroupInfo,
onClose: onCloseGroupInfo
} = useDisclosure();
const {
isOpen: isOpenManageGroupMember,
onOpen: onOpenManageGroupMember,
onClose: onCloseManageGroupMember
} = useDisclosure();
const onEditGroup = (groupId: string) => {
setEditGroupId(groupId);
onOpenGroupInfo();
};
const onManageMember = (groupId: string) => {
setEditGroupId(groupId);
onOpenManageGroupMember();
};
const { ConfirmModal: ConfirmDeleteGroupModal, openConfirm: openDeleteGroupModal } = useConfirm({
type: 'delete',
@@ -73,145 +90,184 @@ function MemberTable({
onOpen: onOpenChangeOwner,
onClose: onCloseChangeOwner
} = useDisclosure();
const onChangeOwner = (groupId: string) => {
setEditGroupId(groupId);
onOpenChangeOwner();
};
return (
<MyBox>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="6px">
{t('account_team:group_name')}
</Th>
<Th bg="myGray.100">{t('account_team:owner')}</Th>
<Th bg="myGray.100">{t('account_team:member')}</Th>
<Th bg="myGray.100" borderRightRadius="6px">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
<Tbody>
{groups?.map((group) => (
<Tr key={group._id} overflow={'unset'}>
<Td>
<HStack>
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
{userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="support/permission/collaborator" w={'14px'} />}
onClick={onOpenGroupInfo}
>
{t('user:team.group.create')}
</Button>
)}
</Flex>
<MyBox flex={'1 0 0'} overflow={'auto'}>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="6px">
{t('account_team:group_name')}
</Th>
<Th bg="myGray.100">{t('account_team:owner')}</Th>
<Th bg="myGray.100">{t('account_team:member')}</Th>
<Th bg="myGray.100" borderRightRadius="6px">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
<Tbody>
{groups?.map((group) => (
<Tr key={group._id} overflow={'unset'}>
<Td>
<HStack>
<MemberTag
name={
group.name === DefaultGroupName
? userInfo?.team.teamName ?? ''
: group.name
}
avatar={group.avatar}
/>
<Box>
({group.name === DefaultGroupName ? members.length : group.members.length})
</Box>
</HStack>
</Td>
<Td>
<MemberTag
name={
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
group.name === DefaultGroupName
? members.find((item) => item.role === 'owner')?.memberName ?? ''
: members.find(
(item) =>
item.tmbId ===
group.members.find((item) => item.role === 'owner')?.tmbId
)?.memberName ?? ''
}
avatar={
group.name === DefaultGroupName
? members.find((item) => item.role === 'owner')?.avatar ?? ''
: members.find(
(i) =>
i.tmbId ===
group.members.find((item) => item.role === 'owner')?.tmbId
)?.avatar ?? ''
}
avatar={group.avatar}
/>
<Box>
({group.name === DefaultGroupName ? members.length : group.members.length})
</Box>
</HStack>
</Td>
<Td>
<MemberTag
name={
group.name === DefaultGroupName
? members.find((item) => item.role === 'owner')?.memberName ?? ''
: members.find(
(item) =>
item.tmbId ===
group.members.find((item) => item.role === 'owner')?.tmbId
)?.memberName ?? ''
}
avatar={
group.name === DefaultGroupName
? members.find((item) => item.role === 'owner')?.avatar ?? ''
: members.find(
(i) =>
i.tmbId === group.members.find((item) => item.role === 'owner')?.tmbId
)?.avatar ?? ''
}
/>
</Td>
<Td>
{group.name === DefaultGroupName ? (
<AvatarGroup avatars={members.map((v) => v.avatar)} groupId={group._id} />
) : hasGroupManagePer(group) ? (
<MyTooltip label={t('account_team:manage_member')}>
<Box cursor="pointer" onClick={() => onManageMember(group._id)}>
<AvatarGroup
avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
)}
groupId={group._id}
/>
</Box>
</MyTooltip>
) : (
<AvatarGroup
avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
)}
groupId={group._id}
/>
)}
</Td>
<Td>
{hasGroupManagePer(group) && group.name !== DefaultGroupName && (
<MyMenu
Button={<IconButton name={'more'} />}
menuList={[
{
children: [
{
label: t('account_team:edit_info'),
icon: 'edit',
onClick: () => {
onEditGroup(group._id);
}
},
{
label: t('account_team:manage_member'),
icon: 'support/team/group',
onClick: () => {
onManageMember(group._id);
}
},
...(isGroupOwner(group)
? [
{
label: t('account_team:transfer_ownership'),
icon: 'modal/changePer',
onClick: () => {
onChangeOwner(group._id);
</Td>
<Td>
{group.name === DefaultGroupName ? (
<AvatarGroup avatars={members.map((v) => v.avatar)} groupId={group._id} />
) : hasGroupManagePer(group) ? (
<MyTooltip label={t('account_team:manage_member')}>
<Box cursor="pointer" onClick={() => onManageMember(group._id)}>
<AvatarGroup
avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
)}
groupId={group._id}
/>
</Box>
</MyTooltip>
) : (
<AvatarGroup
avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
)}
groupId={group._id}
/>
)}
</Td>
<Td>
{hasGroupManagePer(group) && group.name !== DefaultGroupName && (
<MyMenu
Button={<IconButton name={'more'} />}
menuList={[
{
children: [
{
label: t('account_team:edit_info'),
icon: 'edit',
onClick: () => {
onEditGroup(group._id);
}
},
{
label: t('account_team:manage_member'),
icon: 'support/team/group',
onClick: () => {
onManageMember(group._id);
}
},
...(isGroupOwner(group)
? [
{
label: t('account_team:transfer_ownership'),
icon: 'modal/changePer',
onClick: () => {
onChangeOwner(group._id);
},
type: 'primary' as MenuItemType
},
type: 'primary' as MenuItemType
},
{
label: t('common:common.Delete'),
icon: 'delete',
onClick: () => {
openDeleteGroupModal(() => delDeleteGroup(group._id))();
},
type: 'danger' as MenuItemType
}
]
: [])
]
}
]}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{
label: t('common:common.Delete'),
icon: 'delete',
onClick: () => {
openDeleteGroupModal(() => delDeleteGroup(group._id))();
},
type: 'danger' as MenuItemType
}
]
: [])
]
}
]}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</MyBox>
<ConfirmDeleteGroupModal />
{isOpenChangeOwner && editGroupId && (
<ChangeOwnerModal groupId={editGroupId} onClose={onCloseChangeOwner} />
)}
</MyBox>
{isOpenGroupInfo && (
<GroupInfoModal
onClose={() => {
onCloseGroupInfo();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
{isOpenManageGroupMember && (
<ManageGroupMemberModal
onClose={() => {
onCloseManageGroupMember();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
</>
);
}

View File

@@ -1,106 +1,224 @@
import Avatar from '@fastgpt/web/components/common/Avatar';
import { Box, HStack, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import {
Box,
Button,
Flex,
HStack,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
useDisclosure
} from '@chakra-ui/react';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { useTranslation } from 'next-i18next';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { delRemoveMember } from '@/web/support/user/team/api';
import Tag from '@fastgpt/web/components/common/Tag';
import Icon from '@fastgpt/web/components/common/Icon';
import GroupTags from '@/components/support/permission/Group/GroupTags';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from './context';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyIcon from '@fastgpt/web/components/common/Icon';
import dynamic from 'next/dynamic';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { delLeaveTeam } from '@/web/support/user/team/api';
function MemberTable() {
const { userInfo } = useUserStore();
const InviteModal = dynamic(() => import('./InviteModal'));
const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { toast } = useToast();
const { userInfo, teamPlanStatus } = useUserStore();
const { feConfigs, setNotSufficientModalType } = useSystemStore();
const { groups, refetchGroups, myTeams, refetchTeams, members, refetchMembers, onSwitchTeam } =
useContextSelector(TeamContext, (v) => v);
const {
isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync
} = useDisclosure();
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm({
type: 'delete'
});
const { members, groups, refetchMembers, refetchGroups } = useContextSelector(
TeamContext,
(v) => v
const { runAsync: onLeaveTeam } = useRequest2(
async () => {
const defaultTeam = myTeams.find((item) => item.defaultTeam) || myTeams[0];
// change to personal team
onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam();
},
{
onSuccess() {
refetchTeams();
},
errorToast: t('account_team:user_team_leave_team_failed')
}
);
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('account_team:confirm_leave_team')
});
return (
<MyBox>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
<Tr bgColor={'white !important'}>
<Th borderLeftRadius="6px" bgColor="myGray.100">
{t('account_team:user_name')}
</Th>
<Th bgColor="myGray.100">{t('account_team:member_group')}</Th>
<Th borderRightRadius="6px" bgColor="myGray.100">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
<Tbody>
{members?.map((item) => (
<Tr key={item.userId} overflow={'unset'}>
<Td>
<HStack>
<Avatar src={item.avatar} w={['18px', '22px']} borderRadius={'50%'} />
<Box className={'textEllipsis'}>
{item.memberName}
{item.status === 'waiting' && (
<Tag ml="2" colorSchema="yellow">
{t('account_team:waiting')}
</Tag>
)}
</Box>
</HStack>
</Td>
<Td maxW={'300px'}>
<GroupTags
names={groups
?.filter((group) => group.members.map((m) => m.tmbId).includes(item.tmbId))
.map((g) => g.name)}
max={3}
/>
</Td>
<Td>
{userInfo?.team.permission.hasManagePer &&
item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
<Icon
name={'common/trash'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'red.600',
bgColor: 'myGray.100'
}}
onClick={() => {
openRemoveMember(
() =>
delRemoveMember(item.tmbId).then(() =>
Promise.all([refetchGroups(), refetchMembers()])
),
undefined,
t('account_team:remove_tip', {
username: item.memberName
})
)();
}}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
<HStack>
{userInfo?.team.permission.hasManagePer && feConfigs?.show_team_chat && (
<Button
variant={'whitePrimary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="core/dataset/tag" w={'16px'} />}
onClick={() => {
onOpenTeamTagsAsync();
}}
>
{t('account_team:label_sync')}
</Button>
)}
{userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/inviteLight" w={'16px'} color={'white'} />}
onClick={() => {
if (
teamPlanStatus?.standardConstants?.maxTeamMember &&
teamPlanStatus.standardConstants.maxTeamMember <= members.length
) {
toast({
status: 'warning',
title: t('common:user.team.Over Max Member Tip', {
max: teamPlanStatus.standardConstants.maxTeamMember
})
});
setNotSufficientModalType(TeamErrEnum.teamMemberOverSize);
} else {
onOpenInvite();
}
}}
>
{t('account_team:user_team_invite_member')}
</Button>
)}
{!userInfo?.team.permission.isOwner && (
<Button
variant={'whitePrimary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
onClick={() => openLeaveConfirm(onLeaveTeam)()}
>
{t('account_team:user_team_leave_team')}
</Button>
)}
</HStack>
</Flex>
<ConfirmRemoveMemberModal />
</TableContainer>
</MyBox>
<Box flex={'1 0 0'} overflow={'auto'}>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
<Tr bgColor={'white !important'}>
<Th borderLeftRadius="6px" bgColor="myGray.100">
{t('account_team:user_name')}
</Th>
<Th bgColor="myGray.100">{t('account_team:member_group')}</Th>
<Th borderRightRadius="6px" bgColor="myGray.100">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
<Tbody>
{members?.map((item) => (
<Tr key={item.userId} overflow={'unset'}>
<Td>
<HStack>
<Avatar src={item.avatar} w={['18px', '22px']} borderRadius={'50%'} />
<Box className={'textEllipsis'}>
{item.memberName}
{item.status === 'waiting' && (
<Tag ml="2" colorSchema="yellow">
{t('account_team:waiting')}
</Tag>
)}
</Box>
</HStack>
</Td>
<Td maxW={'300px'}>
<GroupTags
names={groups
?.filter((group) => group.members.map((m) => m.tmbId).includes(item.tmbId))
.map((g) => g.name)}
max={3}
/>
</Td>
<Td>
{userInfo?.team.permission.hasManagePer &&
item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
<Icon
name={'common/trash'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'red.600',
bgColor: 'myGray.100'
}}
onClick={() => {
openRemoveMember(
() =>
delRemoveMember(item.tmbId).then(() =>
Promise.all([refetchGroups(), refetchMembers()])
),
undefined,
t('account_team:remove_tip', {
username: item.memberName
})
)();
}}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
<ConfirmRemoveMemberModal />
</TableContainer>
</Box>
<ConfirmLeaveTeamModal />
{isOpenInvite && userInfo?.team?.teamId && (
<InviteModal
teamId={userInfo.team.teamId}
onClose={onCloseInvite}
onSuccess={refetchMembers}
/>
)}
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
</>
);
}

View File

@@ -71,7 +71,7 @@ function ActionButton({
);
}
function OrgTable() {
function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { userInfo, isTeamAdmin } = useUserStore();
@@ -157,99 +157,69 @@ function OrgTable() {
});
return (
<MyBox isLoading={isLoadingOrgs}>
<Box mb={3}>
<Path paths={paths} rootName={userInfo?.team?.teamName} onClick={setParentPath} />
</Box>
<Flex w={'100%'} gap={'4'}>
{/* Table */}
<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>
{currentOrgs.map((org) => (
<Tr key={org._id} overflow={'unset'}>
<Td>
<HStack
cursor={'pointer'}
onClick={() => setParentPath(getOrgChildrenPath(org))}
>
<MemberTag name={org.name} avatar={org.avatar} />
<Tag size="sm">{org.count}</Tag>
<MyIcon
name="core/chat/chevronRight"
w={'1rem'}
h={'1rem'}
color={'myGray.500'}
/>
</HStack>
</Td>
<Td w={'6rem'}>
{isTeamAdmin && (
<MyMenu
trigger="hover"
Button={<IconButton name="more" />}
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>
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
</Flex>
<MyBox flex={'1 0 0'} overflow={'auto'} isLoading={isLoadingOrgs}>
<Box mb={3}>
<Path paths={paths} rootName={userInfo?.team?.teamName} onClick={setParentPath} />
</Box>
<Flex w={'100%'} gap={'4'}>
{/* Table */}
<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>
))}
{currentOrg?.members.map((member) => {
const memberInfo = members.find((m) => m.tmbId === member.tmbId);
if (!memberInfo) return null;
return (
<Tr key={member.tmbId}>
</Thead>
<Tbody>
{currentOrgs.map((org) => (
<Tr key={org._id} overflow={'unset'}>
<Td>
<MemberTag name={memberInfo.memberName} avatar={memberInfo.avatar} />
<HStack
cursor={'pointer'}
onClick={() => setParentPath(getOrgChildrenPath(org))}
>
<MemberTag name={org.name} avatar={org.avatar} />
<Tag size="sm">{org.count}</Tag>
<MyIcon
name="core/chat/chevronRight"
w={'1rem'}
h={'1rem'}
color={'myGray.500'}
/>
</HStack>
</Td>
<Td w={'6rem'}>
{isTeamAdmin && (
<MyMenu
trigger={'hover'}
trigger="hover"
Button={<IconButton name="more" />}
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: () =>
openDeleteMemberModal(() =>
deleteMemberReq(currentOrg._id, member.tmbId)
)()
onClick: () => deleteOrgHandler(org._id)
}
]
}
@@ -258,91 +228,126 @@ function OrgTable() {
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
{/* Slider */}
<VStack w={'180px'} alignItems={'start'}>
<HStack gap={'6px'}>
<Avatar src={currentOrg?.avatar} w={'1rem'} h={'1rem'} rounded={'xs'} />
<Box fontWeight={500} color={'myGray.900'}>
{currentOrg?.name}
</Box>
{currentOrg?.path !== '' && (
<IconButton name="edit" onClick={() => setEditOrg(currentOrg)} />
)}
</HStack>
<Box fontSize={'xs'}>{currentOrg?.description || t('common:common.no_intro')}</Box>
))}
{currentOrg?.members.map((member) => {
const memberInfo = members.find((m) => m.tmbId === member.tmbId);
if (!memberInfo) return null;
<Divider my={'20px'} />
<Box fontWeight={500} fontSize="sm" color="myGray.900">
{t('common:common.Action')}
</Box>
{currentOrg && isTeamAdmin && (
<VStack gap="13px" w="100%">
<ActionButton
icon="common/add2"
text={t('account_team:create_sub_org')}
onClick={() => {
setEditOrg({
...defaultOrgForm,
parentId: currentOrg?._id
});
}}
/>
<ActionButton
icon="common/administrator"
text={t('account_team:manage_member')}
onClick={() => setManageMemberOrg(currentOrg)}
/>
return (
<Tr key={member.tmbId}>
<Td>
<MemberTag name={memberInfo.memberName} avatar={memberInfo.avatar} />
</Td>
<Td w={'6rem'}>
{isTeamAdmin && (
<MyMenu
trigger={'hover'}
Button={<IconButton name="more" />}
menuList={[
{
children: [
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () =>
openDeleteMemberModal(() =>
deleteMemberReq(currentOrg._id, member.tmbId)
)()
}
]
}
]}
/>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
{/* Slider */}
<VStack w={'180px'} alignItems={'start'}>
<HStack gap={'6px'}>
<Avatar src={currentOrg?.avatar} w={'1rem'} h={'1rem'} rounded={'xs'} />
<Box fontWeight={500} color={'myGray.900'}>
{currentOrg?.name}
</Box>
{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)}
/>
</>
<IconButton name="edit" onClick={() => setEditOrg(currentOrg)} />
)}
</VStack>
)}
</VStack>
</Flex>
</HStack>
<Box fontSize={'xs'}>{currentOrg?.description || t('common:common.no_intro')}</Box>
{!!editOrg && (
<OrgInfoModal
editOrg={editOrg}
onClose={() => setEditOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!movingOrg && (
<OrgMoveModal
orgs={orgs}
movingOrg={movingOrg}
onClose={() => setMovingOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!manageMemberOrg && (
<OrgMemberManageModal
currentOrg={manageMemberOrg}
refetchOrgs={refetchOrgs}
onClose={() => setManageMemberOrg(undefined)}
/>
)}
<Divider my={'20px'} />
<ConfirmDeleteOrgModal />
<ConfirmDeleteMember />
</MyBox>
<Box fontWeight={500} fontSize="sm" color="myGray.900">
{t('common:common.Action')}
</Box>
{currentOrg && isTeamAdmin && (
<VStack gap="13px" w="100%">
<ActionButton
icon="common/add2"
text={t('account_team:create_sub_org')}
onClick={() => {
setEditOrg({
...defaultOrgForm,
parentId: currentOrg?._id
});
}}
/>
<ActionButton
icon="common/administrator"
text={t('account_team:manage_member')}
onClick={() => setManageMemberOrg(currentOrg)}
/>
{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>
</Flex>
{!!editOrg && (
<OrgInfoModal
editOrg={editOrg}
onClose={() => setEditOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!movingOrg && (
<OrgMoveModal
orgs={orgs}
movingOrg={movingOrg}
onClose={() => setMovingOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!manageMemberOrg && (
<OrgMemberManageModal
currentOrg={manageMemberOrg}
refetchOrgs={refetchOrgs}
onClose={() => setManageMemberOrg(undefined)}
/>
)}
<ConfirmDeleteOrgModal />
<ConfirmDeleteMember />
</MyBox>
</>
);
}

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo, useState } from 'react';
import {
Box,
Checkbox,
@@ -10,7 +10,9 @@ import {
Th,
Thead,
Text,
Tr
Tr,
Flex,
Button
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
@@ -26,442 +28,400 @@ import MemberTag from '../../../../../components/support/user/team/Info/MemberTa
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import {
TeamManagePermissionVal,
TeamPermissionList,
TeamWritePermissionVal
} from '@fastgpt/global/support/permission/user/constant';
import { TeamPermission } from '@fastgpt/global/support/permission/user/controller';
import { useCreation, useToggle } from 'ahooks';
import { useToggle } from 'ahooks';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import MemberModal from '@/components/support/permission/MemberManager/MemberModal';
import MyBox from '@fastgpt/web/components/common/MyBox';
import CollaboratorContextProvider, {
CollaboratorContext
} from '@/components/support/permission/MemberManager/context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useContextSelector } from 'use-context-selector';
import { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator';
function PermissionManage({
isOpenAddPermission,
onCloseAddPermission
Tabs,
onOpenAddMember
}: {
isOpenAddPermission: boolean;
onCloseAddPermission: () => void;
Tabs: React.ReactNode;
onOpenAddMember: () => void;
}) {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const [searchKey, setSearchKey] = useState('');
const { runAsync: refetchClbs, data: clbs = { tmb: [], group: [], org: [] } } = useRequest2(
getTeamClbs,
{
manual: false,
refreshDeps: [userInfo?.team?.teamId]
}
const collaboratorList = useContextSelector(
CollaboratorContext,
(state) => state.collaboratorList
);
const onUpdateCollaborators = useContextSelector(
CollaboratorContext,
(state) => state.onUpdateCollaborators
);
const onDelOneCollaborator = useContextSelector(
CollaboratorContext,
(state) => state.onDelOneCollaborator
);
const [isExpandMember, setExpandMember] = useToggle(true);
const [isExpandGroup, setExpandGroup] = useToggle(true);
const [isExpandOrg, setExpandOrg] = useToggle(true);
const members = useCreation(
() =>
clbs.tmb.map((item) => ({
...item,
permission: new TeamPermission({ per: item.permission })
})),
[clbs]
);
const { tmbList, groupList, orgList } = useMemo(() => {
const tmbList: CollaboratorItemType[] = [];
const groupList: CollaboratorItemType[] = [];
const orgList: CollaboratorItemType[] = [];
const groups = useCreation(
() =>
clbs.group.map((item) => ({
...item,
permission: new TeamPermission({ per: item.permission })
})),
[clbs]
);
const orgs = useCreation(
() =>
clbs.org.map((item) => ({
...item,
permission: new TeamPermission({ per: item.permission })
})),
[clbs]
);
const { runAsync: onUpdateMemberPermission } = useRequest2(updateMemberPermission, {
onSuccess: () => {
refetchClbs();
}
});
const { runAsync: onAddPermission, loading: addLoading } = useRequest2(
async ({
orgId,
groupId,
memberId,
per
}: {
orgId?: string;
groupId?: string;
memberId?: string;
per: 'write' | 'manage';
}) => {
if (groupId) {
const group = groups?.find((group) => group.groupId === groupId);
if (group) {
const permission = new TeamPermission({ per: group.permission.value });
switch (per) {
case 'write':
permission.addPer(TeamWritePermissionVal);
return onUpdateMemberPermission({
groupId: group.groupId,
permission: permission.value
});
case 'manage':
permission.addPer(TeamManagePermissionVal);
return onUpdateMemberPermission({
groupId: group.groupId,
permission: permission.value
});
}
}
collaboratorList.forEach((item) => {
if (item.tmbId) {
tmbList.push(item);
} else if (item.groupId) {
groupList.push(item);
} else if (item.orgId) {
orgList.push(item);
}
if (orgId) {
const org = orgs.find((org) => String(org.orgId) === orgId);
if (org) {
const permission = new TeamPermission({ per: org.permission.value });
switch (per) {
case 'write':
permission.addPer(TeamWritePermissionVal);
return onUpdateMemberPermission({
orgId: org.orgId,
permission: permission.value
});
case 'manage':
permission.addPer(TeamManagePermissionVal);
return onUpdateMemberPermission({
orgId: org.orgId,
permission: permission.value
});
}
}
}
if (memberId) {
const member = members?.find((member) => member.tmbId === memberId);
if (member) {
const permission = new TeamPermission({ per: member.permission.value });
switch (per) {
case 'write':
permission.addPer(TeamWritePermissionVal);
return onUpdateMemberPermission({
memberId: member.tmbId,
permission: permission.value
});
case 'manage':
permission.addPer(TeamManagePermissionVal);
return onUpdateMemberPermission({
memberId: member.tmbId,
permission: permission.value
});
}
}
});
return {
tmbList,
groupList,
orgList
};
}, [collaboratorList]);
const { runAsync: onUpdatePermission, loading: addLoading } = useRequest2(
async ({ id, type, per }: { id: string; type: 'add' | 'remove'; per: 'write' | 'manage' }) => {
const clb = collaboratorList.find(
(clb) => clb.tmbId === id || clb.groupId === id || clb.orgId === id
);
if (!clb) return;
const updatePer = per === 'write' ? TeamWritePermissionVal : TeamManagePermissionVal;
const permission = new TeamPermission({ per: clb.permission.value });
if (type === 'add') {
permission.addPer(updatePer);
} else {
permission.removePer(updatePer);
}
return onUpdateCollaborators({
...(clb.tmbId && { members: [clb.tmbId] }),
...(clb.groupId && { groups: [clb.groupId] }),
...(clb.orgId && { orgs: [clb.orgId] }),
permission: permission.value
});
}
);
const { runAsync: onRemovePermission, loading: removeLoading } = useRequest2(
async ({
orgId,
groupId,
memberId,
per
}: {
orgId?: string;
groupId?: string;
memberId?: string;
per: 'write' | 'manage';
}) => {
if (groupId) {
const group = groups?.find((group) => group.groupId === groupId);
if (group) {
const permission = new TeamPermission({ per: group.permission.value });
switch (per) {
case 'write':
permission.removePer(TeamWritePermissionVal);
return onUpdateMemberPermission({
groupId: group.groupId,
permission: permission.value
});
case 'manage':
permission.removePer(TeamManagePermissionVal);
return onUpdateMemberPermission({
groupId: group.groupId,
permission: permission.value
});
}
}
}
if (orgId) {
const org = orgs.find((org) => String(org.orgId) === orgId);
if (org) {
const permission = new TeamPermission({ per: org.permission.value });
switch (per) {
case 'write':
permission.removePer(TeamWritePermissionVal);
return onUpdateMemberPermission({
orgId: org.orgId,
permission: permission.value
});
case 'manage':
permission.removePer(TeamManagePermissionVal);
return onUpdateMemberPermission({
orgId: org.orgId,
permission: permission.value
});
}
}
}
if (memberId) {
const member = members?.find((member) => String(member.tmbId) === memberId);
if (member) {
const permission = new TeamPermission({ per: member.permission.value }); // Hint: member.permission is read-only
switch (per) {
case 'write':
permission.removePer(TeamWritePermissionVal);
return onUpdateMemberPermission({
memberId: String(member.tmbId),
permission: permission.value
});
case 'manage':
permission.removePer(TeamManagePermissionVal);
return onUpdateMemberPermission({
memberId: String(member.tmbId),
permission: permission.value
});
}
}
}
}
);
const { runAsync: onDeleteMemberPermission } = useRequest2(deleteMemberPermission, {
onSuccess: () => {
refetchClbs();
}
});
const { runAsync: onDeleteMemberPermission, loading: deleteLoading } =
useRequest2(onDelOneCollaborator);
const userManage = userInfo?.permission.hasManagePer;
return (
<TableContainer fontSize={'sm'}>
<Table>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="md" maxW={'150px'}>
{`${t('user:team.group.members')} / ${t('user:team.org.org')} / ${t('user:team.group.group')}`}
<QuestionTip ml="1" label={t('user:team.group.permission_tip')} />
</Th>
<Th bg="myGray.100">
<Box mx="auto" w="fit-content">
{t('user:team.group.permission.write')}
</Box>
</Th>
<Th bg="myGray.100">
<Box mx="auto" w="fit-content">
{t('user:team.group.permission.manage')}
<QuestionTip ml="1" label={t('user:team.group.manage_tip')} />
</Box>
</Th>
<Th bg="myGray.100" borderRightRadius="md">
<Box mx="auto" w="fit-content">
{t('common:common.Action')}
</Box>
</Th>
</Tr>
</Thead>
<Tbody>
<Tr overflow={'unset'} border="none">
<HStack paddingX={'8px'} paddingY={'4px'}>
<MyIconButton
icon={isExpandMember ? 'common/downArrowFill' : 'common/rightArrowFill'}
onClick={setExpandMember.toggle}
/>
<Text>{t('user:team.group.members')}</Text>
</HStack>
</Tr>
{isExpandMember &&
members.map((member) => (
<Tr key={member.tmbId} overflow={'unset'} border="none">
<Td border="none">
<HStack>
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box>{member.name}</Box>
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
<Box ml="auto">
{/* <SearchInput
placeholder={t('user:team.group.search_placeholder')}
w="200px"
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
/> */}
</Box>
{userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/add2" w={'14px'} />}
onClick={onOpenAddMember}
>
{t('common:common.Add')}
</Button>
)}
</Flex>
<MyBox isLoading={addLoading || deleteLoading}>
<TableContainer fontSize={'sm'}>
<Table>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="md" maxW={'150px'}>
{`${t('user:team.group.members')} / ${t('user:team.org.org')} / ${t('user:team.group.group')}`}
<QuestionTip ml="1" label={t('user:team.group.permission_tip')} />
</Th>
<Th bg="myGray.100">
<Box mx="auto" w="fit-content">
{t('user:team.group.permission.write')}
</Box>
</Th>
<Th bg="myGray.100">
<Box mx="auto" w="fit-content">
{t('user:team.group.permission.manage')}
<QuestionTip ml="1" label={t('user:team.group.manage_tip')} />
</Box>
</Th>
<Th bg="myGray.100" borderRightRadius="md">
<Box mx="auto" w="fit-content">
{t('common:common.Action')}
</Box>
</Th>
</Tr>
</Thead>
<Tbody>
<>
<Tr userSelect={'none'}>
<HStack pl={3} pt={3} pb={isExpandMember && !!tmbList.length ? 0 : 3}>
<MyIconButton
icon={isExpandMember ? 'common/downArrowFill' : 'common/rightArrowFill'}
onClick={setExpandMember.toggle}
/>
<Box color={'myGray.900'}>{t('user:team.group.members')}</Box>
</HStack>
</Td>
<Td border="none">
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={member.permission.isOwner || !userManage}
isChecked={member.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onAddPermission({ memberId: String(member.tmbId), per: 'write' })
: onRemovePermission({ memberId: String(member.tmbId), per: 'write' })
}
</Tr>
{isExpandMember &&
tmbList.map((member) => (
<Tr key={member.tmbId}>
<Td pl={10}>
<HStack>
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box>{member.name}</Box>
</HStack>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={member.permission.isOwner || !userManage}
isChecked={member.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id: member.tmbId!,
type: 'add',
per: 'write'
})
: onUpdatePermission({
id: member.tmbId!,
type: 'remove',
per: 'write'
})
}
/>
</Box>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={member.permission.isOwner || !userInfo?.permission.isOwner}
isChecked={member.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id: member.tmbId!,
type: 'add',
per: 'manage'
})
: onUpdatePermission({
id: member.tmbId!,
type: 'remove',
per: 'manage'
})
}
/>
</Box>
</Td>
{userManage &&
!member.permission.isOwner &&
userInfo?.team.tmbId !== member.tmbId && (
<Td>
<Box mx="auto" w="fit-content">
<MyIconButton
icon="common/trash"
onClick={() =>
onDeleteMemberPermission({ tmbId: String(member.tmbId) })
}
/>
</Box>
</Td>
)}
</Tr>
))}
</>
<>
<Tr borderBottom={'1px solid'} borderColor={'myGray.200'} />
<Tr userSelect={'none'}>
<HStack pl={3} pt={3} pb={isExpandOrg && !!orgList.length ? 0 : 3}>
<MyIconButton
icon={isExpandOrg ? 'common/downArrowFill' : 'common/rightArrowFill'}
onClick={setExpandOrg.toggle}
/>
</Box>
</Td>
<Td border="none">
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={member.permission.isOwner || !userInfo?.permission.isOwner}
isChecked={member.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onAddPermission({ memberId: String(member.tmbId), per: 'manage' })
: onRemovePermission({ memberId: String(member.tmbId), per: 'manage' })
}
<Text>{t('user:team.org.org')}</Text>
</HStack>
</Tr>
{isExpandOrg &&
orgList.map((org) => (
<Tr key={org.orgId}>
<Td pl={10}>
<MemberTag name={org.name} avatar={org.avatar} />
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userManage}
isChecked={org.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({ id: org.orgId!, type: 'add', per: 'write' })
: onUpdatePermission({
id: org.orgId!,
type: 'remove',
per: 'write'
})
}
/>
</Box>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userInfo?.permission.isOwner}
isChecked={org.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({ id: org.orgId!, type: 'add', per: 'manage' })
: onUpdatePermission({
id: org.orgId!,
type: 'remove',
per: 'manage'
})
}
/>
</Box>
</Td>
{userInfo?.permission.isOwner && (
<Td>
<Box mx="auto" w="fit-content">
<MyIconButton
icon="common/trash"
onClick={() => onDeleteMemberPermission({ orgId: org.orgId! })}
/>
</Box>
</Td>
)}
</Tr>
))}
</>
<>
<Tr borderBottom={'1px solid'} borderColor={'myGray.200'} />
<Tr userSelect={'none'}>
<HStack pl={3} pt={3} pb={isExpandGroup && !!groupList.length ? 0 : 3}>
<MyIconButton
icon={isExpandGroup ? 'common/downArrowFill' : 'common/rightArrowFill'}
onClick={setExpandGroup.toggle}
/>
</Box>
</Td>
{userManage &&
!member.permission.isOwner &&
userInfo?.team.tmbId !== member.tmbId && (
<Td border="none">
<Box mx="auto" w="fit-content">
<MyIconButton
icon="common/trash"
onClick={() => onDeleteMemberPermission({ tmbId: String(member.tmbId) })}
<Text>{t('user:team.group.group')}</Text>
</HStack>
</Tr>
{isExpandGroup &&
groupList.map((group) => (
<Tr key={group.groupId}>
<Td pl={10}>
<MemberTag
name={
group.name === DefaultGroupName
? userInfo?.team.teamName ?? ''
: group.name
}
avatar={group.avatar}
/>
</Box>
</Td>
)}
</Tr>
))}
<Tr borderBottom={'1px solid'} borderColor={'myGray.200'} />
<Tr overflow={'unset'} border="none">
<HStack paddingX={'8px'} paddingY={'4px'}>
<MyIconButton
icon={isExpandOrg ? 'common/downArrowFill' : 'common/rightArrowFill'}
onClick={setExpandOrg.toggle}
/>
<Text>{t('user:team.org.org')}</Text>
</HStack>
</Tr>
{isExpandOrg &&
orgs.map((org) => (
<Tr key={org.orgId} overflow={'unset'} border="none">
<Td border="none">
<MemberTag name={org.name} avatar={org.avatar} />
</Td>
<Td border="none">
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userManage}
isChecked={org.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onAddPermission({ orgId: org.orgId, per: 'write' })
: onRemovePermission({ orgId: org.orgId, per: 'write' })
}
/>
</Box>
</Td>
<Td border="none">
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userInfo?.permission.isOwner}
isChecked={org.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onAddPermission({ orgId: org.orgId, per: 'manage' })
: onRemovePermission({ orgId: org.orgId, per: 'manage' })
}
/>
</Box>
</Td>
{userInfo?.permission.isOwner && (
<Td border="none">
<Box mx="auto" w="fit-content">
<MyIconButton
icon="common/trash"
onClick={() => onDeleteMemberPermission({ orgId: org.orgId })}
/>
</Box>
</Td>
)}
</Tr>
))}
<Tr borderBottom={'1px solid'} borderColor={'myGray.200'} />
<Tr overflow={'unset'} border="none">
<HStack paddingX={'8px'} paddingY={'4px'}>
<MyIconButton
icon={isExpandGroup ? 'common/downArrowFill' : 'common/rightArrowFill'}
onClick={setExpandGroup.toggle}
/>
<Text>{t('user:team.group.group')}</Text>
</HStack>
</Tr>
{isExpandGroup &&
groups.map((group) => (
<Tr key={group.groupId} overflow={'unset'} border="none">
<Td border="none">
<MemberTag
name={
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
}
avatar={group.avatar}
/>
</Td>
<Td border="none">
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userManage}
isChecked={group.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onAddPermission({ groupId: group.groupId, per: 'write' })
: onRemovePermission({ groupId: group.groupId, per: 'write' })
}
/>
</Box>
</Td>
<Td border="none">
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userInfo?.permission.isOwner}
isChecked={group.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onAddPermission({ groupId: group.groupId, per: 'manage' })
: onRemovePermission({ groupId: group.groupId, per: 'manage' })
}
/>
</Box>
</Td>
{userInfo?.permission.isOwner && (
<Td border="none">
<Box mx="auto" w="fit-content">
<MyIconButton
icon="common/trash"
onClick={() => onDeleteMemberPermission({ groupId: group.groupId })}
/>
</Box>
</Td>
)}
</Tr>
))}
</Tbody>
</Table>
{isOpenAddPermission && (
<MemberModal
onClose={() => {
refetchClbs();
onCloseAddPermission();
}}
mode="all"
/>
)}
</TableContainer>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userManage}
isChecked={group.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id: group.groupId!,
type: 'add',
per: 'write'
})
: onUpdatePermission({
id: group.groupId!,
type: 'remove',
per: 'write'
})
}
/>
</Box>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userInfo?.permission.isOwner}
isChecked={group.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id: group.groupId!,
type: 'add',
per: 'manage'
})
: onUpdatePermission({
id: group.groupId!,
type: 'remove',
per: 'manage'
})
}
/>
</Box>
</Td>
{userInfo?.permission.isOwner && (
<Td>
<Box mx="auto" w="fit-content">
<MyIconButton
icon="common/trash"
onClick={() => onDeleteMemberPermission({ groupId: group.groupId! })}
/>
</Box>
</Td>
)}
</Tr>
))}
</>
</Tbody>
</Table>
</TableContainer>
</MyBox>
</>
);
}
export default PermissionManage;
export const Render = ({ Tabs }: { Tabs: React.ReactNode }) => {
const { userInfo } = useUserStore();
return userInfo?.team ? (
<CollaboratorContextProvider
permission={userInfo?.team.permission}
permissionList={TeamPermissionList}
onGetCollaboratorList={getTeamClbs}
onUpdateCollaborators={updateMemberPermission}
onDelOneCollaborator={deleteMemberPermission}
refreshDeps={[userInfo?.team.teamId]}
>
{({ onOpenAddMember }) => <PermissionManage Tabs={Tabs} onOpenAddMember={onOpenAddMember} />}
</CollaboratorContextProvider>
) : null;
};
export default Render;

View File

@@ -25,8 +25,6 @@ type TeamModalContextType = {
refetchMembers: () => void;
refetchTeams: () => void;
refetchGroups: () => void;
searchKey: string;
setSearchKey: React.Dispatch<React.SetStateAction<string>>;
teamSize: number;
};
@@ -51,10 +49,6 @@ export const TeamContext = createContext<TeamModalContextType>({
throw new Error('Function not implemented.');
},
searchKey: '',
setSearchKey: function (_value: React.SetStateAction<string>): void {
throw new Error('Function not implemented.');
},
teamSize: 0
});
@@ -62,7 +56,6 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
const { t } = useTranslation();
const [editTeamData, setEditTeamData] = useState<EditTeamFormDataType>();
const { userInfo, initUserInfo, loadAndGetTeamMembers } = useUserStore();
const [searchKey, setSearchKey] = useState('');
const {
data: myTeams = [],
@@ -115,8 +108,6 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
refetchTeams,
isLoading,
onSwitchTeam,
searchKey,
setSearchKey,
// create | update team
setEditTeamData,

View File

@@ -1,33 +1,25 @@
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import AccountContainer from '../components/AccountContainer';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import { Box, Flex, useDisclosure } from '@chakra-ui/react';
import Icon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import TeamSelector from '../components/TeamSelector';
import { useUserStore } from '@/web/support/user/useUserStore';
import React, { useState } from 'react';
import React, { useMemo } from 'react';
import { useContextSelector } from 'use-context-selector';
import { useRouter } from 'next/router';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { delLeaveTeam } from '@/web/support/user/team/api';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { TeamContext, TeamModalContextProvider } from './components/context';
import dynamic from 'next/dynamic';
import TeamTagModal from '@/components/support/user/team/TeamTagModal';
import MemberTable from './components/MemberTable';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
const InviteModal = dynamic(() => import('./components/InviteModal'));
const PermissionManage = dynamic(() => import('./components/PermissionManage/index'));
const GroupManage = dynamic(() => import('./components/GroupManage/index'));
const GroupInfoModal = dynamic(() => import('./components/GroupManage/GroupInfoModal'));
const ManageGroupMemberModal = dynamic(() => import('./components/GroupManage/GroupManageMember'));
const OrgManage = dynamic(() => import('./components/OrgManage/index'));
export enum TeamTabEnum {
@@ -39,79 +31,37 @@ export enum TeamTabEnum {
const Team = () => {
const router = useRouter();
const { toast } = useToast();
const { t } = useTranslation();
const { userInfo, teamPlanStatus } = useUserStore();
const { feConfigs, setNotSufficientModalType } = useSystemStore();
const {
myTeams,
refetchTeams,
members,
refetchMembers,
setEditTeamData,
onSwitchTeam,
searchKey,
setSearchKey,
teamSize,
isLoading
} = useContextSelector(TeamContext, (v) => v);
const { teamTab = TeamTabEnum.member } = router.query as { teamTab: `${TeamTabEnum}` };
const {
isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync
} = useDisclosure();
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const {
isOpen: isOpenGroupInfo,
onOpen: onOpenGroupInfo,
onClose: onCloseGroupInfo
} = useDisclosure();
const {
isOpen: isOpenManageGroupMember,
onOpen: onOpenManageGroupMember,
onClose: onCloseManageGroupMember
} = useDisclosure();
const {
isOpen: isOpenAddPermission,
onOpen: onOpenAddPermission,
onClose: onCloseAddPermission
} = useDisclosure();
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { runAsync: onLeaveTeam } = useRequest2(
async () => {
const defaultTeam = myTeams.find((item) => item.defaultTeam) || myTeams[0];
// change to personal team
// get members
onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam();
},
{
onSuccess() {
refetchTeams();
},
errorToast: t('account_team:user_team_leave_team_failed')
}
const { setEditTeamData, teamSize, isLoading } = useContextSelector(TeamContext, (v) => v);
const Tabs = useMemo(
() => (
<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 }
]}
px={'1rem'}
value={teamTab}
onChange={(e) => {
router.replace({
query: {
...router.query,
teamTab: e
}
});
}}
/>
),
[router, t, teamTab]
);
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('account_team:confirm_leave_team')
});
const [editGroupId, setEditGroupId] = useState<string>();
const onEditGroup = (groupId: string) => {
setEditGroupId(groupId);
onOpenGroupInfo();
};
const onManageMember = (groupId: string) => {
setEditGroupId(groupId);
onOpenManageGroupMember();
};
return (
<AccountContainer isLoading={isLoading}>
{/* header */}
@@ -175,159 +125,11 @@ const Team = () => {
{/* table */}
<Box py={'1.5rem'} px={'2rem'}>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
<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 }
]}
px={'1rem'}
value={teamTab}
onChange={(e) => {
router.replace({
query: {
...router.query,
teamTab: e
}
});
}}
/>
<Flex alignItems={'center'}>
{teamTab === TeamTabEnum.member &&
userInfo?.team.permission.hasManagePer &&
feConfigs?.show_team_chat && (
<Button
variant={'whitePrimary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="core/dataset/tag" w={'16px'} />}
onClick={() => {
onOpenTeamTagsAsync();
}}
>
{t('account_team:label_sync')}
</Button>
)}
{teamTab === TeamTabEnum.member && userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/inviteLight" w={'16px'} color={'white'} />}
onClick={() => {
if (
teamPlanStatus?.standardConstants?.maxTeamMember &&
teamPlanStatus.standardConstants.maxTeamMember <= members.length
) {
toast({
status: 'warning',
title: t('common:user.team.Over Max Member Tip', {
max: teamPlanStatus.standardConstants.maxTeamMember
})
});
setNotSufficientModalType(TeamErrEnum.teamMemberOverSize);
} else {
onOpenInvite();
}
}}
>
{t('account_team:user_team_invite_member')}
</Button>
)}
{teamTab === TeamTabEnum.member && !userInfo?.team.permission.isOwner && (
<Button
variant={'whitePrimary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
onClick={() => openLeaveConfirm(onLeaveTeam)()}
>
{t('account_team:user_team_leave_team')}
</Button>
)}
{teamTab === TeamTabEnum.group && userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="support/permission/collaborator" w={'14px'} />}
onClick={onOpenGroupInfo}
>
{t('user:team.group.create')}
</Button>
)}
{teamTab === TeamTabEnum.permission && (
<Box ml="auto">
<SearchInput
placeholder={t('user:team.group.search_placeholder')}
w="200px"
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
/>
</Box>
)}
{teamTab === TeamTabEnum.permission && userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/add2" w={'14px'} />}
onClick={onOpenAddPermission}
>
{t('common:common.Add')}
</Button>
)}
</Flex>
</Flex>
<Box flex={'1 0 0'} overflow={'auto'}>
{teamTab === TeamTabEnum.member && <MemberTable />}
{teamTab === TeamTabEnum.group && (
<GroupManage onEditGroup={onEditGroup} onManageMember={onManageMember} />
)}
{teamTab === TeamTabEnum.org && <OrgManage />}
{teamTab === TeamTabEnum.permission && (
<PermissionManage
isOpenAddPermission={isOpenAddPermission}
onCloseAddPermission={onCloseAddPermission}
/>
)}
</Box>
{teamTab === TeamTabEnum.member && <MemberTable Tabs={Tabs} />}
{teamTab === TeamTabEnum.org && <OrgManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.group && <GroupManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.permission && <PermissionManage Tabs={Tabs} />}
</Box>
{isOpenInvite && userInfo?.team?.teamId && (
<InviteModal
teamId={userInfo.team.teamId}
onClose={onCloseInvite}
onSuccess={refetchMembers}
/>
)}
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
{isOpenGroupInfo && (
<GroupInfoModal
onClose={() => {
onCloseGroupInfo();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
{isOpenManageGroupMember && (
<ManageGroupMemberModal
onClose={() => {
onCloseManageGroupMember();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
<ConfirmLeaveTeamModal />
</AccountContainer>
);
};

View File

@@ -0,0 +1,25 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import * as fs from 'fs';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
export type writefileQuery = {};
export type writefileBody = {
name: string;
content: string;
};
export type writefileResponse = {};
async function handler(
req: ApiRequestProps<writefileBody, writefileQuery>,
res: ApiResponseType<any>
): Promise<writefileResponse> {
await authCert({ req, authRoot: true });
const { name, content } = req.body;
await fs.promises.writeFile(`public/${name}`, content);
return {};
}
export default NextAPI(handler);

View File

@@ -187,7 +187,6 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
)}
<Box mt={6}>
<CollaboratorContextProvider
mode="all"
permission={appDetail.permission}
onGetCollaboratorList={() => getCollaboratorList(appDetail._id)}
permissionList={AppPermissionList}

View File

@@ -57,7 +57,7 @@ const NodeCard = (props: Props) => {
name = t('common:core.module.template.UnKnow Module'),
intro,
minW = '300px',
maxW = '600px',
maxW = '666px',
minH = 0,
w = 'full',
h = 'full',

View File

@@ -431,7 +431,6 @@ const ListItem = () => {
avatar={editPerApp.avatar}
name={editPerApp.name}
managePer={{
mode: 'all',
permission: editPerApp.permission,
onGetCollaboratorList: () => getCollaboratorList(editPerApp._id),
permissionList: AppPermissionList,

View File

@@ -301,7 +301,6 @@ const MyApps = () => {
deleteTip={t('app:confirm_delete_folder_tip')}
onDelete={() => onDeleFolder(folderDetail._id)}
managePer={{
mode: 'all',
permission: folderDetail.permission,
onGetCollaboratorList: () => getCollaboratorList(folderDetail._id),
permissionList: AppPermissionList,

View File

@@ -1,4 +1,4 @@
import { Box, Button, Flex, FormLabel } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import React from 'react';
import CollaboratorContextProvider, {
MemberManagerInputPropsType

View File

@@ -354,7 +354,6 @@ const Info = ({ datasetId }: { datasetId: string }) => {
<Box>
<MemberManager
managePer={{
mode: 'all',
permission: datasetDetail.permission,
onGetCollaboratorList: () => getCollaboratorList(datasetId),
permissionList: DatasetPermissionList,

View File

@@ -440,7 +440,6 @@ function List() {
avatar={editPerDataset.avatar}
name={editPerDataset.name}
managePer={{
mode: 'all',
permission: editPerDataset.permission,
onGetCollaboratorList: () => getCollaboratorList(editPerDataset._id),
permissionList: DatasetPermissionList,

View File

@@ -238,7 +238,6 @@ const Dataset = () => {
})
}
managePer={{
mode: 'all',
permission: folderDetail.permission,
onGetCollaboratorList: () => getCollaboratorList(folderDetail._id),
permissionList: DatasetPermissionList,

View File

@@ -0,0 +1,5 @@
export type CreateModelParams = {
name: string;
description: string;
prompt: string;
};

View File

@@ -0,0 +1,66 @@
import { addLog } from '@fastgpt/service/common/system/log';
import axios, { Method } from 'axios';
const url = process.env.API_PROXY_URL;
const token = process.env.API_PROXY_TOKEN;
const instance = axios.create({
baseURL: url,
timeout: 60000, // 超时时间
headers: {
Authorization: `Bearer ${token}`
}
});
/**
* 响应数据检查
*/
const checkRes = (data: any) => {
if (data === undefined) {
addLog.info('api proxy data is empty');
return Promise.reject('服务器异常');
}
return data.data;
};
const responseError = (err: any) => {
console.log('error->', '请求错误', err);
if (!err) {
return Promise.reject({ message: '未知错误' });
}
if (typeof err === 'string') {
return Promise.reject({ message: err });
}
if (typeof err.message === 'string') {
return Promise.reject({ message: err.message });
}
if (typeof err.data === 'string') {
return Promise.reject({ message: err.data });
}
if (err?.response?.data) {
return Promise.reject(err?.response?.data);
}
return Promise.reject(err);
};
const request = <T>(url: string, data: any, method: Method): Promise<T> => {
/* 去空 */
for (const key in data) {
if (data[key] === undefined) {
delete data[key];
}
}
return instance
.request({
url,
method,
data: ['POST', 'PUT'].includes(method) ? data : undefined,
params: !['POST', 'PUT'].includes(method) ? data : undefined
})
.then((res) => checkRes(res.data))
.catch((err) => responseError(err));
};
// TODO: channel crud
export const ApiProxy = {};

View File

@@ -9,6 +9,7 @@ import { TOKEN_ERROR_CODE } from '@fastgpt/global/common/error/errorCode';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import { useSystemStore } from '../system/useSystemStore';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
import { i18nT } from '@fastgpt/web/i18n/utils';
interface ConfigType {
headers?: { [key: string]: string };
@@ -108,17 +109,15 @@ function responseError(err: any) {
return Promise.reject({ message: err });
}
// 有报错响应
if (err?.code in TOKEN_ERROR_CODE) {
if (
!(window.location.pathname === '/chat/share' || window.location.pathname === '/chat/team')
) {
if (err?.code in TOKEN_ERROR_CODE || err?.response?.data?.code in TOKEN_ERROR_CODE) {
if (!['/chat/share', '/chat/team', '/login'].includes(window.location.pathname)) {
clearToken();
window.location.replace(
getWebReqUrl(`/login?lastRoute=${encodeURIComponent(location.pathname + location.search)}`)
);
}
return Promise.reject({ message: '无权操作' });
return Promise.reject({ message: i18nT('common:unauth_token') });
}
if (
err?.statusText === TeamErrEnum.aiPointsNotEnough ||

View File

@@ -1,9 +1,8 @@
import { GET, POST, PUT, DELETE } from '@/web/common/api/request';
import {
CreatePermissionBody,
CollaboratorItemType,
DeletePermissionQuery,
ListPermissionResponse,
UpdatePermissionBody
UpdateClbPermissionProps
} from '@fastgpt/global/support/permission/collaborator';
import {
CreateTeamProps,
@@ -43,16 +42,11 @@ export const updateInviteResult = (data: UpdateInviteProps) =>
PUT('/proApi/support/user/team/member/updateInvite', data);
export const delLeaveTeam = () => DELETE('/proApi/support/user/team/member/leave');
export const getTeamClbs = () =>
GET<ListPermissionResponse>(`/proApi/support/user/team/collaborator/list`);
/* -------------- team collaborator -------------------- */
export const updateMemberPermission = (data: UpdatePermissionBody) =>
PUT('/proApi/support/user/team/collaborator/updatePermission', data);
export const createMemberPermission = (data: CreatePermissionBody) =>
POST('/proApi/support/user/team/collaborator/create', data);
export const getTeamClbs = () =>
GET<CollaboratorItemType[]>(`/proApi/support/user/team/collaborator/list`);
export const updateMemberPermission = (data: UpdateClbPermissionProps) =>
PUT('/proApi/support/user/team/collaborator/update', data);
export const deleteMemberPermission = (id: DeletePermissionQuery) =>
DELETE('/proApi/support/user/team/collaborator/delete', id);