From 2c2b485f818d9a356dfd49c1586c753758799672 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 28 Jan 2026 12:31:38 -0800 Subject: [PATCH] fix(workflow): update container dimensions on keyboard movement (#3043) * fix(workflow): update container dimensions on keyboard movement * fix(workflow): avoid duplicate container updates during drag Add !change.dragging check to only handle keyboard movements in onNodesChange, since mouse drags are already handled by onNodeDrag. * fix(workflow): persist keyboard movements to backend Keyboard arrow key movements now call collaborativeBatchUpdatePositions to sync position changes to the backend for persistence and real-time collaboration. * improvement(cmdk): refactor search modal to use cmdk + fix icon SVG IDs (#3044) * improvement(cmdk): refactor search modal to use cmdk + fix icon SVG IDs * chore: remove unrelated workflow.tsx changes * chore: remove comments * chore: add devtools middleware to search modal store * fix: allow search data re-initialization when permissions change * fix: include keywords in search filter + show service name in tool operations * fix: correct filterBlocks type signature * fix: move generic to function parameter position * fix(mcp): correct event handler type for onInput * perf: always render command palette for instant opening * fix: clear search input when modal reopens * fix(helm): move rotationPolicy under privateKey for cert-manager compatibility (#3046) * fix(helm): move rotationPolicy under privateKey for cert-manager compatibility * docs(helm): add reclaimPolicy Retain guidance for production database storage * fix(helm): prevent empty branding ConfigMap creation * fix(workflow): avoid duplicate position updates on drag end Check isInDragOperation before persisting in onNodesChange to prevent duplicate calls. Drag-end events have dragStartPosition still set, while keyboard movements don't, allowing proper distinction. --- .../[workspaceId]/w/[workflowId]/workflow.tsx | 84 ++++++++++++------- 1 file changed, 56 insertions(+), 28 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 22fe3c8ce..690aad48c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -2302,33 +2302,12 @@ const WorkflowContent = React.memo(() => { window.removeEventListener('remove-from-subflow', handleRemoveFromSubflow as EventListener) }, [blocks, edgesForDisplay, getNodeAbsolutePosition, collaborativeBatchUpdateParent]) - /** Handles node changes - applies changes and resolves parent-child selection conflicts. */ - const onNodesChange = useCallback( - (changes: NodeChange[]) => { - selectedIdsRef.current = null - setDisplayNodes((nds) => { - const updated = applyNodeChanges(changes, nds) - const hasSelectionChange = changes.some((c) => c.type === 'select') - if (!hasSelectionChange) return updated - const resolved = resolveParentChildSelectionConflicts(updated, blocks) - selectedIdsRef.current = resolved.filter((node) => node.selected).map((node) => node.id) - return resolved - }) - const selectedIds = selectedIdsRef.current as string[] | null - if (selectedIds !== null) { - syncPanelWithSelection(selectedIds) - } - }, - [blocks] - ) - /** - * Updates container dimensions in displayNodes during drag. - * This allows live resizing of containers as their children are dragged. + * Updates container dimensions in displayNodes during drag or keyboard movement. */ - const updateContainerDimensionsDuringDrag = useCallback( - (draggedNodeId: string, draggedNodePosition: { x: number; y: number }) => { - const parentId = blocks[draggedNodeId]?.data?.parentId + const updateContainerDimensionsDuringMove = useCallback( + (movedNodeId: string, movedNodePosition: { x: number; y: number }) => { + const parentId = blocks[movedNodeId]?.data?.parentId if (!parentId) return setDisplayNodes((currentNodes) => { @@ -2336,7 +2315,7 @@ const WorkflowContent = React.memo(() => { if (childNodes.length === 0) return currentNodes const childPositions = childNodes.map((node) => { - const nodePosition = node.id === draggedNodeId ? draggedNodePosition : node.position + const nodePosition = node.id === movedNodeId ? movedNodePosition : node.position const { width, height } = getBlockDimensions(node.id) return { x: nodePosition.x, y: nodePosition.y, width, height } }) @@ -2367,6 +2346,55 @@ const WorkflowContent = React.memo(() => { [blocks, getBlockDimensions] ) + /** Handles node changes - applies changes and resolves parent-child selection conflicts. */ + const onNodesChange = useCallback( + (changes: NodeChange[]) => { + selectedIdsRef.current = null + setDisplayNodes((nds) => { + const updated = applyNodeChanges(changes, nds) + const hasSelectionChange = changes.some((c) => c.type === 'select') + if (!hasSelectionChange) return updated + const resolved = resolveParentChildSelectionConflicts(updated, blocks) + selectedIdsRef.current = resolved.filter((node) => node.selected).map((node) => node.id) + return resolved + }) + const selectedIds = selectedIdsRef.current as string[] | null + if (selectedIds !== null) { + syncPanelWithSelection(selectedIds) + } + + // Handle position changes (e.g., from keyboard arrow key movement) + // Update container dimensions when child nodes are moved and persist to backend + // Only persist if not in a drag operation (drag-end is handled by onNodeDragStop) + const isInDragOperation = + getDragStartPosition() !== null || multiNodeDragStartRef.current.size > 0 + const keyboardPositionUpdates: Array<{ id: string; position: { x: number; y: number } }> = [] + for (const change of changes) { + if ( + change.type === 'position' && + !change.dragging && + 'position' in change && + change.position + ) { + updateContainerDimensionsDuringMove(change.id, change.position) + if (!isInDragOperation) { + keyboardPositionUpdates.push({ id: change.id, position: change.position }) + } + } + } + // Persist keyboard movements to backend for collaboration sync + if (keyboardPositionUpdates.length > 0) { + collaborativeBatchUpdatePositions(keyboardPositionUpdates) + } + }, + [ + blocks, + updateContainerDimensionsDuringMove, + collaborativeBatchUpdatePositions, + getDragStartPosition, + ] + ) + /** * Effect to resize loops when nodes change (add/remove/position change). * Runs on structural changes only - not during drag (position-only changes). @@ -2611,7 +2639,7 @@ const WorkflowContent = React.memo(() => { // If the node is inside a container, update container dimensions during drag if (currentParentId) { - updateContainerDimensionsDuringDrag(node.id, node.position) + updateContainerDimensionsDuringMove(node.id, node.position) } // Check if this is a starter block - starter blocks should never be in containers @@ -2728,7 +2756,7 @@ const WorkflowContent = React.memo(() => { blocks, getNodeAbsolutePosition, getNodeDepth, - updateContainerDimensionsDuringDrag, + updateContainerDimensionsDuringMove, highlightContainerNode, ] )