diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/import-controls/import-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/import-controls/import-controls.tsx index 5f85599c9d..a90010ab92 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/import-controls/import-controls.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/import-controls/import-controls.tsx @@ -1,14 +1,10 @@ 'use client' -import { useState, useRef } from 'react' -import { Upload, FileText, Plus, AlertCircle, CheckCircle } from 'lucide-react' +import { useRef, useState } from 'react' +import { AlertCircle, CheckCircle, FileText, Plus, Upload } from 'lucide-react' +import { useParams, useRouter } from 'next/navigation' +import { Alert, AlertDescription } from '@/components/ui/alert' import { Button } from '@/components/ui/button' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu' import { Dialog, DialogContent, @@ -17,16 +13,19 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' import { Textarea } from '@/components/ui/textarea' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import { Alert, AlertDescription } from '@/components/ui/alert' import { createLogger } from '@/lib/logs/console-logger' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { importWorkflowFromYaml, parseWorkflowYaml } from '@/stores/workflows/yaml/importer' -import { useRouter } from 'next/navigation' -import { useParams } from 'next/navigation' const logger = createLogger('ImportControls') @@ -44,12 +43,12 @@ export function ImportControls({ disabled = false }: ImportControlsProps) { warnings: string[] summary?: string } | null>(null) - + const fileInputRef = useRef(null) const router = useRouter() const params = useParams() const workspaceId = params.workspaceId as string - + // Stores and hooks const { createWorkflow } = useWorkflowRegistry() const { collaborativeAddBlock, collaborativeAddEdge } = useCollaborativeWorkflow() @@ -67,8 +66,10 @@ export function ImportControls({ disabled = false }: ImportControlsProps) { logger.error('Failed to read file:', error) setImportResult({ success: false, - errors: [`Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`], - warnings: [] + errors: [ + `Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`, + ], + warnings: [], }) } @@ -83,7 +84,7 @@ export function ImportControls({ disabled = false }: ImportControlsProps) { setImportResult({ success: false, errors: ['YAML content is required'], - warnings: [] + warnings: [], }) return } @@ -94,12 +95,12 @@ export function ImportControls({ disabled = false }: ImportControlsProps) { try { // First validate the YAML without importing const { data: yamlWorkflow, errors: parseErrors } = parseWorkflowYaml(yamlContent) - + if (!yamlWorkflow || parseErrors.length > 0) { setImportResult({ success: false, errors: parseErrors, - warnings: [] + warnings: [], }) return } @@ -116,9 +117,9 @@ export function ImportControls({ disabled = false }: ImportControlsProps) { router.push(`/workspace/${workspaceId}/w/${newWorkflowId}`) // Small delay to ensure navigation and workflow initialization - await new Promise(resolve => setTimeout(resolve, 1000)) + await new Promise((resolve) => setTimeout(resolve, 1000)) - // Import the YAML into the new workflow + // Import the YAML into the new workflow const result = await importWorkflowFromYaml(yamlContent, { addBlock: collaborativeAddBlock, addEdge: collaborativeAddEdge, @@ -133,7 +134,7 @@ export function ImportControls({ disabled = false }: ImportControlsProps) { // This will be called after navigation, so we need to get blocks from the store const { useWorkflowStore } = require('@/stores/workflows/workflow/store') return useWorkflowStore.getState().blocks - } + }, }) setImportResult(result) @@ -146,13 +147,12 @@ export function ImportControls({ disabled = false }: ImportControlsProps) { setImportResult(null) }, 2000) } - } catch (error) { logger.error('Failed to import YAML workflow:', error) setImportResult({ success: false, errors: [`Import failed: ${error instanceof Error ? error.message : 'Unknown error'}`], - warnings: [] + warnings: [], }) } finally { setIsImporting(false) @@ -198,10 +198,12 @@ export function ImportControls({ disabled = false }: ImportControlsProps) {
Upload YAML File - Import from .yaml or .yml file + + Import from .yaml or .yml file +
- + - + Import Workflow from YAML - Paste your workflow YAML content below. This will create a new workflow with the blocks and connections defined in the YAML. + Paste your workflow YAML content below. This will create a new workflow with the + blocks and connections defined in the YAML. @@ -284,10 +287,12 @@ blocks: )} {importResult.warnings.length > 0 && (
-
Warnings:
+
Warnings:
    {importResult.warnings.map((warning, index) => ( -
  • • {warning}
  • +
  • + • {warning} +
  • ))}
@@ -321,10 +326,7 @@ blocks: > Cancel - @@ -332,4 +334,4 @@ blocks: ) -} \ No newline at end of file +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx index 9edad27dc0..b8798d18d9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx @@ -57,8 +57,8 @@ import { import { useWorkflowExecution } from '../../hooks/use-workflow-execution' import { DeploymentControls } from './components/deployment-controls/deployment-controls' import { ExportControls } from './components/export-controls/export-controls' -import { ImportControls } from './components/import-controls/import-controls' import { HistoryDropdownItem } from './components/history-dropdown-item/history-dropdown-item' +import { ImportControls } from './components/import-controls/import-controls' import { MarketplaceModal } from './components/marketplace-modal/marketplace-modal' import { NotificationDropdownItem } from './components/notification-dropdown-item/notification-dropdown-item' import { UserAvatarStack } from './components/user-avatar-stack/user-avatar-stack' diff --git a/apps/sim/stores/workflows/yaml/importer.ts b/apps/sim/stores/workflows/yaml/importer.ts index 76d700aa32..868c4de983 100644 --- a/apps/sim/stores/workflows/yaml/importer.ts +++ b/apps/sim/stores/workflows/yaml/importer.ts @@ -1,7 +1,6 @@ import { load as yamlParse } from 'js-yaml' import { createLogger } from '@/lib/logs/console-logger' import { getBlock } from '@/blocks' -import type { BlockConfig } from '@/blocks/types' const logger = createLogger('WorkflowYamlImporter') @@ -48,59 +47,62 @@ interface ImportResult { /** * Parse YAML content and validate its structure */ -export function parseWorkflowYaml(yamlContent: string): { data: YamlWorkflow | null; errors: string[] } { +export function parseWorkflowYaml(yamlContent: string): { + data: YamlWorkflow | null + errors: string[] +} { const errors: string[] = [] - + try { const data = yamlParse(yamlContent) as any - + // Validate top-level structure if (!data || typeof data !== 'object') { errors.push('Invalid YAML: Root must be an object') return { data: null, errors } } - + if (!data.version) { errors.push('Missing required field: version') } - + if (!data.blocks || typeof data.blocks !== 'object') { errors.push('Missing or invalid field: blocks') return { data: null, errors } } - + // Validate blocks structure Object.entries(data.blocks).forEach(([blockId, block]: [string, any]) => { if (!block || typeof block !== 'object') { errors.push(`Invalid block definition for '${blockId}': must be an object`) return } - + if (!block.type || typeof block.type !== 'string') { errors.push(`Invalid block '${blockId}': missing or invalid 'type' field`) } - + if (!block.name || typeof block.name !== 'string') { errors.push(`Invalid block '${blockId}': missing or invalid 'name' field`) } - + if (block.inputs && typeof block.inputs !== 'object') { errors.push(`Invalid block '${blockId}': 'inputs' must be an object`) } - + if (block.preceding && !Array.isArray(block.preceding)) { errors.push(`Invalid block '${blockId}': 'preceding' must be an array`) } - + if (block.following && !Array.isArray(block.following)) { errors.push(`Invalid block '${blockId}': 'following' must be an array`) } }) - + if (errors.length > 0) { return { data: null, errors } } - + return { data: data as YamlWorkflow, errors: [] } } catch (error) { errors.push(`YAML parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`) @@ -114,63 +116,65 @@ export function parseWorkflowYaml(yamlContent: string): { data: YamlWorkflow | n function validateBlockReferences(yamlWorkflow: YamlWorkflow): string[] { const errors: string[] = [] const blockIds = new Set(Object.keys(yamlWorkflow.blocks)) - + Object.entries(yamlWorkflow.blocks).forEach(([blockId, block]) => { // Check preceding references if (block.preceding) { - block.preceding.forEach(precedingId => { + block.preceding.forEach((precedingId) => { if (!blockIds.has(precedingId)) { errors.push(`Block '${blockId}' references non-existent preceding block '${precedingId}'`) } }) } - + // Check following references if (block.following) { - block.following.forEach(followingId => { + block.following.forEach((followingId) => { if (!blockIds.has(followingId)) { errors.push(`Block '${blockId}' references non-existent following block '${followingId}'`) } }) } }) - + return errors } /** * Validate that block types exist and are valid */ -function validateBlockTypes(yamlWorkflow: YamlWorkflow): { errors: string[], warnings: string[] } { +function validateBlockTypes(yamlWorkflow: YamlWorkflow): { errors: string[]; warnings: string[] } { const errors: string[] = [] const warnings: string[] = [] - + Object.entries(yamlWorkflow.blocks).forEach(([blockId, block]) => { // Check if block type exists const blockConfig = getBlock(block.type) - + // Special handling for container blocks if (block.type === 'loop' || block.type === 'parallel') { // These are valid container types return } - + if (!blockConfig) { errors.push(`Unknown block type '${block.type}' for block '${blockId}'`) return } - + // Validate inputs against block configuration if (block.inputs && blockConfig.subBlocks) { - Object.keys(block.inputs).forEach(inputKey => { - const subBlockConfig = blockConfig.subBlocks.find(sb => sb.id === inputKey) + Object.keys(block.inputs).forEach((inputKey) => { + const subBlockConfig = blockConfig.subBlocks.find((sb) => sb.id === inputKey) if (!subBlockConfig) { - warnings.push(`Block '${blockId}' has unknown input '${inputKey}' for type '${block.type}'`) + warnings.push( + `Block '${blockId}' has unknown input '${inputKey}' for type '${block.type}'` + ) } }) } }) - + return { errors, warnings } } @@ -178,75 +182,77 @@ function validateBlockTypes(yamlWorkflow: YamlWorkflow): { errors: string[], war * Calculate positions for blocks based on their connections * Uses a simple layered approach similar to the auto-layout algorithm */ -function calculateBlockPositions(yamlWorkflow: YamlWorkflow): Record { +function calculateBlockPositions( + yamlWorkflow: YamlWorkflow +): Record { const positions: Record = {} const blockIds = Object.keys(yamlWorkflow.blocks) - + // Find starter blocks (no preceding connections) - const starterBlocks = blockIds.filter(id => { + const starterBlocks = blockIds.filter((id) => { const block = yamlWorkflow.blocks[id] return !block.preceding || block.preceding.length === 0 }) - + // If no starter blocks found, use first block as starter if (starterBlocks.length === 0 && blockIds.length > 0) { starterBlocks.push(blockIds[0]) } - + // Build layers const layers: string[][] = [] const visited = new Set() const queue = [...starterBlocks] - + // BFS to organize blocks into layers while (queue.length > 0) { const currentLayer: string[] = [] const currentLayerSize = queue.length - + for (let i = 0; i < currentLayerSize; i++) { const blockId = queue.shift()! if (visited.has(blockId)) continue - + visited.add(blockId) currentLayer.push(blockId) - + // Add following blocks to queue const block = yamlWorkflow.blocks[blockId] if (block.following) { - block.following.forEach(followingId => { + block.following.forEach((followingId) => { if (!visited.has(followingId)) { queue.push(followingId) } }) } } - + if (currentLayer.length > 0) { layers.push(currentLayer) } } - + // Add any remaining blocks as isolated layer - const remainingBlocks = blockIds.filter(id => !visited.has(id)) + const remainingBlocks = blockIds.filter((id) => !visited.has(id)) if (remainingBlocks.length > 0) { layers.push(remainingBlocks) } - + // Calculate positions const horizontalSpacing = 600 const verticalSpacing = 200 const startX = 150 const startY = 300 - + layers.forEach((layer, layerIndex) => { const layerX = startX + layerIndex * horizontalSpacing - + layer.forEach((blockId, blockIndex) => { const blockY = startY + (blockIndex - layer.length / 2) * verticalSpacing positions[blockId] = { x: layerX, y: blockY } }) }) - + return positions } @@ -258,27 +264,27 @@ export function convertYamlToWorkflow(yamlWorkflow: YamlWorkflow): ImportResult const warnings: string[] = [] const blocks: ImportedBlock[] = [] const edges: ImportedEdge[] = [] - + // Validate block references const referenceErrors = validateBlockReferences(yamlWorkflow) errors.push(...referenceErrors) - + // Validate block types const { errors: typeErrors, warnings: typeWarnings } = validateBlockTypes(yamlWorkflow) errors.push(...typeErrors) warnings.push(...typeWarnings) - + if (errors.length > 0) { return { blocks: [], edges: [], errors, warnings } } - + // Calculate positions const positions = calculateBlockPositions(yamlWorkflow) - + // Convert blocks Object.entries(yamlWorkflow.blocks).forEach(([blockId, yamlBlock]) => { const position = positions[blockId] || { x: 100, y: 100 } - + const importedBlock: ImportedBlock = { id: blockId, type: yamlBlock.type, @@ -286,7 +292,7 @@ export function convertYamlToWorkflow(yamlWorkflow: YamlWorkflow): ImportResult inputs: yamlBlock.inputs || {}, position, } - + // Add container-specific data if (yamlBlock.type === 'loop' || yamlBlock.type === 'parallel') { importedBlock.data = { @@ -295,16 +301,16 @@ export function convertYamlToWorkflow(yamlWorkflow: YamlWorkflow): ImportResult type: yamlBlock.type === 'loop' ? 'loopNode' : 'parallelNode', } } - + blocks.push(importedBlock) }) - + // Convert edges from connections Object.entries(yamlWorkflow.blocks).forEach(([blockId, yamlBlock]) => { if (yamlBlock.following) { - yamlBlock.following.forEach(targetId => { + yamlBlock.following.forEach((targetId) => { const edgeId = `${blockId}-${targetId}-${Date.now()}` - + const edge: ImportedEdge = { id: edgeId, source: blockId, @@ -313,12 +319,12 @@ export function convertYamlToWorkflow(yamlWorkflow: YamlWorkflow): ImportResult targetHandle: 'target', type: 'workflowEdge', } - + edges.push(edge) }) } }) - + return { blocks, edges, errors, warnings } } @@ -344,31 +350,33 @@ export async function importWorkflowFromYaml( } ): Promise<{ success: boolean; errors: string[]; warnings: string[]; summary?: string }> { logger.info('Starting YAML workflow import') - + try { // Parse YAML const { data: yamlWorkflow, errors: parseErrors } = parseWorkflowYaml(yamlContent) - + if (!yamlWorkflow || parseErrors.length > 0) { return { success: false, errors: parseErrors, warnings: [] } } - + // Convert to importable format const { blocks, edges, errors, warnings } = convertYamlToWorkflow(yamlWorkflow) - + if (errors.length > 0) { return { success: false, errors, warnings } } - + logger.info(`Importing ${blocks.length} blocks and ${edges.length} edges`) - + // Check for existing blocks (new workflows already have a starter block) const existingBlocks = workflowActions.getExistingBlocks() - const existingStarterBlocks = Object.values(existingBlocks).filter((block: any) => block.type === 'starter') - + const existingStarterBlocks = Object.values(existingBlocks).filter( + (block: any) => block.type === 'starter' + ) + let actualBlocksCreated = 0 let starterBlockId: string | null = null - + // Create blocks, but handle starter blocks specially for (const block of blocks) { if (block.type === 'starter') { @@ -377,7 +385,7 @@ export async function importWorkflowFromYaml( const existingStarter = existingStarterBlocks[0] as any starterBlockId = existingStarter.id logger.debug(`Using existing starter block: ${starterBlockId}`) - + // Update the starter block's inputs if needed Object.entries(block.inputs).forEach(([inputKey, inputValue]) => { if (inputValue !== undefined && inputValue !== null && starterBlockId !== null) { @@ -415,9 +423,9 @@ export async function importWorkflowFromYaml( block.extent ) actualBlocksCreated++ - + // Update edges to use the new generated ID - edges.forEach(edge => { + edges.forEach((edge) => { if (edge.source === block.id) { edge.source = generatedId } @@ -425,15 +433,15 @@ export async function importWorkflowFromYaml( edge.target = generatedId } }) - + // Store mapping for setting inputs later block.id = generatedId } - + // Update edges to use the starter block ID if (block.type === 'starter' && starterBlockId !== null) { const starterId = starterBlockId // TypeScript now knows this is string - edges.forEach(edge => { + edges.forEach((edge) => { if (edge.source === block.id) { edge.source = starterId } @@ -443,10 +451,10 @@ export async function importWorkflowFromYaml( }) } } - + // Small delay to ensure blocks are created before adding edges - await new Promise(resolve => setTimeout(resolve, 200)) - + await new Promise((resolve) => setTimeout(resolve, 200)) + // Create edges let edgesCreated = 0 for (const edge of edges) { @@ -454,7 +462,7 @@ export async function importWorkflowFromYaml( logger.debug(`Creating edge: ${edge.source} -> ${edge.target}`) workflowActions.addEdge({ ...edge, - id: crypto.randomUUID() // Generate unique edge ID + id: crypto.randomUUID(), // Generate unique edge ID }) edgesCreated++ } catch (error) { @@ -462,10 +470,10 @@ export async function importWorkflowFromYaml( warnings.push(`Failed to create connection from ${edge.source} to ${edge.target}`) } } - + // Small delay before setting input values - await new Promise(resolve => setTimeout(resolve, 200)) - + await new Promise((resolve) => setTimeout(resolve, 200)) + // Set input values for non-starter blocks for (const block of blocks) { if (block.type !== 'starter') { @@ -482,30 +490,29 @@ export async function importWorkflowFromYaml( }) } } - + // Apply auto layout after a delay setTimeout(() => { logger.debug('Applying auto layout') workflowActions.applyAutoLayout() }, 800) - + const summary = `Successfully imported ${actualBlocksCreated} new blocks and ${edgesCreated} connections` logger.info(summary) - + return { success: true, errors: [], warnings, - summary + summary, } - } catch (error) { const errorMessage = `Import failed: ${error instanceof Error ? error.message : 'Unknown error'}` logger.error(errorMessage, error) return { success: false, errors: [errorMessage], - warnings: [] + warnings: [], } } -} \ No newline at end of file +}