Skip to content

Commit 60e25fd

Browse files
committed
keep edges on subflow actions intact
1 parent 24b918a commit 60e25fd

File tree

4 files changed

+149
-38
lines changed

4 files changed

+149
-38
lines changed

apps/sim/app/_styles/globals.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@
5151
border: 1px solid var(--brand-secondary) !important;
5252
}
5353

54-
.react-flow__nodesselection-rect {
54+
.react-flow__nodesselection-rect,
55+
.react-flow__nodesselection {
5556
background: transparent !important;
5657
border: none !important;
58+
pointer-events: none !important;
5759
}
5860

5961
/**

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export {
22
clearDragHighlights,
33
computeClampedPositionUpdates,
4+
computeParentUpdateEntries,
45
getClampedPositionForNode,
56
isInEditableElement,
67
selectNodesDeferred,

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Node } from 'reactflow'
1+
import type { Edge, Node } from 'reactflow'
22
import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
33
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
44
import { clampPositionToContainer } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities'
@@ -139,3 +139,43 @@ export function computeClampedPositionUpdates(
139139
position: getClampedPositionForNode(node.id, node.position, blocks, allNodes),
140140
}))
141141
}
142+
143+
interface ParentUpdateEntry {
144+
blockId: string
145+
newParentId: string
146+
affectedEdges: Edge[]
147+
}
148+
149+
/**
150+
* Computes parent update entries for nodes being moved into a subflow.
151+
* Only includes "boundary edges" - edges that cross the selection boundary
152+
* (one end inside selection, one end outside). Edges between nodes in the
153+
* selection are preserved.
154+
*/
155+
export function computeParentUpdateEntries(
156+
validNodes: Node[],
157+
allEdges: Edge[],
158+
targetParentId: string
159+
): ParentUpdateEntry[] {
160+
const movingNodeIds = new Set(validNodes.map((n) => n.id))
161+
162+
// Find edges that cross the boundary (one end inside selection, one end outside)
163+
// Edges between nodes in the selection should stay intact
164+
const boundaryEdges = allEdges.filter((e) => {
165+
const sourceInSelection = movingNodeIds.has(e.source)
166+
const targetInSelection = movingNodeIds.has(e.target)
167+
// Only remove if exactly one end is in the selection (crosses boundary)
168+
return sourceInSelection !== targetInSelection
169+
})
170+
171+
// Build updates for all valid nodes
172+
return validNodes.map((n) => {
173+
// Only include boundary edges connected to this specific node
174+
const edgesForThisNode = boundaryEdges.filter((e) => e.source === n.id || e.target === n.id)
175+
return {
176+
blockId: n.id,
177+
newParentId: targetParentId,
178+
affectedEdges: edgesForThisNode,
179+
}
180+
})
181+
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Lines changed: 104 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2286,9 +2286,6 @@ const WorkflowContent = React.memo(() => {
22862286
// Only consider container nodes that aren't the dragged node
22872287
if (n.type !== 'subflowNode' || n.id === node.id) return false
22882288

2289-
// Skip if this container is already the parent of the node being dragged
2290-
if (n.id === currentParentId) return false
2291-
22922289
// Get the container's absolute position
22932290
const containerAbsolutePos = getNodeAbsolutePosition(n.id)
22942291

@@ -2439,32 +2436,55 @@ const WorkflowContent = React.memo(() => {
24392436
previousPositions: multiNodeDragStartRef.current,
24402437
})
24412438

2442-
// Process parent updates for all selected nodes if dropping into a subflow
2443-
if (potentialParentId && potentialParentId !== dragStartParentId) {
2444-
// Filter out nodes that cannot be moved into subflows
2445-
const validNodes = selectedNodes.filter((n) => {
2446-
const block = blocks[n.id]
2447-
if (!block) return false
2448-
// Starter blocks cannot be in containers
2449-
if (n.data?.type === 'starter') return false
2450-
// Trigger blocks cannot be in containers
2451-
if (TriggerUtils.isTriggerBlock(block)) return false
2452-
// Subflow nodes (loop/parallel) cannot be nested
2453-
if (n.type === 'subflowNode') return false
2439+
// Process parent updates for nodes whose parent is changing
2440+
// Check each node individually - don't rely on dragStartParentId since
2441+
// multi-node selections can contain nodes from different parents
2442+
const selectedNodeIds = new Set(selectedNodes.map((n) => n.id))
2443+
const nodesNeedingParentUpdate = selectedNodes.filter((n) => {
2444+
const block = blocks[n.id]
2445+
if (!block) return false
2446+
const currentParent = block.data?.parentId || null
2447+
// Skip if the node's parent is also being moved (keep children with their parent)
2448+
if (currentParent && selectedNodeIds.has(currentParent)) return false
2449+
// Node needs update if current parent !== target parent
2450+
return currentParent !== potentialParentId
2451+
})
2452+
2453+
if (nodesNeedingParentUpdate.length > 0) {
2454+
// Filter out nodes that cannot be moved into subflows (when target is a subflow)
2455+
const validNodes = nodesNeedingParentUpdate.filter((n) => {
2456+
// These restrictions only apply when moving INTO a subflow
2457+
if (potentialParentId) {
2458+
if (n.data?.type === 'starter') return false
2459+
const block = blocks[n.id]
2460+
if (block && TriggerUtils.isTriggerBlock(block)) return false
2461+
if (n.type === 'subflowNode') return false
2462+
}
24542463
return true
24552464
})
24562465

24572466
if (validNodes.length > 0) {
2467+
// Use boundary edge logic - only remove edges crossing the boundary
2468+
const movingNodeIds = new Set(validNodes.map((n) => n.id))
2469+
const boundaryEdges = edgesForDisplay.filter((e) => {
2470+
const sourceInSelection = movingNodeIds.has(e.source)
2471+
const targetInSelection = movingNodeIds.has(e.target)
2472+
return sourceInSelection !== targetInSelection
2473+
})
2474+
24582475
const rawUpdates = validNodes.map((n) => {
2459-
const edgesToRemove = edgesForDisplay.filter(
2476+
const edgesForThisNode = boundaryEdges.filter(
24602477
(e) => e.source === n.id || e.target === n.id
24612478
)
2462-
const newPosition = calculateRelativePosition(n.id, potentialParentId, true)
2479+
// Use relative position when moving into a container, absolute position when moving to root
2480+
const newPosition = potentialParentId
2481+
? calculateRelativePosition(n.id, potentialParentId, true)
2482+
: getNodeAbsolutePosition(n.id)
24632483
return {
24642484
blockId: n.id,
24652485
newParentId: potentialParentId,
24662486
newPosition,
2467-
affectedEdges: edgesToRemove,
2487+
affectedEdges: edgesForThisNode,
24682488
}
24692489
})
24702490

@@ -2494,7 +2514,7 @@ const WorkflowContent = React.memo(() => {
24942514
return {
24952515
...node,
24962516
position: update.newPosition,
2497-
parentId: update.newParentId,
2517+
parentId: update.newParentId ?? undefined,
24982518
}
24992519
}
25002520
return node
@@ -2503,7 +2523,7 @@ const WorkflowContent = React.memo(() => {
25032523

25042524
resizeLoopNodesWrapper()
25052525

2506-
logger.info('Batch moved nodes into subflow', {
2526+
logger.info('Batch moved nodes to new parent', {
25072527
targetParentId: potentialParentId,
25082528
nodeCount: validNodes.length,
25092529
})
@@ -2631,6 +2651,30 @@ const WorkflowContent = React.memo(() => {
26312651
edgesToAdd.forEach((edge) => addEdge(edge))
26322652

26332653
window.dispatchEvent(new CustomEvent('skip-edge-recording', { detail: { skip: false } }))
2654+
} else if (!potentialParentId && dragStartParentId) {
2655+
// Moving OUT of a subflow to canvas
2656+
// Remove edges connected to this node since it's leaving its parent
2657+
const edgesToRemove = edgesForDisplay.filter(
2658+
(e) => e.source === node.id || e.target === node.id
2659+
)
2660+
2661+
if (edgesToRemove.length > 0) {
2662+
removeEdgesForNode(node.id, edgesToRemove)
2663+
2664+
logger.info('Removed edges when moving node out of subflow', {
2665+
blockId: node.id,
2666+
sourceParentId: dragStartParentId,
2667+
edgeCount: edgesToRemove.length,
2668+
})
2669+
}
2670+
2671+
// Clear the parent relationship
2672+
updateNodeParent(node.id, null, edgesToRemove)
2673+
2674+
logger.info('Moved node out of subflow', {
2675+
blockId: node.id,
2676+
sourceParentId: dragStartParentId,
2677+
})
26342678
}
26352679

26362680
// Reset state
@@ -2829,29 +2873,55 @@ const WorkflowContent = React.memo(() => {
28292873
previousPositions: multiNodeDragStartRef.current,
28302874
})
28312875

2832-
// Process parent updates if dropping into a subflow
2833-
if (potentialParentId && potentialParentId !== dragStartParentId) {
2834-
// Filter out nodes that cannot be moved into subflows
2835-
const validNodes = nodes.filter((n: Node) => {
2836-
const block = blocks[n.id]
2837-
if (!block) return false
2838-
if (n.data?.type === 'starter') return false
2839-
if (TriggerUtils.isTriggerBlock(block)) return false
2840-
if (n.type === 'subflowNode') return false
2876+
// Process parent updates for nodes whose parent is changing
2877+
// Check each node individually - don't rely on dragStartParentId since
2878+
// multi-node selections can contain nodes from different parents
2879+
const selectedNodeIds = new Set(nodes.map((n: Node) => n.id))
2880+
const nodesNeedingParentUpdate = nodes.filter((n: Node) => {
2881+
const block = blocks[n.id]
2882+
if (!block) return false
2883+
const currentParent = block.data?.parentId || null
2884+
// Skip if the node's parent is also being moved (keep children with their parent)
2885+
if (currentParent && selectedNodeIds.has(currentParent)) return false
2886+
// Node needs update if current parent !== target parent
2887+
return currentParent !== potentialParentId
2888+
})
2889+
2890+
if (nodesNeedingParentUpdate.length > 0) {
2891+
// Filter out nodes that cannot be moved into subflows (when target is a subflow)
2892+
const validNodes = nodesNeedingParentUpdate.filter((n: Node) => {
2893+
// These restrictions only apply when moving INTO a subflow
2894+
if (potentialParentId) {
2895+
if (n.data?.type === 'starter') return false
2896+
const block = blocks[n.id]
2897+
if (block && TriggerUtils.isTriggerBlock(block)) return false
2898+
if (n.type === 'subflowNode') return false
2899+
}
28412900
return true
28422901
})
28432902

28442903
if (validNodes.length > 0) {
2904+
// Use boundary edge logic - only remove edges crossing the boundary
2905+
const movingNodeIds = new Set(validNodes.map((n: Node) => n.id))
2906+
const boundaryEdges = edgesForDisplay.filter((e) => {
2907+
const sourceInSelection = movingNodeIds.has(e.source)
2908+
const targetInSelection = movingNodeIds.has(e.target)
2909+
return sourceInSelection !== targetInSelection
2910+
})
2911+
28452912
const rawUpdates = validNodes.map((n: Node) => {
2846-
const edgesToRemove = edgesForDisplay.filter(
2913+
const edgesForThisNode = boundaryEdges.filter(
28472914
(e) => e.source === n.id || e.target === n.id
28482915
)
2849-
const newPosition = calculateRelativePosition(n.id, potentialParentId, true)
2916+
// Use relative position when moving into a container, absolute position when moving to root
2917+
const newPosition = potentialParentId
2918+
? calculateRelativePosition(n.id, potentialParentId, true)
2919+
: getNodeAbsolutePosition(n.id)
28502920
return {
28512921
blockId: n.id,
28522922
newParentId: potentialParentId,
28532923
newPosition,
2854-
affectedEdges: edgesToRemove,
2924+
affectedEdges: edgesForThisNode,
28552925
}
28562926
})
28572927

@@ -2881,7 +2951,7 @@ const WorkflowContent = React.memo(() => {
28812951
return {
28822952
...node,
28832953
position: update.newPosition,
2884-
parentId: update.newParentId,
2954+
parentId: update.newParentId ?? undefined,
28852955
}
28862956
}
28872957
return node
@@ -2890,7 +2960,7 @@ const WorkflowContent = React.memo(() => {
28902960

28912961
resizeLoopNodesWrapper()
28922962

2893-
logger.info('Batch moved selection into subflow', {
2963+
logger.info('Batch moved selection to new parent', {
28942964
targetParentId: potentialParentId,
28952965
nodeCount: validNodes.length,
28962966
})
@@ -2910,7 +2980,6 @@ const WorkflowContent = React.memo(() => {
29102980
calculateRelativePosition,
29112981
resizeLoopNodesWrapper,
29122982
potentialParentId,
2913-
dragStartParentId,
29142983
edgesForDisplay,
29152984
clearDragHighlights,
29162985
]
@@ -2995,7 +3064,6 @@ const WorkflowContent = React.memo(() => {
29953064

29963065
/** Transforms edges to include selection state and delete handlers. Memoized to prevent re-renders. */
29973066
const edgesWithSelection = useMemo(() => {
2998-
// Build node lookup map once - O(n) instead of O(n) per edge
29993067
const nodeMap = new Map(displayNodes.map((n) => [n.id, n]))
30003068

30013069
return edgesForDisplay.map((edge) => {

0 commit comments

Comments
 (0)