From 8e9d4d8d8090b797d70f4c42d8d9c24bc51e1aaa Mon Sep 17 00:00:00 2001 From: yyh Date: Thu, 15 Oct 2026 23:10:53 +0800 Subject: [PATCH] fix(workflow): preserve shared App cache across editor teardown --- web/app/components/rag-pipeline/index.tsx | 2 +- web/app/components/snippets/index.tsx | 2 +- web/app/components/workflow-app/index.tsx | 2 +- .../workflow/editor-resource/provider.tsx | 20 +++---- .../workflow/editor-resource/state.ts | 59 +++++-------------- web/app/components/workflow/index.tsx | 5 +- .../workflow/operator/export-image.tsx | 12 +++- 7 files changed, 37 insertions(+), 65 deletions(-) diff --git a/web/app/components/rag-pipeline/index.tsx b/web/app/components/rag-pipeline/index.tsx index ea2b3a70a92..2f4af2739a1 100644 --- a/web/app/components/rag-pipeline/index.tsx +++ b/web/app/components/rag-pipeline/index.tsx @@ -49,7 +49,7 @@ const RagPipelineWrapper = () => { if (!pipelineId) return return ( - + diff --git a/web/app/components/snippets/index.tsx b/web/app/components/snippets/index.tsx index b49406d9466..a9a31282e87 100644 --- a/web/app/components/snippets/index.tsx +++ b/web/app/components/snippets/index.tsx @@ -80,7 +80,7 @@ const SnippetPage = () => { const SnippetPageWrapper = ({ snippetId }: SnippetPageProps) => { return ( - + diff --git a/web/app/components/workflow-app/index.tsx b/web/app/components/workflow-app/index.tsx index 033e6a85d7d..c180e791381 100644 --- a/web/app/components/workflow-app/index.tsx +++ b/web/app/components/workflow-app/index.tsx @@ -150,7 +150,7 @@ const WorkflowAppWrapper = () => { if (!isWorkflowAppMode(mode)) throw new Error('This app does not have a workflow editor') return ( - + diff --git a/web/app/components/workflow/editor-resource/provider.tsx b/web/app/components/workflow/editor-resource/provider.tsx index bc66ce0794f..ee0f51710fe 100644 --- a/web/app/components/workflow/editor-resource/provider.tsx +++ b/web/app/components/workflow/editor-resource/provider.tsx @@ -1,28 +1,22 @@ 'use client' import type { ReactNode } from 'react' -import type { EditorIdentity } from './state' +import type { EditorResource } from './resource' import { ScopeProvider } from 'jotai-scope' -import { editorIdentityStateAtom } from './state' +import { getEditorResourceId } from './resource' +import { editorResourceAtom } from './state' export function EditorResourceScope({ - identity, + resource, children, }: { - identity: EditorIdentity + resource: EditorResource children: ReactNode }) { - const id = - identity.kind === 'app' - ? identity.appId - : identity.kind === 'knowledge-pipeline' - ? identity.pipelineId - : identity.snippetId - return ( {children} diff --git a/web/app/components/workflow/editor-resource/state.ts b/web/app/components/workflow/editor-resource/state.ts index dcf6e14fb26..6dd538c8547 100644 --- a/web/app/components/workflow/editor-resource/state.ts +++ b/web/app/components/workflow/editor-resource/state.ts @@ -1,68 +1,39 @@ import type { EditorResource } from './resource' import { atom } from 'jotai' import { atomWithLazy } from 'jotai/utils' -import { isWorkflowAppMode } from '@/app/components/apps/app-type' -import { consoleQuery } from '@/service/console' -import { atomWithResolvedSuspenseQuery } from '@/utils/query-atoms' import { isChatflow } from './resource' -export type EditorIdentity = - | Readonly<{ kind: 'app'; appId: string }> - | Readonly<{ kind: 'knowledge-pipeline'; pipelineId: string }> - | Readonly<{ kind: 'snippet'; snippetId: string }> - -export const editorIdentityStateAtom = atomWithLazy(() => { +export const editorResourceAtom = atomWithLazy(() => { throw new Error('EditorResourceScope is required for the workflow editor') }) -export const editorResourceKindAtom = atom((get) => get(editorIdentityStateAtom).kind) +export const editorResourceKindAtom = atom((get) => get(editorResourceAtom).kind) export const editorAppIdAtom = atom((get) => { - const identity = get(editorIdentityStateAtom) - return identity.kind === 'app' ? identity.appId : undefined + const resource = get(editorResourceAtom) + return resource.kind === 'app' ? resource.appId : undefined }) export const editorPipelineIdAtom = atom((get) => { - const identity = get(editorIdentityStateAtom) - return identity.kind === 'knowledge-pipeline' ? identity.pipelineId : undefined + const resource = get(editorResourceAtom) + return resource.kind === 'knowledge-pipeline' ? resource.pipelineId : undefined }) -// The app entrypoint resolves this query before mounting its editor session. -const editorAppQueryAtom = atomWithResolvedSuspenseQuery((get) => { - const identity = get(editorIdentityStateAtom) - if (identity.kind !== 'app') throw new Error('App details require an app editor') - return consoleQuery.apps.byAppId.get.queryOptions({ - input: { params: { app_id: identity.appId } }, - }) -}) -const appModeAtom = atom((get) => get(editorAppQueryAtom).data.mode) - -export const editorAppNameAtom = atom((get) => - get(editorResourceKindAtom) === 'app' ? get(editorAppQueryAtom).data.name : undefined, -) - -export const editorResourceAtom = atom((get) => { - const identity = get(editorIdentityStateAtom) - if (identity.kind !== 'app') return identity - const mode = get(appModeAtom) - if (!isWorkflowAppMode(mode)) throw new Error('This app does not have a workflow editor') - return { ...identity, mode } -}) export const isChatflowAtom = atom((get) => isChatflow(get(editorResourceAtom))) export const workflowAppIdentityAtom = atom((get) => { - const identity = get(editorIdentityStateAtom) - if (identity.kind !== 'app') throw new Error('This operation requires a workflow app editor') - return identity + const resource = get(editorResourceAtom) + if (resource.kind !== 'app') throw new Error('This operation requires a workflow app editor') + return resource }) export const knowledgePipelineIdentityAtom = atom((get) => { - const identity = get(editorIdentityStateAtom) - if (identity.kind !== 'knowledge-pipeline') + const resource = get(editorResourceAtom) + if (resource.kind !== 'knowledge-pipeline') throw new Error('This operation requires a knowledge pipeline editor') - return identity + return resource }) export const snippetIdentityAtom = atom((get) => { - const identity = get(editorIdentityStateAtom) - if (identity.kind !== 'snippet') throw new Error('This operation requires a snippet editor') - return identity + const resource = get(editorResourceAtom) + if (resource.kind !== 'snippet') throw new Error('This operation requires a snippet editor') + return resource }) diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 76e51476aa8..b37856e42a6 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -577,17 +577,16 @@ export const Workflow: FC = memo( const dataSourceList = useStore((s) => s.dataSourceList) // buildInTools, customTools, workflowTools, mcpTools, dataSourceList const resource = useAtomValue(editorResourceAtom) - const resourceId = getEditorResourceId(resource) const [isLoadedVars, setIsLoadedVars] = useState(false) const [vars, setVars] = useState([]) useEffect(() => { ;(async () => { - if (!resource.kind || !resourceId) return + if (!getEditorResourceId(resource)) return const data = await fetchAllInspectVars(resource) setVars(data) setIsLoadedVars(true) })() - }, [resource.kind, resourceId]) + }, [resource]) useEffect(() => { if (schemaTypeDefinitions && isLoadedVars) { fetchInspectVars({ diff --git a/web/app/components/workflow/operator/export-image.tsx b/web/app/components/workflow/operator/export-image.tsx index 2463c6cc960..8f4d4925ce3 100644 --- a/web/app/components/workflow/operator/export-image.tsx +++ b/web/app/components/workflow/operator/export-image.tsx @@ -8,14 +8,16 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { IconButton } from '@langgenius/dify-ui/icon-button' +import { skipToken, useQuery } from '@tanstack/react-query' import { toJpeg, toPng, toSvg } from 'html-to-image' import { useAtomValue } from 'jotai' import { memo, useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { getNodesBounds, useReactFlow } from 'reactflow' import ImagePreview from '@/app/components/base/image-uploader/image-preview' -import { editorAppNameAtom } from '@/app/components/workflow/editor-resource/state' +import { editorAppIdAtom } from '@/app/components/workflow/editor-resource/state' import { useStore } from '@/app/components/workflow/store' +import { consoleQuery } from '@/service/console' import { downloadUrl } from '@/utils/download' import { useNodesReadOnly } from '../hooks/use-workflow' import TipPopup from './tip-popup' @@ -28,7 +30,13 @@ const ExportImage = memo(() => { const [previewUrl, setPreviewUrl] = useState('') const [previewTitle, setPreviewTitle] = useState('') const knowledgeName = useStore((s) => s.knowledgeName) - const appName = useAtomValue(editorAppNameAtom) + const appId = useAtomValue(editorAppIdAtom) + const { data: appName } = useQuery( + consoleQuery.apps.byAppId.get.queryOptions({ + input: appId ? { params: { app_id: appId } } : skipToken, + select: (app) => app.name, + }), + ) const isReadOnly = getNodesReadOnly() const handleExportImage = useCallback(