mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
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
This commit is contained in:
committed by
GitHub
parent
87c00cec6d
commit
c35c8d1f31
@@ -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<typeof AutoLayoutRequestSchema>
|
||||
|
||||
/**
|
||||
* 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`)
|
||||
|
||||
@@ -148,6 +148,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
)
|
||||
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<WorkflowBlockProps>) {
|
||||
? (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<WorkflowBlockProps>) {
|
||||
}, [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<WorkflowBlockProps>) {
|
||||
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<WorkflowBlockProps>) {
|
||||
// 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<WorkflowBlockProps>) {
|
||||
cancelAnimationFrame(rafId)
|
||||
}
|
||||
}
|
||||
}, [id, blockHeight, updateBlockHeight, updateNodeInternals, lastUpdate])
|
||||
}, [id, blockHeight, blockWidth, updateBlockLayoutMetrics, updateNodeInternals, lastUpdate])
|
||||
|
||||
// SubBlock layout management
|
||||
function groupSubBlocks(subBlocks: SubBlockConfig[], blockId: string) {
|
||||
|
||||
@@ -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
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, BlockState>, 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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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<string>()
|
||||
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<string, number>()
|
||||
|
||||
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<string>()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string>
|
||||
outgoing: Set<string>
|
||||
layer: number
|
||||
|
||||
@@ -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<string, GraphNode>): void {
|
||||
for (const node of nodes.values()) {
|
||||
node.metrics = getBlockMetrics(node.block)
|
||||
}
|
||||
}
|
||||
|
||||
export function createBoundingBox(
|
||||
position: { x: number; y: number },
|
||||
dimensions: BlockDimensions
|
||||
dimensions: Pick<BlockMetrics, 'width' | 'height'>
|
||||
): BoundingBox {
|
||||
return {
|
||||
x: position.x,
|
||||
@@ -75,5 +126,5 @@ export function isStarterBlock(block: BlockState): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
return block.triggerMode === true
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -185,6 +185,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
advancedMode: blockProperties?.advancedMode ?? false,
|
||||
triggerMode: blockProperties?.triggerMode ?? false,
|
||||
height: blockProperties?.height ?? 0,
|
||||
layout: {},
|
||||
data: nodeData,
|
||||
},
|
||||
},
|
||||
@@ -233,6 +234,11 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
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<WorkflowStoreWithHistory>()(
|
||||
// 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) =>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user