From 86436d55ff6a6f56475cf4386edf5987ec3953b7 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Thu, 26 Sep 2024 11:02:09 +0800 Subject: [PATCH] 4.8.11 test (#2794) * perf: version list type * perf: add node default value * perf: snapshot status * fix: version detail auth * fix: export defalt --- .vscode/nextapi.code-snippets | 68 ++++++ dev.md | 4 + packages/global/core/app/version.d.ts | 9 + packages/web/i18n/en/app.json | 11 +- packages/web/i18n/zh/app.json | 7 +- .../app/src/pages/api/__mocks__/db/init.ts | 2 - .../src/pages/api/__mocks__/demo/demo.test.ts | 61 ++++++ .../app/src/pages/api/__mocks__/demo/demo.ts | 17 ++ .../src/pages/api/core/app/version/detail.ts | 22 +- .../pages/api/core/app/version/lis.test.ts | 69 ++++++ .../core/app/version/{list.tsx => list.ts} | 25 ++- .../app/detail/components/Plugin/Header.tsx | 129 ++++++------ .../components/PublishHistoriesSlider.tsx | 131 +++++------- .../app/detail/components/SimpleApp/Edit.tsx | 40 ++-- .../detail/components/SimpleApp/Header.tsx | 121 +++++------ .../app/detail/components/SimpleApp/index.tsx | 18 +- .../components/SimpleApp/useSnapshots.tsx | 124 ++++++----- .../app/detail/components/Workflow/Header.tsx | 132 ++++++------ .../components/WorkflowComponents/AppCard.tsx | 6 +- .../Flow/NodeTemplatesModal.tsx | 30 ++- .../nodes/NodePluginIO/NodePluginConfig.tsx | 6 +- .../components/WorkflowComponents/context.tsx | 198 ++++++++++-------- .../app/src/service/common/system/index.ts | 6 +- projects/app/src/web/core/app/api/version.ts | 6 +- 24 files changed, 752 insertions(+), 490 deletions(-) create mode 100644 projects/app/src/pages/api/__mocks__/demo/demo.test.ts create mode 100644 projects/app/src/pages/api/__mocks__/demo/demo.ts create mode 100644 projects/app/src/pages/api/core/app/version/lis.test.ts rename projects/app/src/pages/api/core/app/version/{list.tsx => list.ts} (61%) diff --git a/.vscode/nextapi.code-snippets b/.vscode/nextapi.code-snippets index 8f7457743..ddc53f3b8 100644 --- a/.vscode/nextapi.code-snippets +++ b/.vscode/nextapi.code-snippets @@ -50,5 +50,73 @@ "export default ContextProvider" ], "description": "FastGPT usecontext template" + }, + + "Jest test template": { + "scope": "typescriptreact", + "prefix": "jesttest", + "body": [ + "import '@/pages/api/__mocks__/base';", + "import { root } from '@/pages/api/__mocks__/db/init';", + "import { getTestRequest } from '@/test/utils';", + "import { AppErrEnum } from '@fastgpt/global/common/error/code/app';", + "import handler from './demo';", + "", + "// Import the schema", + "import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';", + "", + "beforeAll(async () => {", + " // await MongoOutLink.create({", + " // shareId: 'aaa',", + " // appId: root.appId,", + " // tmbId: root.tmbId,", + " // teamId: root.teamId,", + " // type: 'share',", + " // name: 'aaa'", + " // })", + "});", + "", + "test('Should return a list of outLink', async () => {", + " // Mock request", + " const res = (await handler(", + " ...getTestRequest({", + " query: {", + " appId: root.appId,", + " type: 'share'", + " },", + " user: root", + " })", + " )) as any;", + "", + " expect(res.code).toBe(200);", + " expect(res.data.length).toBe(2);", + "});", + "", + "test('appId is required', async () => {", + " const res = (await handler(", + " ...getTestRequest({", + " query: {", + " type: 'share'", + " },", + " user: root", + " })", + " )) as any;", + " expect(res.code).toBe(500);", + " expect(res.error).toBe(AppErrEnum.unExist);", + "});", + "", + "test('if type is not provided, return nothing', async () => {", + " const res = (await handler(", + " ...getTestRequest({", + " query: {", + " appId: root.appId", + " },", + " user: root", + " })", + " )) as any;", + " expect(res.code).toBe(200);", + " expect(res.data.length).toBe(0);", + "});" + ] } } \ No newline at end of file diff --git a/dev.md b/dev.md index 94cfe5ed0..373ac626b 100644 --- a/dev.md +++ b/dev.md @@ -29,6 +29,10 @@ Note: If the Node version is >= 20, you need to pass the `--no-node-snapshot` pa NODE_OPTIONS=--no-node-snapshot pnpm i ``` +### Jest + +https://fael3z0zfze.feishu.cn/docx/ZOI1dABpxoGhS7xzhkXcKPxZnDL + ## I18N ### Install i18n-ally Plugin diff --git a/packages/global/core/app/version.d.ts b/packages/global/core/app/version.d.ts index 6d2266727..178834ce4 100644 --- a/packages/global/core/app/version.d.ts +++ b/packages/global/core/app/version.d.ts @@ -12,3 +12,12 @@ export type AppVersionSchemaType = { versionName: string; tmbId: string; }; + +export type VersionListItemType = { + _id: string; + appId: string; + versionName: string; + time: Date; + isPublish: boolean | undefined; + tmbId: string; +}; diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 86608913c..c47186cc4 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -5,11 +5,11 @@ "app.Version name": "Version Name", "app.modules.click to update": "Click to Refresh", "app.modules.has new version": "New Version Available", - "app.version_back": "Revert to Original State", - "app.version_copy": "Duplicate", + "version_back": "Revert to Original State", + "version_copy": "Duplicate", "app.version_current": "Current Version", "app.version_initial": "Initial Version", - "app.version_initial_copy": "Duplicate - Original State", + "version_initial_copy": "Duplicate - Original State", "app.version_name_tips": "Version name cannot be empty", "app.version_past": "Previously Published", "app.version_publish_tips": "This version will be saved to the team cloud, synchronized with the entire team, and update the app version on all release channels.", @@ -51,6 +51,7 @@ "import_configs": "Import Configurations", "import_configs_failed": "Import configuration failed, please ensure the configuration is correct!", "import_configs_success": "Import Successful", + "initial_form": "initial state", "interval.12_hours": "Every 12 Hours", "interval.2_hours": "Every 2 Hours", "interval.3_hours": "Every 3 Hours", @@ -87,11 +88,11 @@ "search_app": "Search Application", "setting_app": "Application Settings", "setting_plugin": "Plugin Settings", - "template.simple_robot": "Simple robot", "template.hard_strict": "Strict Q&A template", "template.hard_strict_des": "Based on the question and answer template, stricter requirements are imposed on the model's answers.", "template.qa_template": "Q&A template", "template.qa_template_des": "A knowledge base suitable for QA question and answer structure, which allows AI to answer strictly according to preset content", + "template.simple_robot": "Simple robot", "template.standard_strict": "Standard strict template", "template.standard_strict_des": "Based on the standard template, stricter requirements are imposed on the model's answers.", "template.standard_template": "Standard template", @@ -154,4 +155,4 @@ "workflow.user_file_input_desc": "Links to documents and images uploaded by users.", "workflow.user_select": "User Selection", "workflow.user_select_tip": "This module can configure multiple options for selection during the dialogue. Different options can lead to different workflow branches." -} \ No newline at end of file +} diff --git a/packages/web/i18n/zh/app.json b/packages/web/i18n/zh/app.json index acf1de8a0..5405b0b53 100644 --- a/packages/web/i18n/zh/app.json +++ b/packages/web/i18n/zh/app.json @@ -5,11 +5,11 @@ "app.Version name": "版本名称", "app.modules.click to update": "点击更新", "app.modules.has new version": "有新版本", - "app.version_back": "回到初始状态", - "app.version_copy": "副本", + "version_back": "回到初始状态", + "version_copy": "副本", "app.version_current": "当前版本", "app.version_initial": "初始版本", - "app.version_initial_copy": "副本-初始状态", + "version_initial_copy": "副本-初始状态", "app.version_name_tips": "版本名称不能为空", "app.version_past": "发布过", "app.version_publish_tips": "该版本将被保存至团队云端,同步给整个团队,同时更新所有发布渠道的应用版本", @@ -51,6 +51,7 @@ "import_configs": "导入配置", "import_configs_failed": "导入配置失败,请确保配置正常!", "import_configs_success": "导入成功", + "initial_form": "初始状态", "interval.12_hours": "每12小时", "interval.2_hours": "每2小时", "interval.3_hours": "每3小时", diff --git a/projects/app/src/pages/api/__mocks__/db/init.ts b/projects/app/src/pages/api/__mocks__/db/init.ts index 2f3f88644..f53f14675 100644 --- a/projects/app/src/pages/api/__mocks__/db/init.ts +++ b/projects/app/src/pages/api/__mocks__/db/init.ts @@ -43,6 +43,4 @@ export const initMockData = async () => { root.tmbId = rootTeamMember._id; root.teamId = rootTeam._id; root.appId = rootApp._id; - - await Promise.all([rootUser.save(), rootTeam.save(), rootTeamMember.save(), rootApp.save()]); }; diff --git a/projects/app/src/pages/api/__mocks__/demo/demo.test.ts b/projects/app/src/pages/api/__mocks__/demo/demo.test.ts new file mode 100644 index 000000000..be5b9ec73 --- /dev/null +++ b/projects/app/src/pages/api/__mocks__/demo/demo.test.ts @@ -0,0 +1,61 @@ +import '@/pages/api/__mocks__/base'; +import { root } from '@/pages/api/__mocks__/db/init'; +import { getTestRequest } from '@/test/utils'; +import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; +import handler from './demo'; + +// Import the schema +import { MongoOutLink } from '@fastgpt/service/support/outLink/schema'; + +beforeAll(async () => { + // await MongoOutLink.create({ + // shareId: 'aaa', + // appId: root.appId, + // tmbId: root.tmbId, + // teamId: root.teamId, + // type: 'share', + // name: 'aaa' + // }) +}); + +test('Should return a list of outLink', async () => { + // Mock request + const res = (await handler( + ...getTestRequest({ + query: { + appId: root.appId, + type: 'share' + }, + user: root + }) + )) as any; + + expect(res.code).toBe(200); + expect(res.data.length).toBe(2); +}); + +test('appId is required', async () => { + const res = (await handler( + ...getTestRequest({ + query: { + type: 'share' + }, + user: root + }) + )) as any; + expect(res.code).toBe(500); + expect(res.error).toBe(AppErrEnum.unExist); +}); + +test('if type is not provided, return nothing', async () => { + const res = (await handler( + ...getTestRequest({ + query: { + appId: root.appId + }, + user: root + }) + )) as any; + expect(res.code).toBe(200); + expect(res.data.length).toBe(0); +}); diff --git a/projects/app/src/pages/api/__mocks__/demo/demo.ts b/projects/app/src/pages/api/__mocks__/demo/demo.ts new file mode 100644 index 000000000..d0e2ceb3b --- /dev/null +++ b/projects/app/src/pages/api/__mocks__/demo/demo.ts @@ -0,0 +1,17 @@ +import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; +import { NextAPI } from '@/service/middleware/entry'; + +export type demoQuery = {}; + +export type demoBody = {}; + +export type demoResponse = {}; + +async function handler( + req: ApiRequestProps, + res: ApiResponseType +): Promise { + return {}; +} + +export default NextAPI(handler); diff --git a/projects/app/src/pages/api/core/app/version/detail.ts b/projects/app/src/pages/api/core/app/version/detail.ts index bcb758e98..a11fa7b8c 100644 --- a/projects/app/src/pages/api/core/app/version/detail.ts +++ b/projects/app/src/pages/api/core/app/version/detail.ts @@ -2,20 +2,32 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { NextAPI } from '@/service/middleware/entry'; import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema'; import { authApp } from '@fastgpt/service/support/permission/app/auth'; -import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; +import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { AppVersionSchemaType } from '@fastgpt/global/core/app/version'; +import { formatTime2YMDHM } from '@fastgpt/global/common/string/time'; type Props = { versionId: string; appId: string; }; -async function handler(req: NextApiRequest, res: NextApiResponse) { +async function handler( + req: NextApiRequest, + res: NextApiResponse +): Promise { const { versionId, appId } = req.query as Props; - await authApp({ req, authToken: true, appId, per: ReadPermissionVal }); - const result = await MongoAppVersion.findById(versionId); + await authApp({ req, authToken: true, appId, per: WritePermissionVal }); + const result = await MongoAppVersion.findById(versionId).lean(); - return result; + if (!result) { + return Promise.reject('version not found'); + } + + return { + ...result, + versionName: result?.versionName || formatTime2YMDHM(result?.time) + }; } export default NextAPI(handler); diff --git a/projects/app/src/pages/api/core/app/version/lis.test.ts b/projects/app/src/pages/api/core/app/version/lis.test.ts new file mode 100644 index 000000000..078d2b5e4 --- /dev/null +++ b/projects/app/src/pages/api/core/app/version/lis.test.ts @@ -0,0 +1,69 @@ +import '@/pages/api/__mocks__/base'; +import { root } from '@/pages/api/__mocks__/db/init'; +import { getTestRequest } from '@/test/utils'; +import handler, { versionListBody, versionListResponse } from './list'; + +// Import the schema +import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema'; + +const total = 22; + +beforeAll(async () => { + const arr = new Array(total).fill(0); + await MongoAppVersion.insertMany( + arr.map((_, index) => ({ + appId: root.appId, + nodes: [], + edges: [], + chatConfig: {}, + isPublish: index % 2 === 0, + versionName: `v` + index, + tmbId: root.tmbId, + time: new Date(index * 1000) + })) + ); +}); + +test('Get version list and check', async () => { + const offset = 0; + const pageSize = 10; + + const _res = (await handler( + ...getTestRequest<{}, versionListBody>({ + body: { + offset, + pageSize, + appId: root.appId + }, + user: root + }) + )) as any; + const res = _res.data as versionListResponse; + + expect(res.total).toBe(total); + expect(res.list.length).toBe(pageSize); + expect(res.list[0].versionName).toBe('v21'); + expect(res.list[9].versionName).toBe('v12'); +}); + +test('Get version list with offset 20', async () => { + const offset = 20; + const pageSize = 10; + + const _res = (await handler( + ...getTestRequest<{}, versionListBody>({ + body: { + offset, + pageSize, + appId: root.appId + }, + user: root + }) + )) as any; + const res = _res.data as versionListResponse; + + expect(res.total).toBe(total); + expect(res.list.length).toBe(2); + expect(res.list[0].versionName).toBe('v1'); + expect(res.list[1].versionName).toBe('v0'); +}); diff --git a/projects/app/src/pages/api/core/app/version/list.tsx b/projects/app/src/pages/api/core/app/version/list.ts similarity index 61% rename from projects/app/src/pages/api/core/app/version/list.tsx rename to projects/app/src/pages/api/core/app/version/list.ts index f1a94ac2a..b0337c33e 100644 --- a/projects/app/src/pages/api/core/app/version/list.tsx +++ b/projects/app/src/pages/api/core/app/version/list.ts @@ -1,27 +1,26 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; +import type { 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 { ApiRequestProps } from '@fastgpt/service/type/next'; +import { authApp } from '@fastgpt/service/support/permission/app/auth'; +import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { VersionListItemType } from '@fastgpt/global/core/app/version'; -type Props = PaginationProps<{ +export type versionListBody = PaginationProps<{ appId: string; }>; -export type versionListResponse = { - _id: string; - appId: string; - versionName: string; - time: Date; - isPublish: boolean | undefined; - tmbId: string; -}; +export type versionListResponse = PaginationResponse; -type Response = PaginationResponse; - -async function handler(req: ApiRequestProps, res: NextApiResponse): Promise { +async function handler( + req: ApiRequestProps, + res: NextApiResponse +): Promise { const { offset, pageSize, appId } = req.body; + await authApp({ appId, req, per: WritePermissionVal, authToken: true }); + const [result, total] = await Promise.all([ MongoAppVersion.find( { 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 738ccb5c9..5058611c7 100644 --- a/projects/app/src/pages/app/detail/components/Plugin/Header.tsx +++ b/projects/app/src/pages/app/detail/components/Plugin/Header.tsx @@ -10,17 +10,15 @@ import { useDisclosure } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; -import dynamic from 'next/dynamic'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { useContextSelector } from 'use-context-selector'; -import { WorkflowContext, getWorkflowStore } from '../WorkflowComponents/context'; +import { WorkflowContext, WorkflowSnapshotsType } from '../WorkflowComponents/context'; import { AppContext, TabEnum } from '../context'; import RouteTab from '../RouteTab'; import { useRouter } from 'next/router'; import AppCard from '../WorkflowComponents/AppCard'; -import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import MyModal from '@fastgpt/web/components/common/MyModal'; @@ -30,8 +28,7 @@ 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('../PublishHistoriesSlider')); +import PublishHistories from '../PublishHistoriesSlider'; const Header = () => { const { t } = useTranslation(); @@ -51,15 +48,15 @@ const Header = () => { flowData2StoreData, flowData2StoreDataAndCheck, setWorkflowTestData, - setHistoriesDefaultData, - historiesDefaultData, + setShowHistoryModal, + showHistoryModal, nodes, edges, past, future, setPast, - saveSnapshot, - resetSnapshot + onSwitchTmpVersion, + onSwitchCloudVersion } = useContextSelector(WorkflowContext, (v) => v); const { lastAppListRouteType } = useSystemStore(); @@ -187,21 +184,15 @@ const Header = () => { {currentTab === TabEnum.appEdit && ( - {!historiesDefaultData && ( + {!showHistoryModal && ( } aria-label={''} size={'sm'} w={'30px'} variant={'whitePrimary'} - onClick={async () => { - const { nodes, edges } = uiWorkflow2StoreWorkflow(await getWorkflowStore()); - - setHistoriesDefaultData({ - nodes, - edges, - chatConfig: appDetail.chatConfig - }); + onClick={() => { + setShowHistoryModal(true); }} /> )} @@ -218,7 +209,7 @@ const Header = () => { > {t('common:core.workflow.Run')} - {!historiesDefaultData && ( + {!showHistoryModal && ( { )} - {historiesDefaultData && isV2Workflow && currentTab === TabEnum.appEdit && ( - { - setHistoriesDefaultData(undefined); - }} - past={past} - saveSnapshot={saveSnapshot} - resetSnapshot={resetSnapshot} - /> - )} - - - {t('workflow:workflow.exit_tips')} - - - - - - ); }, [ @@ -276,22 +226,63 @@ const Header = () => { currentTab, isPublished, onBack, - isOpenBackConfirm, onOpenBackConfirm, - onCloseBackConfirm, + isV2Workflow, + showHistoryModal, t, loading, - isV2Workflow, - historiesDefaultData, onClickSave, - setHistoriesDefaultData, - appDetail.chatConfig, flowData2StoreDataAndCheck, - setWorkflowTestData, - toast + setShowHistoryModal, + setWorkflowTestData ]); - return Render; + return ( + <> + {Render} + {showHistoryModal && isV2Workflow && currentTab === TabEnum.appEdit && ( + + onClose={() => { + setShowHistoryModal(false); + }} + past={past} + onSwitchTmpVersion={onSwitchTmpVersion} + onSwitchCloudVersion={onSwitchCloudVersion} + /> + )} + + + {t('workflow:workflow.exit_tips')} + + + + + + + + ); }; export default React.memo(Header); diff --git a/projects/app/src/pages/app/detail/components/PublishHistoriesSlider.tsx b/projects/app/src/pages/app/detail/components/PublishHistoriesSlider.tsx index b7e3d1b22..50b327d32 100644 --- a/projects/app/src/pages/app/detail/components/PublishHistoriesSlider.tsx +++ b/projects/app/src/pages/app/detail/components/PublishHistoriesSlider.tsx @@ -7,38 +7,36 @@ import { 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 { Box, BoxProps, 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 { SaveSnapshotParams, SnapshotsType } from './WorkflowComponents/context'; +import type { WorkflowSnapshotsType } 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 type { versionListResponse } from '@/pages/api/core/app/version/list'; +import type { AppVersionSchemaType, VersionListItemType } from '@fastgpt/global/core/app/version'; +import type { SimpleAppSnapshotType } from './SimpleApp/useSnapshots'; -const PublishHistoriesSlider = ({ +const PublishHistoriesSlider = ({ onClose, past, - saveSnapshot, - resetSnapshot, - top, - bottom + onSwitchTmpVersion, + onSwitchCloudVersion, + positionStyles }: { onClose: () => void; - past: SnapshotsType[]; - saveSnapshot: (params: SaveSnapshotParams) => Promise; - resetSnapshot: (state: SnapshotsType) => void; - top?: string | number; - bottom?: string | number; + past: T[]; + onSwitchTmpVersion: (params: T, customTitle: string) => void; + onSwitchCloudVersion: (appVersion: AppVersionSchemaType) => void; + positionStyles?: BoxProps; }) => { const { t } = useTranslation(); const [currentTab, setCurrentTab] = useState<'myEdit' | 'teamCloud'>('myEdit'); @@ -69,29 +67,26 @@ const PublishHistoriesSlider = ({ px={0} showMask={false} overflow={'unset'} - top={top} - bottom={bottom} + {...positionStyles} > {currentTab === 'myEdit' ? ( - + ) : ( - + )} ); }; -export default React.memo(PublishHistoriesSlider); +export default PublishHistoriesSlider; -const MyEdit = ({ +const MyEdit = ({ past, - saveSnapshot, - resetSnapshot + onSwitchTmpVersion }: { - past: SnapshotsType[]; - saveSnapshot: (params: SaveSnapshotParams) => Promise; - resetSnapshot: (state: SnapshotsType) => void; + past: T[]; + onSwitchTmpVersion: (params: T, customTitle: string) => void; }) => { const { t } = useTranslation(); const { toast } = useToast(); @@ -107,24 +102,14 @@ const MyEdit = ({ 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); - } - + onSwitchTmpVersion(initialSnapshot, t(`app:version_initial_copy`)); toast({ title: t('workflow:workflow.Switch_success'), status: 'success' }); }} > - {t('app:app.version_back')} + {t('app:version_back')} )} @@ -142,17 +127,8 @@ const MyEdit = ({ _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); - } - + onClick={() => { + onSwitchTmpVersion(item, `${t('app:version_copy')}-${item.title}`); toast({ title: t('workflow:workflow.Switch_success'), status: 'success' @@ -201,18 +177,16 @@ const MyEdit = ({ }; const TeamCloud = ({ - saveSnapshot, - resetSnapshot + onSwitchCloudVersion }: { - saveSnapshot: (params: SaveSnapshotParams) => Promise; - resetSnapshot: (state: SnapshotsType) => void; + onSwitchCloudVersion: (appVersion: AppVersionSchemaType) => void; }) => { const { t } = useTranslation(); const { appDetail } = useContextSelector(AppContext, (v) => v); const { loadAndGetTeamMembers } = useUserStore(); const { feConfigs } = useSystemStore(); - const { scrollDataList, ScrollList, isLoading, fetchData } = useVirtualScrollPagination( + const { scrollDataList, ScrollList, isLoading, fetchData, setData } = useVirtualScrollPagination( getWorkflowVersionList, { itemHeight: 40, @@ -230,30 +204,15 @@ const TeamCloud = ({ const [editIndex, setEditIndex] = useState(undefined); const [hoveredIndex, setHoveredIndex] = useState(undefined); - const [isEditing, setIsEditing] = useState(false); const { toast } = useToast(); const { runAsync: onChangeVersion, loading: isLoadingVersion } = useRequest2( - async (versionItem: versionListResponse) => { + async (versionItem: VersionListItemType) => { 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); + onSwitchCloudVersion(versionDetail); toast({ title: t('workflow:workflow.Switch_success'), status: 'success' @@ -261,6 +220,22 @@ const TeamCloud = ({ } ); + const { runAsync: onUpdateVersion, loading: isEditing } = useRequest2( + async (item: VersionListItemType, name: string) => { + await updateAppVersion({ + appId: item.appId, + versionName: name, + versionId: item._id + }); + setData((state) => + state.map((version) => + version._id === item._id ? { ...version, versionName: name } : version + ) + ); + setEditIndex(undefined); + } + ); + return ( {scrollDataList.map((data, index) => { @@ -361,16 +336,12 @@ const TeamCloud = ({ 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); + onBlur={(e) => onUpdateVersion(item, e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + // @ts-ignore + onUpdateVersion(item, e.target.value); + } }} /> diff --git a/projects/app/src/pages/app/detail/components/SimpleApp/Edit.tsx b/projects/app/src/pages/app/detail/components/SimpleApp/Edit.tsx index 1699fd848..68af2e896 100644 --- a/projects/app/src/pages/app/detail/components/SimpleApp/Edit.tsx +++ b/projects/app/src/pages/app/detail/components/SimpleApp/Edit.tsx @@ -15,11 +15,8 @@ import { cardStyles } from '../constants'; import styles from './styles.module.scss'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; -import { storeNode2FlowNode } from '@/web/core/workflow/utils'; import { useTranslation } from 'next-i18next'; -import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils'; -import { SnapshotsType } from '../WorkflowComponents/context'; -import { SaveSnapshotFnType } from './useSnapshots'; +import { onSaveSnapshotFnType, SimpleAppSnapshotType } from './useSnapshots'; const Edit = ({ appForm, @@ -29,8 +26,8 @@ const Edit = ({ }: { appForm: AppSimpleEditFormType; setAppForm: React.Dispatch>; - past: SnapshotsType[]; - saveSnapshot: SaveSnapshotFnType; + past: SimpleAppSnapshotType[]; + saveSnapshot: onSaveSnapshotFnType; }) => { const { isPc } = useSystem(); const { loadAllDatasets } = useDatasetStore(); @@ -43,26 +40,25 @@ const Edit = ({ loadAllDatasets(); // Get the latest snapshot - if (past.length > 0) { - const storeWorkflow = uiWorkflow2StoreWorkflow(past[0]); - const currentAppForm = appWorkflow2Form({ ...storeWorkflow, chatConfig: past[0].chatConfig }); - - return setAppForm(currentAppForm); + if (past?.[0]?.appForm) { + return setAppForm(past[0].appForm); } - // Set the first snapshot - saveSnapshot({ - pastNodes: appDetail.modules?.map((item) => storeNode2FlowNode({ item, t })), - chatConfig: appDetail.chatConfig, - isSaved: true + const appForm = appWorkflow2Form({ + nodes: appDetail.modules, + chatConfig: appDetail.chatConfig }); - setAppForm( - appWorkflow2Form({ - nodes: appDetail.modules, - chatConfig: appDetail.chatConfig - }) - ); + // Set the first snapshot + if (past.length === 0) { + saveSnapshot({ + appForm, + title: t('app:initial_form'), + isSaved: true + }); + } + + setAppForm(appForm); if (appDetail.version !== 'v2') { setAppForm( diff --git a/projects/app/src/pages/app/detail/components/SimpleApp/Header.tsx b/projects/app/src/pages/app/detail/components/SimpleApp/Header.tsx index ac5cd1776..c52f4d5ff 100644 --- a/projects/app/src/pages/app/detail/components/SimpleApp/Header.tsx +++ b/projects/app/src/pages/app/detail/components/SimpleApp/Header.tsx @@ -20,32 +20,30 @@ 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 { useBoolean, useDebounceEffect } from 'ahooks'; import { appWorkflow2Form } from '@fastgpt/global/core/app/utils'; import { - compareSnapshot, - storeEdgesRenderEdge, - storeNode2FlowNode -} from '@/web/core/workflow/utils'; -import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils'; -import { SaveSnapshotFnType } from './useSnapshots'; - -const PublishHistories = dynamic(() => import('../PublishHistoriesSlider')); + compareSimpleAppSnapshot, + onSaveSnapshotFnType, + SimpleAppSnapshotType +} from './useSnapshots'; +import PublishHistories from '../PublishHistoriesSlider'; +import { AppVersionSchemaType } from '@fastgpt/global/core/app/version'; const Header = ({ + forbiddenSaveSnapshot, appForm, setAppForm, past, setPast, saveSnapshot }: { + forbiddenSaveSnapshot: React.MutableRefObject; appForm: AppSimpleEditFormType; setAppForm: (form: AppSimpleEditFormType) => void; - past: SnapshotsType[]; - setPast: (value: React.SetStateAction) => void; - saveSnapshot: SaveSnapshotFnType; + past: SimpleAppSnapshotType[]; + setPast: (value: React.SetStateAction) => void; + saveSnapshot: onSaveSnapshotFnType; }) => { const { t } = useTranslation(); const { isPc } = useSystem(); @@ -101,30 +99,43 @@ const Header = ({ } ); - const [historiesDefaultData, setHistoriesDefaultData] = useState(); + const [isShowHistories, { setTrue: setIsShowHistories, setFalse: closeHistories }] = + useBoolean(false); - const resetSnapshot = useCallback( - (data: SnapshotsType) => { - const storeWorkflow = uiWorkflow2StoreWorkflow(data); - const currentAppForm = appWorkflow2Form({ ...storeWorkflow, chatConfig: data.chatConfig }); + const onSwitchTmpVersion = useCallback( + (data: SimpleAppSnapshotType, customTitle: string) => { + setAppForm(data.appForm); - setAppForm(currentAppForm); - }, - [setAppForm] - ); + // Remove multiple "copy-" + const copyText = t('app:version_copy'); + const regex = new RegExp(`(${copyText}-)\\1+`, 'g'); + const title = customTitle.replace(regex, `$1`); - // Save snapshot to local - useDebounceEffect( - () => { - const data = form2AppWorkflow(appForm, t); - - saveSnapshot({ - pastNodes: data.nodes?.map((item) => storeNode2FlowNode({ item, t })), - chatConfig: data.chatConfig + return saveSnapshot({ + appForm: data.appForm, + title }); }, - [appForm], - { wait: 500 } + [saveSnapshot, setAppForm, t] + ); + const onSwitchCloudVersion = useCallback( + (appVersion: AppVersionSchemaType) => { + const appForm = appWorkflow2Form({ + nodes: appVersion.nodes, + chatConfig: appVersion.chatConfig + }); + + const res = saveSnapshot({ + appForm, + title: `${t('app:version_copy')}-${appVersion.versionName}` + }); + forbiddenSaveSnapshot.current = true; + + setAppForm(appForm); + + return res; + }, + [forbiddenSaveSnapshot, saveSnapshot, setAppForm, t] ); // Check if the workflow is published @@ -132,20 +143,7 @@ const Header = ({ useDebounceEffect( () => { const savedSnapshot = past.find((snapshot) => snapshot.isSaved); - const editFormData = form2AppWorkflow(appForm, t); - console.log(savedSnapshot?.nodes, editFormData.chatConfig); - const val = compareSnapshot( - { - nodes: savedSnapshot?.nodes, - edges: [], - chatConfig: savedSnapshot?.chatConfig - }, - { - nodes: editFormData.nodes?.map((item) => storeNode2FlowNode({ item, t })), - edges: [], - chatConfig: editFormData.chatConfig - } - ); + const val = compareSimpleAppSnapshot(savedSnapshot?.appForm, appForm); setIsPublished(val); }, [past, allDatasets], @@ -176,7 +174,7 @@ const Header = ({ )} {currentTab === TabEnum.appEdit && ( - {!historiesDefaultData && ( + {!isShowHistories && ( <> {isPc && ( { - const { nodes, edges } = form2AppWorkflow(appForm, t); - setHistoriesDefaultData({ - nodes, - edges, - chatConfig: appForm.chatConfig - }); - }} + onClick={setIsShowHistories} /> @@ -220,16 +211,16 @@ const Header = ({ )} - {historiesDefaultData && currentTab === TabEnum.appEdit && ( - { - setHistoriesDefaultData(undefined); - }} + {isShowHistories && currentTab === TabEnum.appEdit && ( + + onClose={closeHistories} past={past} - saveSnapshot={saveSnapshot} - resetSnapshot={resetSnapshot} - top={14} - bottom={3} + onSwitchTmpVersion={onSwitchTmpVersion} + onSwitchCloudVersion={onSwitchCloudVersion} + positionStyles={{ + top: 14, + bottom: 3 + }} /> )} diff --git a/projects/app/src/pages/app/detail/components/SimpleApp/index.tsx b/projects/app/src/pages/app/detail/components/SimpleApp/index.tsx index 6c3a62b96..834246826 100644 --- a/projects/app/src/pages/app/detail/components/SimpleApp/index.tsx +++ b/projects/app/src/pages/app/detail/components/SimpleApp/index.tsx @@ -9,7 +9,8 @@ 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'; +import { useSimpleAppSnapshots } from './useSnapshots'; +import { useDebounceEffect } from 'ahooks'; const Logs = dynamic(() => import('../Logs/index')); const PublishChannel = dynamic(() => import('../Publish')); @@ -17,9 +18,21 @@ const PublishChannel = dynamic(() => import('../Publish')); const SimpleEdit = () => { const { t } = useTranslation(); const { currentTab, appDetail } = useContextSelector(AppContext, (v) => v); - const { past, setPast, saveSnapshot } = useSnapshots(appDetail._id); + const { forbiddenSaveSnapshot, past, setPast, saveSnapshot } = useSimpleAppSnapshots( + appDetail._id + ); const [appForm, setAppForm] = useState(getDefaultAppForm()); + // Save snapshot to local + useDebounceEffect( + () => { + saveSnapshot({ + appForm + }); + }, + [appForm], + { wait: 500 } + ); useBeforeunload({ tip: t('common:core.common.tip.leave page') @@ -29,6 +42,7 @@ const SimpleEdit = () => {
Promise; + +export const compareSimpleAppSnapshot = ( + appForm1?: AppSimpleEditFormType, + appForm2?: AppSimpleEditFormType +) => { + if ( + appForm1?.chatConfig && + appForm2?.chatConfig && + !isEqual( + { + welcomeText: appForm1.chatConfig?.welcomeText || '', + variables: appForm1.chatConfig?.variables || [], + questionGuide: appForm1.chatConfig?.questionGuide || false, + ttsConfig: appForm1.chatConfig?.ttsConfig || undefined, + whisperConfig: appForm1.chatConfig?.whisperConfig || undefined, + scheduledTriggerConfig: appForm1.chatConfig?.scheduledTriggerConfig || undefined, + chatInputGuide: appForm1.chatConfig?.chatInputGuide || undefined, + fileSelectConfig: appForm1.chatConfig?.fileSelectConfig || undefined, + instruction: appForm1.chatConfig?.instruction || '' + }, + { + welcomeText: appForm2.chatConfig?.welcomeText || '', + variables: appForm2.chatConfig?.variables || [], + questionGuide: appForm2.chatConfig?.questionGuide || false, + ttsConfig: appForm2.chatConfig?.ttsConfig || undefined, + whisperConfig: appForm2.chatConfig?.whisperConfig || undefined, + scheduledTriggerConfig: appForm2.chatConfig?.scheduledTriggerConfig || undefined, + chatInputGuide: appForm2.chatConfig?.chatInputGuide || undefined, + fileSelectConfig: appForm2.chatConfig?.fileSelectConfig || undefined, + instruction: appForm2.chatConfig?.instruction || '' + } + ) + ) { + console.log('chatConfig not equal'); + return false; } -) => Promise; -const useSnapshots = (appId: string) => { - const [past, setPast] = useLocalStorageState(`${appId}-past-simple`, { - defaultValue: [], - listenStorageChange: true - }) as [SnapshotsType[], (value: SetStateAction) => void]; + return isEqual(appForm1, appForm2); +}; - const saveSnapshot: SaveSnapshotFnType = useMemoizedFn( - async ({ pastNodes, chatConfig, customTitle, isSaved }) => { - if (!pastNodes) return false; +export const useSimpleAppSnapshots = (appId: string) => { + const forbiddenSaveSnapshot = useRef(false); + const [past, setPast] = useLocalStorageState(`${appId}-past-simple`, { + defaultValue: [] + }) as [SimpleAppSnapshotType[], (value: SetStateAction) => void]; - const pastState = past[0]; - - const isPastEqual = compareSnapshot( - { - nodes: pastNodes, - edges: [], - chatConfig: chatConfig - }, - { - nodes: pastState?.nodes, - edges: pastState?.edges, - chatConfig: pastState?.chatConfig - } - ); - if (isPastEqual) return false; - - setPast((past) => [ - { - nodes: pastNodes, - edges: [], - title: customTitle || formatTime2YMDHMS(new Date()), - chatConfig, - isSaved - }, - ...past.slice(0, 199) - ]); - return true; + const saveSnapshot: onSaveSnapshotFnType = useMemoizedFn(async ({ appForm, title, isSaved }) => { + if (forbiddenSaveSnapshot.current) { + forbiddenSaveSnapshot.current = false; + return false; } - ); + + const pastState = past[0]; + + const isPastEqual = compareSimpleAppSnapshot(pastState?.appForm, appForm); + if (isPastEqual) return false; + + setPast((past) => [ + { + appForm, + title: title || formatTime2YMDHMS(new Date()), + isSaved + }, + ...past.slice(0, 199) + ]); + return true; + }); // remove other app's snapshot useEffect(() => { @@ -64,7 +94,7 @@ const useSnapshots = (appId: string) => { }); }, [appId]); - return { past, setPast, saveSnapshot }; + return { forbiddenSaveSnapshot, past, setPast, saveSnapshot }; }; -export default useSnapshots; +export default <>; diff --git a/projects/app/src/pages/app/detail/components/Workflow/Header.tsx b/projects/app/src/pages/app/detail/components/Workflow/Header.tsx index 688dd128c..bb4f7af61 100644 --- a/projects/app/src/pages/app/detail/components/Workflow/Header.tsx +++ b/projects/app/src/pages/app/detail/components/Workflow/Header.tsx @@ -10,17 +10,15 @@ import { useDisclosure } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; -import dynamic from 'next/dynamic'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { useContextSelector } from 'use-context-selector'; -import { WorkflowContext, getWorkflowStore } from '../WorkflowComponents/context'; +import { WorkflowContext } from '../WorkflowComponents/context'; import { AppContext, TabEnum } from '../context'; import RouteTab from '../RouteTab'; import { useRouter } from 'next/router'; import AppCard from '../WorkflowComponents/AppCard'; -import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import MyModal from '@fastgpt/web/components/common/MyModal'; @@ -30,8 +28,7 @@ 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('../PublishHistoriesSlider')); +import PublishHistories from '../PublishHistoriesSlider'; const Header = () => { const { t } = useTranslation(); @@ -51,15 +48,15 @@ const Header = () => { flowData2StoreData, flowData2StoreDataAndCheck, setWorkflowTestData, - setHistoriesDefaultData, - historiesDefaultData, + setShowHistoryModal, + showHistoryModal, nodes, edges, past, future, setPast, - saveSnapshot, - resetSnapshot + onSwitchTmpVersion, + onSwitchCloudVersion } = useContextSelector(WorkflowContext, (v) => v); const { lastAppListRouteType } = useSystemStore(); @@ -189,21 +186,15 @@ const Header = () => { {currentTab === TabEnum.appEdit && ( - {!historiesDefaultData && ( + {!showHistoryModal && ( } aria-label={''} size={'sm'} w={'30px'} variant={'whitePrimary'} - onClick={async () => { - const { nodes, edges } = uiWorkflow2StoreWorkflow(await getWorkflowStore()); - - setHistoriesDefaultData({ - nodes, - edges, - chatConfig: appDetail.chatConfig - }); + onClick={() => { + setShowHistoryModal(true); }} /> )} @@ -220,7 +211,7 @@ const Header = () => { > {t('common:core.workflow.Run')} - {!historiesDefaultData && ( + {!showHistoryModal && ( { )} - {historiesDefaultData && isV2Workflow && currentTab === TabEnum.appEdit && ( - { - setHistoriesDefaultData(undefined); - }} - past={past} - saveSnapshot={saveSnapshot} - resetSnapshot={resetSnapshot} - /> - )} - - - - {t('workflow:workflow.exit_tips')} - - - - - - ); }, [ @@ -281,23 +230,62 @@ const Header = () => { onBack, onOpenBackConfirm, isV2Workflow, - historiesDefaultData, + showHistoryModal, t, loading, onClickSave, flowData2StoreDataAndCheck, - past, - saveSnapshot, - resetSnapshot, - isOpenBackConfirm, - onCloseBackConfirm, - setHistoriesDefaultData, - appDetail.chatConfig, - setWorkflowTestData, - toast + setShowHistoryModal, + setWorkflowTestData ]); - return Render; + return ( + <> + {Render} + {showHistoryModal && isV2Workflow && currentTab === TabEnum.appEdit && ( + { + setShowHistoryModal(false); + }} + past={past} + onSwitchCloudVersion={onSwitchCloudVersion} + onSwitchTmpVersion={onSwitchTmpVersion} + /> + )} + + + + {t('workflow:workflow.exit_tips')} + + + + + + + + ); }; export default React.memo(Header); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx index e3db7a4da..18bb55cbe 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx @@ -31,7 +31,7 @@ const AppCard = ({ const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } = useContextSelector(AppContext, (v) => v); - const { historiesDefaultData } = useContextSelector(WorkflowContext, (v) => v); + const { showHistoryModal } = useContextSelector(WorkflowContext, (v) => v); const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure(); @@ -56,7 +56,7 @@ const AppCard = ({ } ] }, - ...(!historiesDefaultData && currentTab === TabEnum.appEdit + ...(!showHistoryModal && currentTab === TabEnum.appEdit ? [ { children: [ @@ -117,7 +117,7 @@ const AppCard = ({ appDetail.permission.isOwner, currentTab, feConfigs?.show_team_chat, - historiesDefaultData, + showHistoryModal, onDelApp, onOpenImport, onOpenInfoEdit, diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx index c0242032e..bab31efb0 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx @@ -49,6 +49,7 @@ import CostTooltip from '@/components/core/app/plugin/CostTooltip'; import { useUserStore } from '@/web/support/user/useUserStore'; import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart'; import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd'; +import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; type ModuleTemplateListProps = { isOpen: boolean; @@ -399,8 +400,7 @@ const RenderList = React.memo(function RenderList({ const { screenToFlowPosition } = useReactFlow(); const { toast } = useToast(); - const reactFlowWrapper = useContextSelector(WorkflowContext, (v) => v.reactFlowWrapper); - const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes); + const { reactFlowWrapper, setNodes, nodeList } = useContextSelector(WorkflowContext, (v) => v); const { computedNewNodeName } = useWorkflowUtils(); const formatTemplates = useMemo(() => { @@ -424,6 +424,7 @@ const RenderList = React.memo(function RenderList({ }) => { if (!reactFlowWrapper?.current) return; + // Load template node const templateNode = await (async () => { try { // get plugin preview module @@ -458,6 +459,19 @@ const RenderList = React.memo(function RenderList({ const mouseX = nodePosition.x - 100; const mouseY = nodePosition.y - 20; + // Add default values to some inputs + const defaultValueMap: Record = { + [NodeInputKeyEnum.userChatInput]: undefined + }; + nodeList.forEach((node) => { + if (node.flowNodeType === FlowNodeTypeEnum.workflowStart) { + defaultValueMap[NodeInputKeyEnum.userChatInput] = [ + node.nodeId, + NodeOutputKeyEnum.userChatInput + ]; + } + }); + const newNode = nodeTemplate2FlowNode({ template: { ...templateNode, @@ -469,6 +483,7 @@ const RenderList = React.memo(function RenderList({ intro: t(templateNode.intro as any), inputs: templateNode.inputs.map((input) => ({ ...input, + value: defaultValueMap[input.key] ?? input.value, valueDesc: t(input.valueDesc as any), label: t(input.label as any), description: t(input.description as any), @@ -516,7 +531,16 @@ const RenderList = React.memo(function RenderList({ return newState; }); }, - [computedNewNodeName, reactFlowWrapper, setLoading, setNodes, t, toast, screenToFlowPosition] + [ + reactFlowWrapper, + screenToFlowPosition, + nodeList, + computedNewNodeName, + t, + setNodes, + setLoading, + toast + ] ); const gridStyle = useMemo(() => { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/NodePluginConfig.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/NodePluginConfig.tsx index 0d84e163d..7e50440dd 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/NodePluginConfig.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/NodePluginConfig.tsx @@ -10,7 +10,7 @@ import MyTextarea from '@/components/common/Textarea/MyTextarea'; import { AppContext } from '../../../../context'; import { AppChatConfigType, AppDetailType } from '@fastgpt/global/core/app/type'; import { getAppChatConfig } from '@fastgpt/global/core/workflow/utils'; -import { useCreation } from 'ahooks'; +import { useCreation, useMount } from 'ahooks'; import ChatFunctionTip from '@/components/core/app/Tip'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import { WorkflowContext } from '../../../context'; @@ -35,7 +35,7 @@ const NodePluginConfig = ({ data, selected }: NodeProps) => { }); }, [data, appDetail]); - useCreation(() => { + useMount(() => { setAppDetail((state) => ({ ...state, chatConfig: { @@ -43,7 +43,7 @@ const NodePluginConfig = ({ data, selected }: NodeProps) => { ...chatConfig } })); - }, []); + }); const componentsProps = useMemo( () => ({ 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 421953d80..13dea0acd 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx @@ -39,37 +39,26 @@ 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, AppSchema } from '@fastgpt/global/core/app/type'; +import { AppChatConfigType } 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'; 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 { cloneDeep } from 'lodash'; import { SetState } from 'ahooks/lib/createUseStorageState'; +import { AppVersionSchemaType } from '@fastgpt/global/core/app/version'; type OnChange = (changes: ChangesType[]) => void; -export type SnapshotsType = { +export type WorkflowSnapshotsType = { nodes: Node[]; edges: Edge[]; title: string; 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; @@ -103,22 +92,11 @@ type WorkflowContextType = { hoverEdgeId?: string; setHoverEdgeId: React.Dispatch>; - // snapshots - saveSnapshot: ({ - pastNodes, - pastEdges, - customTitle, - chatConfig - }: { - pastNodes?: Node[]; - pastEdges?: Edge[]; - customTitle?: string; - chatConfig?: AppChatConfigType; - }) => Promise; - resetSnapshot: (state: SnapshotsType) => void; - past: SnapshotsType[]; - setPast: Dispatch>; - future: SnapshotsType[]; + onSwitchTmpVersion: (data: WorkflowSnapshotsType, customTitle: string) => boolean; + onSwitchCloudVersion: (appVersion: AppVersionSchemaType) => boolean; + past: WorkflowSnapshotsType[]; + setPast: Dispatch>; + future: WorkflowSnapshotsType[]; redo: () => void; undo: () => void; canRedo: boolean; @@ -179,8 +157,8 @@ type WorkflowContextType = { onStopNodeDebug: () => void; // version history - historiesDefaultData?: InitProps; - setHistoriesDefaultData: React.Dispatch>; + showHistoryModal: boolean; + setShowHistoryModal: React.Dispatch>; // chat test setWorkflowTestData: React.Dispatch< @@ -306,19 +284,13 @@ export const WorkflowContext = createContext({ | undefined { throw new Error('Function not implemented.'); }, - historiesDefaultData: undefined, - setHistoriesDefaultData: function (value: React.SetStateAction): void { + showHistoryModal: false, + setShowHistoryModal: function (value: React.SetStateAction): void { throw new Error('Function not implemented.'); }, getNodeDynamicInputs: function (nodeId: string): FlowNodeInputItemType[] { throw new Error('Function not implemented.'); }, - saveSnapshot: function (): Promise { - throw new Error('Function not implemented.'); - }, - resetSnapshot: function (): void { - throw new Error('Function not implemented.'); - }, past: [], setPast: function (): void { throw new Error('Function not implemented.'); @@ -335,6 +307,12 @@ export const WorkflowContext = createContext({ workflowControlMode: 'drag', setWorkflowControlMode: function (value?: SetState<'drag' | 'select'> | undefined): void { throw new Error('Function not implemented.'); + }, + onSwitchTmpVersion: function (data: WorkflowSnapshotsType, customTitle: string): boolean { + throw new Error('Function not implemented.'); + }, + onSwitchCloudVersion: function (appVersion: AppVersionSchemaType): boolean { + throw new Error('Function not implemented.'); } }); @@ -792,16 +770,15 @@ const WorkflowContextProvider = ({ }, [workflowTestData]); /* snapshots */ - const [past, setPast] = useLocalStorageState(`${appId}-past`, { - defaultValue: [], - listenStorageChange: true - }) as [SnapshotsType[], (value: SetStateAction) => void]; - const [future, setFuture] = useLocalStorageState(`${appId}-future`, { - defaultValue: [], - listenStorageChange: true - }) as [SnapshotsType[], (value: SetStateAction) => void]; + const forbiddenSaveSnapshot = useRef(false); + const [past, setPast] = useLocalStorageState(`${appId}-past`, { + defaultValue: [] + }) as [WorkflowSnapshotsType[], (value: SetStateAction) => void]; + const [future, setFuture] = useLocalStorageState(`${appId}-future`, { + defaultValue: [] + }) as [WorkflowSnapshotsType[], (value: SetStateAction) => void]; - const resetSnapshot = useMemoizedFn((state: SnapshotsType) => { + const resetSnapshot = useMemoizedFn((state: Omit) => { setNodes(state.nodes); setEdges(state.edges); setAppDetail((detail) => ({ @@ -809,30 +786,33 @@ const WorkflowContextProvider = ({ chatConfig: state.chatConfig })); }); - - const saveSnapshot = useMemoizedFn( - async ({ + const pushPastSnapshot = useMemoizedFn( + ({ pastNodes, pastEdges, customTitle, chatConfig, isSaved }: { - pastNodes?: Node[]; - pastEdges?: Edge[]; + pastNodes: Node[]; + pastEdges: Edge[]; customTitle?: string; - chatConfig?: AppChatConfigType; + chatConfig: AppChatConfigType; isSaved?: boolean; }) => { + if (!pastNodes || !pastEdges || !chatConfig) return false; + + if (forbiddenSaveSnapshot.current) { + forbiddenSaveSnapshot.current = false; + return false; + } + const pastState = past[0]; - const currentNodes = pastNodes || nodes; - const currentEdges = pastEdges || edges; - const currentChatConfig = chatConfig || appDetail.chatConfig; const isPastEqual = compareSnapshot( { - nodes: currentNodes, - edges: currentEdges, - chatConfig: currentChatConfig + nodes: pastNodes, + edges: pastEdges, + chatConfig: chatConfig }, { nodes: pastState?.nodes, @@ -843,28 +823,60 @@ const WorkflowContextProvider = ({ if (isPastEqual) return false; + setFuture([]); setPast((past) => [ { - nodes: currentNodes, - edges: currentEdges, + nodes: pastNodes, + edges: pastEdges, title: customTitle || formatTime2YMDHMS(new Date()), - chatConfig: currentChatConfig, + chatConfig, isSaved }, ...past.slice(0, 199) ]); - setFuture([]); - return true; } ); + const onSwitchTmpVersion = useMemoizedFn((params: WorkflowSnapshotsType, customTitle: string) => { + // Remove multiple "copy-" + const copyText = t('app:version_copy'); + const regex = new RegExp(`(${copyText}-)\\1+`, 'g'); + const title = customTitle.replace(regex, `$1`); + + resetSnapshot(params); + + return pushPastSnapshot({ + pastNodes: params.nodes, + pastEdges: params.edges, + chatConfig: params.chatConfig, + customTitle: title + }); + }); + const onSwitchCloudVersion = useMemoizedFn((appVersion: AppVersionSchemaType) => { + const nodes = appVersion.nodes.map((item) => storeNode2FlowNode({ item, t })); + const edges = appVersion.edges.map((item) => storeEdgesRenderEdge({ edge: item })); + const chatConfig = appVersion.chatConfig; + + resetSnapshot({ + nodes, + edges, + chatConfig + }); + return pushPastSnapshot({ + pastNodes: nodes, + pastEdges: edges, + chatConfig, + customTitle: `${t('app:version_copy')}-${appVersion.versionName}` + }); + }); // Auto save snapshot useDebounceEffect( () => { - if (!nodes.length) return; - saveSnapshot({ + if (nodes.length === 0 || !appDetail.chatConfig) return; + + pushPastSnapshot({ pastNodes: nodes, pastEdges: edges, customTitle: formatTime2YMDHMS(new Date()), @@ -904,14 +916,23 @@ const WorkflowContextProvider = ({ }); }, [appId]); - const initData = useMemoizedFn( + const initData = useCallback( async (e: Parameters[0], isInit?: boolean) => { - /* - Refresh web page, load init - */ + // Refresh web page, load init if (isInit && past.length > 0) { return resetSnapshot(past[0]); } + // If it is the initial data, save the initial snapshot + if (isInit && past.length === 0) { + pushPastSnapshot({ + pastNodes: e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || [], + pastEdges: e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || [], + customTitle: t(`app:app.version_initial`), + chatConfig: appDetail.chatConfig, + isSaved: true + }); + forbiddenSaveSnapshot.current = true; + } setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || []); setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []); @@ -923,22 +944,21 @@ const WorkflowContextProvider = ({ chatConfig })); } - - // If it is the initial data, save the initial snapshot - if (isInit) { - saveSnapshot({ - pastNodes: e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || [], - pastEdges: e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || [], - customTitle: t(`app:app.version_initial`), - chatConfig: appDetail.chatConfig, - isSaved: true - }); - } - } + }, + [ + appDetail.chatConfig, + past, + resetSnapshot, + pushPastSnapshot, + setAppDetail, + setEdges, + setNodes, + t + ] ); /* Version histories */ - const [historiesDefaultData, setHistoriesDefaultData] = useState(); + const [showHistoryModal, setShowHistoryModal] = useState(false); /* event bus */ useEffect(() => { @@ -990,10 +1010,10 @@ const WorkflowContextProvider = ({ future, undo, redo, - saveSnapshot, - resetSnapshot, canUndo: past.length > 1, canRedo: !!future.length, + onSwitchTmpVersion, + onSwitchCloudVersion, // function splitToolInputs, @@ -1008,8 +1028,8 @@ const WorkflowContextProvider = ({ onStopNodeDebug, // version history - historiesDefaultData, - setHistoriesDefaultData, + showHistoryModal, + setShowHistoryModal, // chat test setWorkflowTestData diff --git a/projects/app/src/service/common/system/index.ts b/projects/app/src/service/common/system/index.ts index a039118bc..30d4505f8 100644 --- a/projects/app/src/service/common/system/index.ts +++ b/projects/app/src/service/common/system/index.ts @@ -5,19 +5,17 @@ import type { FastGPTConfigFileType } from '@fastgpt/global/common/system/types/ import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants'; import { getFastGPTConfigFromDB } from '@fastgpt/service/common/system/config/controller'; import { PluginTemplateType } from '@fastgpt/global/core/plugin/type'; -import { FastGPTProUrl } from '@fastgpt/service/common/system/constants'; +import { FastGPTProUrl, isProduction } from '@fastgpt/service/common/system/constants'; import { initFastGPTConfig } from '@fastgpt/service/common/system/tools'; import json5 from 'json5'; import { SystemPluginTemplateItemType } from '@fastgpt/global/core/workflow/type'; export const readConfigData = (name: string) => { - const isDev = process.env.NODE_ENV === 'development'; - const splitName = name.split('.'); const devName = `${splitName[0]}.local.${splitName[1]}`; const filename = (() => { - if (isDev) { + if (!isProduction) { // check local file exists const hasLocalFile = existsSync(`data/${devName}`); if (hasLocalFile) { diff --git a/projects/app/src/web/core/app/api/version.ts b/projects/app/src/web/core/app/api/version.ts index e9a84e2e8..967d55bfe 100644 --- a/projects/app/src/web/core/app/api/version.ts +++ b/projects/app/src/web/core/app/api/version.ts @@ -1,7 +1,7 @@ -import { PostPublishAppProps, PostRevertAppProps } from '@/global/core/app/api'; +import { PostPublishAppProps } from '@/global/core/app/api'; import { GET, POST, DELETE, PUT } from '@/web/common/api/request'; import type { AppVersionSchemaType } from '@fastgpt/global/core/app/version'; -import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type'; +import { PaginationProps } from '@fastgpt/web/common/fetch/type'; import type { getLatestVersionQuery, getLatestVersionResponse @@ -16,7 +16,7 @@ export const postPublishApp = (appId: string, data: PostPublishAppProps) => POST(`/core/app/version/publish?appId=${appId}`, data); export const getWorkflowVersionList = (data: PaginationProps<{ appId: string }>) => - POST>('/core/app/version/list', data); + POST('/core/app/version/list', data); export const getAppVersionDetail = (versionId: string, appId: string) => GET(`/core/app/version/detail?versionId=${versionId}&appId=${appId}`);