|
@@ -814,7 +814,7 @@ function WorkflowEditorInner() {
|
|
|
if (!cancelled) { setIncomingChanges(null); setMyChanges(null) }
|
|
if (!cancelled) { setIncomingChanges(null); setMyChanges(null) }
|
|
|
})
|
|
})
|
|
|
return () => { cancelled = true }
|
|
return () => { cancelled = true }
|
|
|
- }, [id, savedByOther, saveConflict])
|
|
|
|
|
|
|
+ }, [id, savedByOther, saveConflict, queryClient])
|
|
|
|
|
|
|
|
const { data: nodeDefinitions } = useQuery({
|
|
const { data: nodeDefinitions } = useQuery({
|
|
|
queryKey: ['nodes'],
|
|
queryKey: ['nodes'],
|
|
@@ -1642,7 +1642,9 @@ function WorkflowEditorInner() {
|
|
|
}
|
|
}
|
|
|
}, [])
|
|
}, [])
|
|
|
|
|
|
|
|
- const nodeDefs: NodeDefinition[] = nodeDefinitions?.nodes || []
|
|
|
|
|
|
|
+ // Memoised: `|| []` is a fresh array each render, so nodeDefsMap below was
|
|
|
|
|
+ // rebuilt on every render despite being a useMemo.
|
|
|
|
|
+ const nodeDefs: NodeDefinition[] = useMemo(() => nodeDefinitions?.nodes || [], [nodeDefinitions])
|
|
|
const nodeDefsMap = useMemo(() => {
|
|
const nodeDefsMap = useMemo(() => {
|
|
|
const map: Record<string, NodeDefinition> = {}
|
|
const map: Record<string, NodeDefinition> = {}
|
|
|
nodeDefs.forEach((nd) => { map[nd.id] = nd })
|
|
nodeDefs.forEach((nd) => { map[nd.id] = nd })
|
|
@@ -1715,7 +1717,7 @@ function WorkflowEditorInner() {
|
|
|
// Their change is now an unsaved change here too, and saying otherwise
|
|
// Their change is now an unsaved change here too, and saying otherwise
|
|
|
// would let it be lost on the way out.
|
|
// would let it be lost on the way out.
|
|
|
setHasChanges(true)
|
|
setHasChanges(true)
|
|
|
- }, [setNodes, setEdges, nodeDefsMap, executionState])
|
|
|
|
|
|
|
+ }, [setNodes, setEdges, nodeDefsMap, executionState, executeWorkflow, executeTrigger])
|
|
|
|
|
|
|
|
const {
|
|
const {
|
|
|
lockedByOther, livePositions, claim: claimNode, release: releaseNode,
|
|
lockedByOther, livePositions, claim: claimNode, release: releaseNode,
|
|
@@ -1836,7 +1838,14 @@ function WorkflowEditorInner() {
|
|
|
settings: workflow.settings || {},
|
|
settings: workflow.settings || {},
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- }, [workflow, nodeDefs, nodeDefsMap, setNodes, setEdges])
|
|
|
|
|
|
|
+ // executionState is deliberately not a dependency. This effect rebuilds the
|
|
|
|
|
+ // whole canvas from the stored workflow, and execution state changes on every
|
|
|
|
|
+ // node event of a running workflow - so listing it would reload the graph
|
|
|
|
|
+ // underneath a run, several times a second, discarding live node state as it
|
|
|
|
|
+ // arrived. The nodes read it through the props they are built with instead.
|
|
|
|
|
+ // executeWorkflow and executeTrigger are listed because they are stable.
|
|
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
+ }, [workflow, nodeDefs, nodeDefsMap, setNodes, setEdges, executeWorkflow, executeTrigger])
|
|
|
|
|
|
|
|
// Send what changed here to everybody else watching.
|
|
// Send what changed here to everybody else watching.
|
|
|
//
|
|
//
|
|
@@ -2179,7 +2188,7 @@ function WorkflowEditorInner() {
|
|
|
onExecuteTrigger: executeTrigger,
|
|
onExecuteTrigger: executeTrigger,
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
|
- }, [executeTrigger])
|
|
|
|
|
|
|
+ }, [executeTrigger, executeWorkflow])
|
|
|
|
|
|
|
|
// The middle of what the user is currently looking at. A node dropped at a
|
|
// The middle of what the user is currently looking at. A node dropped at a
|
|
|
// fixed spot lands off screen as soon as anyone has panned away, which is
|
|
// fixed spot lands off screen as soon as anyone has panned away, which is
|
|
@@ -2387,6 +2396,12 @@ function WorkflowEditorInner() {
|
|
|
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown)
|
|
window.addEventListener('keydown', handleKeyDown)
|
|
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
|
|
|
+ // navigateToConnectedNode is declared below this effect, so naming it here
|
|
|
|
|
+ // would read it before initialisation and throw on every render. It is only
|
|
|
|
|
+ // reached from the handler, and the value that would go stale in it -
|
|
|
|
|
+ // focusedNodeId - is already listed, so the effect re-registers with a fresh
|
|
|
|
|
+ // copy whenever it could matter.
|
|
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
}, [hasChanges, saveMutation.isPending, handleSave, focusedNodeId, selectedEdgeId, showNodeConfig, showNodePicker, showDeleteConfirm, deleteEdge, openNodeConfig, nodes, edges, setNodes, setEdges, copySelection, pasteNodes, isViewingExecution, history, showToast])
|
|
}, [hasChanges, saveMutation.isPending, handleSave, focusedNodeId, selectedEdgeId, showNodeConfig, showNodePicker, showDeleteConfirm, deleteEdge, openNodeConfig, nodes, edges, setNodes, setEdges, copySelection, pasteNodes, isViewingExecution, history, showToast])
|
|
|
|
|
|
|
|
// Navigate to connected node based on arrow direction
|
|
// Navigate to connected node based on arrow direction
|
|
@@ -2508,6 +2523,11 @@ function WorkflowEditorInner() {
|
|
|
const nodeDef = nodeDefsMap[nodeType]
|
|
const nodeDef = nodeDefsMap[nodeType]
|
|
|
if (!nodeDef) return
|
|
if (!nodeDef) return
|
|
|
addNode(nodeType, nodeDef, reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }))
|
|
addNode(nodeType, nodeDef, reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }))
|
|
|
|
|
+ // addNode is rebuilt every render, so listing it would rebuild this every
|
|
|
|
|
+ // render too. A captured copy is not stale: every write it makes goes through
|
|
|
|
|
+ // the functional form of setNodes/setEdges, and connectDrop - the one value
|
|
|
|
|
+ // it reads directly - is already listed here.
|
|
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
}, [isViewingExecution, nodeDefsMap, reactFlowInstance, connectDrop])
|
|
}, [isViewingExecution, nodeDefsMap, reactFlowInstance, connectDrop])
|
|
|
|
|
|
|
|
// Delete node
|
|
// Delete node
|