Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 13 additions & 21 deletions apps/sim/app/api/workflows/[id]/autolayout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { applyAutoLayout } from '@/lib/workflows/autolayout'
import {
DEFAULT_HORIZONTAL_SPACING,
DEFAULT_LAYOUT_PADDING,
DEFAULT_VERTICAL_SPACING,
} from '@/lib/workflows/autolayout/constants'
import {
loadWorkflowFromNormalizedTables,
type NormalizedWorkflowData,
Expand All @@ -15,24 +20,18 @@ export const dynamic = 'force-dynamic'
const logger = createLogger('AutoLayoutAPI')

const AutoLayoutRequestSchema = z.object({
strategy: z
.enum(['smart', 'hierarchical', 'layered', 'force-directed'])
.optional()
.default('smart'),
direction: z.enum(['horizontal', 'vertical', 'auto']).optional().default('auto'),
spacing: z
.object({
horizontal: z.number().min(100).max(1000).optional().default(400),
vertical: z.number().min(50).max(500).optional().default(200),
layer: z.number().min(200).max(1200).optional().default(600),
horizontal: z.number().min(100).max(1000).optional(),
vertical: z.number().min(50).max(500).optional(),
})
.optional()
.default({}),
alignment: z.enum(['start', 'center', 'end']).optional().default('center'),
padding: z
.object({
x: z.number().min(50).max(500).optional().default(200),
y: z.number().min(50).max(500).optional().default(200),
x: z.number().min(50).max(500).optional(),
y: z.number().min(50).max(500).optional(),
})
.optional()
.default({}),
Expand Down Expand Up @@ -68,8 +67,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const layoutOptions = AutoLayoutRequestSchema.parse(body)

logger.info(`[${requestId}] Processing autolayout request for workflow ${workflowId}`, {
strategy: layoutOptions.strategy,
direction: layoutOptions.direction,
userId,
})

Expand Down Expand Up @@ -121,20 +118,18 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
}

const autoLayoutOptions = {
horizontalSpacing: layoutOptions.spacing?.horizontal || 550,
verticalSpacing: layoutOptions.spacing?.vertical || 200,
horizontalSpacing: layoutOptions.spacing?.horizontal ?? DEFAULT_HORIZONTAL_SPACING,
verticalSpacing: layoutOptions.spacing?.vertical ?? DEFAULT_VERTICAL_SPACING,
padding: {
x: layoutOptions.padding?.x || 150,
y: layoutOptions.padding?.y || 150,
x: layoutOptions.padding?.x ?? DEFAULT_LAYOUT_PADDING.x,
y: layoutOptions.padding?.y ?? DEFAULT_LAYOUT_PADDING.y,
},
alignment: layoutOptions.alignment,
}

const layoutResult = applyAutoLayout(
currentWorkflowData.blocks,
currentWorkflowData.edges,
currentWorkflowData.loops || {},
currentWorkflowData.parallels || {},
autoLayoutOptions
)

Expand All @@ -156,16 +151,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{

logger.info(`[${requestId}] Autolayout completed successfully in ${elapsed}ms`, {
blockCount,
strategy: layoutOptions.strategy,
workflowId,
})

return NextResponse.json({
success: true,
message: `Autolayout applied successfully to ${blockCount} blocks`,
data: {
strategy: layoutOptions.strategy,
direction: layoutOptions.direction,
blockCount,
elapsed: `${elapsed}ms`,
layoutedBlocks: layoutResult.blocks,
Expand Down
21 changes: 10 additions & 11 deletions apps/sim/app/api/yaml/autolayout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { z } from 'zod'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { applyAutoLayout } from '@/lib/workflows/autolayout'
import {
DEFAULT_HORIZONTAL_SPACING,
DEFAULT_LAYOUT_PADDING,
DEFAULT_VERTICAL_SPACING,
} from '@/lib/workflows/autolayout/constants'

const logger = createLogger('YamlAutoLayoutAPI')

Expand All @@ -15,13 +20,10 @@ const AutoLayoutRequestSchema = z.object({
}),
options: z
.object({
strategy: z.enum(['smart', 'hierarchical', 'layered', 'force-directed']).optional(),
direction: z.enum(['horizontal', 'vertical', 'auto']).optional(),
spacing: z
.object({
horizontal: z.number().optional(),
vertical: z.number().optional(),
layer: z.number().optional(),
})
.optional(),
alignment: z.enum(['start', 'center', 'end']).optional(),
Expand All @@ -45,24 +47,21 @@ export async function POST(request: NextRequest) {
logger.info(`[${requestId}] Applying auto layout`, {
blockCount: Object.keys(workflowState.blocks).length,
edgeCount: workflowState.edges.length,
strategy: options?.strategy || 'smart',
})

const autoLayoutOptions = {
horizontalSpacing: options?.spacing?.horizontal || 550,
verticalSpacing: options?.spacing?.vertical || 200,
horizontalSpacing: options?.spacing?.horizontal ?? DEFAULT_HORIZONTAL_SPACING,
verticalSpacing: options?.spacing?.vertical ?? DEFAULT_VERTICAL_SPACING,
padding: {
x: options?.padding?.x || 150,
y: options?.padding?.y || 150,
x: options?.padding?.x ?? DEFAULT_LAYOUT_PADDING.x,
y: options?.padding?.y ?? DEFAULT_LAYOUT_PADDING.y,
},
alignment: options?.alignment || 'center',
alignment: options?.alignment ?? 'center',
}

const layoutResult = applyAutoLayout(
workflowState.blocks,
workflowState.edges,
workflowState.loops || {},
workflowState.parallels || {},
autoLayoutOptions
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { useEffect, useRef } from 'react'
import { useUpdateNodeInternals } from 'reactflow'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'

// Re-export for backwards compatibility
export { BLOCK_DIMENSIONS } from '@/lib/blocks/block-dimensions'

interface BlockDimensions {
width: number
height: number
Expand All @@ -13,24 +16,6 @@ interface UseBlockDimensionsOptions {
dependencies: React.DependencyList
}

/**
* Shared block dimension constants
*/
export const BLOCK_DIMENSIONS = {
FIXED_WIDTH: 250,
HEADER_HEIGHT: 40,
MIN_HEIGHT: 100,

// Workflow blocks
WORKFLOW_CONTENT_PADDING: 16,
WORKFLOW_ROW_HEIGHT: 29,

// Note blocks
NOTE_CONTENT_PADDING: 14,
NOTE_MIN_CONTENT_HEIGHT: 20,
NOTE_BASE_CONTENT_HEIGHT: 60,
} as const

/**
* Hook to manage deterministic block dimensions without ResizeObserver.
* Calculates dimensions based on content structure and updates the store.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { useCallback } from 'react'
import { useReactFlow } from 'reactflow'
import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/blocks/block-dimensions'
import { createLogger } from '@/lib/logs/console/logger'

const logger = createLogger('NodeUtilities')

const DEFAULT_CONTAINER_WIDTH = 500
const DEFAULT_CONTAINER_HEIGHT = 300

/**
* Hook providing utilities for node position, hierarchy, and dimension calculations
*/
Expand All @@ -27,40 +25,43 @@ export function useNodeUtilities(blocks: Record<string, any>) {
const getBlockDimensions = useCallback(
(blockId: string): { width: number; height: number } => {
const block = blocks[blockId]
if (!block) return { width: 250, height: 100 }
if (!block) {
return { width: BLOCK_DIMENSIONS.FIXED_WIDTH, height: BLOCK_DIMENSIONS.MIN_HEIGHT }
}

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,
width: block.data?.width
? Math.max(block.data.width, CONTAINER_DIMENSIONS.MIN_WIDTH)
: CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
height: block.data?.height
? Math.max(block.data.height, CONTAINER_DIMENSIONS.MIN_HEIGHT)
: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
}
}

// Workflow block nodes have fixed visual width
const width = 250
const width = BLOCK_DIMENSIONS.FIXED_WIDTH

// Prefer deterministic height published by the block component; fallback to estimate
let height = block.height

if (!height) {
// Estimate height for workflow blocks before ResizeObserver measures them
// Block structure: header (40px) + content area with subblocks
// Block structure: header + content area with subblocks
// Each subblock row is approximately 29px (14px text + 8px gap + padding)
const headerHeight = 40
const subblockRowHeight = 29
const contentPadding = 16 // p-[8px] top and bottom = 16px total

// Estimate number of visible subblock rows
// This is a rough estimate - actual rendering may vary
const estimatedRows = 3 // Conservative estimate for typical blocks
const hasErrorRow = block.type !== 'starter' && block.type !== 'response' ? 1 : 0

height = headerHeight + contentPadding + (estimatedRows + hasErrorRow) * subblockRowHeight
height =
BLOCK_DIMENSIONS.HEADER_HEIGHT +
BLOCK_DIMENSIONS.WORKFLOW_CONTENT_PADDING +
(estimatedRows + hasErrorRow) * BLOCK_DIMENSIONS.WORKFLOW_ROW_HEIGHT
}

return {
width,
height: Math.max(height, 100),
height: Math.max(height, BLOCK_DIMENSIONS.MIN_HEIGHT),
}
},
[blocks, isContainerType]
Expand Down Expand Up @@ -205,9 +206,9 @@ export function useNodeUtilities(blocks: Record<string, any>) {
const absolutePos = getNodeAbsolutePosition(n.id)
const rect = {
left: absolutePos.x,
right: absolutePos.x + (n.data?.width || DEFAULT_CONTAINER_WIDTH),
right: absolutePos.x + (n.data?.width || CONTAINER_DIMENSIONS.DEFAULT_WIDTH),
top: absolutePos.y,
bottom: absolutePos.y + (n.data?.height || DEFAULT_CONTAINER_HEIGHT),
bottom: absolutePos.y + (n.data?.height || CONTAINER_DIMENSIONS.DEFAULT_HEIGHT),
}

return (
Expand All @@ -222,8 +223,8 @@ export function useNodeUtilities(blocks: Record<string, any>) {
// Return absolute position so callers can compute relative placement correctly
loopPosition: getNodeAbsolutePosition(n.id),
dimensions: {
width: n.data?.width || DEFAULT_CONTAINER_WIDTH,
height: n.data?.height || DEFAULT_CONTAINER_HEIGHT,
width: n.data?.width || CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
height: n.data?.height || CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
},
}))

Expand All @@ -247,8 +248,8 @@ export function useNodeUtilities(blocks: Record<string, any>) {
*/
const calculateLoopDimensions = useCallback(
(nodeId: string): { width: number; height: number } => {
const minWidth = DEFAULT_CONTAINER_WIDTH
const minHeight = DEFAULT_CONTAINER_HEIGHT
const minWidth = CONTAINER_DIMENSIONS.DEFAULT_WIDTH
const minHeight = CONTAINER_DIMENSIONS.DEFAULT_HEIGHT

// Match styling in subflow-node.tsx:
// - Header section: 50px total height
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { createLogger } from '@/lib/logs/console/logger'
import {
DEFAULT_HORIZONTAL_SPACING,
DEFAULT_LAYOUT_PADDING,
DEFAULT_VERTICAL_SPACING,
} from '@/lib/workflows/autolayout/constants'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'

const logger = createLogger('AutoLayoutUtils')
Expand All @@ -7,12 +12,9 @@ const logger = createLogger('AutoLayoutUtils')
* Auto layout options interface
*/
export interface AutoLayoutOptions {
strategy?: 'smart' | 'hierarchical' | 'layered' | 'force-directed'
direction?: 'horizontal' | 'vertical' | 'auto'
spacing?: {
horizontal?: number
vertical?: number
layer?: number
}
alignment?: 'start' | 'center' | 'end'
padding?: {
Expand All @@ -21,24 +23,6 @@ export interface AutoLayoutOptions {
}
}

/**
* Default auto layout options
*/
const DEFAULT_AUTO_LAYOUT_OPTIONS = {
strategy: 'smart' as const,
direction: 'auto' as const,
spacing: {
horizontal: 550,
vertical: 200,
layer: 550,
},
alignment: 'center' as const,
padding: {
x: 150,
y: 150,
},
}

/**
* Apply auto layout and update store
* Standalone utility for use outside React context (event handlers, tools, etc.)
Expand Down Expand Up @@ -69,17 +53,14 @@ export async function applyAutoLayoutAndUpdateStore(

// Merge with default options
const layoutOptions = {
strategy: options.strategy || DEFAULT_AUTO_LAYOUT_OPTIONS.strategy,
direction: options.direction || DEFAULT_AUTO_LAYOUT_OPTIONS.direction,
spacing: {
horizontal: options.spacing?.horizontal || DEFAULT_AUTO_LAYOUT_OPTIONS.spacing.horizontal,
vertical: options.spacing?.vertical || DEFAULT_AUTO_LAYOUT_OPTIONS.spacing.vertical,
layer: options.spacing?.layer || DEFAULT_AUTO_LAYOUT_OPTIONS.spacing.layer,
horizontal: options.spacing?.horizontal ?? DEFAULT_HORIZONTAL_SPACING,
vertical: options.spacing?.vertical ?? DEFAULT_VERTICAL_SPACING,
},
alignment: options.alignment || DEFAULT_AUTO_LAYOUT_OPTIONS.alignment,
alignment: options.alignment ?? 'center',
padding: {
x: options.padding?.x || DEFAULT_AUTO_LAYOUT_OPTIONS.padding.x,
y: options.padding?.y || DEFAULT_AUTO_LAYOUT_OPTIONS.padding.y,
x: options.padding?.x ?? DEFAULT_LAYOUT_PADDING.x,
y: options.padding?.y ?? DEFAULT_LAYOUT_PADDING.y,
},
}

Expand Down
Loading