From c35c8d1f3102cb6b612e9d74abbb75d22db6dbdb Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 30 Sep 2025 13:24:19 -0700 Subject: [PATCH] improvement(autolayout): use live block heights / widths for autolayout to prevent overlaps (#1505) * improvement(autolayout): use live block heights / widths for autolayout to prevent overlaps * improve layering algo for multiple trigger setting * remove console logs * add type annotation --- .../api/workflows/[id]/autolayout/route.ts | 32 ++++++-- .../workflow-block/workflow-block.tsx | 22 ++++-- .../[workspaceId]/w/[workflowId]/utils.ts | 16 ++-- .../w/[workflowId]/utils/auto-layout.ts | 10 ++- .../lib/workflows/autolayout/containers.ts | 12 ++- .../lib/workflows/autolayout/incremental.ts | 16 ++-- apps/sim/lib/workflows/autolayout/index.ts | 5 +- apps/sim/lib/workflows/autolayout/layering.ts | 59 +++++++++------ .../lib/workflows/autolayout/positioning.ts | 14 ++-- apps/sim/lib/workflows/autolayout/types.ts | 10 ++- apps/sim/lib/workflows/autolayout/utils.ts | 75 ++++++++++++++++--- apps/sim/stores/workflows/workflow/store.ts | 43 ++++++++--- apps/sim/stores/workflows/workflow/types.ts | 8 +- 13 files changed, 227 insertions(+), 95 deletions(-) diff --git a/apps/sim/app/api/workflows/[id]/autolayout/route.ts b/apps/sim/app/api/workflows/[id]/autolayout/route.ts index d497f92df..5d9c89614 100644 --- a/apps/sim/app/api/workflows/[id]/autolayout/route.ts +++ b/apps/sim/app/api/workflows/[id]/autolayout/route.ts @@ -8,7 +8,10 @@ import { createLogger } from '@/lib/logs/console/logger' import { getUserEntityPermissions } from '@/lib/permissions/utils' import { generateRequestId } from '@/lib/utils' import { applyAutoLayout } from '@/lib/workflows/autolayout' -import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers' +import { + loadWorkflowFromNormalizedTables, + type NormalizedWorkflowData, +} from '@/lib/workflows/db-helpers' export const dynamic = 'force-dynamic' @@ -36,10 +39,14 @@ const AutoLayoutRequestSchema = z.object({ }) .optional() .default({}), + // Optional: if provided, use these blocks instead of loading from DB + // This allows using blocks with live measurements from the UI + blocks: z.record(z.any()).optional(), + edges: z.array(z.any()).optional(), + loops: z.record(z.any()).optional(), + parallels: z.record(z.any()).optional(), }) -type AutoLayoutRequest = z.infer - /** * POST /api/workflows/[id]/autolayout * Apply autolayout to an existing workflow @@ -108,8 +115,23 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } - // Load current workflow state - const currentWorkflowData = await loadWorkflowFromNormalizedTables(workflowId) + // Use provided blocks/edges if available (with live measurements from UI), + // otherwise load from database + let currentWorkflowData: NormalizedWorkflowData | null + + if (layoutOptions.blocks && layoutOptions.edges) { + logger.info(`[${requestId}] Using provided blocks with live measurements`) + currentWorkflowData = { + blocks: layoutOptions.blocks, + edges: layoutOptions.edges, + loops: layoutOptions.loops || {}, + parallels: layoutOptions.parallels || {}, + isFromNormalizedTables: false, + } + } else { + logger.info(`[${requestId}] Loading blocks from database`) + currentWorkflowData = await loadWorkflowFromNormalizedTables(workflowId) + } if (!currentWorkflowData) { logger.error(`[${requestId}] Could not load workflow ${workflowId} for autolayout`) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index c725e30b6..1cf40e9de 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -148,6 +148,7 @@ export function WorkflowBlock({ id, data }: NodeProps) { ) const storeIsWide = useWorkflowStore((state) => state.blocks[id]?.isWide ?? false) const storeBlockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0) + const storeBlockLayout = useWorkflowStore((state) => state.blocks[id]?.layout) const storeBlockAdvancedMode = useWorkflowStore( (state) => state.blocks[id]?.advancedMode ?? false ) @@ -168,6 +169,10 @@ export function WorkflowBlock({ id, data }: NodeProps) { ? (currentWorkflow.blocks[id]?.height ?? 0) : storeBlockHeight + const blockWidth = currentWorkflow.isDiffMode + ? (currentWorkflow.blocks[id]?.layout?.measuredWidth ?? 0) + : (storeBlockLayout?.measuredWidth ?? 0) + // Get per-block webhook status by checking if webhook is configured const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) @@ -240,7 +245,7 @@ export function WorkflowBlock({ id, data }: NodeProps) { }, [id, collaborativeSetSubblockValue]) // Workflow store actions - const updateBlockHeight = useWorkflowStore((state) => state.updateBlockHeight) + const updateBlockLayoutMetrics = useWorkflowStore((state) => state.updateBlockLayoutMetrics) // Execution store const isActiveBlock = useExecutionStore((state) => state.activeBlockIds.has(id)) @@ -419,9 +424,9 @@ export function WorkflowBlock({ id, data }: NodeProps) { if (!contentRef.current) return let rafId: number - const debouncedUpdate = debounce((height: number) => { - if (height !== blockHeight) { - updateBlockHeight(id, height) + const debouncedUpdate = debounce((dimensions: { width: number; height: number }) => { + if (dimensions.height !== blockHeight || dimensions.width !== blockWidth) { + updateBlockLayoutMetrics(id, dimensions) updateNodeInternals(id) } }, 100) @@ -435,9 +440,10 @@ export function WorkflowBlock({ id, data }: NodeProps) { // Schedule the update on the next animation frame rafId = requestAnimationFrame(() => { for (const entry of entries) { - const height = - entry.borderBoxSize[0]?.blockSize ?? entry.target.getBoundingClientRect().height - debouncedUpdate(height) + const rect = entry.target.getBoundingClientRect() + const height = entry.borderBoxSize[0]?.blockSize ?? rect.height + const width = entry.borderBoxSize[0]?.inlineSize ?? rect.width + debouncedUpdate({ width, height }) } }) }) @@ -450,7 +456,7 @@ export function WorkflowBlock({ id, data }: NodeProps) { cancelAnimationFrame(rafId) } } - }, [id, blockHeight, updateBlockHeight, updateNodeInternals, lastUpdate]) + }, [id, blockHeight, blockWidth, updateBlockLayoutMetrics, updateNodeInternals, lastUpdate]) // SubBlock layout management function groupSubBlocks(subBlocks: SubBlockConfig[], blockId: string) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils.ts index 6d0d9e974..5a3c008c3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils.ts @@ -98,18 +98,12 @@ const getBlockDimensions = ( } } - if (block.type === 'workflowBlock') { - const nodeWidth = block.data?.width || block.width - const nodeHeight = block.data?.height || block.height - - if (nodeWidth && nodeHeight) { - return { width: nodeWidth, height: nodeHeight } - } - } - return { - width: block.isWide ? 450 : block.data?.width || block.width || 350, - height: Math.max(block.height || block.data?.height || 150, 100), + width: block.layout?.measuredWidth || (block.isWide ? 450 : block.data?.width || 350), + height: Math.max( + block.layout?.measuredHeight || block.height || block.data?.height || 150, + 100 + ), } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout.ts index e393b09c2..49ff7b0f3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout.ts @@ -78,13 +78,19 @@ export async function applyAutoLayoutToWorkflow( }, } - // Call the autolayout API route which has access to the server-side API key + // Call the autolayout API route, sending blocks with live measurements const response = await fetch(`/api/workflows/${workflowId}/autolayout`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(layoutOptions), + body: JSON.stringify({ + ...layoutOptions, + blocks, + edges, + loops, + parallels, + }), }) if (!response.ok) { diff --git a/apps/sim/lib/workflows/autolayout/containers.ts b/apps/sim/lib/workflows/autolayout/containers.ts index 1c89a6580..637063522 100644 --- a/apps/sim/lib/workflows/autolayout/containers.ts +++ b/apps/sim/lib/workflows/autolayout/containers.ts @@ -3,7 +3,12 @@ import type { BlockState } from '@/stores/workflows/workflow/types' import { assignLayers, groupByLayer } from './layering' import { calculatePositions } from './positioning' import type { Edge, LayoutOptions } from './types' -import { DEFAULT_CONTAINER_HEIGHT, DEFAULT_CONTAINER_WIDTH, getBlocksByParent } from './utils' +import { + DEFAULT_CONTAINER_HEIGHT, + DEFAULT_CONTAINER_WIDTH, + getBlocksByParent, + prepareBlockMetrics, +} from './utils' const logger = createLogger('AutoLayout:Containers') @@ -45,6 +50,7 @@ export function layoutContainers( } const childNodes = assignLayers(childBlocks, childEdges) + prepareBlockMetrics(childNodes) const childLayers = groupByLayer(childNodes) calculatePositions(childLayers, containerOptions) @@ -57,8 +63,8 @@ export function layoutContainers( for (const node of childNodes.values()) { minX = Math.min(minX, node.position.x) minY = Math.min(minY, node.position.y) - maxX = Math.max(maxX, node.position.x + node.dimensions.width) - maxY = Math.max(maxY, node.position.y + node.dimensions.height) + maxX = Math.max(maxX, node.position.x + node.metrics.width) + maxY = Math.max(maxY, node.position.y + node.metrics.height) } // Adjust all child positions to start at proper padding from container edges diff --git a/apps/sim/lib/workflows/autolayout/incremental.ts b/apps/sim/lib/workflows/autolayout/incremental.ts index 8a8a5ba9d..c56c4f066 100644 --- a/apps/sim/lib/workflows/autolayout/incremental.ts +++ b/apps/sim/lib/workflows/autolayout/incremental.ts @@ -1,7 +1,7 @@ import { createLogger } from '@/lib/logs/console/logger' import type { BlockState } from '@/stores/workflows/workflow/types' import type { AdjustmentOptions, Edge } from './types' -import { boxesOverlap, createBoundingBox, getBlockDimensions } from './utils' +import { boxesOverlap, createBoundingBox, getBlockMetrics } from './utils' const logger = createLogger('AutoLayout:Incremental') @@ -70,8 +70,8 @@ export function adjustForNewBlock( }) } - const newBlockDims = getBlockDimensions(newBlock) - const newBlockBox = createBoundingBox(newBlock.position, newBlockDims) + const newBlockMetrics = getBlockMetrics(newBlock) + const newBlockBox = createBoundingBox(newBlock.position, newBlockMetrics) const blocksToShift: Array<{ block: BlockState; shiftAmount: number }> = [] @@ -80,11 +80,11 @@ export function adjustForNewBlock( if (block.data?.parentId) continue if (block.position.x >= newBlock.position.x) { - const blockDims = getBlockDimensions(block) - const blockBox = createBoundingBox(block.position, blockDims) + const blockMetrics = getBlockMetrics(block) + const blockBox = createBoundingBox(block.position, blockMetrics) if (boxesOverlap(newBlockBox, blockBox, 50)) { - const requiredShift = newBlock.position.x + newBlockDims.width + 50 - block.position.x + const requiredShift = newBlock.position.x + newBlockMetrics.width + 50 - block.position.x if (requiredShift > 0) { blocksToShift.push({ block, shiftAmount: requiredShift }) } @@ -115,8 +115,8 @@ export function compactHorizontally(blocks: Record, edges: E const prevBlock = blockArray[i - 1] const currentBlock = blockArray[i] - const prevDims = getBlockDimensions(prevBlock) - const expectedX = prevBlock.position.x + prevDims.width + MIN_SPACING + const prevMetrics = getBlockMetrics(prevBlock) + const expectedX = prevBlock.position.x + prevMetrics.width + MIN_SPACING if (currentBlock.position.x > expectedX + 150) { const shift = currentBlock.position.x - expectedX diff --git a/apps/sim/lib/workflows/autolayout/index.ts b/apps/sim/lib/workflows/autolayout/index.ts index fb9b42363..a13ad8a42 100644 --- a/apps/sim/lib/workflows/autolayout/index.ts +++ b/apps/sim/lib/workflows/autolayout/index.ts @@ -5,7 +5,7 @@ import { adjustForNewBlock as adjustForNewBlockInternal, compactHorizontally } f import { assignLayers, groupByLayer } from './layering' import { calculatePositions } from './positioning' import type { AdjustmentOptions, Edge, LayoutOptions, LayoutResult, Loop, Parallel } from './types' -import { getBlocksByParent } from './utils' +import { getBlocksByParent, prepareBlockMetrics } from './utils' const logger = createLogger('AutoLayout') @@ -39,6 +39,7 @@ export function applyAutoLayout( if (Object.keys(rootBlocks).length > 0) { const nodes = assignLayers(rootBlocks, rootEdges) + prepareBlockMetrics(nodes) const layers = groupByLayer(nodes) calculatePositions(layers, options) @@ -99,4 +100,4 @@ export function adjustForNewBlock( } export type { LayoutOptions, LayoutResult, AdjustmentOptions, Edge, Loop, Parallel } -export { getBlockDimensions, isContainerType } from './utils' +export { getBlockMetrics, isContainerType } from './utils' diff --git a/apps/sim/lib/workflows/autolayout/layering.ts b/apps/sim/lib/workflows/autolayout/layering.ts index 10ad8b317..59a1d14bf 100644 --- a/apps/sim/lib/workflows/autolayout/layering.ts +++ b/apps/sim/lib/workflows/autolayout/layering.ts @@ -1,7 +1,7 @@ import { createLogger } from '@/lib/logs/console/logger' import type { BlockState } from '@/stores/workflows/workflow/types' import type { Edge, GraphNode } from './types' -import { getBlockDimensions, isStarterBlock } from './utils' +import { getBlockMetrics } from './utils' const logger = createLogger('AutoLayout:Layering') @@ -15,7 +15,7 @@ export function assignLayers( nodes.set(id, { id, block, - dimensions: getBlockDimensions(block), + metrics: getBlockMetrics(block), incoming: new Set(), outgoing: new Set(), layer: 0, @@ -33,9 +33,9 @@ export function assignLayers( } } - const starterNodes = Array.from(nodes.values()).filter( - (node) => node.incoming.size === 0 || isStarterBlock(node.block) - ) + // Only treat blocks as starters if they have no incoming edges + // This prevents triggers that are mid-flow from being forced to layer 0 + const starterNodes = Array.from(nodes.values()).filter((node) => node.incoming.size === 0) if (starterNodes.length === 0 && nodes.size > 0) { const firstNode = Array.from(nodes.values())[0] @@ -43,35 +43,50 @@ export function assignLayers( logger.warn('No starter blocks found, using first block as starter', { blockId: firstNode.id }) } - const visited = new Set() - const queue: Array<{ nodeId: string; layer: number }> = [] + // Use topological sort to ensure proper layering based on dependencies + // Each node's layer = max(all incoming nodes' layers) + 1 + const inDegreeCount = new Map() - for (const starter of starterNodes) { - starter.layer = 0 - queue.push({ nodeId: starter.id, layer: 0 }) + for (const node of nodes.values()) { + inDegreeCount.set(node.id, node.incoming.size) + if (starterNodes.includes(node)) { + node.layer = 0 + } } - while (queue.length > 0) { - const { nodeId, layer } = queue.shift()! + const queue: string[] = starterNodes.map((n) => n.id) + const processed = new Set() - if (visited.has(nodeId)) { - continue + while (queue.length > 0) { + const nodeId = queue.shift()! + const node = nodes.get(nodeId)! + processed.add(nodeId) + + // Calculate this node's layer based on all incoming edges + if (node.incoming.size > 0) { + let maxIncomingLayer = -1 + for (const incomingId of node.incoming) { + const incomingNode = nodes.get(incomingId) + if (incomingNode) { + maxIncomingLayer = Math.max(maxIncomingLayer, incomingNode.layer) + } + } + node.layer = maxIncomingLayer + 1 } - visited.add(nodeId) - const node = nodes.get(nodeId)! - node.layer = Math.max(node.layer, layer) - + // Add outgoing nodes to queue when all their dependencies are processed for (const targetId of node.outgoing) { - const targetNode = nodes.get(targetId) - if (targetNode) { - queue.push({ nodeId: targetId, layer: layer + 1 }) + const currentCount = inDegreeCount.get(targetId) || 0 + inDegreeCount.set(targetId, currentCount - 1) + + if (inDegreeCount.get(targetId) === 0 && !processed.has(targetId)) { + queue.push(targetId) } } } for (const node of nodes.values()) { - if (!visited.has(node.id)) { + if (!processed.has(node.id)) { logger.debug('Isolated node detected, assigning to layer 0', { blockId: node.id }) node.layer = 0 } diff --git a/apps/sim/lib/workflows/autolayout/positioning.ts b/apps/sim/lib/workflows/autolayout/positioning.ts index 1a1c1981c..741d1197e 100644 --- a/apps/sim/lib/workflows/autolayout/positioning.ts +++ b/apps/sim/lib/workflows/autolayout/positioning.ts @@ -26,7 +26,7 @@ export function calculatePositions( // Calculate total height needed for this layer const totalHeight = nodesInLayer.reduce( - (sum, node, idx) => sum + node.dimensions.height + (idx > 0 ? verticalSpacing : 0), + (sum, node, idx) => sum + node.metrics.height + (idx > 0 ? verticalSpacing : 0), 0 ) @@ -55,7 +55,7 @@ export function calculatePositions( y: yOffset, } - yOffset += node.dimensions.height + verticalSpacing + yOffset += node.metrics.height + verticalSpacing } } @@ -83,8 +83,8 @@ function resolveOverlaps(nodes: GraphNode[], verticalSpacing: number): void { const node1 = sortedNodes[i] const node2 = sortedNodes[j] - const box1 = createBoundingBox(node1.position, node1.dimensions) - const box2 = createBoundingBox(node2.position, node2.dimensions) + const box1 = createBoundingBox(node1.position, node1.metrics) + const box2 = createBoundingBox(node2.position, node2.metrics) // Check for overlap with margin if (boxesOverlap(box1, box2, 30)) { @@ -92,11 +92,11 @@ function resolveOverlaps(nodes: GraphNode[], verticalSpacing: number): void { // If in same layer, shift vertically if (node1.layer === node2.layer) { - const totalHeight = node1.dimensions.height + node2.dimensions.height + verticalSpacing + const totalHeight = node1.metrics.height + node2.metrics.height + verticalSpacing const midpoint = (node1.position.y + node2.position.y) / 2 - node1.position.y = midpoint - node1.dimensions.height / 2 - verticalSpacing / 2 - node2.position.y = midpoint + node2.dimensions.height / 2 + verticalSpacing / 2 + node1.position.y = midpoint - node1.metrics.height / 2 - verticalSpacing / 2 + node2.position.y = midpoint + node2.metrics.height / 2 + verticalSpacing / 2 } else { // Different layers - shift the later one down const requiredSpace = box1.y + box1.height + verticalSpacing diff --git a/apps/sim/lib/workflows/autolayout/types.ts b/apps/sim/lib/workflows/autolayout/types.ts index aec8d11ea..5dd3930c7 100644 --- a/apps/sim/lib/workflows/autolayout/types.ts +++ b/apps/sim/lib/workflows/autolayout/types.ts @@ -35,9 +35,15 @@ export interface Parallel { parallelType?: 'count' | 'collection' } -export interface BlockDimensions { +export interface BlockMetrics { width: number height: number + minWidth: number + minHeight: number + paddingTop: number + paddingBottom: number + paddingLeft: number + paddingRight: number } export interface BoundingBox { @@ -55,7 +61,7 @@ export interface LayerInfo { export interface GraphNode { id: string block: BlockState - dimensions: BlockDimensions + metrics: BlockMetrics incoming: Set outgoing: Set layer: number diff --git a/apps/sim/lib/workflows/autolayout/utils.ts b/apps/sim/lib/workflows/autolayout/utils.ts index 71f7cace5..199bce820 100644 --- a/apps/sim/lib/workflows/autolayout/utils.ts +++ b/apps/sim/lib/workflows/autolayout/utils.ts @@ -1,34 +1,85 @@ import { TriggerUtils } from '@/lib/workflows/triggers' import type { BlockState } from '@/stores/workflows/workflow/types' -import type { BlockDimensions, BoundingBox } from './types' +import type { BlockMetrics, BoundingBox, GraphNode } from './types' export const DEFAULT_BLOCK_WIDTH = 350 export const DEFAULT_BLOCK_WIDTH_WIDE = 480 export const DEFAULT_BLOCK_HEIGHT = 100 export const DEFAULT_CONTAINER_WIDTH = 500 export const DEFAULT_CONTAINER_HEIGHT = 300 +const DEFAULT_PADDING = 40 + +function resolveNumeric(value: number | undefined, fallback: number): number { + return typeof value === 'number' && Number.isFinite(value) ? value : fallback +} export function isContainerType(blockType: string): boolean { return blockType === 'loop' || blockType === 'parallel' } -export function getBlockDimensions(block: BlockState): BlockDimensions { - if (isContainerType(block.type)) { - return { - width: block.data?.width ? Math.max(block.data.width, 400) : DEFAULT_CONTAINER_WIDTH, - height: block.data?.height ? Math.max(block.data.height, 200) : DEFAULT_CONTAINER_HEIGHT, - } - } +function getContainerMetrics(block: BlockState): BlockMetrics { + const measuredWidth = block.layout?.measuredWidth + const measuredHeight = block.layout?.measuredHeight + + const containerWidth = Math.max( + measuredWidth ?? 0, + resolveNumeric(block.data?.width, DEFAULT_CONTAINER_WIDTH) + ) + const containerHeight = Math.max( + measuredHeight ?? 0, + resolveNumeric(block.data?.height, DEFAULT_CONTAINER_HEIGHT) + ) return { - width: block.isWide ? DEFAULT_BLOCK_WIDTH_WIDE : DEFAULT_BLOCK_WIDTH, - height: Math.max(block.height || DEFAULT_BLOCK_HEIGHT, DEFAULT_BLOCK_HEIGHT), + width: containerWidth, + height: containerHeight, + minWidth: DEFAULT_CONTAINER_WIDTH, + minHeight: DEFAULT_CONTAINER_HEIGHT, + paddingTop: DEFAULT_PADDING, + paddingBottom: DEFAULT_PADDING, + paddingLeft: DEFAULT_PADDING, + paddingRight: DEFAULT_PADDING, + } +} + +function getRegularBlockMetrics(block: BlockState): BlockMetrics { + const minWidth = block.isWide ? DEFAULT_BLOCK_WIDTH_WIDE : DEFAULT_BLOCK_WIDTH + const minHeight = DEFAULT_BLOCK_HEIGHT + const measuredH = block.layout?.measuredHeight ?? block.height + const measuredW = block.layout?.measuredWidth + + const width = Math.max(measuredW ?? minWidth, minWidth) + const height = Math.max(measuredH ?? minHeight, minHeight) + + return { + width, + height, + minWidth, + minHeight, + paddingTop: DEFAULT_PADDING, + paddingBottom: DEFAULT_PADDING, + paddingLeft: DEFAULT_PADDING, + paddingRight: DEFAULT_PADDING, + } +} + +export function getBlockMetrics(block: BlockState): BlockMetrics { + if (isContainerType(block.type)) { + return getContainerMetrics(block) + } + + return getRegularBlockMetrics(block) +} + +export function prepareBlockMetrics(nodes: Map): void { + for (const node of nodes.values()) { + node.metrics = getBlockMetrics(node.block) } } export function createBoundingBox( position: { x: number; y: number }, - dimensions: BlockDimensions + dimensions: Pick ): BoundingBox { return { x: position.x, @@ -75,5 +126,5 @@ export function isStarterBlock(block: BlockState): boolean { return true } - return block.triggerMode === true + return false } diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index cd99abec0..8d8a4663d 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -185,6 +185,7 @@ export const useWorkflowStore = create()( advancedMode: blockProperties?.advancedMode ?? false, triggerMode: blockProperties?.triggerMode ?? false, height: blockProperties?.height ?? 0, + layout: {}, data: nodeData, }, }, @@ -233,6 +234,11 @@ export const useWorkflowStore = create()( width: dimensions.width, height: dimensions.height, }, + layout: { + ...block.layout, + measuredWidth: dimensions.width, + measuredHeight: dimensions.height, + }, }, }, edges: [...state.edges], @@ -786,20 +792,33 @@ export const useWorkflowStore = create()( // Note: Socket.IO handles real-time sync automatically }, - updateBlockHeight: (id: string, height: number) => { - set((state) => ({ - blocks: { - ...state.blocks, - [id]: { - ...state.blocks[id], - height, + updateBlockLayoutMetrics: (id: string, dimensions: { width: number; height: number }) => { + set((state) => { + const block = state.blocks[id] + if (!block) { + logger.warn(`Cannot update layout metrics: Block ${id} not found in workflow store`) + return state + } + + return { + blocks: { + ...state.blocks, + [id]: { + ...block, + height: dimensions.height, + layout: { + ...block.layout, + measuredWidth: dimensions.width, + measuredHeight: dimensions.height, + }, + }, }, - }, - edges: [...state.edges], - loops: { ...state.loops }, - })) + edges: [...state.edges], + loops: { ...state.loops }, + } + }) get().updateLastSaved() - // No sync needed for height changes, just visual + // No sync needed for layout changes, just visual }, updateLoopCount: (loopId: string, count: number) => diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts index dfbd998c4..a9681d130 100644 --- a/apps/sim/stores/workflows/workflow/types.ts +++ b/apps/sim/stores/workflows/workflow/types.ts @@ -62,6 +62,11 @@ export interface BlockData { type?: string } +export interface BlockLayoutState { + measuredWidth?: number + measuredHeight?: number +} + export interface BlockState { id: string type: string @@ -76,6 +81,7 @@ export interface BlockState { advancedMode?: boolean triggerMode?: boolean data?: BlockData + layout?: BlockLayoutState } export interface SubBlockState { @@ -197,7 +203,7 @@ export interface WorkflowActions { setBlockWide: (id: string, isWide: boolean) => void setBlockAdvancedMode: (id: string, advancedMode: boolean) => void setBlockTriggerMode: (id: string, triggerMode: boolean) => void - updateBlockHeight: (id: string, height: number) => void + updateBlockLayoutMetrics: (id: string, dimensions: { width: number; height: number }) => void triggerUpdate: () => void updateLoopCount: (loopId: string, count: number) => void updateLoopType: (loopId: string, loopType: 'for' | 'forEach') => void