From 4fa6cb84c6a95bfff7369a047631aa0921688bfc Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 8 Jan 2026 21:34:36 -0800 Subject: [PATCH] fix subflow resizing --- .../[workflowId]/hooks/use-node-utilities.ts | 79 +++++++++--------- .../[workspaceId]/w/[workflowId]/workflow.tsx | 80 ++++++++++++++++++- apps/sim/hooks/use-collaborative-workflow.ts | 5 +- 3 files changed, 119 insertions(+), 45 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts index 77fb9cd7c9..ffa148d881 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts @@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger' import { useReactFlow } from 'reactflow' import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions' import { getBlock } from '@/blocks/registry' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' const logger = createLogger('NodeUtilities') @@ -208,28 +209,30 @@ export function useNodeUtilities(blocks: Record) { * to the content area bounds (after header and padding). * @param nodeId ID of the node being repositioned * @param newParentId ID of the new parent + * @param skipClamping If true, returns raw relative position without clamping to container bounds * @returns Relative position coordinates {x, y} within the parent */ const calculateRelativePosition = useCallback( - (nodeId: string, newParentId: string): { x: number; y: number } => { + (nodeId: string, newParentId: string, skipClamping?: boolean): { x: number; y: number } => { const nodeAbsPos = getNodeAbsolutePosition(nodeId) const parentAbsPos = getNodeAbsolutePosition(newParentId) - const parentNode = getNodes().find((n) => n.id === newParentId) - // Calculate raw relative position (relative to parent origin) const rawPosition = { x: nodeAbsPos.x - parentAbsPos.x, y: nodeAbsPos.y - parentAbsPos.y, } - // Get container and block dimensions + if (skipClamping) { + return rawPosition + } + + const parentNode = getNodes().find((n) => n.id === newParentId) const containerDimensions = { width: parentNode?.data?.width || CONTAINER_DIMENSIONS.DEFAULT_WIDTH, height: parentNode?.data?.height || CONTAINER_DIMENSIONS.DEFAULT_HEIGHT, } const blockDimensions = getBlockDimensions(nodeId) - // Clamp position to keep block inside content area return clampPositionToContainer(rawPosition, containerDimensions, blockDimensions) }, [getNodeAbsolutePosition, getNodes, getBlockDimensions] @@ -298,12 +301,12 @@ export function useNodeUtilities(blocks: Record) { */ const calculateLoopDimensions = useCallback( (nodeId: string): { width: number; height: number } => { - // Check both React Flow's node.parentId AND blocks store's data.parentId - // This ensures we catch children even if React Flow hasn't re-rendered yet - const childNodes = getNodes().filter( - (node) => node.parentId === nodeId || blocks[node.id]?.data?.parentId === nodeId + const currentBlocks = useWorkflowStore.getState().blocks + const childBlockIds = Object.keys(currentBlocks).filter( + (id) => currentBlocks[id]?.data?.parentId === nodeId ) - if (childNodes.length === 0) { + + if (childBlockIds.length === 0) { return { width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH, height: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT, @@ -313,30 +316,28 @@ export function useNodeUtilities(blocks: Record) { let maxRight = 0 let maxBottom = 0 - childNodes.forEach((node) => { - const { width: nodeWidth, height: nodeHeight } = getBlockDimensions(node.id) - // Use ReactFlow's node.position which is already in the correct coordinate system - // (relative to parent for child nodes). The store's block.position may be stale - // or still in absolute coordinates during parent updates. - maxRight = Math.max(maxRight, node.position.x + nodeWidth) - maxBottom = Math.max(maxBottom, node.position.y + nodeHeight) - }) + for (const childId of childBlockIds) { + const child = currentBlocks[childId] + if (!child?.position) continue + + const { width: childWidth, height: childHeight } = getBlockDimensions(childId) + + maxRight = Math.max(maxRight, child.position.x + childWidth) + maxBottom = Math.max(maxBottom, child.position.y + childHeight) + } const width = Math.max( CONTAINER_DIMENSIONS.DEFAULT_WIDTH, - CONTAINER_DIMENSIONS.LEFT_PADDING + maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING + maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING ) const height = Math.max( CONTAINER_DIMENSIONS.DEFAULT_HEIGHT, - CONTAINER_DIMENSIONS.HEADER_HEIGHT + - CONTAINER_DIMENSIONS.TOP_PADDING + - maxBottom + - CONTAINER_DIMENSIONS.BOTTOM_PADDING + maxBottom + CONTAINER_DIMENSIONS.BOTTOM_PADDING ) return { width, height } }, - [getNodes, getBlockDimensions, blocks] + [getBlockDimensions] ) /** @@ -345,29 +346,27 @@ export function useNodeUtilities(blocks: Record) { */ const resizeLoopNodes = useCallback( (updateNodeDimensions: (id: string, dimensions: { width: number; height: number }) => void) => { - const containerNodes = getNodes() - .filter((node) => node.type && isContainerType(node.type)) - .map((node) => ({ - ...node, - depth: getNodeDepth(node.id), + const currentBlocks = useWorkflowStore.getState().blocks + const containerBlocks = Object.entries(currentBlocks) + .filter(([, block]) => block?.type && isContainerType(block.type)) + .map(([id, block]) => ({ + id, + block, + depth: getNodeDepth(id), })) - // Sort by depth descending - process innermost containers first - // so their dimensions are correct when outer containers calculate sizes .sort((a, b) => b.depth - a.depth) - containerNodes.forEach((node) => { - const dimensions = calculateLoopDimensions(node.id) - // Get current dimensions from the blocks store rather than React Flow's potentially stale state - const currentWidth = blocks[node.id]?.data?.width - const currentHeight = blocks[node.id]?.data?.height + for (const { id, block } of containerBlocks) { + const dimensions = calculateLoopDimensions(id) + const currentWidth = block?.data?.width + const currentHeight = block?.data?.height - // Only update if dimensions actually changed to avoid unnecessary re-renders if (dimensions.width !== currentWidth || dimensions.height !== currentHeight) { - updateNodeDimensions(node.id, dimensions) + updateNodeDimensions(id, dimensions) } - }) + } }, - [getNodes, isContainerType, getNodeDepth, calculateLoopDimensions, blocks] + [isContainerType, getNodeDepth, calculateLoopDimensions] ) /** diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index db0462a0ca..4b1ed19e4c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -303,6 +303,7 @@ const WorkflowContent = React.memo(() => { const { getNodeDepth, getNodeAbsolutePosition, + calculateRelativePosition, isPointInLoopNode, resizeLoopNodes, updateNodeParent: updateNodeParentUtil, @@ -2442,20 +2443,54 @@ const WorkflowContent = React.memo(() => { }) if (validNodes.length > 0) { - // Build updates for all valid nodes - const updates = validNodes.map((n) => { + const rawUpdates = validNodes.map((n) => { const edgesToRemove = edgesForDisplay.filter( (e) => e.source === n.id || e.target === n.id ) + const newPosition = calculateRelativePosition(n.id, potentialParentId, true) return { blockId: n.id, newParentId: potentialParentId, + newPosition, affectedEdges: edgesToRemove, } }) + const minX = Math.min(...rawUpdates.map((u) => u.newPosition.x)) + const minY = Math.min(...rawUpdates.map((u) => u.newPosition.y)) + + const targetMinX = CONTAINER_DIMENSIONS.LEFT_PADDING + const targetMinY = CONTAINER_DIMENSIONS.HEADER_HEIGHT + CONTAINER_DIMENSIONS.TOP_PADDING + + const shiftX = minX < targetMinX ? targetMinX - minX : 0 + const shiftY = minY < targetMinY ? targetMinY - minY : 0 + + const updates = rawUpdates.map((u) => ({ + ...u, + newPosition: { + x: u.newPosition.x + shiftX, + y: u.newPosition.y + shiftY, + }, + })) + collaborativeBatchUpdateParent(updates) + setDisplayNodes((nodes) => + nodes.map((node) => { + const update = updates.find((u) => u.blockId === node.id) + if (update) { + return { + ...node, + position: update.newPosition, + parentId: update.newParentId, + } + } + return node + }) + ) + + resizeLoopNodesWrapper() + logger.info('Batch moved nodes into subflow', { targetParentId: potentialParentId, nodeCount: validNodes.length, @@ -2601,6 +2636,8 @@ const WorkflowContent = React.memo(() => { edgesForDisplay, removeEdgesForNode, getNodeAbsolutePosition, + calculateRelativePosition, + resizeLoopNodesWrapper, getDragStartPosition, setDragStartPosition, addNotification, @@ -2793,19 +2830,54 @@ const WorkflowContent = React.memo(() => { }) if (validNodes.length > 0) { - const updates = validNodes.map((n: Node) => { + const rawUpdates = validNodes.map((n: Node) => { const edgesToRemove = edgesForDisplay.filter( (e) => e.source === n.id || e.target === n.id ) + const newPosition = calculateRelativePosition(n.id, potentialParentId, true) return { blockId: n.id, newParentId: potentialParentId, + newPosition, affectedEdges: edgesToRemove, } }) + const minX = Math.min(...rawUpdates.map((u) => u.newPosition.x)) + const minY = Math.min(...rawUpdates.map((u) => u.newPosition.y)) + + const targetMinX = CONTAINER_DIMENSIONS.LEFT_PADDING + const targetMinY = CONTAINER_DIMENSIONS.HEADER_HEIGHT + CONTAINER_DIMENSIONS.TOP_PADDING + + const shiftX = minX < targetMinX ? targetMinX - minX : 0 + const shiftY = minY < targetMinY ? targetMinY - minY : 0 + + const updates = rawUpdates.map((u) => ({ + ...u, + newPosition: { + x: u.newPosition.x + shiftX, + y: u.newPosition.y + shiftY, + }, + })) + collaborativeBatchUpdateParent(updates) + setDisplayNodes((nodes) => + nodes.map((node) => { + const update = updates.find((u) => u.blockId === node.id) + if (update) { + return { + ...node, + position: update.newPosition, + parentId: update.newParentId, + } + } + return node + }) + ) + + resizeLoopNodesWrapper() + logger.info('Batch moved selection into subflow', { targetParentId: potentialParentId, nodeCount: validNodes.length, @@ -2823,6 +2895,8 @@ const WorkflowContent = React.memo(() => { getNodes, collaborativeBatchUpdatePositions, collaborativeBatchUpdateParent, + calculateRelativePosition, + resizeLoopNodesWrapper, potentialParentId, dragStartParentId, edgesForDisplay, diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index a81ecaf670..2bd9ce819a 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -929,6 +929,7 @@ export function useCollaborativeWorkflow() { updates: Array<{ blockId: string newParentId: string | null + newPosition: { x: number; y: number } affectedEdges: Edge[] }> ) => { @@ -943,14 +944,13 @@ export function useCollaborativeWorkflow() { const block = workflowStore.blocks[u.blockId] const oldParentId = block?.data?.parentId const oldPosition = block?.position || { x: 0, y: 0 } - const newPosition = oldPosition return { blockId: u.blockId, oldParentId, newParentId: u.newParentId || undefined, oldPosition, - newPosition, + newPosition: u.newPosition, affectedEdges: u.affectedEdges, } }) @@ -959,6 +959,7 @@ export function useCollaborativeWorkflow() { if (update.affectedEdges.length > 0) { update.affectedEdges.forEach((e) => workflowStore.removeEdge(e.id)) } + workflowStore.updateBlockPosition(update.blockId, update.newPosition) if (update.newParentId) { workflowStore.updateParentId(update.blockId, update.newParentId, 'parent') }