mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
keep edges on subflow actions intact
This commit is contained in:
@@ -51,9 +51,11 @@
|
||||
border: 1px solid var(--brand-secondary) !important;
|
||||
}
|
||||
|
||||
.react-flow__nodesselection-rect {
|
||||
.react-flow__nodesselection-rect,
|
||||
.react-flow__nodesselection {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export {
|
||||
clearDragHighlights,
|
||||
computeClampedPositionUpdates,
|
||||
computeParentUpdateEntries,
|
||||
getClampedPositionForNode,
|
||||
isInEditableElement,
|
||||
selectNodesDeferred,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Node } from 'reactflow'
|
||||
import type { Edge, Node } from 'reactflow'
|
||||
import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
|
||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||
import { clampPositionToContainer } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities'
|
||||
@@ -139,3 +139,43 @@ export function computeClampedPositionUpdates(
|
||||
position: getClampedPositionForNode(node.id, node.position, blocks, allNodes),
|
||||
}))
|
||||
}
|
||||
|
||||
interface ParentUpdateEntry {
|
||||
blockId: string
|
||||
newParentId: string
|
||||
affectedEdges: Edge[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes parent update entries for nodes being moved into a subflow.
|
||||
* Only includes "boundary edges" - edges that cross the selection boundary
|
||||
* (one end inside selection, one end outside). Edges between nodes in the
|
||||
* selection are preserved.
|
||||
*/
|
||||
export function computeParentUpdateEntries(
|
||||
validNodes: Node[],
|
||||
allEdges: Edge[],
|
||||
targetParentId: string
|
||||
): ParentUpdateEntry[] {
|
||||
const movingNodeIds = new Set(validNodes.map((n) => n.id))
|
||||
|
||||
// Find edges that cross the boundary (one end inside selection, one end outside)
|
||||
// Edges between nodes in the selection should stay intact
|
||||
const boundaryEdges = allEdges.filter((e) => {
|
||||
const sourceInSelection = movingNodeIds.has(e.source)
|
||||
const targetInSelection = movingNodeIds.has(e.target)
|
||||
// Only remove if exactly one end is in the selection (crosses boundary)
|
||||
return sourceInSelection !== targetInSelection
|
||||
})
|
||||
|
||||
// Build updates for all valid nodes
|
||||
return validNodes.map((n) => {
|
||||
// Only include boundary edges connected to this specific node
|
||||
const edgesForThisNode = boundaryEdges.filter((e) => e.source === n.id || e.target === n.id)
|
||||
return {
|
||||
blockId: n.id,
|
||||
newParentId: targetParentId,
|
||||
affectedEdges: edgesForThisNode,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2273,9 +2273,6 @@ const WorkflowContent = React.memo(() => {
|
||||
// Only consider container nodes that aren't the dragged node
|
||||
if (n.type !== 'subflowNode' || n.id === node.id) return false
|
||||
|
||||
// Skip if this container is already the parent of the node being dragged
|
||||
if (n.id === currentParentId) return false
|
||||
|
||||
// Get the container's absolute position
|
||||
const containerAbsolutePos = getNodeAbsolutePosition(n.id)
|
||||
|
||||
@@ -2426,37 +2423,56 @@ const WorkflowContent = React.memo(() => {
|
||||
previousPositions: multiNodeDragStartRef.current,
|
||||
})
|
||||
|
||||
// Process parent updates for all selected nodes if dropping into a subflow
|
||||
if (potentialParentId && potentialParentId !== dragStartParentId) {
|
||||
// Filter out nodes that cannot be moved into subflows
|
||||
const validNodes = selectedNodes.filter((n) => {
|
||||
const block = blocks[n.id]
|
||||
if (!block) return false
|
||||
// Starter blocks cannot be in containers
|
||||
if (n.data?.type === 'starter') return false
|
||||
// Trigger blocks cannot be in containers
|
||||
if (TriggerUtils.isTriggerBlock(block)) return false
|
||||
// Subflow nodes (loop/parallel) cannot be nested
|
||||
if (n.type === 'subflowNode') return false
|
||||
// Process parent updates for nodes whose parent is changing
|
||||
// Check each node individually - don't rely on dragStartParentId since
|
||||
// multi-node selections can contain nodes from different parents
|
||||
const selectedNodeIds = new Set(selectedNodes.map((n) => n.id))
|
||||
const nodesNeedingParentUpdate = selectedNodes.filter((n) => {
|
||||
const block = blocks[n.id]
|
||||
if (!block) return false
|
||||
const currentParent = block.data?.parentId || null
|
||||
// Skip if the node's parent is also being moved (keep children with their parent)
|
||||
if (currentParent && selectedNodeIds.has(currentParent)) return false
|
||||
// Node needs update if current parent !== target parent
|
||||
return currentParent !== potentialParentId
|
||||
})
|
||||
|
||||
if (nodesNeedingParentUpdate.length > 0) {
|
||||
// Filter out nodes that cannot be moved into subflows (when target is a subflow)
|
||||
const validNodes = nodesNeedingParentUpdate.filter((n) => {
|
||||
// These restrictions only apply when moving INTO a subflow
|
||||
if (potentialParentId) {
|
||||
if (n.data?.type === 'starter') return false
|
||||
const block = blocks[n.id]
|
||||
if (block && TriggerUtils.isTriggerBlock(block)) return false
|
||||
if (n.type === 'subflowNode') return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if (validNodes.length > 0) {
|
||||
// Build updates for all valid nodes
|
||||
// Use boundary edge logic - only remove edges crossing the boundary
|
||||
const movingNodeIds = new Set(validNodes.map((n) => n.id))
|
||||
const boundaryEdges = edgesForDisplay.filter((e) => {
|
||||
const sourceInSelection = movingNodeIds.has(e.source)
|
||||
const targetInSelection = movingNodeIds.has(e.target)
|
||||
return sourceInSelection !== targetInSelection
|
||||
})
|
||||
|
||||
const updates = validNodes.map((n) => {
|
||||
const edgesToRemove = edgesForDisplay.filter(
|
||||
const edgesForThisNode = boundaryEdges.filter(
|
||||
(e) => e.source === n.id || e.target === n.id
|
||||
)
|
||||
return {
|
||||
blockId: n.id,
|
||||
newParentId: potentialParentId,
|
||||
affectedEdges: edgesToRemove,
|
||||
affectedEdges: edgesForThisNode,
|
||||
}
|
||||
})
|
||||
|
||||
collaborativeBatchUpdateParent(updates)
|
||||
|
||||
logger.info('Batch moved nodes into subflow', {
|
||||
logger.info('Batch moved nodes to new parent', {
|
||||
targetParentId: potentialParentId,
|
||||
nodeCount: validNodes.length,
|
||||
})
|
||||
@@ -2584,6 +2600,30 @@ const WorkflowContent = React.memo(() => {
|
||||
edgesToAdd.forEach((edge) => addEdge(edge))
|
||||
|
||||
window.dispatchEvent(new CustomEvent('skip-edge-recording', { detail: { skip: false } }))
|
||||
} else if (!potentialParentId && dragStartParentId) {
|
||||
// Moving OUT of a subflow to canvas
|
||||
// Remove edges connected to this node since it's leaving its parent
|
||||
const edgesToRemove = edgesForDisplay.filter(
|
||||
(e) => e.source === node.id || e.target === node.id
|
||||
)
|
||||
|
||||
if (edgesToRemove.length > 0) {
|
||||
removeEdgesForNode(node.id, edgesToRemove)
|
||||
|
||||
logger.info('Removed edges when moving node out of subflow', {
|
||||
blockId: node.id,
|
||||
sourceParentId: dragStartParentId,
|
||||
edgeCount: edgesToRemove.length,
|
||||
})
|
||||
}
|
||||
|
||||
// Clear the parent relationship
|
||||
updateNodeParent(node.id, null, edgesToRemove)
|
||||
|
||||
logger.info('Moved node out of subflow', {
|
||||
blockId: node.id,
|
||||
sourceParentId: dragStartParentId,
|
||||
})
|
||||
}
|
||||
|
||||
// Reset state
|
||||
@@ -2780,33 +2820,56 @@ const WorkflowContent = React.memo(() => {
|
||||
previousPositions: multiNodeDragStartRef.current,
|
||||
})
|
||||
|
||||
// Process parent updates if dropping into a subflow
|
||||
if (potentialParentId && potentialParentId !== dragStartParentId) {
|
||||
// Filter out nodes that cannot be moved into subflows
|
||||
const validNodes = nodes.filter((n: Node) => {
|
||||
const block = blocks[n.id]
|
||||
if (!block) return false
|
||||
if (n.data?.type === 'starter') return false
|
||||
if (TriggerUtils.isTriggerBlock(block)) return false
|
||||
if (n.type === 'subflowNode') return false
|
||||
// Process parent updates for nodes whose parent is changing
|
||||
// Check each node individually - don't rely on dragStartParentId since
|
||||
// multi-node selections can contain nodes from different parents
|
||||
const selectedNodeIds = new Set(nodes.map((n: Node) => n.id))
|
||||
const nodesNeedingParentUpdate = nodes.filter((n: Node) => {
|
||||
const block = blocks[n.id]
|
||||
if (!block) return false
|
||||
const currentParent = block.data?.parentId || null
|
||||
// Skip if the node's parent is also being moved (keep children with their parent)
|
||||
if (currentParent && selectedNodeIds.has(currentParent)) return false
|
||||
// Node needs update if current parent !== target parent
|
||||
return currentParent !== potentialParentId
|
||||
})
|
||||
|
||||
if (nodesNeedingParentUpdate.length > 0) {
|
||||
// Filter out nodes that cannot be moved into subflows (when target is a subflow)
|
||||
const validNodes = nodesNeedingParentUpdate.filter((n: Node) => {
|
||||
// These restrictions only apply when moving INTO a subflow
|
||||
if (potentialParentId) {
|
||||
if (n.data?.type === 'starter') return false
|
||||
const block = blocks[n.id]
|
||||
if (block && TriggerUtils.isTriggerBlock(block)) return false
|
||||
if (n.type === 'subflowNode') return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if (validNodes.length > 0) {
|
||||
// Use boundary edge logic - only remove edges crossing the boundary
|
||||
const movingNodeIds = new Set(validNodes.map((n: Node) => n.id))
|
||||
const boundaryEdges = edgesForDisplay.filter((e) => {
|
||||
const sourceInSelection = movingNodeIds.has(e.source)
|
||||
const targetInSelection = movingNodeIds.has(e.target)
|
||||
return sourceInSelection !== targetInSelection
|
||||
})
|
||||
|
||||
const updates = validNodes.map((n: Node) => {
|
||||
const edgesToRemove = edgesForDisplay.filter(
|
||||
const edgesForThisNode = boundaryEdges.filter(
|
||||
(e) => e.source === n.id || e.target === n.id
|
||||
)
|
||||
return {
|
||||
blockId: n.id,
|
||||
newParentId: potentialParentId,
|
||||
affectedEdges: edgesToRemove,
|
||||
affectedEdges: edgesForThisNode,
|
||||
}
|
||||
})
|
||||
|
||||
collaborativeBatchUpdateParent(updates)
|
||||
|
||||
logger.info('Batch moved selection into subflow', {
|
||||
logger.info('Batch moved selection to new parent', {
|
||||
targetParentId: potentialParentId,
|
||||
nodeCount: validNodes.length,
|
||||
})
|
||||
@@ -2824,7 +2887,6 @@ const WorkflowContent = React.memo(() => {
|
||||
collaborativeBatchUpdatePositions,
|
||||
collaborativeBatchUpdateParent,
|
||||
potentialParentId,
|
||||
dragStartParentId,
|
||||
edgesForDisplay,
|
||||
clearDragHighlights,
|
||||
]
|
||||
@@ -2909,7 +2971,6 @@ const WorkflowContent = React.memo(() => {
|
||||
|
||||
/** Transforms edges to include selection state and delete handlers. Memoized to prevent re-renders. */
|
||||
const edgesWithSelection = useMemo(() => {
|
||||
// Build node lookup map once - O(n) instead of O(n) per edge
|
||||
const nodeMap = new Map(displayNodes.map((n) => [n.id, n]))
|
||||
|
||||
return edgesForDisplay.map((edge) => {
|
||||
|
||||
Reference in New Issue
Block a user