refactor(workflow): establish scoped editor identity lifecycles

Bind each editor session to its resource kind and ID. Resolve App query facts at the entry boundary and derive mode/name from the shared cache. Resource switches remount the canvas session; legacy consumers are migrated in the following commit.
This commit is contained in:
yyh
2026-10-15 22:37:28 +08:00
parent e7cc9c55aa
commit 58e5da4a33
6 changed files with 152 additions and 12 deletions
+6 -3
View File
@@ -2,6 +2,7 @@ import { useMemo } from 'react'
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
import WorkflowWithDefaultContext from '@/app/components/workflow'
import { WorkflowContextProvider } from '@/app/components/workflow/context'
import { EditorResourceScope } from '@/app/components/workflow/editor-resource/provider'
import { initialEdges, initialNodes } from '@/app/components/workflow/utils'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import Conversion from './components/conversion'
@@ -48,9 +49,11 @@ const RagPipelineWrapper = () => {
if (!pipelineId) return <Conversion />
return (
<WorkflowContextProvider injectWorkflowStoreSliceFn={createRagPipelineSliceSlice}>
<RagPipeline />
</WorkflowContextProvider>
<EditorResourceScope identity={{ kind: 'knowledge-pipeline', pipelineId }}>
<WorkflowContextProvider injectWorkflowStoreSliceFn={createRagPipelineSliceSlice}>
<RagPipeline />
</WorkflowContextProvider>
</EditorResourceScope>
)
}
+6 -3
View File
@@ -4,6 +4,7 @@ import { useMemo } from 'react'
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
import WorkflowWithDefaultContext from '@/app/components/workflow'
import { WorkflowContextProvider } from '@/app/components/workflow/context'
import { EditorResourceScope } from '@/app/components/workflow/editor-resource/provider'
import { initialEdges, initialNodes } from '@/app/components/workflow/utils'
import SnippetLayout from './components/snippet-layout'
import SnippetMain from './components/snippet-main'
@@ -78,9 +79,11 @@ const SnippetPage = ({ snippetId }: SnippetPageProps) => {
const SnippetPageWrapper = ({ snippetId }: SnippetPageProps) => {
return (
<WorkflowContextProvider key={snippetId}>
<SnippetPage snippetId={snippetId} />
</WorkflowContextProvider>
<EditorResourceScope identity={{ kind: 'snippet', snippetId }}>
<WorkflowContextProvider>
<SnippetPage snippetId={snippetId} />
</WorkflowContextProvider>
</EditorResourceScope>
)
}
+20 -6
View File
@@ -5,10 +5,13 @@ import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useEffect, useMemo } from 'react'
import { isWorkflowAppMode } from '@/app/components/apps/app-type'
import { FeaturesProvider } from '@/app/components/base/features'
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
import WorkflowWithDefaultContext from '@/app/components/workflow'
import { WorkflowContextProvider } from '@/app/components/workflow/context'
import { EditorResourceScope } from '@/app/components/workflow/editor-resource/provider'
import { workflowAppIdentityAtom } from '@/app/components/workflow/editor-resource/state'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { useTriggerStatusStore } from '@/app/components/workflow/store/trigger-status'
import { initialEdges, initialNodes } from '@/app/components/workflow/utils'
@@ -39,7 +42,7 @@ const WorkflowAppWithAdditionalContext = () => {
// Initialize trigger status at application level
const { setTriggerStatuses } = useTriggerStatusStore()
const { appId } = useParams<{ appId: string }>()
const { appId } = useAtomValue(workflowAppIdentityAtom)
const { data: appDetail } = useSuspenseQuery(
consoleQuery.apps.byAppId.get.queryOptions({ input: { params: { app_id: appId } } }),
)
@@ -137,12 +140,23 @@ const WorkflowAppWithAdditionalContext = () => {
}
const WorkflowAppWrapper = () => {
const { appId } = useParams<{ appId: string }>()
const { data: mode } = useSuspenseQuery(
consoleQuery.apps.byAppId.get.queryOptions({
input: { params: { app_id: appId } },
select: (app) => app.mode,
}),
)
if (!isWorkflowAppMode(mode)) throw new Error('This app does not have a workflow editor')
return (
<WorkflowContextProvider
injectWorkflowStoreSliceFn={createWorkflowSlice as InjectWorkflowStoreSliceFn}
>
<WorkflowAppWithAdditionalContext />
</WorkflowContextProvider>
<EditorResourceScope identity={{ kind: 'app', appId }}>
<WorkflowContextProvider
injectWorkflowStoreSliceFn={createWorkflowSlice as InjectWorkflowStoreSliceFn}
>
<WorkflowAppWithAdditionalContext />
</WorkflowContextProvider>
</EditorResourceScope>
)
}
@@ -0,0 +1,31 @@
'use client'
import type { ReactNode } from 'react'
import type { EditorIdentity } from './state'
import { ScopeProvider } from 'jotai-scope'
import { editorIdentityStateAtom } from './state'
export function EditorResourceScope({
identity,
children,
}: {
identity: EditorIdentity
children: ReactNode
}) {
const id =
identity.kind === 'app'
? identity.appId
: identity.kind === 'knowledge-pipeline'
? identity.pipelineId
: identity.snippetId
return (
<ScopeProvider
key={`${identity.kind}:${id}`}
atoms={[[editorIdentityStateAtom, identity]]}
name="WorkflowEditor"
>
{children}
</ScopeProvider>
)
}
@@ -0,0 +1,21 @@
import type { WorkflowAppMode } from '@/app/components/apps/app-type'
export type EditorResource =
| Readonly<{ kind: 'app'; appId: string; mode: WorkflowAppMode }>
| Readonly<{ kind: 'knowledge-pipeline'; pipelineId: string }>
| Readonly<{ kind: 'snippet'; snippetId: string }>
export function getEditorResourceId(resource: EditorResource): string {
switch (resource.kind) {
case 'app':
return resource.appId
case 'knowledge-pipeline':
return resource.pipelineId
case 'snippet':
return resource.snippetId
}
}
export function isChatflow(resource: EditorResource): boolean {
return resource.kind === 'app' && resource.mode === 'advanced-chat'
}
@@ -0,0 +1,68 @@
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<EditorIdentity>(() => {
throw new Error('EditorResourceScope is required for the workflow editor')
})
export const editorResourceKindAtom = atom((get) => get(editorIdentityStateAtom).kind)
export const editorAppIdAtom = atom((get) => {
const identity = get(editorIdentityStateAtom)
return identity.kind === 'app' ? identity.appId : undefined
})
export const editorPipelineIdAtom = atom((get) => {
const identity = get(editorIdentityStateAtom)
return identity.kind === 'knowledge-pipeline' ? identity.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<EditorResource>((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
})
export const knowledgePipelineIdentityAtom = atom((get) => {
const identity = get(editorIdentityStateAtom)
if (identity.kind !== 'knowledge-pipeline')
throw new Error('This operation requires a knowledge pipeline editor')
return identity
})
export const snippetIdentityAtom = atom((get) => {
const identity = get(editorIdentityStateAtom)
if (identity.kind !== 'snippet') throw new Error('This operation requires a snippet editor')
return identity
})