Commit f89452ac by Archer Committed by GitHub

Group role (#2993)

* feat: app/dataset support group (#2898)

* pref: member-group (#2862)

* feat: group list ordered by updateTime

* fix: transfer ownership of group when deleting member

* fix: i18n fix

* feat: can not set member as admin/owner when user is not active

* fix: GroupInfoModal hover input do not change color

* fix(fe): searchinput do not scroll

* feat: app collaborator with group, remove default permission

* feat: dataset collaborator with group, remove default permission

* chore(test): pref mock

* chore: remove useless code

* chore: adjust

* fix: add self as collaborator when creating folder

* fix(fe): folder manage menu do not show when user has write permission
only

* fix: dataset folder create

* feat: Add code comment

* Pref: app move (#2952)

* perf: app schema

* doc

---------

Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
parent 74d58d56
......@@ -23,7 +23,9 @@ weight: 812
9. 新增 - 数据库连接和操作插件
10. 新增 - Cookie 隐私协议提示
11. 新增 - HTTP 节点支持 JSONPath 表达式
12. 修复 - 文件后缀判断,去除 query 影响。
13. 修复 - AI 响应为空时,会造成 LLM 历史记录合并。
14. 修复 - 用户交互节点未阻塞流程。
15. 修复 - 新建 APP,有时候会导致空指针报错。
12. 新增 - 应用和知识库支持成员组配置权限
13. 优化 - 循环节点支持选择外部节点的变量
14. 修复 - 文件后缀判断,去除 query 影响。
15. 修复 - AI 响应为空时,会造成 LLM 历史记录合并。
16. 修复 - 用户交互节点未阻塞流程。
17. 修复 - 新建 APP,有时候会导致空指针报错。
import { UpdateClbPermissionProps } from '../../support/permission/collaborator';
import { RequireOnlyOne } from '../../common/type/utils';
import {
UpdateClbPermissionProps,
UpdatePermissionBody
} from '../../support/permission/collaborator';
import { PermissionValueType } from '../../support/permission/type';
export type UpdateAppCollaboratorBody = UpdateClbPermissionProps & {
......@@ -7,5 +11,7 @@ export type UpdateAppCollaboratorBody = UpdateClbPermissionProps & {
export type AppCollaboratorDeleteParams = {
appId: string;
} & RequireOnlyOne<{
tmbId: string;
};
groupId: string;
}>;
......@@ -10,7 +10,6 @@ import { SelectedDatasetType } from '../workflow/api';
import { DatasetSearchModeEnum } from '../dataset/constants';
import { TeamTagSchema as TeamTagsSchemaType } from '@fastgpt/global/support/user/team/type.d';
import { StoreEdgeItemType } from '../workflow/type/edge';
import { PermissionSchemaType, PermissionValueType } from '../../support/permission/type';
import { AppPermission } from '../../support/permission/app/controller';
import { ParentIdType } from '../../common/parentFolder/type';
import { FlowNodeInputTypeEnum } from 'core/workflow/node/constant';
......@@ -45,7 +44,11 @@ export type AppSchema = {
inited?: boolean;
teamTags: string[];
} & PermissionSchemaType;
inheritPermission?: boolean;
// abandon
defaultPermission?: number;
};
export type AppListItemType = {
_id: string;
......@@ -57,7 +60,9 @@ export type AppListItemType = {
updateTime: Date;
pluginData?: AppSchema['pluginData'];
permission: AppPermission;
} & PermissionSchemaType;
inheritPermission?: boolean;
private?: boolean;
};
export type AppDetailType = AppSchema & {
permission: AppPermission;
......
import { UpdateClbPermissionProps } from '../../support/permission/collaborator';
import { PermissionValueType } from '../../support/permission/type';
import { RequireOnlyOne } from '../../common/type/utils';
export type UpdateDatasetCollaboratorBody = UpdateClbPermissionProps & {
datasetId: string;
......@@ -7,5 +8,7 @@ export type UpdateDatasetCollaboratorBody = UpdateClbPermissionProps & {
export type DatasetCollaboratorDeleteParams = {
datasetId: string;
} & RequireOnlyOne<{
tmbId: string;
};
groupId: string;
}>;
import { PermissionSchemaType } from '../../support/permission/type';
import type { LLMModelItemType, VectorModelItemType } from '../../core/ai/model.d';
import { PermissionTypeEnum } from '../../support/permission/constant';
import { PushDatasetDataChunkProps } from './api';
......@@ -32,8 +31,11 @@ export type DatasetSchemaType = {
selector: string;
};
externalReadUrl?: string;
} & PermissionSchemaType;
// } & PermissionSchemaType;
inheritPermission: boolean;
// abandon
defaultPermission?: number;
};
export type DatasetCollectionSchemaType = {
_id: string;
......@@ -146,7 +148,9 @@ export type DatasetListItemType = {
type: `${DatasetTypeEnum}`;
permission: DatasetPermission;
vectorModel: VectorModelItemType;
} & PermissionSchemaType;
inheritPermission: boolean;
private?: boolean;
};
export type DatasetItemType = Omit<DatasetSchemaType, 'vectorModel' | 'agentModel'> & {
vectorModel: VectorModelItemType;
......
......@@ -4,11 +4,13 @@ import { PermissionValueType } from './type';
export type CollaboratorItemType = {
teamId: string;
tmbId: string;
permission: Permission;
name: string;
avatar: string;
};
} & RequireOnlyOne<{
tmbId: string;
groupId: string;
}>;
export type UpdateClbPermissionProps = {
members?: string[];
......
import { Permission } from './controller';
import { PermissionListType } from './type';
import { i18nT } from '../../../web/i18n/utils';
export enum AuthUserTypeEnum {
......
import { RequireOnlyOne } from '../../common/type/utils';
import { TeamMemberWithUserSchema } from '../user/team/type';
import { AuthUserTypeEnum, PermissionKeyEnum, PerResourceTypeEnum } from './constant';
import { MemberGroupSchemaType } from './memberGroup/type';
// PermissionValueType, the type of permission's value is a number, which is a bit field actually.
// It is spired by the permission system in Linux.
......@@ -33,6 +34,10 @@ export type ResourcePerWithTmbWithUser = Omit<ResourcePermissionType, 'tmbId'> &
tmbId: TeamMemberWithUserSchema;
};
export type ResourcePerWithGroup = Omit<ResourcePermissionType, 'groupId'> & {
groupId: MemberGroupSchemaType;
};
export type PermissionSchemaType = {
defaultPermission: PermissionValueType;
inheritPermission: boolean;
......
......@@ -5,8 +5,6 @@ import {
TeamCollectionName,
TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant';
import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant';
import { getPermissionSchema } from '@fastgpt/global/support/permission/utils';
export const AppCollectionName = 'apps';
......@@ -111,8 +109,13 @@ const AppSchema = new Schema({
inited: {
type: Boolean
},
inheritPermission: {
type: Boolean,
default: true
},
...getPermissionSchema(AppDefaultPermissionVal)
// abandoned
defaultPermission: Number
});
AppSchema.index({ teamId: 1, updateTime: -1 });
......
......@@ -9,8 +9,6 @@ import {
TeamCollectionName,
TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant';
import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant';
import { getPermissionSchema } from '@fastgpt/global/support/permission/utils';
import type { DatasetSchemaType } from '@fastgpt/global/core/dataset/type.d';
export const DatasetCollectionName = 'datasets';
......@@ -88,7 +86,13 @@ const DatasetSchema = new Schema({
externalReadUrl: {
type: String
},
...getPermissionSchema(DatasetDefaultPermissionVal)
inheritPermission: {
type: Boolean,
default: true
},
// abandoned
defaultPermission: Number
});
try {
......
......@@ -13,6 +13,7 @@ import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { splitCombinePluginId } from '../../../core/app/plugin/controller';
import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
import { AuthModeType, AuthResponseType } from '../type';
import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant';
export const authPluginByTmbId = async ({
tmbId,
......@@ -60,7 +61,6 @@ export const authAppByTmbId = async ({
if (isRoot) {
return {
...app,
defaultPermission: app.defaultPermission,
permission: new AppPermission({ isOwner: true })
};
}
......@@ -71,7 +71,7 @@ export const authAppByTmbId = async ({
const isOwner = tmbPer.isOwner || String(app.tmbId) === String(tmbId);
const { Per, defaultPermission } = await (async () => {
const { Per } = await (async () => {
if (
AppFolderTypeList.includes(app.type) ||
app.inheritPermission === false ||
......@@ -86,10 +86,9 @@ export const authAppByTmbId = async ({
resourceId: appId,
resourceType: PerResourceTypeEnum.app
});
const Per = new AppPermission({ per: rp ?? app.defaultPermission, isOwner });
const Per = new AppPermission({ per: rp ?? AppDefaultPermissionVal, isOwner });
return {
Per,
defaultPermission: app.defaultPermission
Per
};
} else {
// is not folder and inheritPermission is true and is not root folder.
......@@ -104,8 +103,7 @@ export const authAppByTmbId = async ({
isOwner
});
return {
Per,
defaultPermission: parent.defaultPermission
Per
};
}
})();
......@@ -116,7 +114,6 @@ export const authAppByTmbId = async ({
return {
...app,
defaultPermission,
permission: Per
};
})();
......
......@@ -10,12 +10,17 @@ import { MongoResourcePermission } from './schema';
import { ClientSession } from 'mongoose';
import {
PermissionValueType,
ResourcePermissionType
ResourcePermissionType,
ResourcePerWithGroup,
ResourcePerWithTmbWithUser
} from '@fastgpt/global/support/permission/type';
import { bucketNameMap } from '@fastgpt/global/common/file/constants';
import { addMinutes } from 'date-fns';
import { getGroupsByTmbId } from './memberGroup/controllers';
import { Permission } from '@fastgpt/global/support/permission/controller';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
/** get resource permission for a team member
* If there is no permission for the team member, it will return undefined
......@@ -123,20 +128,94 @@ export async function getResourceAllClbs({
).lean();
}
export async function getResourceClbsAndGroups({
resourceId,
resourceType,
teamId,
session
}: {
resourceId: ParentIdType;
resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>;
teamId: string;
session: ClientSession;
}) {
return MongoResourcePermission.find(
{
resourceId,
resourceType,
teamId
},
undefined,
{ session }
).lean();
}
export const getClbsAndGroupsWithInfo = async ({
resourceId,
resourceType,
teamId
}: {
resourceId: ParentIdType;
resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>;
teamId: string;
}) =>
Promise.all([
(await MongoResourcePermission.find({
teamId,
resourceId,
resourceType,
tmbId: {
$exists: true
}
}).populate({
path: 'tmbId',
select: 'name userId',
populate: {
path: 'userId',
select: 'avatar'
}
})) as ResourcePerWithTmbWithUser[],
(await MongoResourcePermission.find({
teamId,
resourceId,
resourceType,
groupId: {
$exists: true
}
}).populate({
path: 'groupId',
select: 'name avatar'
})) as ResourcePerWithGroup[]
]);
export const delResourcePermissionById = (id: string) => {
return MongoResourcePermission.findByIdAndRemove(id);
};
export const delResourcePermission = ({
session,
tmbId,
groupId,
...props
}: {
resourceType: PerResourceTypeEnum;
teamId: string;
resourceId: string;
tmbId: string;
session?: ClientSession;
tmbId?: string;
groupId?: string;
}) => {
return MongoResourcePermission.deleteOne(props, { session });
// tmbId or groupId only one and not both
if (!!tmbId === !!groupId) {
return Promise.reject(CommonErrEnum.missingParams);
}
return MongoResourcePermission.deleteOne(
{
...(tmbId ? { tmbId } : {}),
...(groupId ? { groupId } : {}),
...props
},
{ session }
);
};
/* 下面代码等迁移 */
......
......@@ -20,6 +20,7 @@ import { MongoDatasetData } from '../../../core/dataset/data/schema';
import { AuthModeType, AuthResponseType } from '../type';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant';
export const authDatasetByTmbId = async ({
tmbId,
......@@ -62,7 +63,7 @@ export const authDatasetByTmbId = async ({
const isOwner = tmbPer.isOwner || String(dataset.tmbId) === String(tmbId);
// get dataset permission or inherit permission from parent folder.
const { Per, defaultPermission } = await (async () => {
const { Per } = await (async () => {
if (
dataset.type === DatasetTypeEnum.folder ||
dataset.inheritPermission === false ||
......@@ -78,12 +79,11 @@ export const authDatasetByTmbId = async ({
resourceType: PerResourceTypeEnum.dataset
});
const Per = new DatasetPermission({
per: rp ?? dataset.defaultPermission,
per: rp ?? DatasetDefaultPermissionVal,
isOwner
});
return {
Per,
defaultPermission: dataset.defaultPermission
Per
};
} else {
// is not folder and inheritPermission is true and is not root folder.
......@@ -100,8 +100,7 @@ export const authDatasetByTmbId = async ({
});
return {
Per,
defaultPermission: parent.defaultPermission
Per
};
}
})();
......@@ -112,7 +111,6 @@ export const authDatasetByTmbId = async ({
return {
...dataset,
defaultPermission,
permission: Per
};
})();
......@@ -179,14 +177,15 @@ export async function authDatasetCollection({
tmbId,
datasetId: collection.datasetId._id,
per,
isRoot: isRootFromHeader || isRoot
isRoot: isRootFromHeader
});
return {
teamId,
tmbId,
collection,
permission: dataset.permission
permission: dataset.permission,
isRoot: isRootFromHeader
};
}
......@@ -231,7 +230,8 @@ export async function authDatasetFile({
teamId,
tmbId,
file,
permission
permission,
isRoot
};
} catch (error) {
return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
......
import { mongoSessionRun } from '../../common/mongo/sessionRun';
import { MongoResourcePermission } from './schema';
import { ClientSession, Model } from 'mongoose';
import { NullPermission, PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { getResourceAllClbs } from './controller';
import { getResourceClbsAndGroups } from './controller';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
......@@ -28,7 +28,6 @@ export async function syncChildrenPermission({
resourceModel,
session,
defaultPermission,
collaborators
}: {
resource: SyncChildrenPermissionResourceType;
......@@ -42,7 +41,6 @@ export async function syncChildrenPermission({
// should be provided when inheritPermission is true
session: ClientSession;
defaultPermission?: PermissionValueType;
collaborators?: UpdateCollaboratorItem[];
}) {
// only folder has permission
......@@ -76,19 +74,6 @@ export async function syncChildrenPermission({
}
if (!children.length) return;
// Sync default permission
if (defaultPermission !== undefined) {
await resourceModel.updateMany(
{
_id: { $in: children }
},
{
defaultPermission
},
{ session }
);
}
// sync the resource permission
if (collaborators) {
// Update the collaborators of all children
......@@ -124,28 +109,20 @@ export async function resumeInheritPermission({
const isFolder = folderTypeList.includes(resource.type);
const fn = async (session: ClientSession) => {
const parentResource = await resourceModel
.findById(resource.parentId, 'defaultPermission')
.lean<SyncChildrenPermissionResourceType & { defaultPermission: PermissionValueType }>()
.session(session);
const parentDefaultPermissionVal = parentResource?.defaultPermission ?? NullPermission;
// update the resource permission
await resourceModel.updateOne(
{
_id: resource._id
},
{
inheritPermission: true,
defaultPermission: parentDefaultPermissionVal
inheritPermission: true
},
{ session }
);
// Folder resource, need to sync children
if (isFolder) {
const parentClbs = await getResourceAllClbs({
const parentClbsAndGroups = await getResourceClbsAndGroups({
resourceId: resource.parentId,
teamId: resource.teamId,
resourceType,
......@@ -155,7 +132,7 @@ export async function resumeInheritPermission({
// sync self
await syncCollaborators({
resourceType,
collaborators: parentClbs,
collaborators: parentClbsAndGroups,
teamId: resource.teamId,
resourceId: resource._id,
session
......@@ -169,8 +146,7 @@ export async function resumeInheritPermission({
folderTypeList,
resourceType,
session,
defaultPermission: parentDefaultPermissionVal,
collaborators: parentClbs
collaborators: parentClbsAndGroups
});
} else {
// Not folder, delete all clb
......@@ -215,6 +191,7 @@ export async function syncCollaborators({
resourceId,
resourceType: resourceType,
tmbId: item.tmbId,
groupId: item.groupId,
permission: item.permission
})),
{
......
......@@ -64,7 +64,7 @@ export const getGroupsByTmbId = async ({
groupId: {
$exists: true
},
role: role ? { $in: role } : undefined
...(role ? { role: { $in: role } } : {})
})
.populate('groupId')
.lean()
......
......@@ -28,5 +28,6 @@ export type AuthResponseType<T extends Permission = Permission> = {
authType?: `${AuthUserTypeEnum}`;
appId?: string;
apikey?: string;
isRoot: boolean;
permission: T;
};
......@@ -8,7 +8,7 @@ import { TeamPermission } from '@fastgpt/global/support/permission/user/controll
/* auth user role */
export async function authUserPer(props: AuthModeType): Promise<
AuthResponseType & {
AuthResponseType<TeamPermission> & {
tmb: TeamTmbItemType;
}
> {
......
......@@ -71,6 +71,7 @@
"modules.Title is required": "模块名不能为空",
"month.unit": "号",
"move_app": "移动应用",
"move.hint": "移动后,所选应用/文件夹将继承新文件夹的权限设置,原先的权限设置失效。",
"not_json_file": "请选择JSON文件",
"or_drag_JSON": "或拖入JSON文件",
"paste_config": "粘贴配置",
......
......@@ -20,6 +20,7 @@
"Folder": "文件夹",
"Login": "登录",
"Move": "移动",
"move.confirm": "确认移动",
"Name": "名称",
"None": "无",
"Rename": "重命名",
......@@ -82,6 +83,8 @@
"code_error.team_error.un_auth": "无权操作该团队",
"code_error.team_error.user_not_active": "用户未接受或已离开团队",
"code_error.team_error.website_sync_not_enough": "无权使用Web站点同步~",
"code_error.team_error.group_name_duplicate": "群组名称重复",
"code_error.team_error.user_not_active": "用户未接受或已离开团队",
"code_error.token_error_code.403": "登录状态无效,请重新登录",
"code_error.user_error.balance_not_enough": "账号余额不足~",
"code_error.user_error.bin_visitor": "您的身份校验未通过",
......@@ -915,7 +918,7 @@
"permission.Permission config": "权限配置",
"permission.Private": "私有",
"permission.Private Tip": "仅自己可用",
"permission.Public": "团队",
"permission.Public": "协作",
"permission.Public Tip": "团队所有成员可使用",
"permission.Remove InheritPermission Confirm": "此操作会导致权限继承失效,是否进行?",
"permission.Resume InheritPermission Confirm": "是否恢复为继承父级文件夹的权限?",
......@@ -1194,7 +1197,7 @@
"user.team.invite.Reject Confirm": "确认拒绝该邀请?",
"user.team.invite.accept": "接受",
"user.team.invite.reject": "拒绝",
"user.team.member.Confirm Leave": "确认离开该团队?",
"user.team.member.Confirm Leave": "确认离开该团队?\n退出后,您在该团队所有的资源( 应用、知识库、文件夹、管理的群组等)均转让给团队所有者。",
"user.team.member.active": "已加入",
"user.team.member.reject": "拒绝",
"user.team.member.waiting": "待接受",
......
......@@ -34,5 +34,6 @@
"website_dataset_desc": "Web 站点同步允许你直接使用一个网页链接构建知识库",
"permission.des.read": "可查看知识库内容",
"permission.des.write": "可增加和变更知识库内容",
"permission.des.manage": "可管理整个知识库数据和信息"
}
\ No newline at end of file
"permission.des.manage": "可管理整个知识库数据和信息",
"move.hint": "移动后,所选知识库/文件夹将继承新文件夹的权限设置,原先的权限设置失效。"
}
......@@ -34,12 +34,7 @@ const config = {
// coverageProvider: "babel",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
coverageReporters: ['json', 'text', 'lcov', 'clover'],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
......@@ -163,7 +158,7 @@ const config = {
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
// transform: undefined,
transform: {},
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
transformIgnorePatterns: [`/node_modules/(?!${esModules})`]
......
......@@ -71,8 +71,10 @@
"zustand": "^4.3.5"
},
"devDependencies": {
"@faker-js/faker": "^9.0.3",
"@shelf/jest-mongodb": "^4.3.2",
"@svgr/webpack": "^6.5.1",
"@types/faker": "^6.6.9",
"@types/formidable": "^2.0.5",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.3",
......
import React, { useCallback, useState } from 'react';
import React, { useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { Box, Button, Flex, ModalBody, ModalFooter } from '@chakra-ui/react';
......@@ -11,6 +11,7 @@ import { useMemoizedFn, useMount } from 'ahooks';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { FolderIcon } from '@fastgpt/global/common/file/image/constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import LightTip from '@fastgpt/web/components/common/LightTip';
type FolderItemType = {
id: string;
......@@ -27,9 +28,10 @@ type Props = {
server: (e: GetResourceFolderListProps) => Promise<GetResourceFolderListItemResponse[]>;
onConfirm: (id: ParentIdType) => Promise<any>;
onClose: () => void;
moveHint?: string;
};
const MoveModal = ({ moveResourceId, title, server, onConfirm, onClose }: Props) => {
const MoveModal = ({ moveResourceId, title, server, onConfirm, onClose, moveHint }: Props) => {
const { t } = useTranslation();
const [selectedId, setSelectedId] = React.useState<string>();
const [requestingIdList, setRequestingIdList] = useState<ParentIdType[]>([]);
......@@ -170,6 +172,7 @@ const MoveModal = ({ moveResourceId, title, server, onConfirm, onClose }: Props)
onClose={onClose}
>
<ModalBody flex={'1 0 0'} overflow={'auto'} minH={'400px'}>
{moveHint && <LightTip text={moveHint} />}
<RenderList list={folderList} />
</ModalBody>
<ModalFooter>
......
import { Box, Button, Flex, HStack } from '@chakra-ui/react';
import { useToast } from '@fastgpt/web/hooks/useToast';
import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { FolderIcon } from '@fastgpt/global/common/file/image/constants';
......@@ -40,7 +39,7 @@ const FolderSlideCard = ({
deleteTip: string;
onDelete: () => void;
defaultPer: {
defaultPer?: {
value: PermissionValueType;
defaultValue: PermissionValueType;
onChange: (v: PermissionValueType) => Promise<any>;
......@@ -54,7 +53,6 @@ const FolderSlideCard = ({
}) => {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const { toast } = useToast();
const { ConfirmModal, openConfirm } = useConfirm({
type: 'delete',
......@@ -136,7 +134,7 @@ const FolderSlideCard = ({
</Box>
)}
{managePer.permission.hasManagePer && (
{managePer.permission.hasManagePer && !!defaultPer && (
<Box mt={5}>
<Box fontSize={'sm'} color={'myGray.500'}>
{t('common:permission.Default permission')}
......
import React from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import CollaboratorContextProvider, { MemberManagerInputPropsType } from '../MemberManager/context';
import { Box, Button, Flex, HStack, ModalBody, useDisclosure } from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import DefaultPermissionList from '../DefaultPerList';
import MyIcon from '@fastgpt/web/components/common/Icon';
import ResumeInherit from '../ResumeInheritText';
import { ChangeOwnerModal } from '../ChangeOwnerModal';
......@@ -14,11 +12,6 @@ export type ConfigPerModalProps = {
avatar?: string;
name: string;
defaultPer: {
value: PermissionValueType;
defaultValue: PermissionValueType;
onChange: (v: PermissionValueType) => Promise<any>;
};
managePer: MemberManagerInputPropsType;
isInheritPermission?: boolean;
resumeInheritPermission?: () => void;
......@@ -30,7 +23,6 @@ export type ConfigPerModalProps = {
const ConfigPerModal = ({
avatar,
name,
defaultPer,
managePer,
isInheritPermission,
resumeInheritPermission,
......@@ -66,17 +58,6 @@ const ConfigPerModal = ({
<ResumeInherit onResume={resumeInheritPermission} />
</Box>
)}
<Box mt={5}>
<Box fontSize={'sm'}>{t('common:permission.Default permission')}</Box>
<DefaultPermissionList
mt="1"
per={defaultPer.value}
defaultPer={defaultPer.defaultValue}
isInheritPermission={isInheritPermission}
onChange={(v) => defaultPer.onChange(v)}
hasParent={hasParent}
/>
</Box>
<Box mt={4}>
<CollaboratorContextProvider
{...managePer}
......
import React, { useMemo } from 'react';
import { PermissionTypeEnum, PermissionTypeMap } from '@fastgpt/global/support/permission/constant';
import React from 'react';
import { PermissionTypeMap } from '@fastgpt/global/support/permission/constant';
import { Box, StackProps, HStack } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { Permission } from '@fastgpt/global/support/permission/controller';
const PermissionIconText = ({
permission,
defaultPermission,
w = '1rem',
fontSize = 'mini',
iconColor = 'myGray.500',
private: Private = false,
...props
}: {
permission?: `${PermissionTypeEnum}`;
defaultPermission?: PermissionValueType;
private?: boolean;
iconColor?: string;
} & StackProps) => {
const { t } = useTranslation();
const per = useMemo(() => {
if (permission) return permission;
if (defaultPermission !== undefined) {
const Per = new Permission({ per: defaultPermission });
if (Per.hasWritePer) return PermissionTypeEnum.publicWrite;
if (Per.hasReadPer) return PermissionTypeEnum.publicRead;
return PermissionTypeEnum.clbPrivate;
}
return 'private';
}, [defaultPermission, permission]);
const per = Private ? 'private' : 'public';
return PermissionTypeMap[per] ? (
<HStack spacing={1} fontSize={fontSize} {...props}>
......
......@@ -8,11 +8,12 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
import { CollaboratorContext } from './context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { useUserStore } from '@/web/support/user/useUserStore';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import Loading from '@fastgpt/web/components/common/MyLoading';
import { useTranslation } from 'next-i18next';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
export type ManageModalProps = {
onClose: () => void;
};
......@@ -23,21 +24,12 @@ function ManageModal({ onClose }: ManageModalProps) {
const { permission, collaboratorList, onUpdateCollaborators, onDelOneCollaborator } =
useContextSelector(CollaboratorContext, (v) => v);
const { runAsync: onDelete, loading: isDeleting } = useRequest2((tmbId: string) =>
onDelOneCollaborator(tmbId)
);
const { runAsync: onDelete, loading: isDeleting } = useRequest2(onDelOneCollaborator);
const { runAsync: onUpdate, loading: isUpdating } = useRequest2(
({ tmbId, per }: { tmbId: string; per: PermissionValueType }) =>
onUpdateCollaborators({
members: [tmbId],
permission: per
}),
{
successToast: t('common.Update Success'),
errorToast: 'Error'
}
);
const { runAsync: onUpdate, loading: isUpdating } = useRequest2(onUpdateCollaborators, {
successToast: t('common.Update Success'),
errorToast: 'Error'
});
const loading = isDeleting || isUpdating;
......@@ -74,7 +66,7 @@ function ManageModal({ onClose }: ManageModalProps) {
<Td border="none">
<Flex alignItems="center">
<Avatar src={item.avatar} w="24px" mr={2} />
{item.name}
{item.name === DefaultGroupName ? userInfo?.team.teamName : item.name}
</Flex>
</Td>
<Td border="none">
......@@ -89,14 +81,18 @@ function ManageModal({ onClose }: ManageModalProps) {
<MyIcon name={'edit'} w={'16px'} _hover={{ color: 'primary.600' }} />
}
value={item.permission.value}
onChange={(per) => {
onChange={(permission) => {
onUpdate({
tmbId: item.tmbId,
per
members: item.tmbId ? [item.tmbId] : undefined,
groups: item.groupId ? [item.groupId] : undefined,
permission
});
}}
onDelete={() => {
onDelete(item.tmbId);
onDelete({
tmbId: item.tmbId,
groupId: item.groupId
} as RequireOnlyOne<{ tmbId: string; groupId: string }>);
}}
/>
)}
......
......@@ -6,11 +6,14 @@ import { CollaboratorContext } from './context';
import Tag, { TagProps } from '@fastgpt/web/components/common/Tag';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useTranslation } from 'next-i18next';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { useUserStore } from '@/web/support/user/useUserStore';
export type MemberListCardProps = BoxProps & { tagStyle?: Omit<TagProps, 'children'> };
const MemberListCard = ({ tagStyle, ...props }: MemberListCardProps) => {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { collaboratorList, isFetchingCollaborator } = useContextSelector(
CollaboratorContext,
......@@ -27,10 +30,15 @@ const MemberListCard = ({ tagStyle, ...props }: MemberListCardProps) => {
<Flex gap="2" flexWrap={'wrap'}>
{collaboratorList?.map((member) => {
return (
<Tag key={member.tmbId} type={'fill'} colorSchema="white" {...tagStyle}>
<Tag
key={member.tmbId || member.groupId}
type={'fill'}
colorSchema="white"
{...tagStyle}
>
<Avatar src={member.avatar} w="1.25rem" />
<Box fontSize={'sm'} ml={1}>
{member.name}
{member.name === DefaultGroupName ? userInfo?.team.teamName : member.name}
</Box>
</Tag>
);
......
......@@ -15,6 +15,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
const AddMemberModal = dynamic(() => import('./AddMemberModal'));
const ManageModal = dynamic(() => import('./ManageModal'));
......@@ -22,10 +23,12 @@ export type MemberManagerInputPropsType = {
permission: Permission;
onGetCollaboratorList: () => Promise<CollaboratorItemType[]>;
permissionList: PermissionListType;
onUpdateCollaborators: (props: any) => any; // TODO: type. should be UpdatePermissionBody after app and dataset permission refactored
onDelOneCollaborator: (tmbId: string) => any;
onUpdateCollaborators: (props: UpdateClbPermissionProps) => Promise<any>;
onDelOneCollaborator: (props: RequireOnlyOne<{ tmbId: string; groupId: string }>) => Promise<any>;
refreshDeps?: any[];
mode?: 'member' | 'all';
};
export type MemberManagerPropsType = MemberManagerInputPropsType & {
collaboratorList: CollaboratorItemType[];
refetchCollaboratorList: () => void;
......@@ -72,7 +75,8 @@ const CollaboratorContextProvider = ({
refetchResource,
refreshDeps = [],
isInheritPermission,
hasParent
hasParent,
mode = 'member'
}: MemberManagerInputPropsType & {
children: (props: ChildrenProps) => ReactNode;
refetchResource?: () => void;
......@@ -83,8 +87,10 @@ const CollaboratorContextProvider = ({
await onUpdateCollaborators(props);
refetchCollaboratorList();
};
const onDelOneCollaboratorThen = async (tmbId: string) => {
await onDelOneCollaborator(tmbId);
const onDelOneCollaboratorThen = async (
props: RequireOnlyOne<{ tmbId: string; groupId: string }>
) => {
await onDelOneCollaborator(props);
refetchCollaboratorList();
};
......@@ -197,6 +203,7 @@ const CollaboratorContextProvider = ({
onCloseAddMember();
refetchResource?.();
}}
mode={mode}
/>
)}
{isOpenManageModal && (
......
import React, { useMemo, useState } from 'react';
import {
Box,
Checkbox,
Flex,
Grid,
HStack,
Input,
InputGroup,
InputLeftElement
} from '@chakra-ui/react';
import { Box, Checkbox, Flex, Grid, HStack } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useTranslation } from 'next-i18next';
......@@ -16,6 +7,7 @@ import { Control, Controller } from 'react-hook-form';
import { RequireAtLeastOne } from '@fastgpt/global/common/type/utils';
import { useUserStore } from '@/web/support/user/useUserStore';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
type memberType = {
type: 'member';
......@@ -120,19 +112,14 @@ function SelectMember({
h={'100%'}
>
<Flex flexDirection="column" p="4" h={'100%'} overflow={'auto'}>
<InputGroup alignItems="center" size={'sm'}>
<InputLeftElement>
<MyIcon name="common/searchLight" w="16px" color={'myGray.500'} />
</InputLeftElement>
<Input
placeholder={t('user:search_user')}
fontSize="sm"
bg={'myGray.50'}
onChange={(e) => {
setSearchKey(e.target.value);
}}
/>
</InputGroup>
<SearchInput
placeholder={t('user:search_user')}
fontSize="sm"
bg={'myGray.50'}
onChange={(e) => {
setSearchKey(e.target.value);
}}
/>
<Flex flexDirection="column" mt={3}>
{filtered.map((member) => {
return (
......
......@@ -12,7 +12,6 @@ export type AppUpdateParams = {
edges?: AppSchema['edges'];
chatConfig?: AppSchema['chatConfig'];
teamTags?: AppSchema['teamTags'];
defaultPermission?: AppSchema['defaultPermission'];
};
export type PostPublishAppProps = {
......
import { MongoMemoryServer } from 'mongodb-memory-server';
import { MongoMemoryReplSet } from 'mongodb-memory-server';
import mongoose from 'mongoose';
import { MockParseHeaderCert } from '@/test/utils';
import { initMockData } from './db/init';
import { parseHeaderCertMock } from '@/test/utils';
import { initMockData, root } from './db/init';
import { faker } from '@faker-js/faker/locale/zh_CN';
jest.mock('nanoid', () => {
return {
......@@ -13,24 +14,40 @@ jest.mock('@fastgpt/global/common/string/tools', () => {
return {
hashStr(str: string) {
return str;
},
getNanoid() {
return faker.string.alphanumeric(12);
}
};
});
jest.mock('@fastgpt/service/common/system/log', jest.fn());
jest.mock('@fastgpt/service/common/system/log', () => ({
addLog: {
log: jest.fn(),
warn: jest.fn((...prop) => {
console.warn(prop);
}),
error: jest.fn((...prop) => {
console.error(prop);
}),
info: jest.fn(),
debug: jest.fn()
}
}));
jest.mock('@fastgpt/service/support/permission/controller', () => {
return {
parseHeaderCert: MockParseHeaderCert,
getResourcePermission: jest.requireActual('@fastgpt/service/support/permission/controller')
.getResourcePermission,
getResourceAllClbs: jest.requireActual('@fastgpt/service/support/permission/controller')
.getResourceAllClbs
};
});
jest.setMock(
'@fastgpt/service/support/permission/controller',
(() => {
const origin = jest.requireActual<
typeof import('@fastgpt/service/support/permission/controller')
>('@fastgpt/service/support/permission/controller');
const parse = jest.createMockFromModule('@fastgpt/service/support/permission/controller') as any;
parse.parseHeaderCert = MockParseHeaderCert;
return {
...origin,
parseHeaderCert: parseHeaderCertMock
};
})()
);
jest.mock('@/service/middleware/entry', () => {
return {
......@@ -59,11 +76,30 @@ jest.mock('@/service/middleware/entry', () => {
beforeAll(async () => {
// 新建一个内存数据库,然后让 mongoose 连接这个数据库
if (!global.mongod || !global.mongodb) {
const mongod = await MongoMemoryServer.create();
global.mongod = mongod;
const replSet = new MongoMemoryReplSet({
instanceOpts: [
{
storageEngine: 'wiredTiger'
},
{
storageEngine: 'wiredTiger'
}
]
});
replSet.start();
await replSet.waitUntilRunning();
const uri = replSet.getUri();
// const mongod = await MongoMemoryServer.create({
// instance: {
// replSet: 'testset'
// }
// });
// global.mongod = mongod;
global.replSet = replSet;
global.mongodb = mongoose;
await global.mongodb.connect(mongod.getUri(), {
await global.mongodb.connect(uri, {
dbName: 'fastgpt_test',
bufferCommands: true,
maxConnecting: 50,
maxPoolSize: 50,
......@@ -77,6 +113,7 @@ beforeAll(async () => {
});
await initMockData();
console.log(root);
}
});
......@@ -84,6 +121,9 @@ afterAll(async () => {
if (global.mongodb) {
await global.mongodb.disconnect();
}
if (global.replSet) {
await global.replSet.stop();
}
if (global.mongod) {
await global.mongod.stop();
}
......
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoMemberGroupModel } from '@fastgpt/service/support/permission/memberGroup/memberGroupSchema';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
......@@ -13,37 +14,43 @@ export const root = {
};
export const initMockData = async () => {
const initRootUser = async () => {
// init root user
const rootUser = await MongoUser.create({
const [rootUser] = await MongoUser.create([
{
username: 'root',
password: '123456'
});
const rootTeam = await MongoTeam.create({
name: 'root-default-team',
ownerId: rootUser._id
});
const rootTeamMember = await MongoTeamMember.create({
}
]);
root.uid = String(rootUser._id);
const [rootTeam] = await MongoTeam.create([
{
name: 'root Team'
}
]);
root.teamId = String(rootTeam._id);
const [rootTmb] = await MongoTeamMember.create([
{
teamId: rootTeam._id,
name: 'owner',
role: 'owner',
userId: rootUser._id,
name: 'root-default-team-member',
status: 'active',
role: TeamMemberRoleEnum.owner
});
const rootApp = await MongoApp.create({
name: 'root-default-app',
teamId: rootTeam._id,
tmbId: rootTeam._id,
type: 'advanced'
});
status: 'active'
}
]);
root.tmbId = String(rootTmb._id);
await MongoMemberGroupModel.create([
{
name: DefaultGroupName,
teamId: rootTeam._id
}
]);
root.uid = rootUser._id;
root.tmbId = rootTeamMember._id;
root.teamId = rootTeam._id;
root.appId = rootApp._id;
};
const [rootApp] = await MongoApp.create([
{
name: 'root Test App',
teamId: rootTeam._id,
tmbId: rootTmb._id
}
]);
await initRootUser();
root.appId = String(rootApp._id);
};
import { MongoMemoryServer } from 'mongodb-memory-server';
import type { MongoMemoryReplSet, MongoMemoryServer } from 'mongodb-memory-server';
declare global {
var mongod: MongoMemoryServer | undefined;
var replSet: MongoMemoryReplSet | undefined;
}
export type RequestResponse<T = any> = {
code: number;
error?: string;
data?: T;
};
......@@ -2,6 +2,7 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import {
OwnerPermissionVal,
PerResourceTypeEnum,
WritePermissionVal
} from '@fastgpt/global/support/permission/constant';
......@@ -11,11 +12,12 @@ import { NextAPI } from '@/service/middleware/entry';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission';
import { getResourceAllClbs } from '@fastgpt/service/support/permission/controller';
import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller';
import { TeamWritePermissionVal } from '@fastgpt/global/support/permission/user/constant';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
export type CreateAppFolderBody = {
parentId?: ParentIdType;
......@@ -31,20 +33,21 @@ async function handler(req: ApiRequestProps<CreateAppFolderBody>) {
}
// 凭证校验
const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
const parentApp = await (async () => {
if (parentId) {
// if it is not a root folder
return (
await authApp({
req,
appId: parentId,
per: WritePermissionVal,
authToken: true
})
).app; // check the parent folder permission
}
})();
const { teamId, tmbId } = await authUserPer({
req,
authToken: true,
per: TeamWritePermissionVal
});
if (parentId) {
// if it is not a root folder
await authApp({
req,
appId: parentId,
per: WritePermissionVal,
authToken: true
});
}
// Create app
await mongoSessionRun(async (session) => {
......@@ -55,13 +58,11 @@ async function handler(req: ApiRequestProps<CreateAppFolderBody>) {
intro,
teamId,
tmbId,
type: AppTypeEnum.folder,
// inheritPermission: !!parentApp ? true : false,
defaultPermission: !!parentApp ? parentApp.defaultPermission : AppDefaultPermissionVal
type: AppTypeEnum.folder
});
if (parentId) {
const parentClbs = await getResourceAllClbs({
const parentClbsAndGroups = await getResourceClbsAndGroups({
teamId,
resourceId: parentId,
resourceType: PerResourceTypeEnum.app,
......@@ -72,9 +73,25 @@ async function handler(req: ApiRequestProps<CreateAppFolderBody>) {
resourceType: PerResourceTypeEnum.app,
teamId,
resourceId: app._id,
collaborators: parentClbs,
collaborators: parentClbsAndGroups,
session
});
} else {
// Create default permission
await MongoResourcePermission.create(
[
{
resourceType: PerResourceTypeEnum.app,
teamId,
resourceId: app._id,
tmbId,
permission: OwnerPermissionVal
}
],
{
session
}
);
}
});
}
......
......@@ -15,6 +15,8 @@ import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { getGroupPer } from '@fastgpt/service/support/permission/controller';
import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers';
export type ListAppBody = {
parentId?: ParentIdType;
......@@ -31,7 +33,7 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
app: ParentApp,
tmbId,
teamId,
permission: tmbPer
permission: myPer
} = await (async () => {
if (parentId) {
return await authApp({
......@@ -87,10 +89,17 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
})();
/* temp: get all apps and per */
const [myApps, rpList] = await Promise.all([
const myGroupIds = (
await getGroupsByTmbId({
tmbId,
teamId
})
).map((item) => String(item._id));
const [myApps, perList] = await Promise.all([
MongoApp.find(
findAppsQuery,
'_id parentId avatar type name intro tmbId updateTime pluginData defaultPermission inheritPermission'
'_id parentId avatar type name intro tmbId updateTime pluginData inheritPermission'
)
.sort({
updateTime: -1
......@@ -98,41 +107,67 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
.limit(searchKey ? 20 : 1000)
.lean(),
MongoResourcePermission.find({
resourceType: PerResourceTypeEnum.app,
teamId,
tmbId
$and: [
{
resourceType: PerResourceTypeEnum.app,
teamId,
resourceId: {
$exists: true
}
},
{ $or: [{ tmbId }, { groupId: { $in: myGroupIds } }] }
]
}).lean()
]);
const filterApps = myApps
.map((app) => {
const Per = (() => {
const { Per, privateApp } = (() => {
// Inherit app
if (app.inheritPermission && ParentApp && !AppFolderTypeList.includes(app.type)) {
// get its parent's permission as its permission
app.defaultPermission = ParentApp.defaultPermission;
const perVal = rpList.find(
(item) => String(item.resourceId) === String(ParentApp._id)
const tmbPer = perList.find(
(item) => String(item.resourceId) === String(ParentApp._id) && !!item.tmbId
)?.permission;
const groupPer = getGroupPer(
perList
.filter(
(item) =>
String(item.resourceId) === String(ParentApp._id) &&
myGroupIds.includes(String(item.groupId))
)
.map((item) => item.permission)
);
return new AppPermission({
per: perVal ?? app.defaultPermission,
isOwner: String(app.tmbId) === String(tmbId) || tmbPer.isOwner
});
return {
Per: new AppPermission({
per: tmbPer ?? groupPer ?? AppDefaultPermissionVal,
isOwner: String(app.tmbId) === String(tmbId) || myPer.isOwner
}),
privateApp: !tmbPer && !groupPer
};
} else {
const perVal = rpList.find(
(item) => String(item.resourceId) === String(app._id)
const tmbPer = perList.find(
(item) => String(item.resourceId) === String(app._id) && !!item.tmbId
)?.permission;
return new AppPermission({
per: perVal ?? app.defaultPermission,
isOwner: String(app.tmbId) === String(tmbId) || tmbPer.isOwner
});
const group = perList.filter(
(item) =>
String(item.resourceId) === String(app._id) &&
myGroupIds.includes(String(item.groupId))
);
const groupPer = getGroupPer(group.map((item) => item.permission));
return {
Per: new AppPermission({
per: tmbPer ?? groupPer ?? AppDefaultPermissionVal,
isOwner: String(app.tmbId) === String(tmbId) || myPer.isOwner
}),
privateApp: !tmbPer && !groupPer
};
}
})();
return {
...app,
permission: Per
permission: Per,
privateApp: privateApp
};
})
.filter((app) => app.permission.hasReadPer);
......@@ -148,9 +183,9 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
intro: app.intro,
updateTime: app.updateTime,
permission: app.permission,
defaultPermission: app.defaultPermission || AppDefaultPermissionVal,
pluginData: app.pluginData,
inheritPermission: app.inheritPermission ?? true
inheritPermission: app.inheritPermission ?? true,
private: app.privateApp
}));
}
......
......@@ -6,6 +6,7 @@ import { NextAPI } from '@/service/middleware/entry';
import {
ManagePermissionVal,
PerResourceTypeEnum,
ReadPermissionVal,
WritePermissionVal
} from '@fastgpt/global/support/permission/constant';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
......@@ -18,62 +19,78 @@ import {
import { AppFolderTypeList } from '@fastgpt/global/core/app/constants';
import { ClientSession } from 'mongoose';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { getResourceAllClbs } from '@fastgpt/service/support/permission/controller';
import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant';
/*
修改默认权限
1. 继承态目录:关闭继承态,修改权限,同步子目录默认权限
2. 继承态资源:关闭继承态,修改权限, 复制父级协作者。
3. 非继承目录:修改权限,同步子目录默认权限
4. 非继承资源:修改权限
移动
1. 继承态目录:改 parentId, 修改成父的默认权限,同步子目录默认权限和协作者
2. 继承态资源:改 parentId
3. 非继承:改 parentId
*/
async function handler(req: ApiRequestProps<AppUpdateParams, { appId: string }>) {
const {
parentId,
name,
avatar,
type,
intro,
nodes,
edges,
chatConfig,
teamTags,
defaultPermission
} = req.body as AppUpdateParams;
const { appId } = req.query as { appId: string };
import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { TeamWritePermissionVal } from '@fastgpt/global/support/permission/user/constant';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
export type AppUpdateQuery = {
appId: string;
};
export type AppUpdateBody = AppUpdateParams;
// 更新应用接口
// 包括如下功能:
// 1. 更新应用的信息(包括名称,类型,头像,介绍等)
// 2. 更新应用的编排信息
// 3. 移动应用
// 操作权限:
// 1. 更新信息和工作流编排需要有应用的写权限
// 2. 移动应用需要有
// (1) 父目录的管理权限
// (2) 目标目录的管理权限
// (3) 如果从根目录移动或移动到根目录,需要有团队的应用创建权限
async function handler(req: ApiRequestProps<AppUpdateBody, AppUpdateQuery>) {
const { parentId, name, avatar, type, intro, nodes, edges, chatConfig, teamTags } = req.body;
const { appId } = req.query;
if (!appId) {
Promise.reject(CommonErrEnum.missingParams);
}
const isMove = parentId !== undefined;
// this step is to get the app and its permission, and we will check the permission manually for
// different cases
const { app, permission } = await authApp({
req,
authToken: true,
appId,
per: ReadPermissionVal
});
if (!app) {
Promise.reject(AppErrEnum.unExist);
}
const { app } = await (async () => {
if (defaultPermission !== undefined) {
// if defaultPermission or inheritPermission is set, then need manage permission
return authApp({ req, authToken: true, appId, per: ManagePermissionVal });
} else {
return authApp({ req, authToken: true, appId, per: WritePermissionVal });
if (isMove) {
if (parentId) {
// move to a folder, check the target folder's permission
await authApp({ req, authToken: true, appId: parentId, per: ManagePermissionVal });
}
})();
// format nodes data
// 1. dataset search limit, less than model quoteMaxToken
const isDefaultPermissionChanged =
defaultPermission !== undefined && defaultPermission !== app.defaultPermission;
const isFolder = AppFolderTypeList.includes(app.type);
if (app.parentId) {
// move from a folder, check the (old) folder's permission
await authApp({ req, authToken: true, appId: app.parentId, per: ManagePermissionVal });
}
if (parentId === null || !app.parentId) {
// move to root or move from root
await authUserPer({
req,
authToken: true,
per: TeamWritePermissionVal
});
}
} else {
// is not move, write permission of the app.
if (!permission.hasWritePer) {
return Promise.reject(AppErrEnum.unAuthApp);
}
}
const onUpdate = async (
session?: ClientSession,
updatedDefaultPermission?: PermissionValueType
) => {
const onUpdate = async (session?: ClientSession) => {
// format nodes data
// 1. dataset search limit, less than model quoteMaxToken
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });
return MongoApp.findByIdAndUpdate(
......@@ -84,12 +101,6 @@ async function handler(req: ApiRequestProps<AppUpdateParams, { appId: string }>)
...(type && { type }),
...(avatar && { avatar }),
...(intro !== undefined && { intro }),
// update default permission(Maybe move update)
...(updatedDefaultPermission !== undefined && {
defaultPermission: updatedDefaultPermission
}),
// Not root, update default permission
...(app.parentId && isDefaultPermissionChanged && { inheritPermission: false }),
...(teamTags && { teamTags }),
...(formatNodes && {
modules: formatNodes
......@@ -97,34 +108,19 @@ async function handler(req: ApiRequestProps<AppUpdateParams, { appId: string }>)
...(edges && {
edges
}),
...(chatConfig && { chatConfig })
...(chatConfig && { chatConfig }),
...(isMove && { inheritPermission: true })
},
{ session }
);
};
// Move
if (parentId !== undefined) {
if (isMove) {
await mongoSessionRun(async (session) => {
// Auth
const parentDefaultPermission = await (async () => {
if (parentId) {
const { app: parentApp } = await authApp({
req,
authToken: true,
appId: parentId,
per: WritePermissionVal
});
return parentApp.defaultPermission;
}
return AppDefaultPermissionVal;
})();
// Inherit folder: Sync children permission and it's clbs
if (isFolder && app.inheritPermission) {
const parentClbs = await getResourceAllClbs({
if (AppFolderTypeList.includes(app.type)) {
const parentClbsAndGroups = await getResourceClbsAndGroups({
teamId: app.teamId,
resourceId: parentId,
resourceType: PerResourceTypeEnum.app,
......@@ -134,7 +130,7 @@ async function handler(req: ApiRequestProps<AppUpdateParams, { appId: string }>)
await syncCollaborators({
resourceId: app._id,
resourceType: PerResourceTypeEnum.app,
collaborators: parentClbs,
collaborators: parentClbsAndGroups,
session,
teamId: app.teamId
});
......@@ -144,53 +140,12 @@ async function handler(req: ApiRequestProps<AppUpdateParams, { appId: string }>)
resourceType: PerResourceTypeEnum.app,
resourceModel: MongoApp,
folderTypeList: AppFolderTypeList,
defaultPermission: parentDefaultPermission,
collaborators: parentClbs,
collaborators: parentClbsAndGroups,
session
});
return onUpdate(session, parentDefaultPermission);
}
return onUpdate(session);
});
} else if (isDefaultPermissionChanged) {
// Update default permission
await mongoSessionRun(async (session) => {
if (isFolder) {
// Sync children default permission
await syncChildrenPermission({
resource: {
_id: app._id,
type: app.type,
teamId: app.teamId,
parentId: app.parentId
},
folderTypeList: AppFolderTypeList,
resourceModel: MongoApp,
resourceType: PerResourceTypeEnum.app,
session,
defaultPermission
});
} else if (app.inheritPermission && app.parentId) {
// Inherit app
const parentClbs = await getResourceAllClbs({
teamId: app.teamId,
resourceId: app.parentId,
resourceType: PerResourceTypeEnum.app,
session
});
await syncCollaborators({
resourceId: app._id,
resourceType: PerResourceTypeEnum.app,
collaborators: parentClbs,
session,
teamId: app.teamId
});
}
return onUpdate(session, defaultPermission);
});
} else {
return onUpdate();
}
......
......@@ -2,7 +2,6 @@ import type { NextApiRequest } from 'next';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { getVectorModel } from '@fastgpt/service/core/ai/model';
import type { DatasetSimpleItemType } from '@fastgpt/global/core/dataset/type.d';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { NextAPI } from '@/service/middleware/entry';
import {
PerResourceTypeEnum,
......@@ -11,6 +10,9 @@ import {
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant';
import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers';
import { getGroupPer } from '@fastgpt/service/support/permission/controller';
/* get all dataset by teamId or tmbId */
async function handler(req: NextApiRequest): Promise<DatasetSimpleItemType[]> {
......@@ -25,7 +27,14 @@ async function handler(req: NextApiRequest): Promise<DatasetSimpleItemType[]> {
per: ReadPermissionVal
});
const [myDatasets, rpList] = await Promise.all([
const myGroupIds = (
await getGroupsByTmbId({
tmbId,
teamId
})
).map((item) => String(item._id));
const [myDatasets, perList] = await Promise.all([
MongoDataset.find({
teamId
})
......@@ -34,39 +43,59 @@ async function handler(req: NextApiRequest): Promise<DatasetSimpleItemType[]> {
})
.lean(),
MongoResourcePermission.find({
resourceType: PerResourceTypeEnum.dataset,
teamId,
tmbId
$and: [
{
resourceType: PerResourceTypeEnum.dataset,
teamId,
resourceId: {
$exists: true
}
},
{ $or: [{ tmbId }, { groupId: { $in: myGroupIds } }] }
]
}).lean()
]);
const filterDatasets = myDatasets
.map((dataset) => {
const perVal = (() => {
const perVal = rpList.find(
(item) => String(item.resourceId) === String(dataset._id)
)?.permission;
if (perVal) {
return perVal;
}
const parentDataset = myDatasets.find(
(item) => String(item._id) === String(dataset.parentId)
);
if (dataset.inheritPermission && dataset.parentId) {
const parentDataset = myDatasets.find(
(item) => String(item._id) === String(dataset.parentId)
if (dataset.inheritPermission && dataset.parentId && parentDataset) {
const tmbPer = perList.find(
(item) => String(item.resourceId) === String(parentDataset._id) && !!item.tmbId
)?.permission;
const groupPer = getGroupPer(
perList
.filter(
(item) =>
String(item.resourceId) === String(parentDataset._id) &&
myGroupIds.includes(String(item.groupId))
)
.map((item) => item.permission)
);
if (parentDataset) {
const parentPerVal =
rpList.find((item) => String(item.resourceId) === String(parentDataset._id))
?.permission ?? parentDataset.defaultPermission;
if (parentPerVal) {
return parentPerVal;
}
}
return tmbPer ?? groupPer ?? DatasetDefaultPermissionVal;
} else {
const tmbPer = perList.find(
(item) => String(item.resourceId) === String(dataset._id) && !!item.tmbId
)?.permission;
const groupPer = getGroupPer(
perList
.filter(
(item) =>
String(item.resourceId) === String(dataset._id) &&
myGroupIds.includes(String(item.groupId))
)
.map((item) => item.permission)
);
return tmbPer ?? groupPer ?? DatasetDefaultPermissionVal;
}
})();
const Per = new DatasetPermission({
per: perVal ?? dataset.defaultPermission,
per: perVal ?? DatasetDefaultPermissionVal,
isOwner: String(dataset.tmbId) === tmbId || tmbPer.isOwner
});
......
......@@ -4,6 +4,7 @@ import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import {
OwnerPermissionVal,
PerResourceTypeEnum,
WritePermissionVal
} from '@fastgpt/global/support/permission/constant';
......@@ -12,9 +13,9 @@ import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant';
import { getResourceAllClbs } from '@fastgpt/service/support/permission/controller';
import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller';
import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
export type DatasetFolderCreateQuery = {};
export type DatasetFolderCreateBody = {
parentId?: string;
......@@ -38,35 +39,28 @@ async function handler(
authToken: true
});
const parentFolder = await (async () => {
if (parentId) {
return (
await authDataset({
datasetId: parentId,
per: WritePermissionVal,
req,
authToken: true
})
).dataset;
}
})();
if (parentId) {
await authDataset({
datasetId: parentId,
per: WritePermissionVal,
req,
authToken: true
});
}
await mongoSessionRun(async (session) => {
const app = await MongoDataset.create({
const dataset = await MongoDataset.create({
...parseParentIdInMongo(parentId),
avatar: FolderImgUrl,
name,
intro,
teamId,
tmbId,
type: DatasetTypeEnum.folder,
defaultPermission: !!parentFolder
? parentFolder.defaultPermission
: DatasetDefaultPermissionVal
type: DatasetTypeEnum.folder
});
if (parentId) {
const parentClbs = await getResourceAllClbs({
const parentClbsAndGroups = await getResourceClbsAndGroups({
teamId,
resourceId: parentId,
resourceType: PerResourceTypeEnum.dataset,
......@@ -76,11 +70,26 @@ async function handler(
await syncCollaborators({
resourceType: PerResourceTypeEnum.dataset,
teamId,
resourceId: app._id,
collaborators: parentClbs,
resourceId: dataset._id,
collaborators: parentClbsAndGroups,
session
});
}
if (!parentId) {
await MongoResourcePermission.create(
[
{
resourceType: PerResourceTypeEnum.dataset,
teamId,
resourceId: dataset._id,
tmbId,
permission: OwnerPermissionVal
}
],
{ session }
);
}
});
return {};
......
......@@ -16,6 +16,8 @@ import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers';
import { getGroupPer } from '@fastgpt/service/support/permission/controller';
export type GetDatasetListBody = {
parentId: ParentIdType;
......@@ -30,7 +32,7 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
dataset: parentDataset,
teamId,
tmbId,
permission: tmbPer
permission: myPer
} = await (async () => {
if (parentId) {
return await authDataset({
......@@ -76,44 +78,84 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
};
})();
const [myDatasets, rpList] = await Promise.all([
const myGroupIds = (
await getGroupsByTmbId({
tmbId,
teamId
})
).map((item) => String(item._id));
const [myDatasets, perList] = await Promise.all([
MongoDataset.find(findDatasetQuery)
.sort({
updateTime: -1
})
.lean(),
MongoResourcePermission.find({
resourceType: PerResourceTypeEnum.dataset,
teamId,
tmbId
$and: [
{
resourceType: PerResourceTypeEnum.dataset,
teamId,
resourceId: {
$exists: true
}
},
{ $or: [{ tmbId }, { groupId: { $in: myGroupIds } }] }
]
}).lean()
]);
const filterDatasets = myDatasets
.map((dataset) => {
const Per = (() => {
const { Per, privateDataset } = (() => {
// inherit
if (dataset.inheritPermission && parentDataset && dataset.type !== DatasetTypeEnum.folder) {
dataset.defaultPermission = parentDataset.defaultPermission;
const perVal = rpList.find(
(item) => String(item.resourceId) === String(parentDataset._id)
const tmbPer = perList.find(
(item) => String(item.resourceId) === String(parentDataset._id) && !!item.tmbId
)?.permission;
return new DatasetPermission({
per: perVal ?? parentDataset.defaultPermission,
isOwner: String(parentDataset.tmbId) === tmbId || tmbPer.isOwner
});
const groupPer = getGroupPer(
perList
.filter(
(item) =>
String(item.resourceId) === String(parentDataset._id) &&
myGroupIds.includes(String(item.groupId))
)
.map((item) => item.permission)
);
return {
Per: new DatasetPermission({
per: tmbPer ?? groupPer ?? DatasetDefaultPermissionVal,
isOwner: String(parentDataset.tmbId) === tmbId || myPer.isOwner
}),
privateDataset: !tmbPer && !groupPer
};
} else {
const perVal = rpList.find(
(item) => String(item.resourceId) === String(dataset._id)
const tmbPer = perList.find(
(item) =>
String(item.resourceId) === String(dataset._id) && !!item.tmbId && !!item.permission
)?.permission;
return new DatasetPermission({
per: perVal ?? dataset.defaultPermission,
isOwner: String(dataset.tmbId) === tmbId || tmbPer.isOwner
});
const groupPer = getGroupPer(
perList
.filter(
(item) =>
String(item.resourceId) === String(dataset._id) &&
myGroupIds.includes(String(item.groupId))
)
.map((item) => item.permission)
);
return {
Per: new DatasetPermission({
per: tmbPer ?? groupPer ?? DatasetDefaultPermissionVal,
isOwner: String(dataset.tmbId) === tmbId || myPer.isOwner
}),
privateDataset: !tmbPer && !groupPer
};
}
})();
return {
...dataset,
permission: Per
permission: Per,
privateDataset
};
})
.filter((app) => app.permission.hasReadPer);
......@@ -127,10 +169,10 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
type: item.type,
permission: item.permission,
vectorModel: getVectorModel(item.vectorModel),
defaultPermission: item.defaultPermission ?? DatasetDefaultPermissionVal,
inheritPermission: item.inheritPermission,
tmbId: item.tmbId,
updateTime: item.updateTime
updateTime: item.updateTime,
private: item.privateDataset
}))
);
......
......@@ -5,63 +5,86 @@ import { NextAPI } from '@/service/middleware/entry';
import {
ManagePermissionVal,
PerResourceTypeEnum,
WritePermissionVal
ReadPermissionVal
} from '@fastgpt/global/support/permission/constant';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { ClientSession } from 'mongoose';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant';
import { DatasetSchemaType } from '@fastgpt/global/core/dataset/type';
import { getResourceAllClbs } from '@fastgpt/service/support/permission/controller';
import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller';
import {
syncChildrenPermission,
syncCollaborators
} from '@fastgpt/service/support/permission/inheritPermission';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { TeamWritePermissionVal } from '@fastgpt/global/support/permission/user/constant';
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
export type DatasetUpdateQuery = {};
export type DatasetUpdateResponse = any;
// 更新知识库接口
// 包括如下功能:
// 1. 更新应用的信息(包括名称,类型,头像,介绍等)
// 2. 更新数据库的配置信息
// 3. 移动知识库
// 操作权限:
// 1. 更新信息和配置编排需要有知识库的写权限
// 2. 移动应用需要有
// (1) 父目录的管理权限
// (2) 目标目录的管理权限
// (3) 如果从根目录移动或移动到根目录,需要有团队的应用创建权限
async function handler(
req: ApiRequestProps<DatasetUpdateBody, DatasetUpdateQuery>,
_res: ApiResponseType<any>
): Promise<DatasetUpdateResponse> {
const {
id,
parentId,
name,
avatar,
intro,
agentModel,
websiteConfig,
externalReadUrl,
defaultPermission,
status
} = req.body;
const { id, parentId, name, avatar, intro, agentModel, websiteConfig, externalReadUrl, status } =
req.body;
if (!id) {
return Promise.reject(CommonErrEnum.missingParams);
}
const { dataset } = (await (async () => {
if (defaultPermission !== undefined) {
return await authDataset({ req, authToken: true, datasetId: id, per: ManagePermissionVal });
} else {
return await authDataset({ req, authToken: true, datasetId: id, per: WritePermissionVal });
const isMove = parentId !== undefined;
const { dataset, permission } = await authDataset({
req,
authToken: true,
datasetId: id,
per: ReadPermissionVal
});
if (isMove) {
if (parentId) {
// move to a folder, check the target folder's permission
await authDataset({ req, authToken: true, datasetId: parentId, per: ManagePermissionVal });
}
if (dataset.parentId) {
// move from a folder, check the (old) folder's permission
await authDataset({
req,
authToken: true,
datasetId: dataset.parentId,
per: ManagePermissionVal
});
}
})()) as { dataset: DatasetSchemaType };
if (parentId === null || !dataset.parentId) {
// move to root or move from root
await authUserPer({
req,
authToken: true,
per: TeamWritePermissionVal
});
}
} else {
// is not move
if (!permission.hasWritePer) return Promise.reject(DatasetErrEnum.unAuthDataset);
}
const isDefaultPermissionChanged =
defaultPermission !== undefined && dataset.defaultPermission !== defaultPermission;
const isFolder = dataset.type === DatasetTypeEnum.folder;
const onUpdate = async (
session?: ClientSession,
updatedDefaultPermission?: PermissionValueType
) => {
const onUpdate = async (session?: ClientSession) => {
await MongoDataset.findByIdAndUpdate(
id,
{
......@@ -73,35 +96,16 @@ async function handler(
...(status && { status }),
...(intro !== undefined && { intro }),
...(externalReadUrl !== undefined && { externalReadUrl }),
// move
...(updatedDefaultPermission !== undefined && {
defaultPermission: updatedDefaultPermission
}),
// update the defaultPermission
...(dataset.parentId && isDefaultPermissionChanged && { inheritPermission: false })
...(isMove && { inheritPermission: true })
},
{ session }
);
};
// move
if (parentId !== undefined) {
if (isMove) {
await mongoSessionRun(async (session) => {
const parentDefaultPermission = await (async () => {
if (parentId) {
const { dataset: parentDataset } = await authDataset({
req,
authToken: true,
datasetId: parentId,
per: WritePermissionVal
});
return parentDataset.defaultPermission;
}
return DatasetDefaultPermissionVal;
})();
if (isFolder && dataset.inheritPermission) {
const parentClbs = await getResourceAllClbs({
const parentClbsAndGroups = await getResourceClbsAndGroups({
teamId: dataset.teamId,
resourceId: parentId,
resourceType: PerResourceTypeEnum.dataset,
......@@ -112,7 +116,7 @@ async function handler(
teamId: dataset.teamId,
resourceId: id,
resourceType: PerResourceTypeEnum.dataset,
collaborators: parentClbs,
collaborators: parentClbsAndGroups,
session
});
......@@ -121,48 +125,13 @@ async function handler(
resourceType: PerResourceTypeEnum.dataset,
resourceModel: MongoDataset,
folderTypeList: [DatasetTypeEnum.folder],
collaborators: parentClbs,
defaultPermission: parentDefaultPermission,
collaborators: parentClbsAndGroups,
session
});
return onUpdate(session, parentDefaultPermission);
return onUpdate(session);
}
return onUpdate(session);
});
} else if (isDefaultPermissionChanged) {
await mongoSessionRun(async (session) => {
if (isFolder) {
await syncChildrenPermission({
defaultPermission,
resource: {
_id: dataset._id,
type: dataset.type,
teamId: dataset.teamId,
parentId: dataset.parentId
},
resourceType: PerResourceTypeEnum.dataset,
resourceModel: MongoDataset,
folderTypeList: [DatasetTypeEnum.folder],
session
});
} else if (dataset.inheritPermission && dataset.parentId) {
const parentClbs = await getResourceAllClbs({
teamId: dataset.teamId,
resourceId: parentId,
resourceType: PerResourceTypeEnum.dataset,
session
});
await syncCollaborators({
teamId: dataset.teamId,
resourceId: id,
resourceType: PerResourceTypeEnum.dataset,
collaborators: parentClbs,
session
});
}
return onUpdate(session, defaultPermission);
});
} else {
return onUpdate();
}
......
import { getTestRequest } from '@/test/utils';
import '../../__mocks__/base';
import { getTestRequest } from '@/test/utils';
import handler, { OutLinkUpdateBody, OutLinkUpdateQuery } from './update';
import { root } from '../../__mocks__/db/init';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { root } from '../../__mocks__/db/init';
test('Update Outlink', async () => {
const outlink = await MongoOutLink.create({
beforeAll(async () => {
await MongoOutLink.create({
shareId: 'aaa',
appId: root.appId,
tmbId: root.tmbId,
......@@ -14,8 +14,13 @@ test('Update Outlink', async () => {
type: 'share',
name: 'aaa'
});
});
await outlink.save();
test('Update Outlink', async () => {
const outlink = await MongoOutLink.findOne({ name: 'aaa' }).lean();
if (!outlink) {
throw new Error('Outlink not found');
}
const res = (await handler(
...getTestRequest<OutLinkUpdateQuery, OutLinkUpdateBody>({
......@@ -27,6 +32,7 @@ test('Update Outlink', async () => {
})
)) as any;
console.log(res);
expect(res.code).toBe(200);
const link = await MongoOutLink.findById(outlink._id).lean();
......
......@@ -28,16 +28,13 @@ import {
} from '@/web/core/app/api/collaborator';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pages/app/detail/components/context';
import {
AppDefaultPermissionVal,
AppPermissionList
} from '@fastgpt/global/support/permission/app/constant';
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
import { AppPermissionList } from '@fastgpt/global/support/permission/app/constant';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { resumeInheritPer } from '@/web/core/app/api';
import { useI18n } from '@/web/context/I18n';
import ResumeInherit from '@/components/support/permission/ResumeInheritText';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
const InfoModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
......@@ -67,8 +64,7 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
await updateAppDetail({
name: data.name,
avatar: data.avatar,
intro: data.intro,
defaultPermission: data.defaultPermission
intro: data.intro
});
},
{
......@@ -129,24 +125,25 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
const onUpdateCollaborators = ({
members,
groups,
permission
}: {
members: string[];
members?: string[];
groups?: string[];
permission: PermissionValueType;
}) => {
return postUpdateAppCollaborators({
}) =>
postUpdateAppCollaborators({
members,
groups,
permission,
appId: appDetail._id
});
};
const onDelCollaborator = (tmbId: string) => {
return deleteAppCollaborators({
const onDelCollaborator = async (props: RequireOnlyOne<{ tmbId: string; groupId: string }>) =>
deleteAppCollaborators({
appId: appDetail._id,
tmbId
...props
});
};
const { runAsync: resumeInheritPermission } = useRequest2(() => resumeInheritPer(appDetail._id), {
errorToast: t('common:resume_failed'),
......@@ -204,33 +201,19 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
<ResumeInherit onResume={resumeInheritPermission} />
</Box>
)}
<Box mt="4">
<Box fontSize={'sm'}>{t('common:permission.Default permission')}</Box>
<DefaultPermissionList
mt="2"
per={appDetail.defaultPermission}
defaultPer={AppDefaultPermissionVal}
isInheritPermission={appDetail.inheritPermission}
onChange={(v) => {
setValue('defaultPermission', v);
return handleSubmit((data) => saveSubmitSuccess(data), saveSubmitError)();
}}
hasParent={!!appDetail.parentId}
/>
</Box>
<Box mt={6}>
<CollaboratorContextProvider
mode="all"
permission={appDetail.permission}
onGetCollaboratorList={() => getCollaboratorList(appDetail._id)}
permissionList={AppPermissionList}
onUpdateCollaborators={(props) => {
if (props.members) {
return onUpdateCollaborators({
permission: props.permission,
members: props.members
});
}
}}
onUpdateCollaborators={async (props) =>
onUpdateCollaborators({
permission: props.permission,
members: props.members,
groups: props.groups
})
}
onDelOneCollaborator={onDelCollaborator}
refreshDeps={[appDetail.inheritPermission]}
isInheritPermission={appDetail.inheritPermission}
......
......@@ -5,7 +5,6 @@ import {
Button,
IconButton,
HStack,
Modal,
ModalBody,
Checkbox,
ModalFooter
......@@ -19,26 +18,23 @@ import TagsEditModal from '../TagsEditModal';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { AppContext } from '@/pages/app/detail/components/context';
import { useContextSelector } from 'use-context-selector';
import PermissionIconText from '@/components/support/permission/IconText';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useI18n } from '@/web/context/I18n';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { postTransition2Workflow } from '@/web/core/app/api/app';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const AppCard = () => {
const router = useRouter();
const { t } = useTranslation();
const { appT } = useI18n();
const { isPc } = useSystem();
const { appDetail, setAppDetail, onOpenInfoEdit, onDelApp } = useContextSelector(
AppContext,
(v) => v
);
const appId = appDetail._id;
const { feConfigs } = useSystemStore();
const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>();
......@@ -150,15 +146,15 @@ const AppCard = () => {
/>
)}
<Box flex={1} />
{isPc && (
<MyTag
type="borderFill"
colorSchema="gray"
onClick={() => (appDetail.permission.hasManagePer ? onOpenInfoEdit() : undefined)}
>
<PermissionIconText defaultPermission={appDetail.defaultPermission} />
</MyTag>
)}
{/* {isPc && ( */}
{/* <MyTag */}
{/* type="borderFill" */}
{/* colorSchema="gray" */}
{/* onClick={() => (appDetail.permission.hasManagePer ? onOpenInfoEdit() : undefined)} */}
{/* > */}
{/* <PermissionIconText defaultPermission={appDetail.defaultPermission} /> */}
{/* </MyTag> */}
{/* )} */}
</HStack>
</Box>
{TeamTagsSet && <TagsEditModal onClose={() => setTeamTagsSet(undefined)} />}
......
import { Dispatch, ReactNode, SetStateAction, useCallback, useEffect, useState } from 'react';
import { Dispatch, ReactNode, SetStateAction, useCallback, useState } from 'react';
import { createContext } from 'use-context-selector';
import { defaultApp } from '@/web/core/app/constants';
import { delAppById, getAppDetailById, putAppById } from '@/web/core/app/api';
......
......@@ -17,10 +17,7 @@ import { useFolderDrag } from '@/components/common/folder/useFolderDrag';
import dynamic from 'next/dynamic';
import type { EditResourceInfoFormType } from '@/components/common/Modal/EditResourceModal';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import {
AppDefaultPermissionVal,
AppPermissionList
} from '@fastgpt/global/support/permission/app/constant';
import { AppPermissionList } from '@fastgpt/global/support/permission/app/constant';
import {
deleteAppCollaborators,
getCollaboratorList,
......@@ -38,6 +35,7 @@ import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useChatStore } from '@/web/core/chat/context/storeChat';
import { useUserStore } from '@/web/support/user/useUserStore';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
const HttpEditModal = dynamic(() => import('./HttpPluginEditModal'));
const ListItem = () => {
......@@ -49,11 +47,16 @@ const ListItem = () => {
const { loadAndGetTeamMembers } = useUserStore();
const { lastChatAppId, setLastChatAppId } = useChatStore();
const { openConfirm: openMoveConfirm, ConfirmModal: MoveConfirmModal } = useConfirm({
type: 'common',
title: t('common:move.confirm'),
content: t('app:move.hint')
});
const { myApps, loadMyApps, onUpdateApp, setMoveAppId, folderDetail } = useContextSelector(
AppListContext,
(v) => v
);
const [loadingAppId, setLoadingAppId] = useState<string>();
const [editedApp, setEditedApp] = useState<EditResourceInfoFormType>();
const [editHttpPlugin, setEditHttpPlugin] = useState<EditHttpPluginProps>();
......@@ -64,17 +67,20 @@ const ListItem = () => {
[editPerAppIndex, myApps]
);
const parentApp = useMemo(() => myApps.find((item) => item._id === parentId), [parentId, myApps]);
const { runAsync: onPutAppById } = useRequest2(putAppById, {
onSuccess() {
loadMyApps();
}
});
const { getBoxProps } = useFolderDrag({
activeStyles: {
borderColor: 'primary.600'
},
onDrop: async (dragId: string, targetId: string) => {
setLoadingAppId(dragId);
try {
await putAppById(dragId, { parentId: targetId });
loadMyApps();
} catch (error) {}
setLoadingAppId(undefined);
onDrop: (dragId: string, targetId: string) => {
openMoveConfirm(async () => onPutAppById(dragId, { parentId: targetId }))();
}
});
......@@ -152,7 +158,6 @@ const ListItem = () => {
}
>
<MyBox
isLoading={loadingAppId === app._id}
lineHeight={1.5}
h="100%"
pt={5}
......@@ -233,7 +238,7 @@ const ListItem = () => {
)}
<PermissionIconText
defaultPermission={app.defaultPermission}
private={app.private}
color={'myGray.500'}
iconColor={'myGray.400'}
w={'0.875rem'}
......@@ -247,7 +252,9 @@ const ListItem = () => {
<Box color={'myGray.500'}>{formatTimeToChatTime(app.updateTime)}</Box>
</HStack>
)}
{app.permission.hasWritePer && (
{(AppFolderTypeList.includes(app.type)
? app.permission.hasManagePer
: app.permission.hasWritePer) && (
<Box className="more" display={['', 'none']}>
<MyMenu
Button={
......@@ -315,7 +322,9 @@ const ListItem = () => {
}
}
},
...(folderDetail?.type === AppTypeEnum.httpPlugin
...(folderDetail?.type === AppTypeEnum.httpPlugin &&
!(parentApp ? parentApp.permission : app.permission)
.hasManagePer
? []
: [
{
......@@ -412,34 +421,29 @@ const ListItem = () => {
isInheritPermission={editPerApp.inheritPermission}
avatar={editPerApp.avatar}
name={editPerApp.name}
defaultPer={{
value: editPerApp.defaultPermission,
defaultValue: AppDefaultPermissionVal,
onChange: (e) => {
return onUpdateApp(editPerApp._id, { defaultPermission: e });
}
}}
managePer={{
mode: 'all',
permission: editPerApp.permission,
onGetCollaboratorList: () => getCollaboratorList(editPerApp._id),
permissionList: AppPermissionList,
onUpdateCollaborators: ({
members = [], // TODO: remove the default value after group is ready
permission
}: {
onUpdateCollaborators: (props: {
members?: string[];
groups?: string[];
permission: number;
}) => {
return postUpdateAppCollaborators({
members,
permission,
}) =>
postUpdateAppCollaborators({
...props,
appId: editPerApp._id
});
},
onDelOneCollaborator: (tmbId: string) =>
}),
onDelOneCollaborator: async (
props: RequireOnlyOne<{
tmbId?: string;
groupId?: string;
}>
) =>
deleteAppCollaborators({
appId: editPerApp._id,
tmbId
...props,
appId: editPerApp._id
}),
refreshDeps: [editPerApp.inheritPermission]
}}
......@@ -452,6 +456,7 @@ const ListItem = () => {
onClose={() => setEditHttpPlugin(undefined)}
/>
)}
<MoveConfirmModal />
</>
);
};
......
......@@ -12,9 +12,9 @@ import {
} from '@fastgpt/global/common/parentFolder/type';
import { AppUpdateParams } from '@/global/core/app/api';
import dynamic from 'next/dynamic';
import { useI18n } from '@/web/context/I18n';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next';
const MoveModal = dynamic(() => import('@/components/common/folder/MoveModal'));
type AppListContextType = {
......@@ -58,7 +58,7 @@ export const AppListContext = createContext<AppListContextType>({
});
const AppListContextProvider = ({ children }: { children: ReactNode }) => {
const { appT } = useI18n();
const { t } = useTranslation();
const router = useRouter();
const { parentId = null, type = 'ALL' } = router.query as {
parentId?: string | null;
......@@ -129,10 +129,12 @@ const AppListContextProvider = ({ children }: { children: ReactNode }) => {
parentId,
type: AppTypeEnum.folder
}).then((res) =>
res.map((item) => ({
id: item._id,
name: item.name
}))
res
.filter((item) => item.permission.hasWritePer)
.map((item) => ({
id: item._id,
name: item.name
}))
);
}, []);
......@@ -162,9 +164,10 @@ const AppListContextProvider = ({ children }: { children: ReactNode }) => {
<MoveModal
moveResourceId={moveAppId}
server={getAppFolderList}
title={appT('move_app')}
title={t('app:move_app')}
onClose={() => setMoveAppId(undefined)}
onConfirm={onMoveApp}
moveHint={t('app:move.hint')}
/>
)}
</AppListContext.Provider>
......
import React, { useEffect, useMemo, useState } from 'react';
import React, { useMemo, useState } from 'react';
import {
Box,
Flex,
......@@ -14,8 +14,6 @@ import { useUserStore } from '@/web/support/user/useUserStore';
import { useI18n } from '@/web/context/I18n';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import List from './components/List';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { FolderIcon } from '@fastgpt/global/common/file/image/constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
......@@ -27,10 +25,7 @@ import FolderPath from '@/components/common/folder/Path';
import { useRouter } from 'next/router';
import FolderSlideCard from '@/components/common/folder/SlideCard';
import { delAppById, resumeInheritPer } from '@/web/core/app/api';
import {
AppDefaultPermissionVal,
AppPermissionList
} from '@fastgpt/global/support/permission/app/constant';
import { AppPermissionList } from '@fastgpt/global/support/permission/app/constant';
import {
deleteAppCollaborators,
getCollaboratorList,
......@@ -49,6 +44,7 @@ const EditFolderModal = dynamic(
() => import('@fastgpt/web/components/common/MyModal/EditFolderModal')
);
const HttpEditModal = dynamic(() => import('./components/HttpPluginEditModal'));
const List = dynamic(() => import('./components/List'));
const MyApps = () => {
const { t } = useTranslation();
......@@ -273,36 +269,47 @@ const MyApps = () => {
onMove={() => setMoveAppId(folderDetail._id)}
deleteTip={appT('confirm_delete_folder_tip')}
onDelete={() => onDeleFolder(folderDetail._id)}
defaultPer={{
value: folderDetail.defaultPermission,
defaultValue: AppDefaultPermissionVal,
onChange: (e) => {
return onUpdateApp(folderDetail._id, { defaultPermission: e });
}
}}
managePer={{
mode: 'all',
permission: folderDetail.permission,
onGetCollaboratorList: () => getCollaboratorList(folderDetail._id),
permissionList: AppPermissionList,
onUpdateCollaborators: ({
members = [], // TODO: remove the default value after group is ready
members,
groups,
permission
}: {
members?: string[];
groups?: string[];
permission: number;
}) => {
return postUpdateAppCollaborators({
members,
groups,
permission,
appId: folderDetail._id
});
},
refreshDeps: [folderDetail._id, folderDetail.inheritPermission],
onDelOneCollaborator: (tmbId: string) =>
deleteAppCollaborators({
appId: folderDetail._id,
tmbId
})
onDelOneCollaborator: async ({
tmbId,
groupId
}: {
tmbId?: string;
groupId?: string;
}) => {
if (tmbId) {
return deleteAppCollaborators({
appId: folderDetail._id,
tmbId
});
} else if (groupId) {
return deleteAppCollaborators({
appId: folderDetail._id,
groupId
});
}
}
}}
/>
</Box>
......
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { Box, Flex, Input } from '@chakra-ui/react';
import { delDatasetById } from '@/web/core/dataset/api';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useForm } from 'react-hook-form';
......@@ -10,7 +8,7 @@ import type { DatasetItemType } from '@fastgpt/global/core/dataset/type.d';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import AIModelSelector from '@/components/Select/AIModelSelector';
import { postRebuildEmbedding } from '@/web/core/dataset/api';
......@@ -21,12 +19,8 @@ import MyDivider from '@fastgpt/web/components/common/MyDivider/index';
import { DatasetTypeEnum, DatasetTypeMap } from '@fastgpt/global/core/dataset/constants';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
import MyIcon from '@fastgpt/web/components/common/Icon';
import {
DatasetDefaultPermissionVal,
DatasetPermissionList
} from '@fastgpt/global/support/permission/dataset/constant';
import { DatasetPermissionList } from '@fastgpt/global/support/permission/dataset/constant';
import MemberManager from '../../component/MemberManager';
import {
getCollaboratorList,
......@@ -39,7 +33,6 @@ import { EditResourceInfoFormType } from '@/components/common/Modal/EditResource
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
const Info = ({ datasetId }: { datasetId: string }) => {
const router = useRouter();
const [openBaseConfig, setOpenBaseConfig] = useState(true);
const [openPermissionConfig, setOpenPermissionConfig] = useState(true);
const { t } = useTranslation();
......@@ -56,10 +49,9 @@ const Info = ({ datasetId }: { datasetId: string }) => {
const vectorModel = watch('vectorModel');
const agentModel = watch('agentModel');
const defaultPermission = watch('defaultPermission');
const { datasetModelList, vectorModelList } = useSystemStore();
const { openConfirm: onOpenConfirmDel, ConfirmModal: ConfirmDelModal } = useConfirm({
const { ConfirmModal: ConfirmDelModal } = useConfirm({
content: t('common:core.dataset.Delete Confirm'),
type: 'delete'
});
......@@ -69,30 +61,17 @@ const Info = ({ datasetId }: { datasetId: string }) => {
type: 'delete'
});
const { File, onOpen: onOpenSelectFile } = useSelectFile({
const { File } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
/* 点击删除 */
const { mutate: onclickDelete, isLoading: isDeleting } = useRequest({
mutationFn: () => {
return delDatasetById(datasetId);
},
onSuccess() {
router.replace(`/dataset/list`);
},
successToast: t('common:common.Delete Success'),
errorToast: t('common:common.Delete Failed')
});
const { runAsync: onSave, loading: isSaving } = useRequest2(
const { runAsync: onSave } = useRequest2(
(data: DatasetItemType) => {
return updateDataset({
id: datasetId,
agentModel: data.agentModel,
externalReadUrl: data.externalReadUrl,
defaultPermission: data.defaultPermission
externalReadUrl: data.externalReadUrl
});
},
{
......@@ -101,7 +80,7 @@ const Info = ({ datasetId }: { datasetId: string }) => {
}
);
const { runAsync: onSelectFile, loading: isSelecting } = useRequest2(
const { runAsync: onSelectFile } = useRequest2(
(e: File[]) => {
const file = e[0];
if (!file) return Promise.resolve(null);
......@@ -122,7 +101,7 @@ const Info = ({ datasetId }: { datasetId: string }) => {
}
);
const { runAsync: onRebuilding, loading: isRebuilding } = useRequest2(
const { runAsync: onRebuilding } = useRequest2(
(vectorModel: VectorModelItemType) => {
return postRebuildEmbedding({
datasetId,
......@@ -242,10 +221,9 @@ const Info = ({ datasetId }: { datasetId: string }) => {
onchange={(e) => {
const vectorModel = vectorModelList.find((item) => item.model === e);
if (!vectorModel) return;
return onOpenConfirmRebuild(() => {
return onRebuilding(vectorModel).then(() => {
setValue('vectorModel', vectorModel);
});
return onOpenConfirmRebuild(async () => {
await onRebuilding(vectorModel);
setValue('vectorModel', vectorModel);
})();
}}
/>
......@@ -326,20 +304,12 @@ const Info = ({ datasetId }: { datasetId: string }) => {
<FormLabel fontWeight={'500'} fontSize={'mini'} pb={3} userSelect={'none'}>
{t('common:permission.Default permission')}
</FormLabel>
<DefaultPermissionList
fontSize={'mini'}
per={defaultPermission}
defaultPer={DatasetDefaultPermissionVal}
onChange={(v) => {
setValue('defaultPermission', v);
return handleSubmit((data) => onSave({ ...data, defaultPermission: v }))();
}}
/>
</Box>
<Box py={4}>
<MemberManager
managePer={{
mode: 'all',
permission: datasetDetail.permission,
onGetCollaboratorList: () => getCollaboratorList(datasetId),
permissionList: DatasetPermissionList,
......@@ -348,11 +318,19 @@ const Info = ({ datasetId }: { datasetId: string }) => {
...body,
datasetId
}),
onDelOneCollaborator: (tmbId) =>
deleteDatasetCollaborators({
datasetId,
tmbId
})
onDelOneCollaborator: async ({ groupId, tmbId }) => {
if (tmbId) {
return deleteDatasetCollaborators({
datasetId,
tmbId
});
} else if (groupId) {
return deleteDatasetCollaborators({
datasetId,
groupId
});
}
}
}}
/>
</Box>
......
......@@ -18,10 +18,7 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic';
import { useContextSelector } from 'use-context-selector';
import { DatasetsContext } from '../context';
import {
DatasetDefaultPermissionVal,
DatasetPermissionList
} from '@fastgpt/global/support/permission/dataset/constant';
import { DatasetPermissionList } from '@fastgpt/global/support/permission/dataset/constant';
import ConfigPerModal from '@/components/support/permission/ConfigPerModal';
import {
deleteDatasetCollaborators,
......@@ -34,7 +31,6 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
import { useI18n } from '@/web/context/I18n';
import { useTranslation } from 'next-i18next';
import { useUserStore } from '@/web/support/user/useUserStore';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import SideTag from './SideTag';
......@@ -42,7 +38,6 @@ const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditRe
function List() {
const { setLoading } = useSystemStore();
const { toast } = useToast();
const { isPc } = useSystem();
const { t } = useTranslation();
const { commonT } = useI18n();
......@@ -59,21 +54,32 @@ function List() {
folderDetail
} = useContextSelector(DatasetsContext, (v) => v);
const [editPerDatasetIndex, setEditPerDatasetIndex] = useState<number>();
const [loadingDatasetId, setLoadingDatasetId] = useState<string>();
const router = useRouter();
const { parentId = null } = router.query as { parentId?: string | null };
const parentDataset = useMemo(
() => myDatasets.find((item) => String(item._id) === parentId),
[parentId, myDatasets]
);
const { openConfirm: openMoveConfirm, ConfirmModal: MoveConfirmModal } = useConfirm({
type: 'common',
title: t('common:move.confirm'),
content: t('dataset:move.hint')
});
const { runAsync: updateDataset } = useRequest2(onUpdateDataset);
const { getBoxProps } = useFolderDrag({
activeStyles: {
borderColor: 'primary.600'
},
onDrop: async (dragId: string, targetId: string) => {
setLoadingDatasetId(dragId);
try {
await onUpdateDataset({
onDrop: (dragId: string, targetId: string) => {
openMoveConfirm(() =>
updateDataset({
id: dragId,
parentId: targetId
});
} catch (error) {}
setLoadingDatasetId(undefined);
})
)();
}
});
......@@ -86,10 +92,6 @@ function List() {
[editPerDatasetIndex, myDatasets]
);
const router = useRouter();
const { parentId = null } = router.query as { parentId?: string | null };
const { mutate: exportDataset } = useRequest({
mutationFn: async (dataset: DatasetItemType) => {
setLoading(true);
......@@ -100,15 +102,10 @@ function List() {
filename: `${dataset.name}.csv`
});
},
onSuccess() {
toast({
status: 'success',
title: t('common:core.dataset.Start export')
});
},
onSettled() {
setLoading(false);
},
successToast: t('common:core.dataset.Start export'),
errorToast: t('common:dataset.Export Dataset Limit Error')
});
......@@ -176,7 +173,6 @@ function List() {
}
>
<MyBox
isLoading={loadingDatasetId === dataset._id}
display={'flex'}
flexDirection={'column'}
lineHeight={1.5}
......@@ -278,8 +274,8 @@ function List() {
</HStack>
)}
<PermissionIconText
private={dataset.private}
iconColor="myGray.400"
defaultPermission={dataset.defaultPermission}
color={'myGray.500'}
/>
</HStack>
......@@ -293,7 +289,9 @@ function List() {
</Box>
</HStack>
)}
{dataset.permission.hasWritePer && (
{(dataset.type === DatasetTypeEnum.folder
? dataset.permission.hasManagePer
: dataset.permission.hasWritePer) && (
<Box
className="more"
display={['', 'none']}
......@@ -336,11 +334,18 @@ function List() {
avatar: dataset.avatar
})
},
{
icon: 'common/file/move',
label: t('common:Move'),
onClick: () => setMoveDatasetId(dataset._id)
},
...((parentDataset ? parentDataset : dataset)?.permission
.hasManagePer
? [
{
icon: 'common/file/move',
label: t('common:Move'),
onClick: () => {
setMoveDatasetId(dataset._id);
}
}
]
: []),
...(dataset.permission.hasManagePer
? [
{
......@@ -427,36 +432,20 @@ function List() {
}
avatar={editPerDataset.avatar}
name={editPerDataset.name}
defaultPer={{
value: editPerDataset.defaultPermission,
defaultValue: DatasetDefaultPermissionVal,
onChange: (e) =>
onUpdateDataset({
id: editPerDataset._id,
defaultPermission: e
})
}}
managePer={{
mode: 'all',
permission: editPerDataset.permission,
onGetCollaboratorList: () => getCollaboratorList(editPerDataset._id),
permissionList: DatasetPermissionList,
onUpdateCollaborators: ({
members = [], // TODO: remove default value after group is ready
permission
}: {
members?: string[];
permission: number;
}) => {
return postUpdateDatasetCollaborators({
members,
permission,
onUpdateCollaborators: (props) =>
postUpdateDatasetCollaborators({
...props,
datasetId: editPerDataset._id
});
},
onDelOneCollaborator: (tmbId: string) =>
}),
onDelOneCollaborator: async (props) =>
deleteDatasetCollaborators({
datasetId: editPerDataset._id,
tmbId
...props,
datasetId: editPerDataset._id
}),
refreshDeps: [editPerDataset._id, editPerDataset.inheritPermission]
}}
......@@ -464,6 +453,7 @@ function List() {
/>
)}
<ConfirmModal />
<MoveConfirmModal />
</>
);
}
......
import React, { useMemo, useState } from 'react';
import {
Card,
Flex,
Box,
Button,
ModalBody,
ModalHeader,
ModalFooter,
useTheme,
Grid
} from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { getDatasets, putDatasetById, getDatasetPaths } from '@/web/core/dataset/api';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
const MoveModal = ({
onClose,
onSuccess,
moveDataId
}: {
onClose: () => void;
onSuccess: () => void;
moveDataId: string;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const [parentId, setParentId] = useState<string>('');
const { data } = useQuery(['getDatasets', parentId], () => {
return Promise.all([
getDatasets({ parentId, type: DatasetTypeEnum.folder }),
getDatasetPaths(parentId)
]);
});
const paths = useMemo(
() => [
{
parentId: '',
parentName: t('common:core.dataset.My Dataset')
},
...(data?.[1] || [])
],
[data, t]
);
const folderList = useMemo(
() => (data?.[0] || []).filter((item) => item._id !== moveDataId),
[moveDataId, data]
);
const { mutate, isLoading } = useRequest({
mutationFn: () => putDatasetById({ id: moveDataId, parentId }),
onSuccess,
errorToast: t('common:dataset.Move Failed')
});
return (
<MyModal
isOpen={true}
maxW={['90vw', '800px']}
w={'800px'}
iconSrc="/imgs/modal/move.svg"
title={
<>
{!!parentId ? (
<Flex flex={1} userSelect={'none'} fontSize={['sm', 'md']} fontWeight={'normal'}>
{paths.map((item, i) => (
<Flex key={item.parentId} mr={2} alignItems={'center'}>
<Box
borderRadius={'md'}
{...(i === paths.length - 1
? {
cursor: 'default'
}
: {
cursor: 'pointer',
_hover: {
color: 'primary.500'
},
onClick: () => {
setParentId(item.parentId);
}
})}
>
{item.parentName}
</Box>
{i !== paths.length - 1 && (
<MyIcon name={'common/rightArrowLight'} color={'myGray.500'} w={'14px'} />
)}
</Flex>
))}
</Flex>
) : (
<Box>{t('common:core.dataset.My Dataset')}</Box>
)}
</>
}
onClose={onClose}
>
<Flex flexDirection={'column'} h={['90vh', 'auto']}>
<ModalBody
flex={['1 0 0', '0 0 auto']}
maxH={'80vh'}
overflowY={'auto'}
display={'grid'}
userSelect={'none'}
>
<Grid
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
gridGap={3}
>
{folderList.map((item) =>
(() => {
return (
<MyTooltip
key={item._id}
label={
item.type === DatasetTypeEnum.dataset
? t('common:dataset.Select Dataset')
: t('common:dataset.Select Folder')
}
>
<Card
p={3}
border={theme.borders.base}
boxShadow={'sm'}
h={'80px'}
cursor={'pointer'}
_hover={{
boxShadow: 'md'
}}
onClick={() => {
setParentId(item._id);
}}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['24px', '28px']}></Avatar>
<Box
className="textEllipsis"
ml={3}
fontWeight={'bold'}
fontSize={['md', 'md']}
>
{item.name}
</Box>
</Flex>
<Flex justifyContent={'flex-end'} alignItems={'center'} fontSize={'sm'}>
{item.type === DatasetTypeEnum.folder ? (
<Box color={'myGray.500'}>{t('common:Folder')}</Box>
) : (
<>
<MyIcon mr={1} name="kbTest" w={'12px'} />
<Box color={'myGray.500'}>{item.vectorModel.name}</Box>
</>
)}
</Flex>
</Card>
</MyTooltip>
);
})()
)}
</Grid>
{folderList.length === 0 && (
<EmptyTip text={t('common:common.folder.No Folder')}></EmptyTip>
)}
</ModalBody>
<ModalFooter>
<Button isLoading={isLoading} onClick={mutate}>
{t('common:dataset.Confirm move the folder')}
</Button>
</ModalFooter>
</Flex>
</MyModal>
);
};
export default MoveModal;
......@@ -13,7 +13,6 @@ import {
import { useRouter } from 'next/router';
import React, { useCallback, useState } from 'react';
import { createContext } from 'use-context-selector';
import { useI18n } from '@/web/context/I18n';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { DatasetUpdateBody } from '@fastgpt/global/core/dataset/api';
import dynamic from 'next/dynamic';
......@@ -68,7 +67,6 @@ export const DatasetsContext = createContext<DatasetContextType>({
function DatasetContextProvider({ children }: { children: React.ReactNode }) {
const router = useRouter();
const { commonT } = useI18n();
const { t } = useTranslation();
const [moveDatasetId, setMoveDatasetId] = useState<string>();
const [searchKey, setSearchKey] = useState('');
......@@ -127,10 +125,12 @@ function DatasetContextProvider({ children }: { children: React.ReactNode }) {
parentId,
type: DatasetTypeEnum.folder
})
).map((item) => ({
id: item._id,
name: item.name
}));
)
.filter((item) => item.permission.hasManagePer)
.map((item) => ({
id: item._id,
name: item.name
}));
}, []);
const [editedDataset, setEditedDataset] = useState<EditResourceInfoFormType>();
......@@ -164,9 +164,10 @@ function DatasetContextProvider({ children }: { children: React.ReactNode }) {
<MoveModal
moveResourceId={moveDatasetId}
server={getDatasetFolderList}
title={commonT('Move')}
title={t('common:Move')}
onClose={() => setMoveDatasetId(undefined)}
onConfirm={onMoveDataset}
onConfirm={(parentId) => onMoveDataset(parentId)}
moveHint={t('dataset:move.hint')}
/>
)}
</DatasetsContext.Provider>
......
......@@ -17,10 +17,7 @@ import { EditFolderFormType } from '@fastgpt/web/components/common/MyModal/EditF
import dynamic from 'next/dynamic';
import { postCreateDatasetFolder, resumeInheritPer } from '@/web/core/dataset/api';
import FolderSlideCard from '@/components/common/folder/SlideCard';
import {
DatasetDefaultPermissionVal,
DatasetPermissionList
} from '@fastgpt/global/support/permission/dataset/constant';
import { DatasetPermissionList } from '@fastgpt/global/support/permission/dataset/constant';
import {
postUpdateDatasetCollaborators,
deleteDatasetCollaborators,
......@@ -52,7 +49,6 @@ const Dataset = () => {
loadMyDatasets,
refetchFolderDetail,
folderDetail,
setEditedDataset,
setMoveDatasetId,
onDelDataset,
onUpdateDataset,
......@@ -228,38 +224,39 @@ const Dataset = () => {
});
})
}
defaultPer={{
value: folderDetail.defaultPermission,
defaultValue: DatasetDefaultPermissionVal,
onChange: (e) => {
return onUpdateDataset({
id: folderDetail._id,
defaultPermission: e
});
}
}}
managePer={{
mode: 'all',
permission: folderDetail.permission,
onGetCollaboratorList: () => getCollaboratorList(folderDetail._id),
permissionList: DatasetPermissionList,
onUpdateCollaborators: ({
members = [], // TODO: remove the default value after group is ready
members,
groups,
permission
}: {
members?: string[];
groups?: string[];
permission: number;
}) => {
return postUpdateDatasetCollaborators({
}) =>
postUpdateDatasetCollaborators({
members,
groups,
permission,
datasetId: folderDetail._id
});
},
onDelOneCollaborator: (tmbId: string) =>
deleteDatasetCollaborators({
datasetId: folderDetail._id,
tmbId
}),
onDelOneCollaborator: async ({ tmbId, groupId }) => {
if (tmbId) {
return deleteDatasetCollaborators({
datasetId: folderDetail._id,
tmbId
});
} else if (groupId) {
return deleteDatasetCollaborators({
datasetId: folderDetail._id,
groupId
});
}
},
refreshDeps: [folderDetail._id, folderDetail.inheritPermission]
}}
/>
......
......@@ -64,7 +64,7 @@ export function getTestRequest<Q = any, B = any>({
];
}
export const MockParseHeaderCert = async ({
export const parseHeaderCertMock = async ({
req,
authToken = true,
authRoot = false,
......
......@@ -2,8 +2,6 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { AppDetailType } from '@fastgpt/global/core/app/type.d';
import type { FeishuAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type.d';
import { AppPermission } from '@fastgpt/global/support/permission/app/controller';
import { NullPermission } from '@fastgpt/global/support/permission/constant';
import { i18nT } from '@fastgpt/web/i18n/utils';
export const defaultApp: AppDetailType = {
_id: '',
name: 'AI',
......@@ -18,7 +16,6 @@ export const defaultApp: AppDetailType = {
teamTags: [],
edges: [],
version: 'v2',
defaultPermission: NullPermission,
permission: new AppPermission(),
inheritPermission: false
};
......
......@@ -11,5 +11,5 @@ export const getCollaboratorList = (datasetId: string) =>
export const postUpdateDatasetCollaborators = (body: UpdateDatasetCollaboratorBody) =>
POST('/proApi/core/dataset/collaborator/update', body);
export const deleteDatasetCollaborators = ({ ...params }: DatasetCollaboratorDeleteParams) =>
DELETE('/proApi/core/dataset/collaborator/delete', { ...params });
export const deleteDatasetCollaborators = (params: DatasetCollaboratorDeleteParams) =>
DELETE('/proApi/core/dataset/collaborator/delete', params);
......@@ -8,7 +8,6 @@ import type {
DatasetCollectionItemType,
DatasetItemType
} from '@fastgpt/global/core/dataset/type.d';
import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant';
import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller';
export const defaultDatasetDetail: DatasetItemType = {
......@@ -26,7 +25,6 @@ export const defaultDatasetDetail: DatasetItemType = {
permission: new DatasetPermission(),
vectorModel: defaultVectorModels[0],
agentModel: defaultQAModels[0],
defaultPermission: DatasetDefaultPermissionVal,
inheritPermission: true
};
......@@ -48,7 +46,6 @@ export const defaultCollectionDetail: DatasetCollectionItemType = {
status: 'active',
vectorModel: defaultVectorModels[0].model,
agentModel: defaultQAModels[0].model,
defaultPermission: DatasetDefaultPermissionVal,
inheritPermission: true
},
tags: [],
......
......@@ -28,6 +28,7 @@ type State = {
loadAndGetTeamMembers: (init?: boolean) => Promise<TeamMemberItemType[]>;
teamMemberGroups: MemberGroupListType;
myGroups: MemberGroupListType;
loadAndGetGroups: (init?: boolean) => Promise<MemberGroupListType>;
};
......@@ -106,6 +107,7 @@ export const useUserStore = create<State>()(
return res;
},
teamMemberGroups: [],
myGroups: [],
loadAndGetGroups: async (init = false) => {
if (!useSystemStore.getState()?.feConfigs?.isPlus) return [];
......@@ -116,6 +118,9 @@ export const useUserStore = create<State>()(
const res = await getGroupList();
set((state) => {
state.teamMemberGroups = res;
state.myGroups = res.filter((item) =>
item.members.map((i) => String(i.tmbId)).includes(String(state.userInfo?.team?.tmbId))
);
});
return res;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment