mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-22 12:20:34 +00:00
perf: optimize simple app history (#2782)
* simple app history * ui * extract context content into hooks
This commit is contained in:
@@ -40,6 +40,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/line': () => import('./icons/common/line.svg'),
|
||||
'common/lineChange': () => import('./icons/common/lineChange.svg'),
|
||||
'common/linkBlue': () => import('./icons/common/linkBlue.svg'),
|
||||
'common/list': () => import('./icons/common/list.svg'),
|
||||
|
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 12" fill="none">
|
||||
<path d="M4.42017 1.30151L0.999965 10.6984" stroke="#DFE2EA" stroke-linecap="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 164 B |
@@ -16,6 +16,8 @@ const CustomRightDrawer = ({
|
||||
iconSrc,
|
||||
title,
|
||||
maxW = ['90vw', '30vw'],
|
||||
top = 16,
|
||||
bottom = 0,
|
||||
children,
|
||||
isLoading,
|
||||
showMask = true,
|
||||
@@ -31,8 +33,8 @@ const CustomRightDrawer = ({
|
||||
zIndex={100}
|
||||
maxW={maxW}
|
||||
w={'100%'}
|
||||
top={'60px'}
|
||||
bottom={0}
|
||||
top={top}
|
||||
bottom={bottom}
|
||||
borderLeftRadius={'lg'}
|
||||
border={'base'}
|
||||
boxShadow={'2'}
|
||||
|
@@ -2,6 +2,7 @@ import { Box, BoxProps, Flex } from '@chakra-ui/react';
|
||||
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
const FolderPath = (props: {
|
||||
paths: ParentTreePathItemType[];
|
||||
@@ -35,7 +36,7 @@ const FolderPath = (props: {
|
||||
return paths.length === 0 && !!FirstPathDom ? (
|
||||
<>{FirstPathDom}</>
|
||||
) : (
|
||||
<Flex flex={1} ml={-1.5}>
|
||||
<Flex flex={1}>
|
||||
{concatPaths.map((item, i) => (
|
||||
<Flex key={item.parentId || i} alignItems={'center'}>
|
||||
<Box
|
||||
@@ -51,6 +52,7 @@ const FolderPath = (props: {
|
||||
}
|
||||
: {
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'medium',
|
||||
color: 'myGray.500',
|
||||
_hover: {
|
||||
bg: 'myGray.100',
|
||||
@@ -64,9 +66,7 @@ const FolderPath = (props: {
|
||||
{item.parentName}
|
||||
</Box>
|
||||
{i !== concatPaths.length - 1 && (
|
||||
<Box mx={1.5} color={'myGray.500'}>
|
||||
/
|
||||
</Box>
|
||||
<MyIcon name={'common/line'} color={'myGray.500'} mx={1} width={'5px'} />
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -22,18 +22,16 @@ import { useRouter } from 'next/router';
|
||||
import AppCard from '../WorkflowComponents/AppCard';
|
||||
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { compareSnapshot } from '@/web/core/workflow/utils';
|
||||
import SaveAndPublishModal from '../WorkflowComponents/Flow/components/SaveAndPublish';
|
||||
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useDebounceEffect } from 'ahooks';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import SaveButton from '../Workflow/components/SaveButton';
|
||||
|
||||
const PublishHistories = dynamic(() => import('../WorkflowPublishHistoriesSlider'));
|
||||
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -48,12 +46,6 @@ const Header = () => {
|
||||
onOpen: onOpenBackConfirm,
|
||||
onClose: onCloseBackConfirm
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isSaveAndPublishModalOpen,
|
||||
onOpen: onSaveAndPublishModalOpen,
|
||||
onClose: onSaveAndPublishModalClose
|
||||
} = useDisclosure();
|
||||
const [isSave, setIsSave] = useState(false);
|
||||
|
||||
const {
|
||||
flowData2StoreData,
|
||||
@@ -65,7 +57,9 @@ const Header = () => {
|
||||
edges,
|
||||
past,
|
||||
future,
|
||||
setPast
|
||||
setPast,
|
||||
saveSnapshot,
|
||||
resetSnapshot
|
||||
} = useContextSelector(WorkflowContext, (v) => v);
|
||||
const { lastAppListRouteType } = useSystemStore();
|
||||
|
||||
@@ -225,81 +219,11 @@ const Header = () => {
|
||||
{t('common:core.workflow.Run')}
|
||||
</Button>
|
||||
{!historiesDefaultData && (
|
||||
<MyPopover
|
||||
placement={'bottom-end'}
|
||||
hasArrow={false}
|
||||
offset={[2, 4]}
|
||||
w={'116px'}
|
||||
onOpenFunc={() => setIsSave(true)}
|
||||
onCloseFunc={() => setIsSave(false)}
|
||||
trigger={'hover'}
|
||||
Trigger={
|
||||
<Button
|
||||
size={'sm'}
|
||||
rightIcon={
|
||||
<MyIcon
|
||||
name={isSave ? 'core/chat/chevronUp' : 'core/chat/chevronDown'}
|
||||
w={['14px', '16px']}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Box>{t('common:common.Save')}</Box>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{({ onClose }) => (
|
||||
<MyBox p={1.5}>
|
||||
<MyBox
|
||||
display={'flex'}
|
||||
size={'md'}
|
||||
px={1}
|
||||
py={1.5}
|
||||
rounded={'4px'}
|
||||
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
isLoading={loading}
|
||||
onClick={async () => {
|
||||
await onClickSave({});
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('app:saved_success'),
|
||||
position: 'top-right'
|
||||
});
|
||||
onClose();
|
||||
setIsSave(false);
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'core/workflow/upload'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('common:core.workflow.Save to cloud')}</Box>
|
||||
</MyBox>
|
||||
<Flex
|
||||
px={1}
|
||||
py={1.5}
|
||||
rounded={'4px'}
|
||||
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
const data = flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
onSaveAndPublishModalOpen();
|
||||
}
|
||||
onClose();
|
||||
setIsSave(false);
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'core/workflow/publish'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('common:core.workflow.Save and publish')}</Box>
|
||||
{isSaveAndPublishModalOpen && (
|
||||
<SaveAndPublishModal
|
||||
isLoading={loading}
|
||||
onClose={onSaveAndPublishModalClose}
|
||||
onClickSave={onClickSave}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</MyBox>
|
||||
)}
|
||||
</MyPopover>
|
||||
<SaveButton
|
||||
isLoading={loading}
|
||||
onClickSave={onClickSave}
|
||||
checkData={flowData2StoreDataAndCheck}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
)}
|
||||
@@ -309,6 +233,9 @@ const Header = () => {
|
||||
onClose={() => {
|
||||
setHistoriesDefaultData(undefined);
|
||||
}}
|
||||
past={past}
|
||||
saveSnapshot={saveSnapshot}
|
||||
resetSnapshot={resetSnapshot}
|
||||
/>
|
||||
)}
|
||||
<MyModal
|
||||
@@ -356,16 +283,12 @@ const Header = () => {
|
||||
loading,
|
||||
isV2Workflow,
|
||||
historiesDefaultData,
|
||||
isSave,
|
||||
onClickSave,
|
||||
setHistoriesDefaultData,
|
||||
appDetail.chatConfig,
|
||||
flowData2StoreDataAndCheck,
|
||||
setWorkflowTestData,
|
||||
isSaveAndPublishModalOpen,
|
||||
onSaveAndPublishModalClose,
|
||||
toast,
|
||||
onSaveAndPublishModalOpen
|
||||
toast
|
||||
]);
|
||||
|
||||
return Render;
|
||||
|
@@ -1,204 +1,383 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { getPublishList, postRevertVersion } from '@/web/core/app/api/version';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
getAppVersionDetail,
|
||||
getWorkflowVersionList,
|
||||
updateAppVersion
|
||||
} from '@/web/core/app/api/version';
|
||||
import { useVirtualScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
|
||||
import { Box, Button, Flex, Input } from '@chakra-ui/react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { AppContext } from './context';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
|
||||
|
||||
export type InitProps = {
|
||||
nodes: AppSchema['modules'];
|
||||
edges: AppSchema['edges'];
|
||||
chatConfig: AppSchema['chatConfig'];
|
||||
};
|
||||
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
|
||||
import { SaveSnapshotParams, SnapshotsType } from './WorkflowComponents/context';
|
||||
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import Tag from '@fastgpt/web/components/common/Tag';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { storeEdgesRenderEdge, storeNode2FlowNode } from '@/web/core/workflow/utils';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { versionListResponse } from '@/pages/api/core/app/version/listWorkflow';
|
||||
|
||||
const PublishHistoriesSlider = ({
|
||||
onClose,
|
||||
initData,
|
||||
defaultData
|
||||
past,
|
||||
saveSnapshot,
|
||||
resetSnapshot,
|
||||
top,
|
||||
bottom
|
||||
}: {
|
||||
onClose: () => void;
|
||||
initData: (data: InitProps) => void;
|
||||
defaultData: InitProps;
|
||||
past: SnapshotsType[];
|
||||
saveSnapshot: (params: SaveSnapshotParams) => Promise<boolean>;
|
||||
resetSnapshot: (state: SnapshotsType) => void;
|
||||
top?: string | number;
|
||||
bottom?: string | number;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
|
||||
const { appDetail, setAppDetail, reloadAppLatestVersion } = useContextSelector(
|
||||
AppContext,
|
||||
(v) => v
|
||||
);
|
||||
const appId = appDetail._id;
|
||||
|
||||
const [selectedHistoryId, setSelectedHistoryId] = useState<string>();
|
||||
|
||||
const { scrollDataList, ScrollList, isLoading } = useVirtualScrollPagination(getPublishList, {
|
||||
itemHeight: 49,
|
||||
overscan: 20,
|
||||
|
||||
pageSize: 20,
|
||||
defaultParams: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
const onPreview = useCallback(
|
||||
(data: AppVersionSchemaType) => {
|
||||
setSelectedHistoryId(data._id);
|
||||
|
||||
initData({
|
||||
nodes: data.nodes,
|
||||
edges: data.edges,
|
||||
chatConfig: data.chatConfig
|
||||
});
|
||||
},
|
||||
[initData]
|
||||
);
|
||||
const onCloseSlider = useCallback(
|
||||
(data: InitProps) => {
|
||||
setSelectedHistoryId(undefined);
|
||||
initData(data);
|
||||
onClose();
|
||||
},
|
||||
[initData, onClose]
|
||||
);
|
||||
|
||||
const { runAsync: onRevert } = useRequest2(
|
||||
async (data: AppVersionSchemaType) => {
|
||||
if (!appId) return;
|
||||
await postRevertVersion(appId, {
|
||||
versionId: data._id,
|
||||
editNodes: defaultData.nodes, // old workflow
|
||||
editEdges: defaultData.edges,
|
||||
editChatConfig: defaultData.chatConfig
|
||||
});
|
||||
|
||||
setAppDetail((state) => ({
|
||||
...state,
|
||||
modules: data.nodes,
|
||||
edges: data.edges
|
||||
}));
|
||||
|
||||
onCloseSlider(data);
|
||||
reloadAppLatestVersion();
|
||||
},
|
||||
{
|
||||
successToast: appT('version.Revert success')
|
||||
}
|
||||
);
|
||||
|
||||
const showLoading = isLoading;
|
||||
const [currentTab, setCurrentTab] = useState<'myEdit' | 'teamCloud'>('myEdit');
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomRightDrawer
|
||||
onClose={() =>
|
||||
onCloseSlider({
|
||||
nodes: defaultData.nodes,
|
||||
edges: defaultData.edges,
|
||||
chatConfig: defaultData.chatConfig
|
||||
})
|
||||
onClose={() => onClose()}
|
||||
title={
|
||||
(
|
||||
<>
|
||||
<LightRowTabs
|
||||
list={[
|
||||
{ label: t('workflow:workflow.My edit'), value: 'myEdit' },
|
||||
{ label: t('workflow:workflow.Team cloud'), value: 'teamCloud' }
|
||||
]}
|
||||
value={currentTab}
|
||||
onChange={setCurrentTab}
|
||||
inlineStyles={{ px: 0.5, pb: 2 }}
|
||||
gap={5}
|
||||
py={0}
|
||||
fontSize={'sm'}
|
||||
/>
|
||||
</>
|
||||
) as any
|
||||
}
|
||||
iconSrc="core/workflow/versionHistories"
|
||||
title={t('common:core.workflow.publish.histories')}
|
||||
maxW={'300px'}
|
||||
maxW={'340px'}
|
||||
px={0}
|
||||
showMask={false}
|
||||
overflow={'unset'}
|
||||
top={top}
|
||||
bottom={bottom}
|
||||
>
|
||||
<Button
|
||||
mx={'20px'}
|
||||
variant={'whitePrimary'}
|
||||
mb={2}
|
||||
isDisabled={!selectedHistoryId}
|
||||
onClick={() => {
|
||||
setSelectedHistoryId(undefined);
|
||||
initData({
|
||||
nodes: defaultData.nodes,
|
||||
edges: defaultData.edges,
|
||||
chatConfig: defaultData.chatConfig
|
||||
});
|
||||
}}
|
||||
>
|
||||
{appT('current_settings')}
|
||||
</Button>
|
||||
<ScrollList isLoading={showLoading} flex={'1 0 0'} px={5}>
|
||||
{scrollDataList.map((data, index) => {
|
||||
const item = data.data;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
key={data.index}
|
||||
alignItems={'center'}
|
||||
py={3}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
fontWeight={500}
|
||||
_hover={{
|
||||
bg: 'primary.50'
|
||||
}}
|
||||
{...(selectedHistoryId === item._id && {
|
||||
color: 'primary.600'
|
||||
})}
|
||||
onClick={() => onPreview(item)}
|
||||
>
|
||||
<Box
|
||||
w={'12px'}
|
||||
h={'12px'}
|
||||
borderWidth={'2px'}
|
||||
borderColor={'primary.600'}
|
||||
borderRadius={'50%'}
|
||||
position={'relative'}
|
||||
{...(index !== scrollDataList.length - 1 && {
|
||||
_after: {
|
||||
content: '""',
|
||||
height: '40px',
|
||||
width: '2px',
|
||||
bgColor: 'myGray.250',
|
||||
position: 'absolute',
|
||||
top: '10px',
|
||||
left: '3px'
|
||||
}
|
||||
})}
|
||||
></Box>
|
||||
<Box ml={3} flex={'1 0 0'}>
|
||||
{formatTime2YMDHM(item.time)}
|
||||
</Box>
|
||||
{item._id === selectedHistoryId && (
|
||||
<PopoverConfirm
|
||||
showCancel
|
||||
content={t('common:core.workflow.publish.OnRevert version confirm')}
|
||||
onConfirm={() => onRevert(item)}
|
||||
Trigger={
|
||||
<Box>
|
||||
<MyTooltip label={t('common:core.workflow.publish.OnRevert version')}>
|
||||
<MyIcon
|
||||
name={'core/workflow/revertVersion'}
|
||||
w={'20px'}
|
||||
color={'primary.600'}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</ScrollList>
|
||||
{currentTab === 'myEdit' ? (
|
||||
<MyEdit past={past} saveSnapshot={saveSnapshot} resetSnapshot={resetSnapshot} />
|
||||
) : (
|
||||
<TeamCloud saveSnapshot={saveSnapshot} resetSnapshot={resetSnapshot} />
|
||||
)}
|
||||
</CustomRightDrawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(PublishHistoriesSlider);
|
||||
|
||||
const MyEdit = ({
|
||||
past,
|
||||
saveSnapshot,
|
||||
resetSnapshot
|
||||
}: {
|
||||
past: SnapshotsType[];
|
||||
saveSnapshot: (params: SaveSnapshotParams) => Promise<boolean>;
|
||||
resetSnapshot: (state: SnapshotsType) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
<Flex px={5} flex={'1 0 0'} flexDirection={'column'}>
|
||||
{past.length > 0 && (
|
||||
<Box py={2} px={3}>
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
w={'full'}
|
||||
h={'30px'}
|
||||
onClick={async () => {
|
||||
const initialSnapshot = past[past.length - 1];
|
||||
|
||||
const res = await saveSnapshot({
|
||||
pastNodes: initialSnapshot.nodes,
|
||||
pastEdges: initialSnapshot.edges,
|
||||
chatConfig: initialSnapshot.chatConfig,
|
||||
customTitle: t(`app:app.version_initial_copy`)
|
||||
});
|
||||
|
||||
if (res) {
|
||||
resetSnapshot(initialSnapshot);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t('workflow:workflow.Switch_success'),
|
||||
status: 'success'
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('app:app.version_back')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Flex flex={'1 0 0'} flexDirection={'column'} overflow={'auto'}>
|
||||
{past.map((item, index) => {
|
||||
return (
|
||||
<Flex
|
||||
key={index}
|
||||
alignItems={'center'}
|
||||
py={2}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
fontWeight={500}
|
||||
_hover={{
|
||||
bg: 'primary.50'
|
||||
}}
|
||||
onClick={async () => {
|
||||
const res = await saveSnapshot({
|
||||
pastNodes: item.nodes,
|
||||
pastEdges: item.edges,
|
||||
chatConfig: item.chatConfig,
|
||||
customTitle: `${t('app:app.version_copy')}-${item.title}`
|
||||
});
|
||||
if (res) {
|
||||
resetSnapshot(item);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t('workflow:workflow.Switch_success'),
|
||||
status: 'success'
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
w={'12px'}
|
||||
h={'12px'}
|
||||
borderWidth={'2px'}
|
||||
borderColor={'primary.600'}
|
||||
borderRadius={'50%'}
|
||||
position={'relative'}
|
||||
{...(index !== past.length - 1 && {
|
||||
_after: {
|
||||
content: '""',
|
||||
height: '26px',
|
||||
width: '2px',
|
||||
bgColor: 'myGray.250',
|
||||
position: 'absolute',
|
||||
top: '10px',
|
||||
left: '3px'
|
||||
}
|
||||
})}
|
||||
></Box>
|
||||
<Box
|
||||
ml={3}
|
||||
flex={'1 0 0'}
|
||||
fontSize={'sm'}
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
color={'myGray.900'}
|
||||
>
|
||||
{item.title}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
<Box py={2} textAlign={'center'} color={'myGray.600'} fontSize={'xs'}>
|
||||
{t('common:common.No more data')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const TeamCloud = ({
|
||||
saveSnapshot,
|
||||
resetSnapshot
|
||||
}: {
|
||||
saveSnapshot: (params: SaveSnapshotParams) => Promise<boolean>;
|
||||
resetSnapshot: (state: SnapshotsType) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const { loadAndGetTeamMembers } = useUserStore();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const { scrollDataList, ScrollList, isLoading, fetchData } = useVirtualScrollPagination(
|
||||
getWorkflowVersionList,
|
||||
{
|
||||
itemHeight: 40,
|
||||
overscan: 20,
|
||||
|
||||
pageSize: 30,
|
||||
defaultParams: {
|
||||
appId: appDetail._id
|
||||
}
|
||||
}
|
||||
);
|
||||
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
|
||||
manual: !feConfigs.isPlus
|
||||
});
|
||||
const [editIndex, setEditIndex] = useState<number | undefined>(undefined);
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(undefined);
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const { runAsync: onChangeVersion, loading: isLoadingVersion } = useRequest2(
|
||||
async (versionItem: versionListResponse) => {
|
||||
const versionDetail = await getAppVersionDetail(versionItem._id, versionItem.appId);
|
||||
|
||||
if (!versionDetail) return;
|
||||
|
||||
const state = {
|
||||
nodes: versionDetail.nodes?.map((item) => storeNode2FlowNode({ item, t })),
|
||||
edges: versionDetail.edges?.map((item) => storeEdgesRenderEdge({ edge: item })),
|
||||
title: versionItem.versionName,
|
||||
chatConfig: versionDetail.chatConfig
|
||||
};
|
||||
|
||||
await saveSnapshot({
|
||||
pastNodes: state.nodes,
|
||||
pastEdges: state.edges,
|
||||
chatConfig: state.chatConfig,
|
||||
customTitle: `${t('app:app.version_copy')}-${state.title}`
|
||||
});
|
||||
|
||||
resetSnapshot(state);
|
||||
toast({
|
||||
title: t('workflow:workflow.Switch_success'),
|
||||
status: 'success'
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollList isLoading={isLoading || isLoadingVersion} flex={'1 0 0'} px={5}>
|
||||
{scrollDataList.map((data, index) => {
|
||||
const item = data.data;
|
||||
const firstPublishedIndex = scrollDataList.findIndex((data) => data.data.isPublish);
|
||||
const tmb = members.find((member) => member.tmbId === item.tmbId);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
key={data.index}
|
||||
alignItems={'center'}
|
||||
py={editIndex !== index ? 2 : 1}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
fontWeight={500}
|
||||
onMouseEnter={() => setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(undefined)}
|
||||
_hover={{
|
||||
bg: 'primary.50'
|
||||
}}
|
||||
onClick={() => editIndex === undefined && onChangeVersion(item)}
|
||||
>
|
||||
<MyPopover
|
||||
trigger="hover"
|
||||
placement={'bottom-end'}
|
||||
w={'208px'}
|
||||
h={'72px'}
|
||||
Trigger={
|
||||
<Box>
|
||||
<Avatar src={tmb?.avatar} borderRadius={'50%'} w={'24px'} h={'24px'} />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{({ onClose }) => (
|
||||
<Flex alignItems={'center'} h={'full'} pl={5} gap={3}>
|
||||
<Box>
|
||||
<Avatar src={tmb?.avatar} borderRadius={'50%'} w={'36px'} h={'36px'} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Box fontSize={'14px'} color={'myGray.900'}>
|
||||
{tmb?.memberName}
|
||||
</Box>
|
||||
<Box fontSize={'12px'} color={'myGray.500'}>
|
||||
{formatTime2YMDHMS(item.time)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</MyPopover>
|
||||
{editIndex !== index ? (
|
||||
<>
|
||||
<Box
|
||||
ml={3}
|
||||
flex={'1 0 0'}
|
||||
fontSize={'sm'}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
<Box minWidth={0} overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
|
||||
<Box as={'span'} color={'myGray.900'}>
|
||||
{item.versionName || formatTime2YMDHMS(item.time)}
|
||||
</Box>
|
||||
</Box>
|
||||
{item.isPublish && (
|
||||
<Tag
|
||||
ml={3}
|
||||
flexShrink={0}
|
||||
type="borderSolid"
|
||||
colorSchema={index === firstPublishedIndex ? 'green' : 'blue'}
|
||||
>
|
||||
{index === firstPublishedIndex
|
||||
? t('app:app.version_current')
|
||||
: t('app:app.version_past')}
|
||||
</Tag>
|
||||
)}
|
||||
</Box>
|
||||
{hoveredIndex === index && (
|
||||
<MyIcon
|
||||
name="edit"
|
||||
w={'18px'}
|
||||
ml={2}
|
||||
_hover={{ color: 'primary.600' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditIndex(index);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<MyBox ml={3} isLoading={isEditing} size={'md'}>
|
||||
<Input
|
||||
autoFocus
|
||||
h={8}
|
||||
defaultValue={item.versionName || formatTime2YMDHMS(item.time)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onBlur={async (e) => {
|
||||
setIsEditing(true);
|
||||
await updateAppVersion({
|
||||
appId: item.appId,
|
||||
versionName: e.target.value,
|
||||
versionId: item._id
|
||||
});
|
||||
await fetchData();
|
||||
setEditIndex(undefined);
|
||||
setIsEditing(false);
|
||||
}}
|
||||
/>
|
||||
</MyBox>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</ScrollList>
|
||||
);
|
||||
};
|
||||
|
@@ -3,13 +3,11 @@ import React, { useCallback, useMemo } from 'react';
|
||||
import { AppContext, TabEnum } from './context';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
|
||||
const RouteTab = () => {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
const router = useRouter();
|
||||
const { appDetail, currentTab } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
@@ -28,20 +26,21 @@ const RouteTab = () => {
|
||||
const tabList = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: appDetail.type === AppTypeEnum.plugin ? appT('setting_plugin') : appT('setting_app'),
|
||||
label:
|
||||
appDetail.type === AppTypeEnum.plugin ? t('app:setting_plugin') : t('app:setting_app'),
|
||||
id: TabEnum.appEdit
|
||||
},
|
||||
...(appDetail.permission.hasManagePer
|
||||
? [
|
||||
{
|
||||
label: appT('publish_channel'),
|
||||
label: t('app:publish_channel'),
|
||||
id: TabEnum.publish
|
||||
},
|
||||
{ label: appT('chat_logs'), id: TabEnum.logs }
|
||||
{ label: t('app:chat_logs'), id: TabEnum.logs }
|
||||
]
|
||||
: [])
|
||||
],
|
||||
[appDetail.permission.hasManagePer, appDetail.type, appT]
|
||||
[appDetail.permission.hasManagePer, appDetail.type]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -51,6 +50,7 @@ const RouteTab = () => {
|
||||
key={tab.id}
|
||||
px={2}
|
||||
py={0.5}
|
||||
fontWeight={'medium'}
|
||||
{...(currentTab === tab.id
|
||||
? {
|
||||
color: 'primary.700'
|
||||
|
@@ -15,21 +15,46 @@ import { cardStyles } from '../constants';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { storeEdgesRenderEdge, storeNode2FlowNode } from '@/web/core/workflow/utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
|
||||
import { SnapshotsType } from '../WorkflowComponents/context';
|
||||
|
||||
const Edit = ({
|
||||
appForm,
|
||||
setAppForm
|
||||
setAppForm,
|
||||
past,
|
||||
saveSnapshot
|
||||
}: {
|
||||
appForm: AppSimpleEditFormType;
|
||||
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
|
||||
past: SnapshotsType[];
|
||||
saveSnapshot: (
|
||||
this: any,
|
||||
{ pastNodes, pastEdges, chatConfig, customTitle, isSaved }: any
|
||||
) => Promise<boolean>;
|
||||
}) => {
|
||||
const { isPc } = useSystem();
|
||||
const { loadAllDatasets } = useDatasetStore();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// show selected dataset
|
||||
useMount(() => {
|
||||
loadAllDatasets();
|
||||
saveSnapshot({
|
||||
pastNodes: appDetail.modules?.map((item) => storeNode2FlowNode({ item, t })),
|
||||
pastEdges: appDetail.edges?.map((item) => storeEdgesRenderEdge({ edge: item })),
|
||||
chatConfig: appDetail.chatConfig,
|
||||
isSaved: true
|
||||
});
|
||||
if (past.length > 0) {
|
||||
const storeWorkflow = uiWorkflow2StoreWorkflow(past[0]);
|
||||
const currentAppForm = appWorkflow2Form({ ...storeWorkflow, chatConfig: past[0].chatConfig });
|
||||
|
||||
setAppForm(currentAppForm);
|
||||
return;
|
||||
}
|
||||
setAppForm(
|
||||
appWorkflow2Form({
|
||||
nodes: appDetail.modules,
|
||||
@@ -52,7 +77,7 @@ const Edit = ({
|
||||
display={['block', 'flex']}
|
||||
flex={'1 0 0'}
|
||||
h={0}
|
||||
pt={[2, 1.5]}
|
||||
mt={[4, 0]}
|
||||
gap={1}
|
||||
borderRadius={'lg'}
|
||||
overflowY={['auto', 'unset']}
|
||||
@@ -73,7 +98,7 @@ const Edit = ({
|
||||
</Box>
|
||||
</Box>
|
||||
{isPc && (
|
||||
<Box {...cardStyles} boxShadow={'3'} flex={'2 0 0'} w={0}>
|
||||
<Box {...cardStyles} boxShadow={'3'} flex={'2 0 0'} w={0} mb={3}>
|
||||
<ChatTest appForm={appForm} />
|
||||
</Box>
|
||||
)}
|
||||
|
@@ -1,42 +1,57 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../context';
|
||||
import FolderPath from '@/components/common/folder/Path';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getAppFolderPath } from '@/web/core/app/api/app';
|
||||
import { Box, Button, Flex, IconButton } from '@chakra-ui/react';
|
||||
import { Box, Flex, IconButton } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import RouteTab from '../RouteTab';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
|
||||
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
import { TabEnum } from '../context';
|
||||
import PublishHistoriesSlider, { type InitProps } from '../PublishHistoriesSlider';
|
||||
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { compareWorkflow } from '@/web/core/workflow/utils';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import { publishStatusStyle } from '../constants';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
|
||||
import SaveButton from '../Workflow/components/SaveButton';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useDebounceEffect } from 'ahooks';
|
||||
import { InitProps, SnapshotsType } from '../WorkflowComponents/context';
|
||||
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
|
||||
import {
|
||||
compareSnapshot,
|
||||
storeEdgesRenderEdge,
|
||||
storeNode2FlowNode
|
||||
} from '@/web/core/workflow/utils';
|
||||
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
|
||||
|
||||
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
|
||||
|
||||
const Header = ({
|
||||
appForm,
|
||||
setAppForm
|
||||
setAppForm,
|
||||
past,
|
||||
setPast,
|
||||
saveSnapshot
|
||||
}: {
|
||||
appForm: AppSimpleEditFormType;
|
||||
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
|
||||
setAppForm: (form: AppSimpleEditFormType) => void;
|
||||
past: SnapshotsType[];
|
||||
setPast: (value: React.SetStateAction<SnapshotsType[]>) => void;
|
||||
saveSnapshot: (
|
||||
this: any,
|
||||
{ pastNodes, pastEdges, chatConfig, customTitle, isSaved }: any
|
||||
) => Promise<boolean>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useSystem();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { appId, appDetail, onSaveApp, currentTab } = useContextSelector(AppContext, (v) => v);
|
||||
const { lastAppListRouteType } = useSystemStore();
|
||||
const { allDatasets } = useDatasetStore();
|
||||
@@ -45,8 +60,10 @@ const Header = ({
|
||||
manual: false,
|
||||
refreshDeps: [appId]
|
||||
});
|
||||
const onclickRoute = useCallback(
|
||||
const onClickRoute = useCallback(
|
||||
(parentId: string) => {
|
||||
localStorage.removeItem(`${appDetail._id}-past`);
|
||||
|
||||
router.push({
|
||||
pathname: '/app/list',
|
||||
query: {
|
||||
@@ -58,58 +75,98 @@ const Header = ({
|
||||
[router, lastAppListRouteType]
|
||||
);
|
||||
|
||||
const isPublished = useMemo(() => {
|
||||
const data = form2AppWorkflow(appForm, t);
|
||||
return compareWorkflow(
|
||||
{
|
||||
nodes: appDetail.modules,
|
||||
edges: [],
|
||||
chatConfig: appDetail.chatConfig
|
||||
},
|
||||
{
|
||||
nodes: data.nodes,
|
||||
edges: [],
|
||||
chatConfig: data.chatConfig
|
||||
}
|
||||
);
|
||||
}, [appDetail.chatConfig, appDetail.modules, appForm, allDatasets, t]);
|
||||
const [isPublished, setIsPublished] = useState(false);
|
||||
|
||||
const onSubmitPublish = useCallback(
|
||||
async (data: AppSimpleEditFormType) => {
|
||||
const { nodes, edges } = form2AppWorkflow(data, t);
|
||||
const { runAsync: onClickSave, loading } = useRequest2(
|
||||
async ({
|
||||
isPublish,
|
||||
versionName = formatTime2YMDHMS(new Date())
|
||||
}: {
|
||||
isPublish?: boolean;
|
||||
versionName?: string;
|
||||
}) => {
|
||||
const { nodes, edges } = form2AppWorkflow(appForm, t);
|
||||
await onSaveApp({
|
||||
nodes,
|
||||
edges,
|
||||
chatConfig: data.chatConfig,
|
||||
chatConfig: appForm.chatConfig,
|
||||
type: AppTypeEnum.simple,
|
||||
isPublish: true,
|
||||
versionName: formatTime2YMDHMS(new Date())
|
||||
isPublish,
|
||||
versionName
|
||||
});
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('app:publish_success'),
|
||||
position: 'top-right'
|
||||
});
|
||||
},
|
||||
[onSaveApp, t, toast]
|
||||
setPast((prevPast) =>
|
||||
prevPast.map((item, index) =>
|
||||
index === 0
|
||||
? {
|
||||
...item,
|
||||
isSaved: true
|
||||
}
|
||||
: item
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>();
|
||||
|
||||
const resetSnapshot = (data: SnapshotsType) => {
|
||||
const storeWorkflow = uiWorkflow2StoreWorkflow(data);
|
||||
const currentAppForm = appWorkflow2Form({ ...storeWorkflow, chatConfig: data.chatConfig });
|
||||
|
||||
setAppForm(currentAppForm);
|
||||
};
|
||||
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
const data = form2AppWorkflow(appForm, t);
|
||||
|
||||
saveSnapshot({
|
||||
pastNodes: data.nodes?.map((item) => storeNode2FlowNode({ item, t })),
|
||||
pastEdges: data.edges?.map((item) => storeEdgesRenderEdge({ edge: item })),
|
||||
chatConfig: data.chatConfig
|
||||
});
|
||||
},
|
||||
[appForm],
|
||||
{ wait: 500 }
|
||||
);
|
||||
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
const savedSnapshot = past.find((snapshot) => snapshot.isSaved);
|
||||
const data = form2AppWorkflow(appForm, t);
|
||||
const val = compareSnapshot(
|
||||
{
|
||||
nodes: savedSnapshot?.nodes,
|
||||
edges: [],
|
||||
chatConfig: savedSnapshot?.chatConfig
|
||||
},
|
||||
{
|
||||
nodes: data.nodes?.map((item) => storeNode2FlowNode({ item, t })),
|
||||
edges: [],
|
||||
chatConfig: data.chatConfig
|
||||
}
|
||||
);
|
||||
setIsPublished(val);
|
||||
},
|
||||
[past],
|
||||
{ wait: 500 }
|
||||
);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box h={14}>
|
||||
{!isPc && (
|
||||
<Flex pt={2} justifyContent={'center'}>
|
||||
<Flex justifyContent={'center'}>
|
||||
<RouteTab />
|
||||
</Flex>
|
||||
)}
|
||||
<Flex pt={[2, 3]} alignItems={'flex-start'} position={'relative'}>
|
||||
<Flex w={'full'} alignItems={'center'} position={'relative'} h={'full'}>
|
||||
<Box flex={'1'}>
|
||||
<FolderPath
|
||||
rootName={t('app:all_apps')}
|
||||
paths={paths}
|
||||
hoverStyle={{ color: 'primary.600' }}
|
||||
onClick={onclickRoute}
|
||||
onClick={onClickRoute}
|
||||
fontSize={'14px'}
|
||||
/>
|
||||
</Box>
|
||||
{isPc && (
|
||||
@@ -156,36 +213,23 @@ const Header = ({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<PopoverConfirm
|
||||
showCancel
|
||||
content={t('common:core.app.Publish Confirm')}
|
||||
Trigger={
|
||||
<Box>
|
||||
<MyTooltip label={t('common:core.app.Publish app tip')}>
|
||||
<Button isDisabled={isPublished}>{t('common:core.app.Publish')}</Button>
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
}
|
||||
onConfirm={() => onSubmitPublish(appForm)}
|
||||
/>
|
||||
<SaveButton isLoading={loading} onClickSave={onClickSave} />
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{!!historiesDefaultData && (
|
||||
<PublishHistoriesSlider
|
||||
initData={({ nodes, chatConfig }) => {
|
||||
setAppForm(
|
||||
appWorkflow2Form({
|
||||
nodes,
|
||||
chatConfig
|
||||
})
|
||||
);
|
||||
{historiesDefaultData && currentTab === TabEnum.appEdit && (
|
||||
<PublishHistories
|
||||
onClose={() => {
|
||||
setHistoriesDefaultData(undefined);
|
||||
}}
|
||||
onClose={() => setHistoriesDefaultData(undefined)}
|
||||
defaultData={historiesDefaultData}
|
||||
past={past}
|
||||
saveSnapshot={saveSnapshot}
|
||||
resetSnapshot={resetSnapshot}
|
||||
top={14}
|
||||
bottom={3}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
@@ -9,13 +9,15 @@ import dynamic from 'next/dynamic';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import useSnapshots from './useSnapshots';
|
||||
|
||||
const Logs = dynamic(() => import('../Logs/index'));
|
||||
const PublishChannel = dynamic(() => import('../Publish'));
|
||||
|
||||
const SimpleEdit = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentTab } = useContextSelector(AppContext, (v) => v);
|
||||
const { currentTab, appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const { past, setPast, saveSnapshot } = useSnapshots(appDetail._id);
|
||||
|
||||
const [appForm, setAppForm] = useState(getDefaultAppForm());
|
||||
|
||||
@@ -24,12 +26,18 @@ const SimpleEdit = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex h={'100%'} flexDirection={'column'} px={[3, 0]} pr={[3, 3]} pb={3}>
|
||||
<Header appForm={appForm} setAppForm={setAppForm} />
|
||||
<Flex h={'100%'} flexDirection={'column'} px={[3, 0]} pr={[3, 3]}>
|
||||
<Header
|
||||
appForm={appForm}
|
||||
setAppForm={setAppForm}
|
||||
past={past}
|
||||
setPast={setPast}
|
||||
saveSnapshot={saveSnapshot}
|
||||
/>
|
||||
{currentTab === TabEnum.appEdit ? (
|
||||
<Edit appForm={appForm} setAppForm={setAppForm} />
|
||||
<Edit appForm={appForm} setAppForm={setAppForm} past={past} saveSnapshot={saveSnapshot} />
|
||||
) : (
|
||||
<Box flex={'1 0 0'} h={0} mt={4}>
|
||||
<Box flex={'1 0 0'} h={0} mt={[4, 0]}>
|
||||
{currentTab === TabEnum.publish && <PublishChannel />}
|
||||
{currentTab === TabEnum.logs && <Logs />}
|
||||
</Box>
|
||||
|
@@ -0,0 +1,47 @@
|
||||
import { useLocalStorageState, useMemoizedFn } from 'ahooks';
|
||||
import { SnapshotsType } from '../WorkflowComponents/context';
|
||||
import { SetStateAction } from 'react';
|
||||
import { compareSnapshot } from '@/web/core/workflow/utils';
|
||||
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||
|
||||
const useSnapshots = (appId: string) => {
|
||||
const [past, setPast] = useLocalStorageState<SnapshotsType[]>(`${appId}-past-simple`, {
|
||||
defaultValue: [],
|
||||
listenStorageChange: true
|
||||
}) as [SnapshotsType[], (value: SetStateAction<SnapshotsType[]>) => void];
|
||||
|
||||
const saveSnapshot = useMemoizedFn(
|
||||
async ({ pastNodes, pastEdges, chatConfig, customTitle, isSaved }) => {
|
||||
const pastState = past[0];
|
||||
const isPastEqual = compareSnapshot(
|
||||
{
|
||||
nodes: pastNodes,
|
||||
edges: pastEdges,
|
||||
chatConfig: chatConfig
|
||||
},
|
||||
{
|
||||
nodes: pastState?.nodes,
|
||||
edges: pastState?.edges,
|
||||
chatConfig: pastState?.chatConfig
|
||||
}
|
||||
);
|
||||
if (isPastEqual) return false;
|
||||
|
||||
setPast((past) => [
|
||||
{
|
||||
nodes: pastNodes,
|
||||
edges: pastEdges,
|
||||
title: customTitle || formatTime2YMDHMS(new Date()),
|
||||
chatConfig,
|
||||
isSaved
|
||||
},
|
||||
...past.slice(0, 199)
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
return { past, setPast, saveSnapshot };
|
||||
};
|
||||
|
||||
export default useSnapshots;
|
@@ -22,18 +22,16 @@ import { useRouter } from 'next/router';
|
||||
import AppCard from '../WorkflowComponents/AppCard';
|
||||
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { compareSnapshot } from '@/web/core/workflow/utils';
|
||||
import SaveAndPublishModal from '../WorkflowComponents/Flow/components/SaveAndPublish';
|
||||
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useDebounceEffect } from 'ahooks';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import SaveButton from './components/SaveButton';
|
||||
|
||||
const PublishHistories = dynamic(() => import('../WorkflowPublishHistoriesSlider'));
|
||||
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -48,12 +46,6 @@ const Header = () => {
|
||||
onOpen: onOpenBackConfirm,
|
||||
onClose: onCloseBackConfirm
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isSaveAndPublishModalOpen,
|
||||
onOpen: onSaveAndPublishModalOpen,
|
||||
onClose: onSaveAndPublishModalClose
|
||||
} = useDisclosure();
|
||||
const [isSave, setIsSave] = useState(false);
|
||||
|
||||
const {
|
||||
flowData2StoreData,
|
||||
@@ -65,7 +57,9 @@ const Header = () => {
|
||||
edges,
|
||||
past,
|
||||
future,
|
||||
setPast
|
||||
setPast,
|
||||
saveSnapshot,
|
||||
resetSnapshot
|
||||
} = useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
const { lastAppListRouteType } = useSystemStore();
|
||||
@@ -227,81 +221,11 @@ const Header = () => {
|
||||
{t('common:core.workflow.Run')}
|
||||
</Button>
|
||||
{!historiesDefaultData && (
|
||||
<MyPopover
|
||||
placement={'bottom-end'}
|
||||
hasArrow={false}
|
||||
offset={[2, 4]}
|
||||
w={'116px'}
|
||||
onOpenFunc={() => setIsSave(true)}
|
||||
onCloseFunc={() => setIsSave(false)}
|
||||
trigger={'hover'}
|
||||
Trigger={
|
||||
<Button
|
||||
size={'sm'}
|
||||
rightIcon={
|
||||
<MyIcon
|
||||
name={isSave ? 'core/chat/chevronUp' : 'core/chat/chevronDown'}
|
||||
w={['14px', '16px']}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Box>{t('common:common.Save')}</Box>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{({ onClose }) => (
|
||||
<MyBox p={1.5}>
|
||||
<MyBox
|
||||
display={'flex'}
|
||||
size={'md'}
|
||||
px={1}
|
||||
py={1.5}
|
||||
rounded={'4px'}
|
||||
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
isLoading={loading}
|
||||
onClick={async () => {
|
||||
await onClickSave({});
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('app:saved_success'),
|
||||
position: 'top-right'
|
||||
});
|
||||
onClose();
|
||||
setIsSave(false);
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'core/workflow/upload'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('common:core.workflow.Save to cloud')}</Box>
|
||||
</MyBox>
|
||||
<Flex
|
||||
px={1}
|
||||
py={1.5}
|
||||
rounded={'4px'}
|
||||
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
const data = flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
onSaveAndPublishModalOpen();
|
||||
}
|
||||
onClose();
|
||||
setIsSave(false);
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'core/workflow/publish'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('common:core.workflow.Save and publish')}</Box>
|
||||
{isSaveAndPublishModalOpen && (
|
||||
<SaveAndPublishModal
|
||||
isLoading={loading}
|
||||
onClose={onSaveAndPublishModalClose}
|
||||
onClickSave={onClickSave}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</MyBox>
|
||||
)}
|
||||
</MyPopover>
|
||||
<SaveButton
|
||||
isLoading={loading}
|
||||
onClickSave={onClickSave}
|
||||
checkData={flowData2StoreDataAndCheck}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
)}
|
||||
@@ -311,8 +235,12 @@ const Header = () => {
|
||||
onClose={() => {
|
||||
setHistoriesDefaultData(undefined);
|
||||
}}
|
||||
past={past}
|
||||
saveSnapshot={saveSnapshot}
|
||||
resetSnapshot={resetSnapshot}
|
||||
/>
|
||||
)}
|
||||
|
||||
<MyModal
|
||||
isOpen={isOpenBackConfirm}
|
||||
onClose={onCloseBackConfirm}
|
||||
@@ -358,16 +286,13 @@ const Header = () => {
|
||||
loading,
|
||||
isV2Workflow,
|
||||
historiesDefaultData,
|
||||
isSave,
|
||||
onClickSave,
|
||||
setHistoriesDefaultData,
|
||||
appDetail.chatConfig,
|
||||
flowData2StoreDataAndCheck,
|
||||
setWorkflowTestData,
|
||||
isSaveAndPublishModalOpen,
|
||||
onSaveAndPublishModalClose,
|
||||
toast,
|
||||
onSaveAndPublishModalOpen
|
||||
past
|
||||
]);
|
||||
|
||||
return Render;
|
||||
|
@@ -0,0 +1,115 @@
|
||||
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import React, { useState } from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import SaveAndPublishModal from '../../WorkflowComponents/Flow/components/SaveAndPublish';
|
||||
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
|
||||
const SaveButton = ({
|
||||
isLoading,
|
||||
onClickSave,
|
||||
checkData
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
onClickSave: (options: { isPublish?: boolean; versionName?: string }) => Promise<void>;
|
||||
checkData?: (hideTip?: boolean) =>
|
||||
| {
|
||||
nodes: StoreNodeItemType[];
|
||||
edges: StoreEdgeItemType[];
|
||||
}
|
||||
| undefined;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [isSave, setIsSave] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const {
|
||||
isOpen: isSaveAndPublishModalOpen,
|
||||
onOpen: onSaveAndPublishModalOpen,
|
||||
onClose: onSaveAndPublishModalClose
|
||||
} = useDisclosure();
|
||||
|
||||
return (
|
||||
<MyPopover
|
||||
placement={'bottom-end'}
|
||||
hasArrow={false}
|
||||
offset={[2, 4]}
|
||||
w={'116px'}
|
||||
onOpenFunc={() => setIsSave(true)}
|
||||
onCloseFunc={() => setIsSave(false)}
|
||||
trigger={'hover'}
|
||||
Trigger={
|
||||
<Button
|
||||
size={'sm'}
|
||||
rightIcon={
|
||||
<MyIcon
|
||||
name={isSave ? 'core/chat/chevronUp' : 'core/chat/chevronDown'}
|
||||
w={['14px', '16px']}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Box>{t('common:common.Save')}</Box>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{({ onClose }) => (
|
||||
<Box p={1.5}>
|
||||
<MyBox
|
||||
display={'flex'}
|
||||
size={'md'}
|
||||
px={1}
|
||||
py={1.5}
|
||||
rounded={'4px'}
|
||||
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
await onClickSave({});
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('app:saved_success'),
|
||||
position: 'top-right'
|
||||
});
|
||||
onClose();
|
||||
setIsSave(false);
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'core/workflow/upload'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('common:core.workflow.Save to cloud')}</Box>
|
||||
</MyBox>
|
||||
<Flex
|
||||
px={1}
|
||||
py={1.5}
|
||||
rounded={'4px'}
|
||||
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
const canOpen = !checkData || checkData();
|
||||
if (canOpen) {
|
||||
onSaveAndPublishModalOpen();
|
||||
}
|
||||
onClose();
|
||||
setIsSave(false);
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'core/workflow/publish'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('common:core.workflow.Save and publish')}</Box>
|
||||
{isSaveAndPublishModalOpen && (
|
||||
<SaveAndPublishModal
|
||||
isLoading={isLoading}
|
||||
onClose={onSaveAndPublishModalClose}
|
||||
onClickSave={onClickSave}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</MyPopover>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SaveButton);
|
@@ -39,7 +39,7 @@ import { defaultRunningStatus } from './constants';
|
||||
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
|
||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
|
||||
import { AppChatConfigType, AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import { AppContext } from '@/pages/app/detail/components/context';
|
||||
import ChatTest from './Flow/ChatTest';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
@@ -47,7 +47,6 @@ import { uiWorkflow2StoreWorkflow } from './utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { formatTime2YMDHMS, formatTime2YMDHMW } from '@fastgpt/global/common/string/time';
|
||||
import type { InitProps } from '@/pages/app/detail/components/PublishHistoriesSlider';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { SetState } from 'ahooks/lib/createUseStorageState';
|
||||
|
||||
@@ -60,6 +59,18 @@ export type SnapshotsType = {
|
||||
chatConfig: AppChatConfigType;
|
||||
isSaved?: boolean;
|
||||
};
|
||||
export type SaveSnapshotParams = {
|
||||
pastNodes?: Node[];
|
||||
pastEdges?: Edge[];
|
||||
customTitle?: string;
|
||||
chatConfig?: AppChatConfigType;
|
||||
};
|
||||
export type InitProps = {
|
||||
nodes: AppSchema['modules'];
|
||||
edges: AppSchema['edges'];
|
||||
chatConfig: AppSchema['chatConfig'];
|
||||
};
|
||||
|
||||
type WorkflowContextType = {
|
||||
appId?: string;
|
||||
basicNodeTemplates: FlowNodeTemplateType[];
|
||||
|
@@ -1,351 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
getAppVersionDetail,
|
||||
getWorkflowVersionList,
|
||||
updateAppVersion
|
||||
} from '@/web/core/app/api/version';
|
||||
import { useVirtualScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Button, Flex, Input } from '@chakra-ui/react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from './context';
|
||||
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
|
||||
import { WorkflowContext } from './WorkflowComponents/context';
|
||||
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import Tag from '@fastgpt/web/components/common/Tag';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { storeEdgesRenderEdge, storeNode2FlowNode } from '@/web/core/workflow/utils';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { versionListResponse } from '@/pages/api/core/app/version/listWorkflow';
|
||||
|
||||
const WorkflowPublishHistoriesSlider = ({ onClose }: { onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const [currentTab, setCurrentTab] = useState<'myEdit' | 'teamCloud'>('myEdit');
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomRightDrawer
|
||||
onClose={() => onClose()}
|
||||
title={
|
||||
(
|
||||
<>
|
||||
<LightRowTabs
|
||||
list={[
|
||||
{ label: t('workflow:workflow.My edit'), value: 'myEdit' },
|
||||
{ label: t('workflow:workflow.Team cloud'), value: 'teamCloud' }
|
||||
]}
|
||||
value={currentTab}
|
||||
onChange={setCurrentTab}
|
||||
inlineStyles={{ px: 0.5, pb: 2 }}
|
||||
gap={5}
|
||||
py={0}
|
||||
fontSize={'sm'}
|
||||
/>
|
||||
</>
|
||||
) as any
|
||||
}
|
||||
maxW={'340px'}
|
||||
px={0}
|
||||
showMask={false}
|
||||
overflow={'unset'}
|
||||
>
|
||||
{currentTab === 'myEdit' ? <MyEdit /> : <TeamCloud />}
|
||||
</CustomRightDrawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(WorkflowPublishHistoriesSlider);
|
||||
|
||||
const MyEdit = () => {
|
||||
const { past, saveSnapshot, resetSnapshot } = useContextSelector(WorkflowContext, (v) => v);
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
<Flex px={5} flex={'1 0 0'} flexDirection={'column'}>
|
||||
{past.length > 0 && (
|
||||
<Box py={2} px={3}>
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
w={'full'}
|
||||
h={'30px'}
|
||||
onClick={async () => {
|
||||
const initialSnapshot = past[past.length - 1];
|
||||
|
||||
const res = await saveSnapshot({
|
||||
pastNodes: initialSnapshot.nodes,
|
||||
pastEdges: initialSnapshot.edges,
|
||||
chatConfig: initialSnapshot.chatConfig,
|
||||
customTitle: t(`app:app.version_initial_copy`)
|
||||
});
|
||||
|
||||
if (res) {
|
||||
resetSnapshot(initialSnapshot);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t('workflow:workflow.Switch_success'),
|
||||
status: 'success'
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('app:app.version_back')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Flex flex={'1 0 0'} flexDirection={'column'} overflow={'auto'}>
|
||||
{past.map((item, index) => {
|
||||
return (
|
||||
<Flex
|
||||
key={index}
|
||||
alignItems={'center'}
|
||||
py={2}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
fontWeight={500}
|
||||
_hover={{
|
||||
bg: 'primary.50'
|
||||
}}
|
||||
onClick={async () => {
|
||||
const res = await saveSnapshot({
|
||||
pastNodes: item.nodes,
|
||||
pastEdges: item.edges,
|
||||
chatConfig: item.chatConfig,
|
||||
customTitle: `${t('app:app.version_copy')}-${item.title}`
|
||||
});
|
||||
if (res) {
|
||||
resetSnapshot(item);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t('workflow:workflow.Switch_success'),
|
||||
status: 'success'
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
w={'12px'}
|
||||
h={'12px'}
|
||||
borderWidth={'2px'}
|
||||
borderColor={'primary.600'}
|
||||
borderRadius={'50%'}
|
||||
position={'relative'}
|
||||
{...(index !== past.length - 1 && {
|
||||
_after: {
|
||||
content: '""',
|
||||
height: '26px',
|
||||
width: '2px',
|
||||
bgColor: 'myGray.250',
|
||||
position: 'absolute',
|
||||
top: '10px',
|
||||
left: '3px'
|
||||
}
|
||||
})}
|
||||
></Box>
|
||||
<Box
|
||||
ml={3}
|
||||
flex={'1 0 0'}
|
||||
fontSize={'sm'}
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
color={'myGray.900'}
|
||||
>
|
||||
{item.title}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
<Box py={2} textAlign={'center'} color={'myGray.600'} fontSize={'xs'}>
|
||||
{t('common:common.No more data')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const TeamCloud = () => {
|
||||
const { t } = useTranslation();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const { saveSnapshot, resetSnapshot } = useContextSelector(WorkflowContext, (v) => v);
|
||||
const { loadAndGetTeamMembers } = useUserStore();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const { scrollDataList, ScrollList, isLoading, fetchData } = useVirtualScrollPagination(
|
||||
getWorkflowVersionList,
|
||||
{
|
||||
itemHeight: 40,
|
||||
overscan: 20,
|
||||
|
||||
pageSize: 30,
|
||||
defaultParams: {
|
||||
appId: appDetail._id
|
||||
}
|
||||
}
|
||||
);
|
||||
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
|
||||
manual: !feConfigs.isPlus
|
||||
});
|
||||
const [editIndex, setEditIndex] = useState<number | undefined>(undefined);
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(undefined);
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const { runAsync: onChangeVersion, loading: isLoadingVersion } = useRequest2(
|
||||
async (versionItem: versionListResponse) => {
|
||||
const versionDetail = await getAppVersionDetail(versionItem._id, versionItem.appId);
|
||||
|
||||
if (!versionDetail) return;
|
||||
|
||||
const state = {
|
||||
nodes: versionDetail.nodes?.map((item) => storeNode2FlowNode({ item, t })),
|
||||
edges: versionDetail.edges?.map((item) => storeEdgesRenderEdge({ edge: item })),
|
||||
title: versionItem.versionName,
|
||||
chatConfig: versionDetail.chatConfig
|
||||
};
|
||||
|
||||
await saveSnapshot({
|
||||
pastNodes: state.nodes,
|
||||
pastEdges: state.edges,
|
||||
chatConfig: state.chatConfig,
|
||||
customTitle: `${t('app:app.version_copy')}-${state.title}`
|
||||
});
|
||||
|
||||
resetSnapshot(state);
|
||||
toast({
|
||||
title: t('workflow:workflow.Switch_success'),
|
||||
status: 'success'
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollList isLoading={isLoading || isLoadingVersion} flex={'1 0 0'} px={5}>
|
||||
{scrollDataList.map((data, index) => {
|
||||
const item = data.data;
|
||||
const firstPublishedIndex = scrollDataList.findIndex((data) => data.data.isPublish);
|
||||
const tmb = members.find((member) => member.tmbId === item.tmbId);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
key={data.index}
|
||||
alignItems={'center'}
|
||||
py={editIndex !== index ? 2 : 1}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
fontWeight={500}
|
||||
onMouseEnter={() => setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(undefined)}
|
||||
_hover={{
|
||||
bg: 'primary.50'
|
||||
}}
|
||||
onClick={() => editIndex === undefined && onChangeVersion(item)}
|
||||
>
|
||||
<MyPopover
|
||||
trigger="hover"
|
||||
placement={'bottom-end'}
|
||||
w={'208px'}
|
||||
h={'72px'}
|
||||
Trigger={
|
||||
<Box>
|
||||
<Avatar src={tmb?.avatar} borderRadius={'50%'} w={'24px'} h={'24px'} />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{({ onClose }) => (
|
||||
<Flex alignItems={'center'} h={'full'} pl={5} gap={3}>
|
||||
<Box>
|
||||
<Avatar src={tmb?.avatar} borderRadius={'50%'} w={'36px'} h={'36px'} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Box fontSize={'14px'} color={'myGray.900'}>
|
||||
{tmb?.memberName}
|
||||
</Box>
|
||||
<Box fontSize={'12px'} color={'myGray.500'}>
|
||||
{formatTime2YMDHMS(item.time)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</MyPopover>
|
||||
{editIndex !== index ? (
|
||||
<>
|
||||
<Box
|
||||
ml={3}
|
||||
flex={'1 0 0'}
|
||||
fontSize={'sm'}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
<Box minWidth={0} overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
|
||||
<Box as={'span'} color={'myGray.900'}>
|
||||
{item.versionName || formatTime2YMDHMS(item.time)}
|
||||
</Box>
|
||||
</Box>
|
||||
{item.isPublish && (
|
||||
<Tag
|
||||
ml={3}
|
||||
flexShrink={0}
|
||||
type="borderSolid"
|
||||
colorSchema={index === firstPublishedIndex ? 'green' : 'blue'}
|
||||
>
|
||||
{index === firstPublishedIndex
|
||||
? t('app:app.version_current')
|
||||
: t('app:app.version_past')}
|
||||
</Tag>
|
||||
)}
|
||||
</Box>
|
||||
{hoveredIndex === index && (
|
||||
<MyIcon
|
||||
name="edit"
|
||||
w={'18px'}
|
||||
ml={2}
|
||||
_hover={{ color: 'primary.600' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditIndex(index);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<MyBox ml={3} isLoading={isEditing} size={'md'}>
|
||||
<Input
|
||||
autoFocus
|
||||
h={8}
|
||||
defaultValue={item.versionName || formatTime2YMDHMS(item.time)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onBlur={async (e) => {
|
||||
setIsEditing(true);
|
||||
await updateAppVersion({
|
||||
appId: item.appId,
|
||||
versionName: e.target.value,
|
||||
versionId: item._id
|
||||
});
|
||||
await fetchData();
|
||||
setEditIndex(undefined);
|
||||
setIsEditing(false);
|
||||
}}
|
||||
/>
|
||||
</MyBox>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</ScrollList>
|
||||
);
|
||||
};
|
@@ -1,4 +1,4 @@
|
||||
import { Dispatch, ReactNode, SetStateAction, useCallback, useState } from 'react';
|
||||
import { Dispatch, ReactNode, SetStateAction, useCallback, useEffect, 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';
|
||||
@@ -11,7 +11,6 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import type { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
|
||||
|
@@ -15,8 +15,8 @@ export const getAppLatestVersion = (data: getLatestVersionQuery) =>
|
||||
export const postPublishApp = (appId: string, data: PostPublishAppProps) =>
|
||||
POST(`/core/app/version/publish?appId=${appId}`, data);
|
||||
|
||||
export const getPublishList = (data: PaginationProps<{ appId: string }>) =>
|
||||
POST<PaginationResponse<AppVersionSchemaType>>('/core/app/version/list', data);
|
||||
// export const getPublishList = (data: PaginationProps<{ appId: string }>) =>
|
||||
// POST<PaginationResponse<AppVersionSchemaType>>('/core/app/version/list', data);
|
||||
|
||||
export const getWorkflowVersionList = (data: PaginationProps<{ appId: string }>) =>
|
||||
POST<PaginationResponse<versionListResponse>>('/core/app/version/listWorkflow', data);
|
||||
@@ -24,8 +24,8 @@ export const getWorkflowVersionList = (data: PaginationProps<{ appId: string }>)
|
||||
export const getAppVersionDetail = (versionId: string, appId: string) =>
|
||||
GET<AppVersionSchemaType>(`/core/app/version/detail?versionId=${versionId}&appId=${appId}`);
|
||||
|
||||
export const postRevertVersion = (appId: string, data: PostRevertAppProps) =>
|
||||
POST(`/core/app/version/revert?appId=${appId}`, data);
|
||||
// export const postRevertVersion = (appId: string, data: PostRevertAppProps) =>
|
||||
// POST(`/core/app/version/revert?appId=${appId}`, data);
|
||||
|
||||
export const updateAppVersion = (data: UpdateAppVersionBody) =>
|
||||
POST(`/core/app/version/update`, data);
|
||||
|
@@ -438,94 +438,6 @@ export const getLatestNodeTemplate = (
|
||||
return updatedNode;
|
||||
};
|
||||
|
||||
type WorkflowType = {
|
||||
nodes: StoreNodeItemType[];
|
||||
edges: StoreEdgeItemType[];
|
||||
chatConfig: AppChatConfigType;
|
||||
};
|
||||
export const compareWorkflow = (workflow1: WorkflowType, workflow2: WorkflowType) => {
|
||||
const clone1 = cloneDeep(workflow1);
|
||||
const clone2 = cloneDeep(workflow2);
|
||||
|
||||
if (!isEqual(clone1.edges, clone2.edges)) {
|
||||
console.log('Edge not equal');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
clone1.chatConfig &&
|
||||
clone2.chatConfig &&
|
||||
!isEqual(
|
||||
{
|
||||
welcomeText: clone1.chatConfig?.welcomeText || '',
|
||||
variables: clone1.chatConfig?.variables || [],
|
||||
questionGuide: clone1.chatConfig?.questionGuide || false,
|
||||
ttsConfig: clone1.chatConfig?.ttsConfig || undefined,
|
||||
whisperConfig: clone1.chatConfig?.whisperConfig || undefined,
|
||||
scheduledTriggerConfig: clone1.chatConfig?.scheduledTriggerConfig || undefined,
|
||||
chatInputGuide: clone1.chatConfig?.chatInputGuide || undefined,
|
||||
fileSelectConfig: clone1.chatConfig?.fileSelectConfig || undefined
|
||||
},
|
||||
{
|
||||
welcomeText: clone2.chatConfig?.welcomeText || '',
|
||||
variables: clone2.chatConfig?.variables || [],
|
||||
questionGuide: clone2.chatConfig?.questionGuide || false,
|
||||
ttsConfig: clone2.chatConfig?.ttsConfig || undefined,
|
||||
whisperConfig: clone2.chatConfig?.whisperConfig || undefined,
|
||||
scheduledTriggerConfig: clone2.chatConfig?.scheduledTriggerConfig || undefined,
|
||||
chatInputGuide: clone2.chatConfig?.chatInputGuide || undefined,
|
||||
fileSelectConfig: clone2.chatConfig?.fileSelectConfig || undefined
|
||||
}
|
||||
)
|
||||
) {
|
||||
console.log('chatConfig not equal');
|
||||
return false;
|
||||
}
|
||||
|
||||
const formatNodes = (nodes: StoreNodeItemType[]) => {
|
||||
return nodes
|
||||
.filter((node) => {
|
||||
if (!node) return;
|
||||
if ([FlowNodeTypeEnum.systemConfig].includes(node.flowNodeType)) return;
|
||||
|
||||
return true;
|
||||
})
|
||||
.map((node) => ({
|
||||
flowNodeType: node.flowNodeType,
|
||||
inputs: node.inputs.map((input) => ({
|
||||
key: input.key,
|
||||
selectedTypeIndex: input.selectedTypeIndex ?? 0,
|
||||
renderTypeLis: input.renderTypeList,
|
||||
valueType: input.valueType,
|
||||
value: input.value ?? undefined
|
||||
})),
|
||||
outputs: node.outputs.map((item) => ({
|
||||
key: item.key,
|
||||
type: item.type,
|
||||
value: item.value ?? undefined
|
||||
})),
|
||||
name: node.name,
|
||||
intro: node.intro,
|
||||
avatar: node.avatar,
|
||||
version: node.version,
|
||||
position: node.position
|
||||
}));
|
||||
};
|
||||
const node1 = formatNodes(clone1.nodes);
|
||||
const node2 = formatNodes(clone2.nodes);
|
||||
|
||||
// console.log(node1);
|
||||
// console.log(node2);
|
||||
|
||||
node1.forEach((node, i) => {
|
||||
if (!isEqual(node, node2[i])) {
|
||||
console.log('node not equal');
|
||||
}
|
||||
});
|
||||
|
||||
return isEqual(node1, node2);
|
||||
};
|
||||
|
||||
export const compareSnapshot = (
|
||||
snapshot1: {
|
||||
nodes: Node<FlowNodeItemType, string | undefined>[] | undefined;
|
||||
|
Reference in New Issue
Block a user