|
|
|
@ -217,16 +217,56 @@ function getWorkflowNodeDimensions(node: Node): { width: number; height: number |
|
|
|
return defaults; |
|
|
|
} |
|
|
|
|
|
|
|
// Cache the dimension-decorated node keyed on the source node object. Upstream
|
|
|
|
// (`applyNodeChanges` + the `allNodes` memo) keeps the same object reference for
|
|
|
|
// nodes that didn't change this frame, so this WeakMap returns the exact same
|
|
|
|
// decorated reference across frames. Without it, the `{...node}` spread minted a
|
|
|
|
// brand-new object for every node on every drag frame, invalidating React Flow's
|
|
|
|
// per-node wrapper memo and re-committing the transform on EVERY node — not just
|
|
|
|
// the one being dragged. Keeping references stable means an unrelated node's DOM
|
|
|
|
// wrapper is left untouched during a drag.
|
|
|
|
const reactFlowNodeDimensionsCache = new WeakMap<Node, Node>(); |
|
|
|
|
|
|
|
// Cache the visual-state-decorated (className + zIndex) node keyed on the RAW
|
|
|
|
// store node. `applyNodeChanges` keeps the same reference for nodes that didn't
|
|
|
|
// move this frame, so keying on the raw node lets us return the exact same
|
|
|
|
// decorated object across frames — as long as the computed className/zIndex
|
|
|
|
// signature is unchanged. Store nodes never carry className/zIndex themselves
|
|
|
|
// (they're undefined), so without this cache the `{...node}` branch below minted
|
|
|
|
// a new object for EVERY node on EVERY frame, breaking React Flow's per-node
|
|
|
|
// `adoptUserNodes` reference check and re-rendering every wrapper during a drag.
|
|
|
|
const nodeVisualStateCache = new WeakMap< |
|
|
|
WorkflowNode, |
|
|
|
{ decorated: WorkflowNode; className: string; zIndex: number } |
|
|
|
>(); |
|
|
|
|
|
|
|
function withReactFlowNodeDimensions<T extends Node>(node: T): T { |
|
|
|
const cached = reactFlowNodeDimensionsCache.get(node); |
|
|
|
if (cached) return cached as T; |
|
|
|
|
|
|
|
const dimensions = getWorkflowNodeStyleDimensions(node); |
|
|
|
if (!dimensions) return node; |
|
|
|
return { |
|
|
|
// No style dimensions, or the node already carries the right width/height:
|
|
|
|
// reuse the original object so its reference stays stable downstream.
|
|
|
|
if ( |
|
|
|
!dimensions || |
|
|
|
(node.width === dimensions.width && |
|
|
|
node.height === dimensions.height && |
|
|
|
node.initialWidth === dimensions.width && |
|
|
|
node.initialHeight === dimensions.height) |
|
|
|
) { |
|
|
|
reactFlowNodeDimensionsCache.set(node, node); |
|
|
|
return node; |
|
|
|
} |
|
|
|
|
|
|
|
const decorated: T = { |
|
|
|
...node, |
|
|
|
width: dimensions.width, |
|
|
|
height: dimensions.height, |
|
|
|
initialWidth: dimensions.width, |
|
|
|
initialHeight: dimensions.height, |
|
|
|
}; |
|
|
|
reactFlowNodeDimensionsCache.set(node, decorated); |
|
|
|
return decorated; |
|
|
|
} |
|
|
|
|
|
|
|
function getWorkflowNodeAbsolutePosition(node: Node, nodeById: Map<string, Node>): { x: number; y: number } { |
|
|
|
@ -974,9 +1014,20 @@ export function WorkflowCanvas() { |
|
|
|
zIndex = Math.max(zIndex, topStackLayer + 1); |
|
|
|
} |
|
|
|
|
|
|
|
// Only create new node object if className changed
|
|
|
|
if (node.className === newClass && node.zIndex === zIndex) return node; |
|
|
|
return { ...node, className: newClass, zIndex }; |
|
|
|
// Return a reference-stable decorated node. Keyed on the raw store node,
|
|
|
|
// so an unchanged node yields the identical object across frames and React
|
|
|
|
// Flow leaves its wrapper untouched; only the node whose className/zIndex
|
|
|
|
// actually changed (e.g. the one being dragged) gets a new reference.
|
|
|
|
const cached = nodeVisualStateCache.get(node); |
|
|
|
if (cached && cached.className === newClass && cached.zIndex === zIndex) { |
|
|
|
return cached.decorated; |
|
|
|
} |
|
|
|
const decorated = |
|
|
|
node.className === newClass && node.zIndex === zIndex |
|
|
|
? node |
|
|
|
: { ...node, className: newClass, zIndex }; |
|
|
|
nodeVisualStateCache.set(node, { decorated, className: newClass, zIndex }); |
|
|
|
return decorated; |
|
|
|
}); |
|
|
|
}, [nodes, dimmedNodeIds, draggingGroupId, memberLayerMap, skippedNodeIds, stackLayerMap, topStackLayer]); |
|
|
|
|
|
|
|
@ -1706,8 +1757,11 @@ export function WorkflowCanvas() { |
|
|
|
} |
|
|
|
}, [loadWorkflow, showToast, captureSnapshot, t, providerSettings, closeCanvasAssistant, urlWorkflowId, markAsUnsaved]); |
|
|
|
|
|
|
|
// Create lightweight workflow state for chat (strip base64 images)
|
|
|
|
// Create lightweight workflow state for chat (strip base64 images).
|
|
|
|
// Only computed while the chat panel is open — otherwise dragging any node
|
|
|
|
// would run stripBinaryData over every node on each frame for no reason.
|
|
|
|
const chatWorkflowState = useMemo(() => { |
|
|
|
if (!canvasNavigationSettings.assistantEnabled) return undefined; |
|
|
|
const strippedNodes = stripBinaryData(nodes); |
|
|
|
return { |
|
|
|
nodes: strippedNodes.map(n => ({ |
|
|
|
@ -1724,7 +1778,7 @@ export function WorkflowCanvas() { |
|
|
|
targetHandle: e.targetHandle || undefined, |
|
|
|
})), |
|
|
|
}; |
|
|
|
}, [nodes, edges]); |
|
|
|
}, [nodes, edges, canvasNavigationSettings.assistantEnabled]); |
|
|
|
|
|
|
|
// Compute selected node IDs for chat context scoping
|
|
|
|
const selectedNodeIds = useMemo(() => nodes.filter(n => n.selected).map(n => n.id), [nodes]); |
|
|
|
|