Skip to content

Commit 218041d

Browse files
committed
Handle loops/parallel
1 parent a2827a5 commit 218041d

File tree

2 files changed

+134
-6
lines changed

2 files changed

+134
-6
lines changed

apps/sim/lib/workflows/yaml-generator.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface YamlBlock {
1212
inputs?: Record<string, any>
1313
preceding?: string[]
1414
following?: string[]
15+
parentId?: string // Add parentId for nested blocks
1516
}
1617

1718
interface YamlWorkflow {
@@ -34,8 +35,38 @@ function extractBlockInputs(
3435
// Get subblock values for this block (if provided)
3536
const blockSubBlockValues = subBlockValues?.[blockId] || {}
3637

38+
// Special handling for loop and parallel blocks
39+
if (blockState.type === 'loop' || blockState.type === 'parallel') {
40+
// Extract configuration from blockState.data instead of subBlocks
41+
if (blockState.data) {
42+
Object.entries(blockState.data).forEach(([key, value]) => {
43+
// Include relevant configuration properties
44+
if (key === 'count' || key === 'loopType' || key === 'collection' ||
45+
key === 'parallelType' || key === 'distribution') {
46+
if (value !== undefined && value !== null && value !== '') {
47+
inputs[key] = value
48+
}
49+
}
50+
// Also include any override values from subBlockValues if they exist
51+
const overrideValue = blockSubBlockValues[key]
52+
if (overrideValue !== undefined && overrideValue !== null && overrideValue !== '') {
53+
inputs[key] = overrideValue
54+
}
55+
})
56+
}
57+
58+
// Include any additional values from subBlockValues that might not be in data
59+
Object.entries(blockSubBlockValues).forEach(([key, value]) => {
60+
if (value !== undefined && value !== null && value !== '' && !inputs.hasOwnProperty(key)) {
61+
inputs[key] = value
62+
}
63+
})
64+
65+
return inputs
66+
}
67+
3768
if (!blockConfig) {
38-
// For custom blocks like loops/parallels, extract available subBlock values
69+
// For other custom blocks without config, extract available subBlock values
3970
Object.entries(blockState.subBlocks || {}).forEach(([subBlockId, subBlockState]) => {
4071
const value = blockSubBlockValues[subBlockId] ?? subBlockState.value
4172
if (value !== undefined && value !== null && value !== '') {
@@ -45,7 +76,7 @@ function extractBlockInputs(
4576
return inputs
4677
}
4778

48-
// Process each subBlock configuration
79+
// Process each subBlock configuration for regular blocks
4980
blockConfig.subBlocks.forEach((subBlockConfig: SubBlockConfig) => {
5081
const subBlockId = subBlockConfig.id
5182

@@ -175,6 +206,11 @@ export function generateWorkflowYaml(
175206
yamlBlock.following = following
176207
}
177208

209+
// Include parent-child relationship for nested blocks
210+
if (blockState.data?.parentId) {
211+
yamlBlock.parentId = blockState.data.parentId
212+
}
213+
178214
yamlWorkflow.blocks[blockId] = yamlBlock
179215
})
180216

apps/sim/stores/workflows/yaml/importer.ts

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface YamlBlock {
1111
inputs?: Record<string, any>
1212
preceding?: string[]
1313
following?: string[]
14+
parentId?: string // Add parentId for nested blocks
1415
}
1516

1617
interface YamlWorkflow {
@@ -257,6 +258,55 @@ function calculateBlockPositions(
257258
return positions
258259
}
259260

261+
/**
262+
* Sort blocks to ensure parents are processed before children
263+
* This ensures proper creation order for nested blocks
264+
*/
265+
function sortBlocksByParentChildOrder(blocks: ImportedBlock[]): ImportedBlock[] {
266+
const sorted: ImportedBlock[] = []
267+
const processed = new Set<string>()
268+
const visiting = new Set<string>() // Track blocks currently being processed to detect cycles
269+
270+
// Create a map for quick lookup
271+
const blockMap = new Map<string, ImportedBlock>()
272+
blocks.forEach(block => blockMap.set(block.id, block))
273+
274+
// Process blocks recursively, ensuring parents are added first
275+
function processBlock(block: ImportedBlock) {
276+
if (processed.has(block.id)) {
277+
return // Already processed
278+
}
279+
280+
if (visiting.has(block.id)) {
281+
// Circular dependency detected - break the cycle by processing this block without its parent
282+
logger.warn(`Circular parent-child dependency detected for block ${block.id}, breaking cycle`)
283+
sorted.push(block)
284+
processed.add(block.id)
285+
return
286+
}
287+
288+
visiting.add(block.id)
289+
290+
// If this block has a parent, ensure the parent is processed first
291+
if (block.parentId) {
292+
const parentBlock = blockMap.get(block.parentId)
293+
if (parentBlock && !processed.has(block.parentId)) {
294+
processBlock(parentBlock)
295+
}
296+
}
297+
298+
// Now process this block
299+
visiting.delete(block.id)
300+
sorted.push(block)
301+
processed.add(block.id)
302+
}
303+
304+
// Process all blocks
305+
blocks.forEach(block => processBlock(block))
306+
307+
return sorted
308+
}
309+
260310
/**
261311
* Convert YAML workflow to importable format
262312
*/
@@ -296,11 +346,28 @@ export function convertYamlToWorkflow(yamlWorkflow: YamlWorkflow): ImportResult
296346

297347
// Add container-specific data
298348
if (yamlBlock.type === 'loop' || yamlBlock.type === 'parallel') {
349+
// For loop/parallel blocks, map the inputs to the data field since they don't use subBlocks
299350
importedBlock.data = {
300351
width: 500,
301352
height: 300,
302353
type: yamlBlock.type === 'loop' ? 'loopNode' : 'parallelNode',
354+
// Map YAML inputs to data properties for loop/parallel blocks
355+
...(yamlBlock.inputs || {}),
303356
}
357+
// Clear inputs since they're now in data
358+
importedBlock.inputs = {}
359+
}
360+
361+
// Handle parent-child relationships for nested blocks
362+
if (yamlBlock.parentId) {
363+
importedBlock.parentId = yamlBlock.parentId
364+
importedBlock.extent = 'parent'
365+
// Also add to data for consistency with how the system works
366+
if (!importedBlock.data) {
367+
importedBlock.data = {}
368+
}
369+
importedBlock.data.parentId = yamlBlock.parentId
370+
importedBlock.data.extent = 'parent'
304371
}
305372

306373
blocks.push(importedBlock)
@@ -326,7 +393,10 @@ export function convertYamlToWorkflow(yamlWorkflow: YamlWorkflow): ImportResult
326393
}
327394
})
328395

329-
return { blocks, edges, errors, warnings }
396+
// Sort blocks to ensure parents are created before children
397+
const sortedBlocks = sortBlocksByParentChildOrder(blocks)
398+
399+
return { blocks: sortedBlocks, edges, errors, warnings }
330400
}
331401

332402
/**
@@ -474,6 +544,8 @@ export async function importWorkflowFromYaml(
474544
}
475545

476546
// Create all other blocks
547+
// Note: blocks are now sorted to ensure parents come before children,
548+
// but we still need the two-phase approach because we're generating new UUIDs
477549
let blocksProcessed = 0
478550
for (const block of blocks) {
479551
if (block.type === 'starter') {
@@ -499,10 +571,11 @@ export async function importWorkflowFromYaml(
499571
horizontalHandles: true,
500572
isWide: false,
501573
height: 0,
502-
data: block.data || {},
574+
data: block.data || {}, // Configuration is already in block.data from convertYamlToWorkflow
503575
}
504576

505-
completeSubBlockValues[blockId] = { ...block.inputs }
577+
// Loop/parallel blocks don't use subBlocks, their config is in data
578+
// No need to set completeSubBlockValues since they don't have subBlocks
506579
blocksProcessed++
507580
} else if (blockConfig) {
508581
// Handle regular blocks
@@ -526,7 +599,7 @@ export async function importWorkflowFromYaml(
526599
horizontalHandles: true,
527600
isWide: false,
528601
height: 0,
529-
data: block.data || {},
602+
data: block.data || {}, // This already includes parentId and extent from convertYamlToWorkflow
530603
}
531604

532605
// Set block input values
@@ -537,6 +610,25 @@ export async function importWorkflowFromYaml(
537610
}
538611
}
539612

613+
// Update parent-child relationships with mapped IDs
614+
// This two-phase approach is necessary because:
615+
// 1. We generate new UUIDs for all blocks (can't reuse YAML IDs)
616+
// 2. Parent references in YAML use the original IDs, need to map to new UUIDs
617+
// 3. All blocks must exist before we can map their parent references
618+
for (const [blockId, blockData] of Object.entries(completeBlocks)) {
619+
if (blockData.data?.parentId) {
620+
const mappedParentId = yamlIdToActualId.get(blockData.data.parentId)
621+
if (mappedParentId) {
622+
blockData.data.parentId = mappedParentId
623+
} else {
624+
logger.warn(`Parent block not found for mapping: ${blockData.data.parentId}`)
625+
// Remove invalid parent reference
626+
delete blockData.data.parentId
627+
delete blockData.data.extent
628+
}
629+
}
630+
}
631+
540632
// Create complete edges using the ID mapping
541633
const completeEdges: any[] = []
542634
for (const edge of edges) {

0 commit comments

Comments
 (0)