Move app owner (#2280)

* feat: app owner change (#2271)

* feat(app): changeOwner api

* feat: changeOwner api

* feat: ChangeOwnerModal

* feat: update change owner api

* chore: move change owner api into pro version
feat(fe): change owner modal

* feat: add change owner button and modal to InfoModal

* change owner ux

* feat: doc

* perf: remove info change owner btn

---------

Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
This commit is contained in:
Archer
2024-08-06 22:13:16 +08:00
committed by GitHub
parent 91bc573571
commit a109c59cc6
14 changed files with 258 additions and 24 deletions

View File

@@ -36,13 +36,14 @@ curl --location --request POST 'https://{{host}}/api/admin/init/489' \
4. 商业版新增 - 团队通知账号绑定,用于接收重要信息。
5. 商业版新增 - 知识库集合标签功能,可以对知识库进行标签管理。
6. 商业版新增 - 知识库搜索节点支持标签过滤和创建时间过滤。
7. 新增 - 删除所有对话引导内容
8. 优化 - 对话框信息懒加载,减少网络传输
9. 优化 - 清除选文件缓存,支持重复选择同一个文件
10. 修复 - 知识库上传文件,网络不稳定或文件较多情况下,进度无法到 100%
11. 修复 - 删除应用后回到聊天选择最后一次对话的应用为删除的应用时提示无该应用问题
12. 修复 - 插件动态变量配置默认值时,无法正常显示默认值
13. 修复 - 工具调用温度和最大回复值未生效
14. 修复 - 函数调用模式assistant role 中GPT 模型必须传入 content 参数。(不影响大部分模型,目前基本都改用用 ToolChoice 模式FC 模式已弃用)
15. 修复 - 知识库文件上传进度更新可能异常
16. 修复 - 知识库 rebuilding 时候,页面总是刷新到第一页
7. 商业版新增 - 转移 App owner 权限
8. 新增 - 删除所有对话引导内容
9. 优化 - 对话框信息懒加载,减少网络传输
10. 优化 - 清除选文件缓存,支持重复选择同一个文件
11. 修复 - 知识库上传文件,网络不稳定或文件较多情况下,进度无法到 100%
12. 修复 - 删除应用后回到聊天选择最后一次对话的应用为删除的应用时提示无该应用问题
13. 修复 - 插件动态变量配置默认值时,无法正常显示默认值
14. 修复 - 工具调用温度和最大回复值未生效
15. 修复 - 函数调用模式assistant role 中GPT 模型必须传入 content 参数。(不影响大部分模型,目前基本都改用用 ToolChoice 模式FC 模式已弃用)
16. 修复 - 知识库文件上传进度更新可能异常
17. 修复 - 知识库 rebuilding 时候,页面总是刷新到第一页。

View File

@@ -3,7 +3,9 @@ import { i18nT } from '../../../../web/i18n/utils';
/* dataset: 502000 */
export enum AppErrEnum {
unExist = 'appUnExist',
unAuthApp = 'unAuthApp'
unAuthApp = 'unAuthApp',
invalidOwner = 'invalidOwner',
invalidAppType = 'invalidAppType'
}
const appErrList = [
{
@@ -13,6 +15,14 @@ const appErrList = [
{
statusText: AppErrEnum.unAuthApp,
message: i18nT('common:code_error.app_error.un_auth_app')
},
{
statusText: AppErrEnum.invalidOwner,
message: i18nT('common:code_error.app_error.invalid_owner')
},
{
statusText: AppErrEnum.invalidAppType,
message: i18nT('common:code_error.app_error.invalid_app_type')
}
];
export default appErrList.reduce((acc, cur, index) => {

View File

@@ -34,6 +34,7 @@ export const iconPaths = {
'common/language/en': () => import('./icons/common/language/en.svg'),
'common/language/zh': () => import('./icons/common/language/zh.svg'),
'common/leftArrowLight': () => import('./icons/common/leftArrowLight.svg'),
'common/lineChange': () => import('./icons/common/lineChange.svg'),
'common/linkBlue': () => import('./icons/common/linkBlue.svg'),
'common/loading': () => import('./icons/common/loading.svg'),
'common/logLight': () => import('./icons/common/logLight.svg'),
@@ -241,6 +242,7 @@ export const iconPaths = {
menu: () => import('./icons/menu.svg'),
minus: () => import('./icons/minus.svg'),
'modal/AddClb': () => import('./icons/modal/AddClb.svg'),
'modal/changePer': () => import('./icons/modal/changePer.svg'),
'modal/concat': () => import('./icons/modal/concat.svg'),
'modal/confirmPay': () => import('./icons/modal/confirmPay.svg'),
'modal/edit': () => import('./icons/modal/edit.svg'),

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 18 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6688 0.977385C12.9942 0.651948 13.5219 0.651948 13.8473 0.977385L17.1446 4.27471C17.1531 4.28305 17.1613 4.29157 17.1693 4.30026C17.2353 4.37136 17.2865 4.45116 17.3231 4.53579C17.3669 4.63717 17.3912 4.74898 17.3912 4.86647C17.3912 4.98306 17.3673 5.09406 17.3241 5.19481C17.2868 5.28189 17.234 5.3639 17.1656 5.43664C17.1537 5.44941 17.1413 5.46179 17.1285 5.47378C17.0538 5.54406 16.9693 5.59796 16.8796 5.63546C16.7843 5.67534 16.6801 5.69805 16.5708 5.69971L16.5565 5.6998L4.39124 5.6998C3.931 5.6998 3.55791 5.32671 3.55791 4.86647C3.55791 4.40623 3.931 4.03314 4.39124 4.03314L14.5461 4.03314L12.6688 2.1559C12.3434 1.83046 12.3434 1.30282 12.6688 0.977385Z"/>
<path d="M0.968656 8.54428C1.04865 8.46429 1.14086 8.40396 1.23928 8.36329C1.33521 8.32357 1.44016 8.30124 1.55022 8.30024L1.55791 8.30021H13.7246C14.1848 8.30021 14.5579 8.6733 14.5579 9.13354C14.5579 9.59378 14.1848 9.96687 13.7246 9.96687H3.56976L5.447 11.8441C5.77244 12.1696 5.77244 12.6972 5.447 13.0226C5.12156 13.3481 4.59392 13.3481 4.26849 13.0226L0.969527 9.72367L0.961458 9.71551C0.88513 9.63732 0.827247 9.54784 0.787811 9.45253C0.747067 9.35427 0.724579 9.24653 0.724579 9.13354C0.724579 9.02068 0.747013 8.91307 0.787665 8.81491C0.828333 8.71649 0.888664 8.62428 0.968656 8.54428Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none">
<path d="M13.6688 3.97738C13.9942 3.65195 14.5219 3.65195 14.8473 3.97738L18.1446 7.27471C18.1531 7.28305 18.1613 7.29157 18.1693 7.30026C18.2353 7.37136 18.2865 7.45116 18.3231 7.53579C18.3669 7.63717 18.3912 7.74898 18.3912 7.86647C18.3912 7.98306 18.3673 8.09406 18.3241 8.19481C18.2868 8.28189 18.234 8.3639 18.1656 8.43664C18.1537 8.44941 18.1413 8.46179 18.1285 8.47378C18.0538 8.54406 17.9693 8.59796 17.8796 8.63546C17.7843 8.67534 17.6801 8.69805 17.5708 8.69971L17.5565 8.6998L5.39124 8.6998C4.931 8.6998 4.55791 8.32671 4.55791 7.86647C4.55791 7.40623 4.931 7.03314 5.39124 7.03314L15.5461 7.03314L13.6688 5.1559C13.3434 4.83046 13.3434 4.30282 13.6688 3.97738Z" fill="#3370FF"/>
<path d="M1.96866 11.5443C2.04865 11.4643 2.14086 11.404 2.23928 11.3633C2.33521 11.3236 2.44016 11.3012 2.55022 11.3002L2.55791 11.3002H14.7246C15.1848 11.3002 15.5579 11.6733 15.5579 12.1335C15.5579 12.5938 15.1848 12.9669 14.7246 12.9669H4.56976L6.447 14.8441C6.77244 15.1696 6.77244 15.6972 6.447 16.0226C6.12156 16.3481 5.59392 16.3481 5.26849 16.0226L1.96953 12.7237L1.96146 12.7155C1.88513 12.6373 1.82725 12.5478 1.78781 12.4525C1.74707 12.3543 1.72458 12.2465 1.72458 12.1335C1.72458 12.0207 1.74701 11.9131 1.78767 11.8149C1.82833 11.7165 1.88866 11.6243 1.96866 11.5443Z" fill="#3370FF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -60,7 +60,9 @@
"code_error": {
"app_error": {
"not_exist": "应用不存在",
"un_auth_app": "无权操作该应用"
"un_auth_app": "无权操作该应用",
"invalid_owner": "非法的应用所有者",
"invalid_app_type": "错误的应用类型"
},
"chat_error": {
"un_auth": "没有权限操作此对话记录"
@@ -1095,7 +1097,13 @@
"Remove InheritPermission Confirm": "此操作会导致权限继承失效,是否进行?",
"Resume InheritPermission Confirm": "是否恢复为继承父级文件夹的权限?",
"Resume InheritPermission Failed": "恢复失败",
"Resume InheritPermission Success": "恢复成功"
"Resume InheritPermission Success": "恢复成功",
"change_owner": "转移所有权",
"change_owner_to": "转移给",
"change_owner_placeholder": "输入用户名查找账号",
"change_owner_tip": "转移后将保留您的管理员权限",
"change_owner_success": "成功转移所有权",
"change_owner_failed": "转移所有权失败"
},
"plugin": {
"App": "选择应用",

View File

@@ -0,0 +1,162 @@
import { getTeamMembers } from '@/web/support/user/team/api';
import {
Box,
Flex,
HStack,
Input,
ModalBody,
ModalFooter,
Button,
useDisclosure
} from '@chakra-ui/react';
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import Icon from '@fastgpt/web/components/common/Icon';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyTag from '@fastgpt/web/components/common/Tag';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import React, { useState } from 'react';
export type ChangeOwnerModalProps = {
avatar?: string;
name: string;
onChangeOwner: (tmbId: string) => Promise<unknown>;
};
export function ChangeOwnerModal({
onClose,
avatar,
name,
onChangeOwner
}: ChangeOwnerModalProps & { onClose: () => void }) {
const { t } = useTranslation();
const [inputValue, setInputValue] = React.useState('');
const { data: teamMembers = [] } = useRequest2(getTeamMembers, {
manual: false
});
const memberList = teamMembers.filter((item) => {
return item.memberName.includes(inputValue);
});
const {
isOpen: isOpenMemberListMenu,
onClose: onCloseMemberListMenu,
onOpen: onOpenMemberListMenu
} = useDisclosure();
const [selectedMember, setSelectedMember] = useState<TeamMemberItemType | null>(null);
const { runAsync, loading } = useRequest2(onChangeOwner, {
onSuccess: onClose,
successToast: t('common:permission.change_owner_success'),
errorToast: t('common:permission.change_owner_failed')
});
const onConfirm = async () => {
if (!selectedMember) {
return;
}
await runAsync(selectedMember.tmbId);
};
return (
<MyModal
isOpen
iconSrc="modal/changePer"
onClose={onClose}
title={t('common:permission.change_owner')}
isLoading={loading}
>
<ModalBody>
<HStack>
<Avatar src={avatar} w={'1.75rem'} borderRadius={'md'} />
<Box>{name}</Box>
</HStack>
<Flex mt={4} justify="start" flexDirection="column">
<Box fontSize="14px" fontWeight="500" color="myGray.900">
{t('common:permission.change_owner_to')}
</Box>
<Flex mt="4" alignItems="center" position={'relative'}>
{selectedMember && (
<Avatar
src={selectedMember.avatar}
w={'20px'}
borderRadius={'md'}
position="absolute"
left={3}
/>
)}
<Input
placeholder={t('common:permission.change_owner_placeholder')}
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
setSelectedMember(null);
}}
onFocus={() => {
onOpenMemberListMenu();
setSelectedMember(null);
}}
// onBlur={() => {
// setTimeout(() => {
// onCloseMemberListMenu();
// }, 10);
// }}
{...(selectedMember && { pl: '10' })}
/>
</Flex>
{isOpenMemberListMenu && memberList.length > 0 && (
<Flex
mt={2}
w={'100%'}
flexDirection={'column'}
gap={2}
p={1}
boxShadow="lg"
bg="white"
borderRadius="md"
zIndex={10}
maxH={'300px'}
overflow={'auto'}
>
{memberList.map((item) => (
<Box
key={item.tmbId}
p="2"
_hover={{ bg: 'myGray.100' }}
mx="1"
borderRadius="md"
cursor={'pointer'}
onClickCapture={() => {
setInputValue(item.memberName);
setSelectedMember(item);
onCloseMemberListMenu();
}}
>
<Flex align="center">
<Avatar src={item.avatar} w="1.25rem" />
<Box ml="2">{item.memberName}</Box>
</Flex>
</Box>
))}
</Flex>
)}
<MyTag mt="4" colorSchema="blue">
<Icon name="common/info" w="1rem" />
<Box ml="2">{t('common:permission.change_owner_tip')}</Box>
</MyTag>
</Flex>
</ModalBody>
<ModalFooter>
<HStack>
<Button onClick={onClose} variant={'whiteBase'}>
{t('common:common.Cancel')}
</Button>
<Button onClick={onConfirm}>{t('common:common.Confirm')}</Button>
</HStack>
</ModalFooter>
</MyModal>
);
}

View File

@@ -3,13 +3,12 @@ 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 } from '@chakra-ui/react';
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 { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
import ResumeInherit from '../ResumeInheritText';
import { ChangeOwnerModal } from '../ChangeOwnerModal';
export type ConfigPerModalProps = {
avatar?: string;
@@ -25,6 +24,7 @@ export type ConfigPerModalProps = {
resumeInheritPermission?: () => void;
hasParent?: boolean;
refetchResource?: () => void;
onChangeOwner?: (tmbId: string) => Promise<unknown>;
};
const ConfigPerModal = ({
@@ -36,11 +36,17 @@ const ConfigPerModal = ({
resumeInheritPermission,
hasParent,
onClose,
refetchResource
refetchResource,
onChangeOwner
}: ConfigPerModalProps & {
onClose: () => void;
}) => {
const { t } = useTranslation();
const {
isOpen: isChangeOwnerModalOpen,
onOpen: onOpenChangeOwnerModal,
onClose: onCloseChangeOwnerModal
} = useDisclosure();
return (
<>
@@ -113,8 +119,30 @@ const ConfigPerModal = ({
}}
</CollaboratorContextProvider>
</Box>
{onChangeOwner && (
<Box mt={4}>
<Button
size="md"
variant="whitePrimary"
onClick={onOpenChangeOwnerModal}
w="full"
borderRadius="md"
leftIcon={<MyIcon w="4" name="common/lineChange" />}
>
{t('common:permission.change_owner')}
</Button>
</Box>
)}
</ModalBody>
</MyModal>
{isChangeOwnerModalOpen && onChangeOwner && (
<ChangeOwnerModal
onClose={onCloseChangeOwnerModal}
avatar={avatar}
name={name}
onChangeOwner={onChangeOwner}
/>
)}
</>
);
};

View File

@@ -29,3 +29,8 @@ export type PostRevertAppProps = {
editEdges: AppSchema['edges'];
editChatConfig: AppSchema['chatConfig'];
};
export type AppChangeOwnerBody = {
appId: string;
ownerId: string;
};

View File

@@ -7,7 +7,8 @@ import {
Input,
Textarea,
ModalFooter,
ModalBody
ModalBody,
useDisclosure
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
@@ -15,7 +16,7 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
@@ -35,7 +36,6 @@ import {
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { resumeInheritPer } from '@/web/core/app/api';
import { useI18n } from '@/web/context/I18n';
import ResumeInherit from '@/components/support/permission/ResumeInheritText';
@@ -61,6 +61,7 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
defaultValues: appDetail
});
const avatar = getValues('avatar');
const name = getValues('name');
// submit config
const { runAsync: saveSubmitSuccess, loading: btnLoading } = useRequest2(
@@ -135,6 +136,7 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
appId: appDetail._id
});
};
const onDelCollaborator = async (tmbId: string) => {
await deleteAppCollaborators({
appId: appDetail._id,

View File

@@ -53,7 +53,7 @@ const Provider = () => {
export async function getServerSideProps(context: any) {
return {
props: {
...(await serviceSideProps(context, ['app', 'chat', 'file', 'publish', 'workflow']))
...(await serviceSideProps(context, ['app', 'chat', 'user', 'file', 'publish', 'workflow']))
}
};
}

View File

@@ -1,7 +1,7 @@
import React, { useMemo, useState } from 'react';
import { Box, Grid, Flex, IconButton, HStack } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { delAppById, putAppById, resumeInheritPer } from '@/web/core/app/api';
import { delAppById, putAppById, resumeInheritPer, changeOwner } from '@/web/core/app/api';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar';
@@ -399,6 +399,12 @@ const ListItem = () => {
)}
{!!editPerApp && (
<ConfigPerModal
onChangeOwner={(tmbId: string) =>
changeOwner({
appId: editPerApp._id,
ownerId: tmbId
}).then(() => loadMyApps())
}
refetchResource={loadMyApps}
hasParent={Boolean(parentId)}
resumeInheritPermission={onResumeInheritPermission}

View File

@@ -326,7 +326,7 @@ export default ContextRender;
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content, ['app']))
...(await serviceSideProps(content, ['app', 'user']))
}
};
}

View File

@@ -1,7 +1,7 @@
import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
import type { AppDetailType, AppListItemType } from '@fastgpt/global/core/app/type.d';
import type { GetAppChatLogsParams } from '@/global/core/api/appReq.d';
import { AppUpdateParams } from '@/global/core/app/api';
import { AppUpdateParams, AppChangeOwnerBody } from '@/global/core/app/api';
import type { CreateAppBody } from '@/pages/api/core/app/create';
import type { ListAppBody } from '@/pages/api/core/app/list';
@@ -40,3 +40,5 @@ export const getAppChatLogs = (data: GetAppChatLogsParams) => POST(`/core/app/ge
export const resumeInheritPer = (appId: string) =>
GET(`/core/app/resumeInheritPermission`, { appId });
export const changeOwner = (data: AppChangeOwnerBody) => POST(`/proApi/core/app/changeOwner`, data);