Skip to content

Commit 4941b52

Browse files
authored
fix(resize): fix subflow resize on drag, children deselected in subflow on drag (#2771)
* fix(resize): fix subflow resize on drag, children deselected in subflow on drag * ack PR comments * fix copy-paste subflows deselecting children * ack comments
1 parent 7f18d96 commit 4941b52

File tree

4 files changed

+148
-105
lines changed

4 files changed

+148
-105
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ export {
44
computeParentUpdateEntries,
55
getClampedPositionForNode,
66
isInEditableElement,
7-
selectNodesDeferred,
7+
resolveParentChildSelectionConflicts,
88
validateTriggerPaste,
99
} from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers'
1010
export { useFloatBoundarySync, useFloatDrag, useFloatResize } from './float'
1111
export { useAutoLayout } from './use-auto-layout'
1212
export { BLOCK_DIMENSIONS, useBlockDimensions } from './use-block-dimensions'
1313
export { useBlockVisual } from './use-block-visual'
1414
export { type CurrentWorkflow, useCurrentWorkflow } from './use-current-workflow'
15-
export { useNodeUtilities } from './use-node-utilities'
15+
export { calculateContainerDimensions, useNodeUtilities } from './use-node-utilities'
1616
export { usePreventZoom } from './use-prevent-zoom'
1717
export { useScrollManagement } from './use-scroll-management'
1818
export { useWorkflowExecution } from './use-workflow-execution'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,47 @@ export function clampPositionToContainer(
6262
}
6363
}
6464

65+
/**
66+
* Calculates container dimensions based on child block positions.
67+
* Single source of truth for container sizing - ensures consistency between
68+
* live drag updates and final dimension calculations.
69+
*
70+
* @param childPositions - Array of child positions with their dimensions
71+
* @returns Calculated width and height for the container
72+
*/
73+
export function calculateContainerDimensions(
74+
childPositions: Array<{ x: number; y: number; width: number; height: number }>
75+
): { width: number; height: number } {
76+
if (childPositions.length === 0) {
77+
return {
78+
width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
79+
height: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
80+
}
81+
}
82+
83+
let maxRight = 0
84+
let maxBottom = 0
85+
86+
for (const child of childPositions) {
87+
maxRight = Math.max(maxRight, child.x + child.width)
88+
maxBottom = Math.max(maxBottom, child.y + child.height)
89+
}
90+
91+
const width = Math.max(
92+
CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
93+
CONTAINER_DIMENSIONS.LEFT_PADDING + maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING
94+
)
95+
const height = Math.max(
96+
CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
97+
CONTAINER_DIMENSIONS.HEADER_HEIGHT +
98+
CONTAINER_DIMENSIONS.TOP_PADDING +
99+
maxBottom +
100+
CONTAINER_DIMENSIONS.BOTTOM_PADDING
101+
)
102+
103+
return { width, height }
104+
}
105+
65106
/**
66107
* Hook providing utilities for node position, hierarchy, and dimension calculations
67108
*/
@@ -306,36 +347,16 @@ export function useNodeUtilities(blocks: Record<string, any>) {
306347
(id) => currentBlocks[id]?.data?.parentId === nodeId
307348
)
308349

309-
if (childBlockIds.length === 0) {
310-
return {
311-
width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
312-
height: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
313-
}
314-
}
315-
316-
let maxRight = 0
317-
let maxBottom = 0
318-
319-
for (const childId of childBlockIds) {
320-
const child = currentBlocks[childId]
321-
if (!child?.position) continue
322-
323-
const { width: childWidth, height: childHeight } = getBlockDimensions(childId)
324-
325-
maxRight = Math.max(maxRight, child.position.x + childWidth)
326-
maxBottom = Math.max(maxBottom, child.position.y + childHeight)
327-
}
328-
329-
const width = Math.max(
330-
CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
331-
maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING
332-
)
333-
const height = Math.max(
334-
CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
335-
maxBottom + CONTAINER_DIMENSIONS.BOTTOM_PADDING
336-
)
350+
const childPositions = childBlockIds
351+
.map((childId) => {
352+
const child = currentBlocks[childId]
353+
if (!child?.position) return null
354+
const { width, height } = getBlockDimensions(childId)
355+
return { x: child.position.x, y: child.position.y, width, height }
356+
})
357+
.filter((p): p is NonNullable<typeof p> => p !== null)
337358

338-
return { width, height }
359+
return calculateContainerDimensions(childPositions)
339360
},
340361
[getBlockDimensions]
341362
)

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

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -65,27 +65,6 @@ export function clearDragHighlights(): void {
6565
document.body.style.cursor = ''
6666
}
6767

68-
/**
69-
* Selects nodes by their IDs after paste/duplicate operations.
70-
* Defers selection to next animation frame to allow displayNodes to sync from store first.
71-
* This is necessary because the component uses controlled state (nodes={displayNodes})
72-
* and newly added blocks need time to propagate through the store → derivedNodes → displayNodes cycle.
73-
*/
74-
export function selectNodesDeferred(
75-
nodeIds: string[],
76-
setDisplayNodes: (updater: (nodes: Node[]) => Node[]) => void
77-
): void {
78-
const idsSet = new Set(nodeIds)
79-
requestAnimationFrame(() => {
80-
setDisplayNodes((nodes) =>
81-
nodes.map((node) => ({
82-
...node,
83-
selected: idsSet.has(node.id),
84-
}))
85-
)
86-
})
87-
}
88-
8968
interface BlockData {
9069
height?: number
9170
data?: {
@@ -186,3 +165,26 @@ export function computeParentUpdateEntries(
186165
}
187166
})
188167
}
168+
169+
/**
170+
* Resolves parent-child selection conflicts by deselecting children whose parent is also selected.
171+
*/
172+
export function resolveParentChildSelectionConflicts(
173+
nodes: Node[],
174+
blocks: Record<string, { data?: { parentId?: string } }>
175+
): Node[] {
176+
const selectedIds = new Set(nodes.filter((n) => n.selected).map((n) => n.id))
177+
178+
let hasConflict = false
179+
const resolved = nodes.map((n) => {
180+
if (!n.selected) return n
181+
const parentId = n.parentId || blocks[n.id]?.data?.parentId
182+
if (parentId && selectedIds.has(parentId)) {
183+
hasConflict = true
184+
return { ...n, selected: false }
185+
}
186+
return n
187+
})
188+
189+
return hasConflict ? resolved : nodes
190+
}

0 commit comments

Comments
 (0)