chore: snapshot workflow migration progress

This commit is contained in:
2026-04-12 11:49:04 +02:00
parent 0cd02513d5
commit 3e810c74a3
163 changed files with 31773 additions and 2752 deletions
@@ -1,10 +1,21 @@
import { useCallback, useEffect, useMemo, useRef, useState, type MouseEvent as ReactMouseEvent } from 'react'
import { addEdge, useEdgesState, useNodesState, type Connection, type Edge, type Node, type ReactFlowInstance } from '@xyflow/react'
import {
addEdge,
applyNodeChanges,
useEdgesState,
useNodesState,
type Connection,
type Edge,
type Node,
type NodeChange,
type ReactFlowInstance,
} from '@xyflow/react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import {
dispatchWorkflowDraft,
getWorkflowOrderLineContexts,
getNodeDefinitions,
getWorkflowRunComparison,
getWorkflowRuns,
@@ -13,17 +24,25 @@ import {
type WorkflowDefinition,
type WorkflowExecutionMode,
type WorkflowNodeDefinition,
type WorkflowOrderLineContextGroup as WorkflowOrderLineContextGroupApi,
type WorkflowOrderLineContextOption as WorkflowOrderLineContextOptionApi,
type WorkflowParams,
type WorkflowPreflightResponse,
} from '../../api/workflows'
import {
applyAutoLayout,
buildWorkflowCanvasNodeData,
buildCurrentWorkflowConfig,
deriveWorkflowAuthoringFamily,
findOpenNodePosition,
graphNeedsAutoLayout,
inferNodeLabel,
inferNodeType,
inferStepFromNodeType,
normalizeWorkflowParams,
resolveParamsForStepChange,
resolveNodeCollisions,
shouldAutoLayoutAfterInsert,
type WorkflowCanvasNodeData,
validateWorkflowDraft,
workflowToGraph,
@@ -35,6 +54,11 @@ import {
GRAPH_FAMILY_LABELS,
isDefinitionAllowedForGraphFamily,
} from './workflowNodeLibrary'
import { createWorkflowModuleBundleInsertion, type WorkflowModuleBundleId } from './workflowModuleBundles'
import {
createWorkflowReferenceBundleInsertion,
type WorkflowReferenceBundleId,
} from './workflowReferenceBundles'
import type { WorkflowUtilityTab } from './WorkflowCanvasUtilitySidebar'
export type NodeMenuAnchor = {
@@ -43,20 +67,30 @@ export type NodeMenuAnchor = {
flowPosition: { x: number; y: number }
}
function buildNodeData(
step: string,
params: WorkflowParams = {},
definition?: WorkflowNodeDefinition,
overrides?: Partial<WorkflowCanvasNodeData>,
): WorkflowCanvasNodeData {
return {
label: overrides?.label ?? definition?.label ?? inferNodeLabel(step),
params: normalizeWorkflowParams(params),
step,
description: overrides?.description ?? definition?.description,
icon: overrides?.icon ?? definition?.icon,
category: overrides?.category ?? definition?.category,
}
export type WorkflowOrderLineContextOption = {
value: string
label: string
meta: string
}
export type WorkflowOrderLineContextGroup = {
orderId: string
orderLabel: string
options: WorkflowOrderLineContextOption[]
}
function normalizeOrderLineContextGroups(
groups: WorkflowOrderLineContextGroupApi[],
): WorkflowOrderLineContextGroup[] {
return groups.map(group => ({
orderId: group.order_id,
orderLabel: group.order_label,
options: group.options.map((option: WorkflowOrderLineContextOptionApi) => ({
value: option.value,
label: option.label,
meta: option.meta,
})),
}))
}
type UseWorkflowCanvasControllerArgs = {
@@ -73,14 +107,17 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
})
const nodeDefinitions = nodeDefinitionsData?.definitions ?? []
const nodeDefinitionsByStep = Object.fromEntries(nodeDefinitions.map(definition => [definition.step, definition]))
const definitionsLoaded = nodeDefinitions.length > 0
const { nodes: initNodes, edges: initEdges } = workflowToGraph(workflow.config, nodeDefinitionsByStep)
const [nodes, setNodes, onNodesChange] = useNodesState(initNodes)
const [nodes, setNodes] = useNodesState(initNodes)
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges)
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null)
const [selectedEdgeIds, setSelectedEdgeIds] = useState<string[]>([])
const [selectedRunId, setSelectedRunId] = useState<string | null>(null)
const [dispatchContextId, setDispatchContextId] = useState('')
const [preflightResult, setPreflightResult] = useState<WorkflowPreflightResponse | null>(null)
const [lastSuccessfulPreflightFingerprint, setLastSuccessfulPreflightFingerprint] = useState<string | null>(null)
const [executionMode, setExecutionMode] = useState<WorkflowExecutionMode>(workflow.config.ui?.execution_mode ?? 'legacy')
const [nodeMenuAnchor, setNodeMenuAnchor] = useState<NodeMenuAnchor | null>(null)
const [activeUtilityTab, setActiveUtilityTab] = useState<WorkflowUtilityTab>('library')
@@ -88,18 +125,66 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance<Node, Edge> | null>(null)
const validation = validateWorkflowDraft(nodes, edges, nodeDefinitionsByStep, nodeDefinitions.length > 0)
const selectedEdgeIds = useMemo(
() => edges.filter(edge => Boolean((edge as Edge & { selected?: boolean }).selected)).map(edge => edge.id),
[edges],
const authoringFamily = useMemo(
() => deriveWorkflowAuthoringFamily(workflow, nodes, nodeDefinitionsByStep, definitionsLoaded),
[definitionsLoaded, nodeDefinitionsByStep, nodes, workflow],
)
const currentWorkflowConfig = useMemo(
() => buildCurrentWorkflowConfig(workflow, nodes, edges, executionMode, authoringFamily),
[authoringFamily, edges, executionMode, nodes, workflow],
)
const graphFamily = useMemo(
() =>
inferWorkflowFamily(
buildCurrentWorkflowConfig(workflow, nodes, edges, executionMode),
nodeDefinitionsByStep,
),
[edges, executionMode, nodeDefinitionsByStep, nodes, workflow],
() => inferWorkflowFamily(currentWorkflowConfig, nodeDefinitionsByStep),
[currentWorkflowConfig, nodeDefinitionsByStep],
)
const isOrderLineGraph = graphFamily === 'order_line'
const { data: workflowOrderLineContexts = [], isFetching: isOrderLineContextsLoading } = useQuery({
queryKey: ['workflow-order-line-contexts'],
queryFn: () => getWorkflowOrderLineContexts(50),
enabled: isOrderLineGraph,
staleTime: 30_000,
})
const orderLineContextGroups = useMemo<WorkflowOrderLineContextGroup[]>(
() => normalizeOrderLineContextGroups(workflowOrderLineContexts).filter(group => group.options.length > 0),
[workflowOrderLineContexts],
)
const selectedOrderLineContext = useMemo(
() =>
orderLineContextGroups
.flatMap(group => group.options)
.find(option => option.value === dispatchContextId) ?? null,
[dispatchContextId, orderLineContextGroups],
)
const dispatchContextLabel = useMemo(() => {
if (isOrderLineGraph) return 'Order Line'
if (graphFamily === 'cad_file') return 'CAD File'
return 'Context'
}, [graphFamily, isOrderLineGraph])
const dispatchContextSummary = useMemo(() => {
if (isOrderLineGraph) return selectedOrderLineContext?.label ?? null
const trimmed = dispatchContextId.trim()
if (graphFamily === 'cad_file' && trimmed.length > 0) return 'CAD File'
return trimmed.length > 0 ? trimmed : null
}, [dispatchContextId, graphFamily, isOrderLineGraph, selectedOrderLineContext])
const dispatchContextMeta = useMemo(() => {
if (isOrderLineGraph) return selectedOrderLineContext?.meta ?? null
if (graphFamily !== 'cad_file') return null
const trimmed = dispatchContextId.trim()
if (!trimmed) return null
if (preflightResult?.context_id === trimmed && preflightResult.resolved_cad_file_id) {
return `${preflightResult.resolved_cad_file_id} · validated`
}
return trimmed
}, [dispatchContextId, graphFamily, isOrderLineGraph, preflightResult, selectedOrderLineContext])
const currentDispatchFingerprint = useMemo(
() => JSON.stringify({ contextId: dispatchContextId.trim(), config: currentWorkflowConfig }),
[currentWorkflowConfig, dispatchContextId],
)
const hasFreshSuccessfulPreflight =
preflightResult?.graph_dispatch_allowed === true &&
lastSuccessfulPreflightFingerprint === currentDispatchFingerprint
const { data: workflowRuns = [] } = useQuery({
queryKey: ['workflow-runs', workflow.id],
@@ -140,32 +225,47 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
context_id: contextId,
config,
}),
onSuccess: result => {
onSuccess: (result, variables) => {
setPreflightResult(result)
if (result.graph_dispatch_allowed) {
setLastSuccessfulPreflightFingerprint(JSON.stringify({ contextId: variables.contextId, config: variables.config }))
toast.success(result.summary)
} else {
setLastSuccessfulPreflightFingerprint(null)
toast.error(result.summary)
}
},
onError: (error: any) => {
setPreflightResult(null)
setLastSuccessfulPreflightFingerprint(null)
toast.error(error?.response?.data?.detail || 'Failed to preflight workflow')
},
})
useEffect(() => {
const graph = workflowToGraph(workflow.config, nodeDefinitionsByStep)
setNodes(graph.nodes)
const nextNodes = graphNeedsAutoLayout(graph.nodes) ? applyAutoLayout(graph.nodes, graph.edges) : graph.nodes
setNodes(nextNodes)
setEdges(graph.edges)
setSelectedNodeId(null)
setSelectedEdgeIds([])
setSelectedRunId(null)
setNodeMenuAnchor(null)
setPreflightResult(null)
setLastSuccessfulPreflightFingerprint(null)
setExecutionMode(workflow.config.ui?.execution_mode ?? 'legacy')
setActiveUtilityTab('library')
}, [nodeDefinitionsData, setEdges, setNodes, workflow.config])
useEffect(() => {
if (!isOrderLineGraph) return
if (dispatchContextId.trim()) return
const firstOption = orderLineContextGroups[0]?.options[0]
if (firstOption) {
setDispatchContextId(firstOption.value)
}
}, [dispatchContextId, isOrderLineGraph, orderLineContextGroups])
useEffect(() => {
if (!selectedRunId && workflowRuns.length > 0) {
setSelectedRunId(workflowRuns[0].id)
@@ -177,18 +277,110 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
}, [selectedRunId, workflowRuns])
const onConnect = useCallback(
(connection: Connection) => setEdges(currentEdges => addEdge(connection, currentEdges)),
[setEdges],
(connection: Connection) => {
if (!connection.source || !connection.target) return
const sourceNode = nodes.find(node => node.id === connection.source)
const targetNode = nodes.find(node => node.id === connection.target)
const sourceData = sourceNode?.data as WorkflowCanvasNodeData | undefined
const targetData = targetNode?.data as WorkflowCanvasNodeData | undefined
const sourcePorts = sourceData?.outputPorts ?? []
const targetPorts = targetData?.inputPorts ?? []
if (sourcePorts.length === 0) {
toast.error('Selected source node does not expose any downstream outputs.')
return
}
if (targetPorts.length === 0) {
toast.error('Selected target node does not declare any upstream inputs.')
return
}
const requestedTargetPort = connection.targetHandle
? targetPorts.find(port => port.id === connection.targetHandle)
: undefined
const requestedSourcePort = connection.sourceHandle
? sourcePorts.find(port => port.id === connection.sourceHandle)
: undefined
const matchingTargetPort =
requestedTargetPort &&
sourcePorts.some(sourcePort =>
requestedTargetPort.roles.some(role => sourcePort.roles.includes(role)),
)
? requestedTargetPort
: targetPorts.find(port =>
sourcePorts.some(sourcePort => port.roles.some(role => sourcePort.roles.includes(role))),
)
if (!matchingTargetPort) {
toast.error('These nodes do not share a compatible input/output contract.')
return
}
const matchingSourcePort =
requestedSourcePort &&
matchingTargetPort.roles.some(role => requestedSourcePort.roles.includes(role))
? requestedSourcePort
: sourcePorts.find(port => matchingTargetPort.roles.some(role => port.roles.includes(role)))
if (!matchingSourcePort) {
toast.error('The selected source handle does not satisfy the target input contract.')
return
}
setEdges(currentEdges => {
const duplicateEdge = currentEdges.some(
edge => edge.source === connection.source && edge.target === connection.target,
)
if (duplicateEdge) {
toast.error('A connection between these nodes already exists.')
return currentEdges
}
return addEdge(
{
...connection,
sourceHandle: matchingSourcePort.id,
targetHandle: matchingTargetPort.id,
},
currentEdges,
)
})
},
[nodes, setEdges],
)
const onNodesChange = useCallback(
(changes: NodeChange[]) => {
setNodes(currentNodes => {
const nextNodes = applyNodeChanges(changes, currentNodes)
const settledNodeIds = changes
.filter((change): change is Extract<NodeChange, { type: 'position' }> => change.type === 'position')
.filter(change => change.dragging !== true)
.map(change => change.id)
if (settledNodeIds.length === 0) {
return nextNodes
}
return resolveNodeCollisions(nextNodes, settledNodeIds)
})
},
[setNodes],
)
const onNodeClick = useCallback((_: ReactMouseEvent, node: Node) => {
setNodeMenuAnchor(null)
setSelectedEdgeIds([])
setSelectedNodeId(node.id)
setActiveUtilityTab('inspector')
}, [])
const onEdgeClick = useCallback((_: ReactMouseEvent, edge: Edge) => {
setNodeMenuAnchor(null)
setSelectedEdgeIds([edge.id])
setSelectedNodeId(null)
setEdges(currentEdges =>
currentEdges.map(currentEdge => ({
@@ -200,6 +392,7 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
const onPaneClick = useCallback(() => {
setNodeMenuAnchor(null)
setSelectedEdgeIds([])
setSelectedNodeId(null)
setEdges(currentEdges =>
currentEdges.map(edge => ({
@@ -226,8 +419,8 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
const handlePipelineStepChange = useCallback(
(stepName: string) => {
const definition = nodeDefinitionsByStep[stepName]
if (definition && !isDefinitionAllowedForGraphFamily(definition, graphFamily)) {
toast.error(`${definition.label} does not belong to the ${GRAPH_FAMILY_LABELS[graphFamily]} family.`)
if (definition && !isDefinitionAllowedForGraphFamily(definition, authoringFamily)) {
toast.error(`${definition.label} does not belong to the ${GRAPH_FAMILY_LABELS[authoringFamily]} authoring family.`)
return
}
@@ -235,12 +428,12 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
currentNodes.map(node => {
if (node.id !== selectedNodeId) return node
const currentData = (node.data as WorkflowCanvasNodeData | undefined) ?? buildNodeData(stepName)
const currentData = (node.data as WorkflowCanvasNodeData | undefined) ?? buildWorkflowCanvasNodeData(stepName)
return {
...node,
type: definition?.node_type ?? inferNodeType(stepName),
data: {
...buildNodeData(
...buildWorkflowCanvasNodeData(
stepName || inferStepFromNodeType(node.type),
resolveParamsForStepChange(definition, currentData.params),
definition,
@@ -251,7 +444,7 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
}),
)
},
[graphFamily, nodeDefinitionsByStep, selectedNodeId, setNodes],
[authoringFamily, nodeDefinitionsByStep, selectedNodeId, setNodes],
)
const openNodeMenu = useCallback(
@@ -287,27 +480,91 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
const insertNode = useCallback(
(step: string, preferredPosition?: { x: number; y: number }) => {
const definition = nodeDefinitionsByStep[step]
if (definition && !isDefinitionAllowedForGraphFamily(definition, graphFamily)) {
toast.error(`${definition.label} cannot be added to a ${GRAPH_FAMILY_LABELS[graphFamily]} workflow.`)
if (definition && !isDefinitionAllowedForGraphFamily(definition, authoringFamily)) {
toast.error(`${definition.label} cannot be added to a ${GRAPH_FAMILY_LABELS[authoringFamily]} workflow.`)
return
}
const type = definition?.node_type ?? inferNodeType(step)
const fallbackX = nodes.length > 0 ? Math.max(...nodes.map(node => node.position.x)) + 220 : 120
const fallbackY = nodes.length > 0 ? Math.max(...nodes.map(node => node.position.y)) + 40 : 120
const fallbackX = nodes.length > 0 ? Math.max(...nodes.map(node => node.position.x)) + 312 : 120
const fallbackY = nodes.length > 0 ? Math.max(...nodes.map(node => node.position.y)) + 140 : 120
const position = findOpenNodePosition(nodes, preferredPosition ?? { x: fallbackX, y: fallbackY })
const newNode: Node = {
id: `${step}_${Date.now()}`,
type,
position: preferredPosition ?? { x: fallbackX, y: fallbackY },
data: buildNodeData(step, definition?.defaults ?? {}, definition),
position,
data: buildWorkflowCanvasNodeData(step, definition?.defaults ?? {}, definition),
}
const nextNodes = [...nodes, newNode]
const shouldAutoLayout = shouldAutoLayoutAfterInsert(nextNodes, newNode, preferredPosition ?? null)
const laidOutNodes = shouldAutoLayout ? applyAutoLayout(nextNodes, edges) : nextNodes
setNodes(currentNodes => [...currentNodes, newNode])
setNodes(laidOutNodes)
setSelectedNodeId(newNode.id)
setNodeMenuAnchor(null)
setActiveUtilityTab('inspector')
if (shouldAutoLayout) {
window.requestAnimationFrame(() => {
reactFlowInstance?.fitView({ padding: 0.2, duration: 220 })
})
}
},
[graphFamily, nodeDefinitionsByStep, nodes, setNodes],
[authoringFamily, edges, nodeDefinitionsByStep, nodes, reactFlowInstance, setNodes],
)
const insertModuleBundle = useCallback(
(bundleId: WorkflowModuleBundleId, preferredPosition?: { x: number; y: number }) => {
const insertion = createWorkflowModuleBundleInsertion({
bundleId,
graphFamily: authoringFamily,
nodeDefinitionsByStep,
existingNodes: nodes,
preferredPosition,
})
if (!insertion.ok) {
toast.error(insertion.reason)
return
}
const combinedNodes = [...nodes, ...insertion.nodes]
const combinedEdges = [...edges, ...insertion.edges]
setNodes(graphNeedsAutoLayout(combinedNodes) ? applyAutoLayout(combinedNodes, combinedEdges) : combinedNodes)
setEdges(combinedEdges)
setSelectedNodeId(insertion.nodes[0]?.id ?? null)
setNodeMenuAnchor(null)
setActiveUtilityTab('inspector')
toast.success(`${insertion.bundle.label} inserted`)
},
[authoringFamily, edges, nodeDefinitionsByStep, nodes, setEdges, setNodes],
)
const insertReferenceBundle = useCallback(
(bundleId: WorkflowReferenceBundleId, preferredPosition?: { x: number; y: number }) => {
const insertion = createWorkflowReferenceBundleInsertion({
bundleId,
graphFamily: authoringFamily,
nodeDefinitionsByStep,
existingNodes: nodes,
preferredPosition,
})
if (!insertion.ok) {
toast.error(insertion.reason)
return
}
const combinedNodes = [...nodes, ...insertion.nodes]
const combinedEdges = [...edges, ...insertion.edges]
setNodes(graphNeedsAutoLayout(combinedNodes) ? applyAutoLayout(combinedNodes, combinedEdges) : combinedNodes)
setEdges(combinedEdges)
setSelectedNodeId(insertion.nodes[0]?.id ?? null)
setNodeMenuAnchor(null)
setActiveUtilityTab('inspector')
toast.success(`${insertion.bundle.label} inserted`)
},
[authoringFamily, edges, nodeDefinitionsByStep, nodes, setEdges, setNodes],
)
const handleOpenToolbarNodeMenu = useCallback(() => {
@@ -327,6 +584,7 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
const deleteEdgesById = useCallback((edgeIds: string[]) => {
if (edgeIds.length === 0) return
setEdges(currentEdges => currentEdges.filter(edge => !edgeIds.includes(edge.id)))
setSelectedEdgeIds(currentIds => currentIds.filter(edgeId => !edgeIds.includes(edgeId)))
setSelectedNodeId(null)
setNodeMenuAnchor(null)
toast.success(edgeIds.length === 1 ? 'Connection deleted' : `${edgeIds.length} connections deleted`)
@@ -348,6 +606,27 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
deleteEdgesById([edge.id])
}, [deleteEdgesById])
const handleSelectionChange = useCallback(
({ nodes: selectedNodes, edges: selectedEdges }: { nodes: Node[]; edges: Edge[] }) => {
if (selectedNodes.length > 0) {
setSelectedNodeId(selectedNodes[0].id)
setSelectedEdgeIds(selectedEdges.map(edge => edge.id))
setActiveUtilityTab('inspector')
return
}
if (selectedEdges.length > 0) {
setSelectedNodeId(null)
setSelectedEdgeIds(selectedEdges.map(edge => edge.id))
return
}
setSelectedNodeId(null)
setSelectedEdgeIds([])
},
[],
)
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
const target = event.target as HTMLElement | null
@@ -383,8 +662,8 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
toast.error('Resolve workflow validation errors before saving.')
return
}
onSave(buildCurrentWorkflowConfig(workflow, nodes, edges, executionMode))
}, [edges, executionMode, nodes, onSave, validation.errors.length, workflow])
onSave(currentWorkflowConfig)
}, [currentWorkflowConfig, onSave, validation.errors.length])
const handleDispatch = useCallback(() => {
if (!dispatchContextId.trim()) {
@@ -395,11 +674,15 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
toast.error('Resolve workflow validation errors before dispatching.')
return
}
if (!hasFreshSuccessfulPreflight) {
toast.error('Run a fresh Dry Run for the current graph and context before dispatching.')
return
}
dispatchMutation.mutate({
contextId: dispatchContextId.trim(),
config: buildCurrentWorkflowConfig(workflow, nodes, edges, executionMode),
config: currentWorkflowConfig,
})
}, [dispatchContextId, dispatchMutation, edges, executionMode, nodes, validation.errors.length, workflow])
}, [currentWorkflowConfig, dispatchContextId, dispatchMutation, hasFreshSuccessfulPreflight, validation.errors.length])
const handlePreflight = useCallback(() => {
if (!dispatchContextId.trim()) {
@@ -412,14 +695,21 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
}
preflightMutation.mutate({
contextId: dispatchContextId.trim(),
config: buildCurrentWorkflowConfig(workflow, nodes, edges, executionMode),
config: currentWorkflowConfig,
})
}, [dispatchContextId, edges, executionMode, nodes, preflightMutation, validation.errors.length, workflow])
}, [currentWorkflowConfig, dispatchContextId, preflightMutation, validation.errors.length])
const selectedNode = useMemo(
() => nodes.find(node => node.id === selectedNodeId),
[nodes, selectedNodeId],
)
const preflightState = useMemo<'ready' | 'required' | 'stale' | 'blocked'>(() => {
if (!dispatchContextId.trim()) return 'required'
if (preflightResult && !preflightResult.graph_dispatch_allowed) return 'blocked'
if (hasFreshSuccessfulPreflight) return 'ready'
if (preflightResult) return 'stale'
return 'required'
}, [dispatchContextId, hasFreshSuccessfulPreflight, preflightResult])
return {
reactFlowWrapper,
@@ -439,7 +729,15 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
preflightMutation,
dispatchContextId,
setDispatchContextId,
isOrderLineGraph,
isOrderLineContextsLoading,
orderLineContextGroups,
dispatchContextLabel,
dispatchContextSummary,
dispatchContextMeta,
preflightResult,
preflightState,
hasFreshSuccessfulPreflight,
executionMode,
setExecutionMode,
nodeMenuAnchor,
@@ -447,6 +745,7 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
activeUtilityTab,
setActiveUtilityTab,
validation,
authoringFamily,
graphFamily,
onConnect,
onNodeClick,
@@ -457,11 +756,14 @@ export function useWorkflowCanvasController({ workflow, onSave }: UseWorkflowCan
handlePaneContextMenu,
handleNodeContextMenu,
insertNode,
insertModuleBundle,
insertReferenceBundle,
handleOpenToolbarNodeMenu,
handleAutoLayout,
handleDeleteSelectedEdges,
onEdgeContextMenu,
onEdgeDoubleClick,
handleSelectionChange,
handleSave,
handleDispatch,
handlePreflight,