From 218041dba353fe78d54f965ca2ed8b38e10fcc10 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Wed, 9 Jul 2025 10:23:31 -0700 Subject: [PATCH] Handle loops/parallel --- apps/sim/lib/workflows/yaml-generator.ts | 40 ++++++++- apps/sim/stores/workflows/yaml/importer.ts | 100 ++++++++++++++++++++- 2 files changed, 134 insertions(+), 6 deletions(-) diff --git a/apps/sim/lib/workflows/yaml-generator.ts b/apps/sim/lib/workflows/yaml-generator.ts index a1fd7d30de..668c2932b0 100644 --- a/apps/sim/lib/workflows/yaml-generator.ts +++ b/apps/sim/lib/workflows/yaml-generator.ts @@ -12,6 +12,7 @@ interface YamlBlock { inputs?: Record preceding?: string[] following?: string[] + parentId?: string // Add parentId for nested blocks } interface YamlWorkflow { @@ -34,8 +35,38 @@ function extractBlockInputs( // Get subblock values for this block (if provided) const blockSubBlockValues = subBlockValues?.[blockId] || {} + // Special handling for loop and parallel blocks + if (blockState.type === 'loop' || blockState.type === 'parallel') { + // Extract configuration from blockState.data instead of subBlocks + if (blockState.data) { + Object.entries(blockState.data).forEach(([key, value]) => { + // Include relevant configuration properties + if (key === 'count' || key === 'loopType' || key === 'collection' || + key === 'parallelType' || key === 'distribution') { + if (value !== undefined && value !== null && value !== '') { + inputs[key] = value + } + } + // Also include any override values from subBlockValues if they exist + const overrideValue = blockSubBlockValues[key] + if (overrideValue !== undefined && overrideValue !== null && overrideValue !== '') { + inputs[key] = overrideValue + } + }) + } + + // Include any additional values from subBlockValues that might not be in data + Object.entries(blockSubBlockValues).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '' && !inputs.hasOwnProperty(key)) { + inputs[key] = value + } + }) + + return inputs + } + if (!blockConfig) { - // For custom blocks like loops/parallels, extract available subBlock values + // For other custom blocks without config, extract available subBlock values Object.entries(blockState.subBlocks || {}).forEach(([subBlockId, subBlockState]) => { const value = blockSubBlockValues[subBlockId] ?? subBlockState.value if (value !== undefined && value !== null && value !== '') { @@ -45,7 +76,7 @@ function extractBlockInputs( return inputs } - // Process each subBlock configuration + // Process each subBlock configuration for regular blocks blockConfig.subBlocks.forEach((subBlockConfig: SubBlockConfig) => { const subBlockId = subBlockConfig.id @@ -175,6 +206,11 @@ export function generateWorkflowYaml( yamlBlock.following = following } + // Include parent-child relationship for nested blocks + if (blockState.data?.parentId) { + yamlBlock.parentId = blockState.data.parentId + } + yamlWorkflow.blocks[blockId] = yamlBlock }) diff --git a/apps/sim/stores/workflows/yaml/importer.ts b/apps/sim/stores/workflows/yaml/importer.ts index 864939b936..8157aab69e 100644 --- a/apps/sim/stores/workflows/yaml/importer.ts +++ b/apps/sim/stores/workflows/yaml/importer.ts @@ -11,6 +11,7 @@ interface YamlBlock { inputs?: Record preceding?: string[] following?: string[] + parentId?: string // Add parentId for nested blocks } interface YamlWorkflow { @@ -257,6 +258,55 @@ function calculateBlockPositions( return positions } +/** + * Sort blocks to ensure parents are processed before children + * This ensures proper creation order for nested blocks + */ +function sortBlocksByParentChildOrder(blocks: ImportedBlock[]): ImportedBlock[] { + const sorted: ImportedBlock[] = [] + const processed = new Set() + const visiting = new Set() // Track blocks currently being processed to detect cycles + + // Create a map for quick lookup + const blockMap = new Map() + blocks.forEach(block => blockMap.set(block.id, block)) + + // Process blocks recursively, ensuring parents are added first + function processBlock(block: ImportedBlock) { + if (processed.has(block.id)) { + return // Already processed + } + + if (visiting.has(block.id)) { + // Circular dependency detected - break the cycle by processing this block without its parent + logger.warn(`Circular parent-child dependency detected for block ${block.id}, breaking cycle`) + sorted.push(block) + processed.add(block.id) + return + } + + visiting.add(block.id) + + // If this block has a parent, ensure the parent is processed first + if (block.parentId) { + const parentBlock = blockMap.get(block.parentId) + if (parentBlock && !processed.has(block.parentId)) { + processBlock(parentBlock) + } + } + + // Now process this block + visiting.delete(block.id) + sorted.push(block) + processed.add(block.id) + } + + // Process all blocks + blocks.forEach(block => processBlock(block)) + + return sorted +} + /** * Convert YAML workflow to importable format */ @@ -296,11 +346,28 @@ export function convertYamlToWorkflow(yamlWorkflow: YamlWorkflow): ImportResult // Add container-specific data if (yamlBlock.type === 'loop' || yamlBlock.type === 'parallel') { + // For loop/parallel blocks, map the inputs to the data field since they don't use subBlocks importedBlock.data = { width: 500, height: 300, type: yamlBlock.type === 'loop' ? 'loopNode' : 'parallelNode', + // Map YAML inputs to data properties for loop/parallel blocks + ...(yamlBlock.inputs || {}), } + // Clear inputs since they're now in data + importedBlock.inputs = {} + } + + // Handle parent-child relationships for nested blocks + if (yamlBlock.parentId) { + importedBlock.parentId = yamlBlock.parentId + importedBlock.extent = 'parent' + // Also add to data for consistency with how the system works + if (!importedBlock.data) { + importedBlock.data = {} + } + importedBlock.data.parentId = yamlBlock.parentId + importedBlock.data.extent = 'parent' } blocks.push(importedBlock) @@ -326,7 +393,10 @@ export function convertYamlToWorkflow(yamlWorkflow: YamlWorkflow): ImportResult } }) - return { blocks, edges, errors, warnings } + // Sort blocks to ensure parents are created before children + const sortedBlocks = sortBlocksByParentChildOrder(blocks) + + return { blocks: sortedBlocks, edges, errors, warnings } } /** @@ -474,6 +544,8 @@ export async function importWorkflowFromYaml( } // Create all other blocks + // Note: blocks are now sorted to ensure parents come before children, + // but we still need the two-phase approach because we're generating new UUIDs let blocksProcessed = 0 for (const block of blocks) { if (block.type === 'starter') { @@ -499,10 +571,11 @@ export async function importWorkflowFromYaml( horizontalHandles: true, isWide: false, height: 0, - data: block.data || {}, + data: block.data || {}, // Configuration is already in block.data from convertYamlToWorkflow } - completeSubBlockValues[blockId] = { ...block.inputs } + // Loop/parallel blocks don't use subBlocks, their config is in data + // No need to set completeSubBlockValues since they don't have subBlocks blocksProcessed++ } else if (blockConfig) { // Handle regular blocks @@ -526,7 +599,7 @@ export async function importWorkflowFromYaml( horizontalHandles: true, isWide: false, height: 0, - data: block.data || {}, + data: block.data || {}, // This already includes parentId and extent from convertYamlToWorkflow } // Set block input values @@ -537,6 +610,25 @@ export async function importWorkflowFromYaml( } } + // Update parent-child relationships with mapped IDs + // This two-phase approach is necessary because: + // 1. We generate new UUIDs for all blocks (can't reuse YAML IDs) + // 2. Parent references in YAML use the original IDs, need to map to new UUIDs + // 3. All blocks must exist before we can map their parent references + for (const [blockId, blockData] of Object.entries(completeBlocks)) { + if (blockData.data?.parentId) { + const mappedParentId = yamlIdToActualId.get(blockData.data.parentId) + if (mappedParentId) { + blockData.data.parentId = mappedParentId + } else { + logger.warn(`Parent block not found for mapping: ${blockData.data.parentId}`) + // Remove invalid parent reference + delete blockData.data.parentId + delete blockData.data.extent + } + } + } + // Create complete edges using the ID mapping const completeEdges: any[] = [] for (const edge of edges) {