Skip to content

Commit 6e7f3da

Browse files
committed
keep edges on subflow actions intact
1 parent 20d66b9 commit 6e7f3da

File tree

4 files changed

+139
-35
lines changed

4 files changed

+139
-35
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: 94 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2273,9 +2273,6 @@ const WorkflowContent = React.memo(() => {
22732273
// Only consider container nodes that aren't the dragged node
22742274
if (n.type !== 'subflowNode' || n.id === node.id) return false
22752275

2276-
// Skip if this container is already the parent of the node being dragged
2277-
if (n.id === currentParentId) return false
2278-
22792276
// Get the container's absolute position
22802277
const containerAbsolutePos = getNodeAbsolutePosition(n.id)
22812278

@@ -2426,37 +2423,56 @@ const WorkflowContent = React.memo(() => {
24262423
previousPositions: multiNodeDragStartRef.current,
24272424
})
24282425

2429-
// Process parent updates for all selected nodes if dropping into a subflow
2430-
if (potentialParentId && potentialParentId !== dragStartParentId) {
2431-
// Filter out nodes that cannot be moved into subflows
2432-
const validNodes = selectedNodes.filter((n) => {
2433-
const block = blocks[n.id]
2434-
if (!block) return false
2435-
// Starter blocks cannot be in containers
2436-
if (n.data?.type === 'starter') return false
2437-
// Trigger blocks cannot be in containers
2438-
if (TriggerUtils.isTriggerBlock(block)) return false
2439-
// Subflow nodes (loop/parallel) cannot be nested
2440-
if (n.type === 'subflowNode') return false
2426+
// Process parent updates for nodes whose parent is changing
2427+
// Check each node individually - don't rely on dragStartParentId since
2428+
// multi-node selections can contain nodes from different parents
2429+
const selectedNodeIds = new Set(selectedNodes.map((n) => n.id))
2430+
const nodesNeedingParentUpdate = selectedNodes.filter((n) => {
2431+
const block = blocks[n.id]
2432+
if (!block) return false
2433+
const currentParent = block.data?.parentId || null
2434+
// Skip if the node's parent is also being moved (keep children with their parent)
2435+
if (currentParent && selectedNodeIds.has(currentParent)) return false
2436+
// Node needs update if current parent !== target parent
2437+
return currentParent !== potentialParentId
2438+
})
2439+
2440+
if (nodesNeedingParentUpdate.length > 0) {
2441+
// Filter out nodes that cannot be moved into subflows (when target is a subflow)
2442+
const validNodes = nodesNeedingParentUpdate.filter((n) => {
2443+
// These restrictions only apply when moving INTO a subflow
2444+
if (potentialParentId) {
2445+
if (n.data?.type === 'starter') return false
2446+
const block = blocks[n.id]
2447+
if (block && TriggerUtils.isTriggerBlock(block)) return false
2448+
if (n.type === 'subflowNode') return false
2449+
}
24412450
return true
24422451
})
24432452

24442453
if (validNodes.length > 0) {
2445-
// Build updates for all valid nodes
2454+
// Use boundary edge logic - only remove edges crossing the boundary
2455+
const movingNodeIds = new Set(validNodes.map((n) => n.id))
2456+
const boundaryEdges = edgesForDisplay.filter((e) => {
2457+
const sourceInSelection = movingNodeIds.has(e.source)
2458+
const targetInSelection = movingNodeIds.has(e.target)
2459+
return sourceInSelection !== targetInSelection
2460+
})
2461+
24462462
const updates = validNodes.map((n) => {
2447-
const edgesToRemove = edgesForDisplay.filter(
2463+
const edgesForThisNode = boundaryEdges.filter(
24482464
(e) => e.source === n.id || e.target === n.id
24492465
)
24502466
return {
24512467
blockId: n.id,
24522468
newParentId: potentialParentId,
2453-
affectedEdges: edgesToRemove,
2469+
affectedEdges: edgesForThisNode,
24542470
}
24552471
})
24562472

24572473
collaborativeBatchUpdateParent(updates)
24582474

2459-
logger.info('Batch moved nodes into subflow', {
2475+
logger.info('Batch moved nodes to new parent', {
24602476
targetParentId: potentialParentId,
24612477
nodeCount: validNodes.length,
24622478
})
@@ -2584,6 +2600,30 @@ const WorkflowContent = React.memo(() => {
25842600
edgesToAdd.forEach((edge) => addEdge(edge))
25852601

25862602
window.dispatchEvent(new CustomEvent('skip-edge-recording', { detail: { skip: false } }))
2603+
} else if (!potentialParentId && dragStartParentId) {
2604+
// Moving OUT of a subflow to canvas
2605+
// Remove edges connected to this node since it's leaving its parent
2606+
const edgesToRemove = edgesForDisplay.filter(
2607+
(e) => e.source === node.id || e.target === node.id
2608+
)
2609+
2610+
if (edgesToRemove.length > 0) {
2611+
removeEdgesForNode(node.id, edgesToRemove)
2612+
2613+
logger.info('Removed edges when moving node out of subflow', {
2614+
blockId: node.id,
2615+
sourceParentId: dragStartParentId,
2616+
edgeCount: edgesToRemove.length,
2617+
})
2618+
}
2619+
2620+
// Clear the parent relationship
2621+
updateNodeParent(node.id, null, edgesToRemove)
2622+
2623+
logger.info('Moved node out of subflow', {
2624+
blockId: node.id,
2625+
sourceParentId: dragStartParentId,
2626+
})
25872627
}
25882628

25892629
// Reset state
@@ -2780,33 +2820,56 @@ const WorkflowContent = React.memo(() => {
27802820
previousPositions: multiNodeDragStartRef.current,
27812821
})
27822822

2783-
// Process parent updates if dropping into a subflow
2784-
if (potentialParentId && potentialParentId !== dragStartParentId) {
2785-
// Filter out nodes that cannot be moved into subflows
2786-
const validNodes = nodes.filter((n: Node) => {
2787-
const block = blocks[n.id]
2788-
if (!block) return false
2789-
if (n.data?.type === 'starter') return false
2790-
if (TriggerUtils.isTriggerBlock(block)) return false
2791-
if (n.type === 'subflowNode') return false
2823+
// Process parent updates for nodes whose parent is changing
2824+
// Check each node individually - don't rely on dragStartParentId since
2825+
// multi-node selections can contain nodes from different parents
2826+
const selectedNodeIds = new Set(nodes.map((n: Node) => n.id))
2827+
const nodesNeedingParentUpdate = nodes.filter((n: Node) => {
2828+
const block = blocks[n.id]
2829+
if (!block) return false
2830+
const currentParent = block.data?.parentId || null
2831+
// Skip if the node's parent is also being moved (keep children with their parent)
2832+
if (currentParent && selectedNodeIds.has(currentParent)) return false
2833+
// Node needs update if current parent !== target parent
2834+
return currentParent !== potentialParentId
2835+
})
2836+
2837+
if (nodesNeedingParentUpdate.length > 0) {
2838+
// Filter out nodes that cannot be moved into subflows (when target is a subflow)
2839+
const validNodes = nodesNeedingParentUpdate.filter((n: Node) => {
2840+
// These restrictions only apply when moving INTO a subflow
2841+
if (potentialParentId) {
2842+
if (n.data?.type === 'starter') return false
2843+
const block = blocks[n.id]
2844+
if (block && TriggerUtils.isTriggerBlock(block)) return false
2845+
if (n.type === 'subflowNode') return false
2846+
}
27922847
return true
27932848
})
27942849

27952850
if (validNodes.length > 0) {
2851+
// Use boundary edge logic - only remove edges crossing the boundary
2852+
const movingNodeIds = new Set(validNodes.map((n: Node) => n.id))
2853+
const boundaryEdges = edgesForDisplay.filter((e) => {
2854+
const sourceInSelection = movingNodeIds.has(e.source)
2855+
const targetInSelection = movingNodeIds.has(e.target)
2856+
return sourceInSelection !== targetInSelection
2857+
})
2858+
27962859
const updates = validNodes.map((n: Node) => {
2797-
const edgesToRemove = edgesForDisplay.filter(
2860+
const edgesForThisNode = boundaryEdges.filter(
27982861
(e) => e.source === n.id || e.target === n.id
27992862
)
28002863
return {
28012864
blockId: n.id,
28022865
newParentId: potentialParentId,
2803-
affectedEdges: edgesToRemove,
2866+
affectedEdges: edgesForThisNode,
28042867
}
28052868
})
28062869

28072870
collaborativeBatchUpdateParent(updates)
28082871

2809-
logger.info('Batch moved selection into subflow', {
2872+
logger.info('Batch moved selection to new parent', {
28102873
targetParentId: potentialParentId,
28112874
nodeCount: validNodes.length,
28122875
})
@@ -2824,7 +2887,6 @@ const WorkflowContent = React.memo(() => {
28242887
collaborativeBatchUpdatePositions,
28252888
collaborativeBatchUpdateParent,
28262889
potentialParentId,
2827-
dragStartParentId,
28282890
edgesForDisplay,
28292891
clearDragHighlights,
28302892
]
@@ -2909,7 +2971,6 @@ const WorkflowContent = React.memo(() => {
29092971

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

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

0 commit comments

Comments
 (0)