Skip to content

Commit 4fa6cb8

Browse files
committed
fix subflow resizing
1 parent 20d66b9 commit 4fa6cb8

File tree

3 files changed

+119
-45
lines changed

3 files changed

+119
-45
lines changed

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

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
33
import { useReactFlow } from 'reactflow'
44
import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
55
import { getBlock } from '@/blocks/registry'
6+
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
67

78
const logger = createLogger('NodeUtilities')
89

@@ -208,28 +209,30 @@ export function useNodeUtilities(blocks: Record<string, any>) {
208209
* to the content area bounds (after header and padding).
209210
* @param nodeId ID of the node being repositioned
210211
* @param newParentId ID of the new parent
212+
* @param skipClamping If true, returns raw relative position without clamping to container bounds
211213
* @returns Relative position coordinates {x, y} within the parent
212214
*/
213215
const calculateRelativePosition = useCallback(
214-
(nodeId: string, newParentId: string): { x: number; y: number } => {
216+
(nodeId: string, newParentId: string, skipClamping?: boolean): { x: number; y: number } => {
215217
const nodeAbsPos = getNodeAbsolutePosition(nodeId)
216218
const parentAbsPos = getNodeAbsolutePosition(newParentId)
217-
const parentNode = getNodes().find((n) => n.id === newParentId)
218219

219-
// Calculate raw relative position (relative to parent origin)
220220
const rawPosition = {
221221
x: nodeAbsPos.x - parentAbsPos.x,
222222
y: nodeAbsPos.y - parentAbsPos.y,
223223
}
224224

225-
// Get container and block dimensions
225+
if (skipClamping) {
226+
return rawPosition
227+
}
228+
229+
const parentNode = getNodes().find((n) => n.id === newParentId)
226230
const containerDimensions = {
227231
width: parentNode?.data?.width || CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
228232
height: parentNode?.data?.height || CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
229233
}
230234
const blockDimensions = getBlockDimensions(nodeId)
231235

232-
// Clamp position to keep block inside content area
233236
return clampPositionToContainer(rawPosition, containerDimensions, blockDimensions)
234237
},
235238
[getNodeAbsolutePosition, getNodes, getBlockDimensions]
@@ -298,12 +301,12 @@ export function useNodeUtilities(blocks: Record<string, any>) {
298301
*/
299302
const calculateLoopDimensions = useCallback(
300303
(nodeId: string): { width: number; height: number } => {
301-
// Check both React Flow's node.parentId AND blocks store's data.parentId
302-
// This ensures we catch children even if React Flow hasn't re-rendered yet
303-
const childNodes = getNodes().filter(
304-
(node) => node.parentId === nodeId || blocks[node.id]?.data?.parentId === nodeId
304+
const currentBlocks = useWorkflowStore.getState().blocks
305+
const childBlockIds = Object.keys(currentBlocks).filter(
306+
(id) => currentBlocks[id]?.data?.parentId === nodeId
305307
)
306-
if (childNodes.length === 0) {
308+
309+
if (childBlockIds.length === 0) {
307310
return {
308311
width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
309312
height: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
@@ -313,30 +316,28 @@ export function useNodeUtilities(blocks: Record<string, any>) {
313316
let maxRight = 0
314317
let maxBottom = 0
315318

316-
childNodes.forEach((node) => {
317-
const { width: nodeWidth, height: nodeHeight } = getBlockDimensions(node.id)
318-
// Use ReactFlow's node.position which is already in the correct coordinate system
319-
// (relative to parent for child nodes). The store's block.position may be stale
320-
// or still in absolute coordinates during parent updates.
321-
maxRight = Math.max(maxRight, node.position.x + nodeWidth)
322-
maxBottom = Math.max(maxBottom, node.position.y + nodeHeight)
323-
})
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+
}
324328

325329
const width = Math.max(
326330
CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
327-
CONTAINER_DIMENSIONS.LEFT_PADDING + maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING
331+
maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING
328332
)
329333
const height = Math.max(
330334
CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
331-
CONTAINER_DIMENSIONS.HEADER_HEIGHT +
332-
CONTAINER_DIMENSIONS.TOP_PADDING +
333-
maxBottom +
334-
CONTAINER_DIMENSIONS.BOTTOM_PADDING
335+
maxBottom + CONTAINER_DIMENSIONS.BOTTOM_PADDING
335336
)
336337

337338
return { width, height }
338339
},
339-
[getNodes, getBlockDimensions, blocks]
340+
[getBlockDimensions]
340341
)
341342

342343
/**
@@ -345,29 +346,27 @@ export function useNodeUtilities(blocks: Record<string, any>) {
345346
*/
346347
const resizeLoopNodes = useCallback(
347348
(updateNodeDimensions: (id: string, dimensions: { width: number; height: number }) => void) => {
348-
const containerNodes = getNodes()
349-
.filter((node) => node.type && isContainerType(node.type))
350-
.map((node) => ({
351-
...node,
352-
depth: getNodeDepth(node.id),
349+
const currentBlocks = useWorkflowStore.getState().blocks
350+
const containerBlocks = Object.entries(currentBlocks)
351+
.filter(([, block]) => block?.type && isContainerType(block.type))
352+
.map(([id, block]) => ({
353+
id,
354+
block,
355+
depth: getNodeDepth(id),
353356
}))
354-
// Sort by depth descending - process innermost containers first
355-
// so their dimensions are correct when outer containers calculate sizes
356357
.sort((a, b) => b.depth - a.depth)
357358

358-
containerNodes.forEach((node) => {
359-
const dimensions = calculateLoopDimensions(node.id)
360-
// Get current dimensions from the blocks store rather than React Flow's potentially stale state
361-
const currentWidth = blocks[node.id]?.data?.width
362-
const currentHeight = blocks[node.id]?.data?.height
359+
for (const { id, block } of containerBlocks) {
360+
const dimensions = calculateLoopDimensions(id)
361+
const currentWidth = block?.data?.width
362+
const currentHeight = block?.data?.height
363363

364-
// Only update if dimensions actually changed to avoid unnecessary re-renders
365364
if (dimensions.width !== currentWidth || dimensions.height !== currentHeight) {
366-
updateNodeDimensions(node.id, dimensions)
365+
updateNodeDimensions(id, dimensions)
367366
}
368-
})
367+
}
369368
},
370-
[getNodes, isContainerType, getNodeDepth, calculateLoopDimensions, blocks]
369+
[isContainerType, getNodeDepth, calculateLoopDimensions]
371370
)
372371

373372
/**

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

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ const WorkflowContent = React.memo(() => {
303303
const {
304304
getNodeDepth,
305305
getNodeAbsolutePosition,
306+
calculateRelativePosition,
306307
isPointInLoopNode,
307308
resizeLoopNodes,
308309
updateNodeParent: updateNodeParentUtil,
@@ -2442,20 +2443,54 @@ const WorkflowContent = React.memo(() => {
24422443
})
24432444

24442445
if (validNodes.length > 0) {
2445-
// Build updates for all valid nodes
2446-
const updates = validNodes.map((n) => {
2446+
const rawUpdates = validNodes.map((n) => {
24472447
const edgesToRemove = edgesForDisplay.filter(
24482448
(e) => e.source === n.id || e.target === n.id
24492449
)
2450+
const newPosition = calculateRelativePosition(n.id, potentialParentId, true)
24502451
return {
24512452
blockId: n.id,
24522453
newParentId: potentialParentId,
2454+
newPosition,
24532455
affectedEdges: edgesToRemove,
24542456
}
24552457
})
24562458

2459+
const minX = Math.min(...rawUpdates.map((u) => u.newPosition.x))
2460+
const minY = Math.min(...rawUpdates.map((u) => u.newPosition.y))
2461+
2462+
const targetMinX = CONTAINER_DIMENSIONS.LEFT_PADDING
2463+
const targetMinY = CONTAINER_DIMENSIONS.HEADER_HEIGHT + CONTAINER_DIMENSIONS.TOP_PADDING
2464+
2465+
const shiftX = minX < targetMinX ? targetMinX - minX : 0
2466+
const shiftY = minY < targetMinY ? targetMinY - minY : 0
2467+
2468+
const updates = rawUpdates.map((u) => ({
2469+
...u,
2470+
newPosition: {
2471+
x: u.newPosition.x + shiftX,
2472+
y: u.newPosition.y + shiftY,
2473+
},
2474+
}))
2475+
24572476
collaborativeBatchUpdateParent(updates)
24582477

2478+
setDisplayNodes((nodes) =>
2479+
nodes.map((node) => {
2480+
const update = updates.find((u) => u.blockId === node.id)
2481+
if (update) {
2482+
return {
2483+
...node,
2484+
position: update.newPosition,
2485+
parentId: update.newParentId,
2486+
}
2487+
}
2488+
return node
2489+
})
2490+
)
2491+
2492+
resizeLoopNodesWrapper()
2493+
24592494
logger.info('Batch moved nodes into subflow', {
24602495
targetParentId: potentialParentId,
24612496
nodeCount: validNodes.length,
@@ -2601,6 +2636,8 @@ const WorkflowContent = React.memo(() => {
26012636
edgesForDisplay,
26022637
removeEdgesForNode,
26032638
getNodeAbsolutePosition,
2639+
calculateRelativePosition,
2640+
resizeLoopNodesWrapper,
26042641
getDragStartPosition,
26052642
setDragStartPosition,
26062643
addNotification,
@@ -2793,19 +2830,54 @@ const WorkflowContent = React.memo(() => {
27932830
})
27942831

27952832
if (validNodes.length > 0) {
2796-
const updates = validNodes.map((n: Node) => {
2833+
const rawUpdates = validNodes.map((n: Node) => {
27972834
const edgesToRemove = edgesForDisplay.filter(
27982835
(e) => e.source === n.id || e.target === n.id
27992836
)
2837+
const newPosition = calculateRelativePosition(n.id, potentialParentId, true)
28002838
return {
28012839
blockId: n.id,
28022840
newParentId: potentialParentId,
2841+
newPosition,
28032842
affectedEdges: edgesToRemove,
28042843
}
28052844
})
28062845

2846+
const minX = Math.min(...rawUpdates.map((u) => u.newPosition.x))
2847+
const minY = Math.min(...rawUpdates.map((u) => u.newPosition.y))
2848+
2849+
const targetMinX = CONTAINER_DIMENSIONS.LEFT_PADDING
2850+
const targetMinY = CONTAINER_DIMENSIONS.HEADER_HEIGHT + CONTAINER_DIMENSIONS.TOP_PADDING
2851+
2852+
const shiftX = minX < targetMinX ? targetMinX - minX : 0
2853+
const shiftY = minY < targetMinY ? targetMinY - minY : 0
2854+
2855+
const updates = rawUpdates.map((u) => ({
2856+
...u,
2857+
newPosition: {
2858+
x: u.newPosition.x + shiftX,
2859+
y: u.newPosition.y + shiftY,
2860+
},
2861+
}))
2862+
28072863
collaborativeBatchUpdateParent(updates)
28082864

2865+
setDisplayNodes((nodes) =>
2866+
nodes.map((node) => {
2867+
const update = updates.find((u) => u.blockId === node.id)
2868+
if (update) {
2869+
return {
2870+
...node,
2871+
position: update.newPosition,
2872+
parentId: update.newParentId,
2873+
}
2874+
}
2875+
return node
2876+
})
2877+
)
2878+
2879+
resizeLoopNodesWrapper()
2880+
28092881
logger.info('Batch moved selection into subflow', {
28102882
targetParentId: potentialParentId,
28112883
nodeCount: validNodes.length,
@@ -2823,6 +2895,8 @@ const WorkflowContent = React.memo(() => {
28232895
getNodes,
28242896
collaborativeBatchUpdatePositions,
28252897
collaborativeBatchUpdateParent,
2898+
calculateRelativePosition,
2899+
resizeLoopNodesWrapper,
28262900
potentialParentId,
28272901
dragStartParentId,
28282902
edgesForDisplay,

apps/sim/hooks/use-collaborative-workflow.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,7 @@ export function useCollaborativeWorkflow() {
929929
updates: Array<{
930930
blockId: string
931931
newParentId: string | null
932+
newPosition: { x: number; y: number }
932933
affectedEdges: Edge[]
933934
}>
934935
) => {
@@ -943,14 +944,13 @@ export function useCollaborativeWorkflow() {
943944
const block = workflowStore.blocks[u.blockId]
944945
const oldParentId = block?.data?.parentId
945946
const oldPosition = block?.position || { x: 0, y: 0 }
946-
const newPosition = oldPosition
947947

948948
return {
949949
blockId: u.blockId,
950950
oldParentId,
951951
newParentId: u.newParentId || undefined,
952952
oldPosition,
953-
newPosition,
953+
newPosition: u.newPosition,
954954
affectedEdges: u.affectedEdges,
955955
}
956956
})
@@ -959,6 +959,7 @@ export function useCollaborativeWorkflow() {
959959
if (update.affectedEdges.length > 0) {
960960
update.affectedEdges.forEach((e) => workflowStore.removeEdge(e.id))
961961
}
962+
workflowStore.updateBlockPosition(update.blockId, update.newPosition)
962963
if (update.newParentId) {
963964
workflowStore.updateParentId(update.blockId, update.newParentId, 'parent')
964965
}

0 commit comments

Comments
 (0)