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

@@ -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);