From 3248e95d5371a9fba708f847735d94e48ca292a5 Mon Sep 17 00:00:00 2001 From: heheer Date: Sat, 24 Aug 2024 23:15:28 +0800 Subject: [PATCH] fix: undo & redo (#2493) * fix: undo & redo * fix * fix --- .../common/MyDrawer/CustomRightDrawer.tsx | 3 +- packages/web/i18n/en/app.json | 1 + packages/web/i18n/en/workflow.json | 1 + packages/web/i18n/zh/app.json | 1 + packages/web/i18n/zh/workflow.json | 1 + .../src/pages/api/core/app/version/detail.ts | 22 ++++++ .../api/core/app/version/listWorkflow.tsx | 56 +++++++++++++++ .../app/detail/components/Plugin/Header.tsx | 21 ++++-- .../components/PublishHistoriesSlider.tsx | 1 - .../detail/components/SimpleApp/Header.tsx | 8 ++- .../app/detail/components/Workflow/Header.tsx | 17 +++-- .../Flow/components/FlowController.tsx | 6 +- .../Flow/components/SaveAndPublish.tsx | 6 ++ .../components/WorkflowComponents/context.tsx | 25 ++++--- .../WorkflowPublishHistoriesSlider.tsx | 71 ++++++++++++------- .../pages/app/detail/components/context.tsx | 26 +++---- projects/app/src/web/core/app/api/version.ts | 7 ++ 17 files changed, 207 insertions(+), 66 deletions(-) create mode 100644 projects/app/src/pages/api/core/app/version/detail.ts create mode 100644 projects/app/src/pages/api/core/app/version/listWorkflow.tsx diff --git a/packages/web/components/common/MyDrawer/CustomRightDrawer.tsx b/packages/web/components/common/MyDrawer/CustomRightDrawer.tsx index 14d2dceb1..8dc07d3d9 100644 --- a/packages/web/components/common/MyDrawer/CustomRightDrawer.tsx +++ b/packages/web/components/common/MyDrawer/CustomRightDrawer.tsx @@ -31,7 +31,8 @@ const CustomRightDrawer = ({ zIndex={100} maxW={maxW} w={'100%'} - h={'90vh'} + top={'60px'} + bottom={0} borderLeftRadius={'lg'} border={'base'} boxShadow={'2'} diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index eaa6aa68b..7cf92e8bb 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -92,6 +92,7 @@ "plugin_dispatch_tip": "It is up to the model to decide which plug-ins to add additional capabilities to. If the plug-in is selected, the knowledge base call is also treated as a special plug-in.", "publish_channel": "Publish channel", "publish_success": "Publish success", + "saved_success": "Saved success", "search_app": "Search app", "setting_app": "Settings", "setting_plugin": "Setting plugin", diff --git a/packages/web/i18n/en/workflow.json b/packages/web/i18n/en/workflow.json index 2e73d841e..4a897efca 100644 --- a/packages/web/i18n/en/workflow.json +++ b/packages/web/i18n/en/workflow.json @@ -49,6 +49,7 @@ "workflow": { "Back_to_current_version": "Back to current version", "My edit": "My edit", + "Switch_failed": "Switch failed", "Switch_success": "switch successfully", "Team cloud": "Team cloud", "exit_tips": "Your changes have not been saved. Exiting directly will not save your edits." diff --git a/packages/web/i18n/zh/app.json b/packages/web/i18n/zh/app.json index 39d838562..31b1c73b8 100644 --- a/packages/web/i18n/zh/app.json +++ b/packages/web/i18n/zh/app.json @@ -91,6 +91,7 @@ "plugin_dispatch_tip": "给模型附加额外的能力,具体调用哪些插件,将由模型自主决定。\n若选择了插件,知识库调用将自动作为一个特殊的插件。", "publish_channel": "发布渠道", "publish_success": "发布成功", + "saved_success": "保存成功", "search_app": "搜索应用", "setting_app": "应用配置", "setting_plugin": "插件配置", diff --git a/packages/web/i18n/zh/workflow.json b/packages/web/i18n/zh/workflow.json index 3e4ae5b8f..7dab50110 100644 --- a/packages/web/i18n/zh/workflow.json +++ b/packages/web/i18n/zh/workflow.json @@ -49,6 +49,7 @@ "workflow": { "Back_to_current_version": "回到初始状态", "My edit": "我的编辑", + "Switch_failed": "切换失败", "Switch_success": "切换成功", "Team cloud": "团队云端", "exit_tips": "您的更改尚未保存,「直接退出」将不会保存您的编辑记录。" diff --git a/projects/app/src/pages/api/core/app/version/detail.ts b/projects/app/src/pages/api/core/app/version/detail.ts new file mode 100644 index 000000000..fdd2509a4 --- /dev/null +++ b/projects/app/src/pages/api/core/app/version/detail.ts @@ -0,0 +1,22 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { NextAPI } from '@/service/middleware/entry'; +import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema'; +import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type'; +import { authApp } from '@fastgpt/service/support/permission/app/auth'; +import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; + +type Props = { + versionId: string; + appId: string; +}; + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { versionId, appId } = req.query as Props; + + await authApp({ req, authToken: true, appId, per: ReadPermissionVal }); + const result = await MongoAppVersion.findById(versionId); + + return result; +} + +export default NextAPI(handler); diff --git a/projects/app/src/pages/api/core/app/version/listWorkflow.tsx b/projects/app/src/pages/api/core/app/version/listWorkflow.tsx new file mode 100644 index 000000000..f02955513 --- /dev/null +++ b/projects/app/src/pages/api/core/app/version/listWorkflow.tsx @@ -0,0 +1,56 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { NextAPI } from '@/service/middleware/entry'; +import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema'; +import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type'; + +type Props = PaginationProps<{ + appId: string; +}>; + +export type versionListResponse = { + _id: string; + appId: string; + versionName: string; + time: Date; + isPublish: boolean | undefined; + tmbId: string; +}; + +type Response = PaginationResponse; + +async function handler(req: NextApiRequest, res: NextApiResponse): Promise { + const { current, pageSize, appId } = req.body as Props; + + const [result, total] = await Promise.all([ + MongoAppVersion.find( + { + appId + }, + '_id appId versionName time isPublish tmbId' + ) + .sort({ + time: -1 + }) + .skip((current - 1) * pageSize) + .limit(pageSize), + MongoAppVersion.countDocuments({ appId }) + ]); + + const versionList = result.map((item) => { + return { + _id: item._id, + appId: item.appId, + versionName: item.versionName, + time: item.time, + isPublish: item.isPublish, + tmbId: item.tmbId + }; + }); + + return { + total, + list: versionList + }; +} + +export default NextAPI(handler); diff --git a/projects/app/src/pages/app/detail/components/Plugin/Header.tsx b/projects/app/src/pages/app/detail/components/Plugin/Header.tsx index 3448ca0a4..8d4c0361b 100644 --- a/projects/app/src/pages/app/detail/components/Plugin/Header.tsx +++ b/projects/app/src/pages/app/detail/components/Plugin/Header.tsx @@ -29,6 +29,7 @@ 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'; const PublishHistories = dynamic(() => import('../WorkflowPublishHistoriesSlider')); @@ -36,6 +37,7 @@ const Header = () => { const { t } = useTranslation(); const { isPc } = useSystem(); const router = useRouter(); + const { toast } = useToast(); const { appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v); const isV2Workflow = appDetail?.version === 'v2'; @@ -61,6 +63,9 @@ const Header = () => { } = useContextSelector(WorkflowContext, (v) => v); const isPublished = useMemo(() => { + /* + Find the last saved snapshot in the past and future snapshots + */ const savedSnapshot = future.findLast((snapshot) => snapshot.isSaved) || past.find((snapshot) => snapshot.isSaved); @@ -97,9 +102,10 @@ const Header = () => { //@ts-ignore version: 'v2' }); + // Mark the current snapshot as saved setPast((prevPast) => prevPast.map((item, index) => - index === prevPast.length - 1 + index === 0 ? { ...item, isSaved: true @@ -171,6 +177,7 @@ const Header = () => { isLoading={loading} onClick={async () => { await onClickSave({}); + onClose(); onBack(); }} > @@ -246,7 +253,7 @@ const Header = () => { } > - {({}) => ( + {({ onClose }) => ( { isLoading={loading} onClick={async () => { await onClickSave({}); + toast({ + status: 'success', + title: t('app:saved_success') + }); }} > @@ -275,6 +286,7 @@ const Header = () => { if (data) { onSaveAndPublishModalOpen(); } + onClose(); }} > @@ -307,6 +319,8 @@ const Header = () => { isPc, currentTab, isPublished, + onBack, + onOpen, isOpen, onClose, t, @@ -314,8 +328,6 @@ const Header = () => { isV2Workflow, historiesDefaultData, isSave, - onBack, - onOpen, onClickSave, setHistoriesDefaultData, appDetail.chatConfig, @@ -323,6 +335,7 @@ const Header = () => { setWorkflowTestData, isSaveAndPublishModalOpen, onSaveAndPublishModalClose, + toast, onSaveAndPublishModalOpen ]); diff --git a/projects/app/src/pages/app/detail/components/PublishHistoriesSlider.tsx b/projects/app/src/pages/app/detail/components/PublishHistoriesSlider.tsx index 7e8043cd1..d9935cb19 100644 --- a/projects/app/src/pages/app/detail/components/PublishHistoriesSlider.tsx +++ b/projects/app/src/pages/app/detail/components/PublishHistoriesSlider.tsx @@ -113,7 +113,6 @@ const PublishHistoriesSlider = ({ maxW={'300px'} px={0} showMask={false} - top={'60px'} overflow={'unset'} > } > - {({}) => ( + {({ onClose }) => ( { isLoading={loading} onClick={async () => { await onClickSave({}); + toast({ + status: 'success', + title: t('app:saved_success') + }); }} > @@ -279,6 +286,7 @@ const Header = () => { if (data) { onSaveAndPublishModalOpen(); } + onClose(); }} > @@ -311,6 +319,8 @@ const Header = () => { isPc, currentTab, isPublished, + onBack, + onOpen, isOpen, onClose, t, @@ -318,8 +328,6 @@ const Header = () => { isV2Workflow, historiesDefaultData, isSave, - onBack, - onOpen, onClickSave, setHistoriesDefaultData, appDetail.chatConfig, @@ -327,6 +335,7 @@ const Header = () => { setWorkflowTestData, isSaveAndPublishModalOpen, onSaveAndPublishModalClose, + toast, onSaveAndPublishModalOpen ]); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/FlowController.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/FlowController.tsx index bd13ee06d..bbbfdef55 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/FlowController.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/FlowController.tsx @@ -17,7 +17,11 @@ const FlowController = React.memo(function FlowController() { useEffect(() => { const keyDownHandler = (event: KeyboardEvent) => { - if (event.key === 'z' && (event.ctrlKey || event.metaKey) && event.shiftKey) { + if ( + (event.key === 'z' || event.key === 'Z') && + (event.ctrlKey || event.metaKey) && + event.shiftKey + ) { event.preventDefault(); redo(); } else if (event.key === 'z' && (event.ctrlKey || event.metaKey)) { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/SaveAndPublish.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/SaveAndPublish.tsx index a77386b5c..679fc9f5c 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/SaveAndPublish.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/SaveAndPublish.tsx @@ -1,5 +1,6 @@ import { Box, Button, Input, ModalBody, ModalFooter } from '@chakra-ui/react'; import MyModal from '@fastgpt/web/components/common/MyModal'; +import { useToast } from '@fastgpt/web/hooks/useToast'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; type FormType = { @@ -17,6 +18,7 @@ const SaveAndPublishModal = ({ onClickSave: (data: { isPublish: boolean; versionName: string }) => Promise; }) => { const { t } = useTranslation(); + const { toast } = useToast(); const { register, handleSubmit } = useForm({ defaultValues: { versionName: '', @@ -61,6 +63,10 @@ const SaveAndPublishModal = ({ isLoading={isLoading} onClick={handleSubmit(async (data) => { await onClickSave({ ...data, isPublish: true }); + toast({ + status: 'success', + title: t('app:publish_success') + }); onClose(); })} > diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx index c213ed9eb..8ab98998e 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx @@ -15,7 +15,7 @@ import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/wor import { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe'; import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; import { useToast } from '@fastgpt/web/hooks/useToast'; -import { useLocalStorageState, useMemoizedFn, useUpdateEffect } from 'ahooks'; +import { useDebounceEffect, useLocalStorageState, useMemoizedFn, useUpdateEffect } from 'ahooks'; import React, { Dispatch, SetStateAction, @@ -900,20 +900,23 @@ const WorkflowContextProvider = ({ return true; }, { - debounceWait: 500, refreshDeps: [nodes, edges, appDetail.chatConfig, past] } ); - useEffect(() => { - if (!nodes.length) return; - saveSnapshot({ - pastNodes: nodes, - pastEdges: edges, - customTitle: formatTime2YMDHMS(new Date()), - chatConfig: appDetail.chatConfig - }); - }, [nodes, edges, appDetail.chatConfig]); + useDebounceEffect( + () => { + if (!nodes.length) return; + saveSnapshot({ + pastNodes: nodes, + pastEdges: edges, + customTitle: formatTime2YMDHMS(new Date()), + chatConfig: appDetail.chatConfig + }); + }, + [nodes, edges, appDetail.chatConfig], + { wait: 500 } + ); const undo = useCallback(() => { if (past[1]) { diff --git a/projects/app/src/pages/app/detail/components/WorkflowPublishHistoriesSlider.tsx b/projects/app/src/pages/app/detail/components/WorkflowPublishHistoriesSlider.tsx index de4832e5a..96922cef3 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowPublishHistoriesSlider.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowPublishHistoriesSlider.tsx @@ -1,5 +1,9 @@ import React, { useState } from 'react'; -import { getPublishList, updateAppVersion } from '@/web/core/app/api/version'; +import { + getAppVersionDetail, + getWorkflowVersionList, + updateAppVersion +} from '@/web/core/app/api/version'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer'; import { useTranslation } from 'next-i18next'; @@ -19,15 +23,18 @@ 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'); + const [isLoading, setIsLoading] = useState(false); return ( <> onClose()} + isLoading={isLoading} title={ ( <> @@ -49,10 +56,9 @@ const WorkflowPublishHistoriesSlider = ({ onClose }: { onClose: () => void }) => maxW={'340px'} px={0} showMask={false} - top={'60px'} overflow={'unset'} > - {currentTab === 'myEdit' ? : } + {currentTab === 'myEdit' ? : } ); @@ -163,14 +169,14 @@ const MyEdit = () => { ); }; -const TeamCloud = () => { +const TeamCloud = ({ setIsLoading }: { setIsLoading: (value: boolean) => void }) => { const { t } = useTranslation(); const { appDetail } = useContextSelector(AppContext, (v) => v); const { saveSnapshot, resetSnapshot } = useContextSelector(WorkflowContext, (v) => v); const { loadAndGetTeamMembers } = useUserStore(); const { feConfigs } = useSystemStore(); - const { list, ScrollList, isLoading, fetchData } = useScrollPagination(getPublishList, { + const { list, ScrollList, isLoading, fetchData } = useScrollPagination(getWorkflowVersionList, { itemHeight: 40, overscan: 20, @@ -188,6 +194,37 @@ const TeamCloud = () => { const [isEditing, setIsEditing] = useState(false); const { toast } = useToast(); + const onChangeVersion = async (versionItem: versionListResponse) => { + setIsLoading(true); + const versionDetail = await getAppVersionDetail(versionItem._id, versionItem.appId); + setIsLoading(false); + if (!versionDetail) return; + const state = { + nodes: versionDetail.nodes?.map((item) => storeNode2FlowNode({ item })), + edges: versionDetail.edges?.map((item) => storeEdgesRenderEdge({ edge: item })), + title: versionItem.versionName, + chatConfig: versionDetail.chatConfig + }; + + const res = await saveSnapshot({ + pastNodes: state.nodes, + pastEdges: state.edges, + chatConfig: state.chatConfig, + customTitle: `${t('app:app.version_copy')}-${state.title}` + }); + + if (!res) { + return toast({ + title: t('workflow:workflow.Switch_failed'), + status: 'warning' + }); + } + resetSnapshot(state); + toast({ + title: t('workflow:workflow.Switch_success'), + status: 'success' + }); + }; return ( {list.map((data, index) => { @@ -209,28 +246,7 @@ const TeamCloud = () => { _hover={{ bg: 'primary.50' }} - onClick={async () => { - const state = { - nodes: item.nodes?.map((item) => storeNode2FlowNode({ item })), - edges: item.edges?.map((item) => storeEdgesRenderEdge({ edge: item })), - title: item.versionName, - chatConfig: item.chatConfig - }; - - const res = await saveSnapshot({ - pastNodes: state.nodes, - pastEdges: state.edges, - chatConfig: state.chatConfig, - customTitle: `${t('app:app.version_copy')}-${state.title}` - }); - - if (!res) return; - resetSnapshot(state); - toast({ - title: t('workflow:workflow.Switch_success'), - status: 'success' - }); - }} + onClick={() => onChangeVersion(item)} > { autoFocus h={8} defaultValue={item.versionName || formatTime2YMDHMS(item.time)} + onClick={(e) => e.stopPropagation()} onBlur={async (e) => { setIsEditing(true); await updateAppVersion({ diff --git a/projects/app/src/pages/app/detail/components/context.tsx b/projects/app/src/pages/app/detail/components/context.tsx index 112dc74e5..0673e8e0a 100644 --- a/projects/app/src/pages/app/detail/components/context.tsx +++ b/projects/app/src/pages/app/detail/components/context.tsx @@ -84,7 +84,6 @@ export const AppContext = createContext({ const AppContextProvider = ({ children }: { children: ReactNode }) => { const { t } = useTranslation(); - const { appT } = useI18n(); const router = useRouter(); const { appId, currentTab = TabEnum.appEdit } = router.query as { appId: string; @@ -151,23 +150,18 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => { })); }); - const { runAsync: onPublish } = useRequest2( - async (data: PostPublishAppProps) => { - await postPublishApp(appId, data); - setAppDetail((state) => ({ - ...state, - ...data, - modules: data.nodes || state.modules - })); - reloadAppLatestVersion(); - }, - { - successToast: appT('publish_success') - } - ); + const { runAsync: onPublish } = useRequest2(async (data: PostPublishAppProps) => { + await postPublishApp(appId, data); + setAppDetail((state) => ({ + ...state, + ...data, + modules: data.nodes || state.modules + })); + reloadAppLatestVersion(); + }); const { openConfirm: openConfirmDel, ConfirmModal: ConfirmDelModal } = useConfirm({ - content: appT('confirm_del_app_tip'), + content: t('app:confirm_del_app_tip'), type: 'delete' }); const { runAsync: deleteApp } = useRequest2( diff --git a/projects/app/src/web/core/app/api/version.ts b/projects/app/src/web/core/app/api/version.ts index 9e25ecd94..cb308d7b3 100644 --- a/projects/app/src/web/core/app/api/version.ts +++ b/projects/app/src/web/core/app/api/version.ts @@ -7,6 +7,7 @@ import type { getLatestVersionResponse } from '@/pages/api/core/app/version/latest'; import { UpdateAppVersionBody } from '@/pages/api/core/app/version/update'; +import { versionListResponse } from '@/pages/api/core/app/version/listWorkflow'; export const getAppLatestVersion = (data: getLatestVersionQuery) => GET('/core/app/version/latest', data); @@ -17,6 +18,12 @@ export const postPublishApp = (appId: string, data: PostPublishAppProps) => export const getPublishList = (data: PaginationProps<{ appId: string }>) => POST>('/core/app/version/list', data); +export const getWorkflowVersionList = (data: PaginationProps<{ appId: string }>) => + POST>('/core/app/version/listWorkflow', data); + +export const getAppVersionDetail = (versionId: string, appId: string) => + GET(`/core/app/version/detail?versionId=${versionId}&appId=${appId}`); + export const postRevertVersion = (appId: string, data: PostRevertAppProps) => POST(`/core/app/version/revert?appId=${appId}`, data);