mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
fix(yaml): modules that require agent repo (#873)
* fix: import yaml store * fix: removed local dev modules
This commit is contained in:
@@ -32,6 +32,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui'
|
||||
import { useSession } from '@/lib/auth-client'
|
||||
import { isDev } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
@@ -998,10 +999,11 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
|
||||
return (
|
||||
<div className='fixed top-4 right-4 z-20 flex items-center gap-1'>
|
||||
{renderDisconnectionNotice()}
|
||||
{renderToggleButton()}
|
||||
{isExpanded && <ExportControls />}
|
||||
{isExpanded && renderAutoLayoutButton()}
|
||||
{isExpanded && renderDuplicateButton()}
|
||||
{!isDev && renderToggleButton()}
|
||||
{isExpanded && !isDev && <ExportControls />}
|
||||
{isExpanded && !isDev && renderAutoLayoutButton()}
|
||||
{!isDev && isExpanded && renderDuplicateButton()}
|
||||
{isDev && renderDuplicateButton()}
|
||||
{renderDeleteButton()}
|
||||
{!isDebugging && renderDebugModeToggle()}
|
||||
{renderPublishButton()}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { isDev } from '@/lib/environment'
|
||||
import { useCopilotStore } from '@/stores/copilot/store'
|
||||
import { useChatStore } from '@/stores/panel/chat/store'
|
||||
import { useConsoleStore } from '@/stores/panel/console/store'
|
||||
@@ -304,14 +305,16 @@ export function Panel() {
|
||||
>
|
||||
Console
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTabClick('copilot')}
|
||||
className={`panel-tab-base inline-flex flex-1 cursor-pointer items-center justify-center rounded-[10px] border border-transparent py-1 font-[450] text-sm outline-none transition-colors duration-200 ${
|
||||
isOpen && activeTab === 'copilot' ? 'panel-tab-active' : 'panel-tab-inactive'
|
||||
}`}
|
||||
>
|
||||
Copilot
|
||||
</button>
|
||||
{!isDev && (
|
||||
<button
|
||||
onClick={() => handleTabClick('copilot')}
|
||||
className={`panel-tab-base inline-flex flex-1 cursor-pointer items-center justify-center rounded-[10px] border border-transparent py-1 font-[450] text-sm outline-none transition-colors duration-200 ${
|
||||
isOpen && activeTab === 'copilot' ? 'panel-tab-active' : 'panel-tab-inactive'
|
||||
}`}
|
||||
>
|
||||
Copilot
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleTabClick('variables')}
|
||||
className={`panel-tab-base inline-flex flex-1 cursor-pointer items-center justify-center rounded-[10px] border border-transparent py-1 font-[450] text-sm outline-none transition-colors duration-200 ${
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Folder, Plus, Upload } from 'lucide-react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { isDev } from '@/lib/environment'
|
||||
import { generateFolderName } from '@/lib/naming'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
@@ -347,7 +348,7 @@ export function CreateMenu({ onCreateWorkflow, isCreatingWorkflow = false }: Cre
|
||||
</button>
|
||||
|
||||
{/* Import Workflow */}
|
||||
{userPermissions.canEdit && (
|
||||
{userPermissions.canEdit && !isDev && (
|
||||
<button
|
||||
className={cn(menuItemClassName, isImporting && 'cursor-not-allowed opacity-50')}
|
||||
onClick={handleImportWorkflow}
|
||||
|
||||
@@ -464,7 +464,7 @@ export function WorkspaceSelector({
|
||||
{/* Bottom Actions */}
|
||||
<div className='mt-2 flex items-center gap-2 border-t pt-2'>
|
||||
{/* Send Invite - Hide in development */}
|
||||
{isDev && (
|
||||
{!isDev && (
|
||||
<Button
|
||||
variant='secondary'
|
||||
onClick={userPermissions.canAdmin ? () => setShowInviteMembers(true) : undefined}
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
# Workflow YAML Store
|
||||
|
||||
This store dynamically generates a condensed YAML representation of workflows from the JSON workflow state. It extracts input values, connections, and block relationships to create a clean, readable format.
|
||||
|
||||
## Features
|
||||
|
||||
- **Dynamic Input Extraction**: Automatically reads input values from block configurations and subblock stores
|
||||
- **Connection Mapping**: Determines preceding and following blocks from workflow edges
|
||||
- **Type-Aware Processing**: Handles different input types (text, numbers, booleans, objects) appropriately
|
||||
- **Auto-Refresh**: Automatically updates when workflow state or input values change
|
||||
- **Clean Format**: Generates well-formatted YAML with proper indentation
|
||||
|
||||
## YAML Structure
|
||||
|
||||
```yaml
|
||||
version: "1.0"
|
||||
blocks:
|
||||
block-id-1:
|
||||
type: "starter"
|
||||
name: "Start"
|
||||
inputs:
|
||||
startWorkflow: "manual"
|
||||
following:
|
||||
- "block-id-2"
|
||||
|
||||
block-id-2:
|
||||
type: "agent"
|
||||
name: "AI Agent"
|
||||
inputs:
|
||||
systemPrompt: "You are a helpful assistant"
|
||||
userPrompt: "Process the input data"
|
||||
model: "gpt-4"
|
||||
temperature: 0.7
|
||||
preceding:
|
||||
- "block-id-1"
|
||||
following:
|
||||
- "block-id-3"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { useWorkflowYamlStore } from '@/stores/workflows/yaml/store'
|
||||
|
||||
function WorkflowYamlViewer() {
|
||||
const yaml = useWorkflowYamlStore(state => state.getYaml())
|
||||
|
||||
return (
|
||||
<pre>
|
||||
<code>{yaml}</code>
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Manual Refresh
|
||||
|
||||
```typescript
|
||||
import { useWorkflowYamlStore } from '@/stores/workflows/yaml/store'
|
||||
|
||||
function WorkflowControls() {
|
||||
const refreshYaml = useWorkflowYamlStore(state => state.refreshYaml)
|
||||
|
||||
return (
|
||||
<button onClick={refreshYaml}>
|
||||
Refresh YAML
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
```typescript
|
||||
import { useWorkflowYamlStore } from '@/stores/workflows/yaml/store'
|
||||
|
||||
function WorkflowExporter() {
|
||||
const { yaml, lastGenerated, generateYaml } = useWorkflowYamlStore()
|
||||
|
||||
const exportToFile = () => {
|
||||
const blob = new Blob([yaml], { type: 'text/yaml' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'workflow.yaml'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Last generated: {lastGenerated ? new Date(lastGenerated).toLocaleString() : 'Never'}</p>
|
||||
<button onClick={generateYaml}>Regenerate</button>
|
||||
<button onClick={exportToFile}>Export YAML</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Input Types Handled
|
||||
|
||||
The store intelligently processes different subblock input types:
|
||||
|
||||
- **Text Inputs** (`short-input`, `long-input`): Trimmed strings
|
||||
- **Dropdowns/Combobox** (`dropdown`, `combobox`): Selected values
|
||||
- **Tables** (`table`): Arrays of objects (only if non-empty)
|
||||
- **Code Blocks** (`code`): Preserves formatting for strings and objects
|
||||
- **Switches** (`switch`): Boolean values
|
||||
- **Sliders** (`slider`): Numeric values
|
||||
- **Checkbox Lists** (`checkbox-list`): Arrays of selected items
|
||||
|
||||
## Auto-Refresh Behavior
|
||||
|
||||
The store automatically refreshes in these scenarios:
|
||||
|
||||
1. **Workflow Structure Changes**: When blocks are added, removed, or connections change
|
||||
2. **Input Value Changes**: When any subblock input values are modified
|
||||
3. **Debounced Updates**: Changes are debounced to prevent excessive regeneration
|
||||
|
||||
## Performance
|
||||
|
||||
- **Lazy Generation**: YAML is only generated when requested
|
||||
- **Caching**: Results are cached and only regenerated when data changes
|
||||
- **Debouncing**: Rapid changes are debounced to improve performance
|
||||
- **Selective Updates**: Only regenerates when meaningful changes occur
|
||||
|
||||
## Error Handling
|
||||
|
||||
If YAML generation fails, the store returns an error message in YAML comment format:
|
||||
|
||||
```yaml
|
||||
# Error generating YAML: [error message]
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `js-yaml`: For YAML serialization
|
||||
- `zustand`: For state management
|
||||
- `@/blocks`: For block configuration access
|
||||
- `@/stores/workflows/workflow/store`: For workflow state
|
||||
- `@/stores/workflows/subblock/store`: For input values
|
||||
429
apps/sim/stores/workflows/yaml/importer.ts
Normal file
429
apps/sim/stores/workflows/yaml/importer.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
import { load as yamlParse } from 'js-yaml'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getBlock } from '@/blocks'
|
||||
import {
|
||||
type ConnectionsFormat,
|
||||
expandConditionInputs,
|
||||
type ImportedEdge,
|
||||
parseBlockConnections,
|
||||
validateBlockReferences,
|
||||
validateBlockStructure,
|
||||
} from '@/stores/workflows/yaml/parsing-utils'
|
||||
|
||||
const logger = createLogger('WorkflowYamlImporter')
|
||||
|
||||
interface YamlBlock {
|
||||
type: string
|
||||
name: string
|
||||
inputs?: Record<string, any>
|
||||
connections?: ConnectionsFormat
|
||||
parentId?: string // Add parentId for nested blocks
|
||||
}
|
||||
|
||||
interface YamlWorkflow {
|
||||
version: string
|
||||
blocks: Record<string, YamlBlock>
|
||||
}
|
||||
|
||||
interface ImportedBlock {
|
||||
id: string
|
||||
type: string
|
||||
name: string
|
||||
inputs: Record<string, any>
|
||||
position: { x: number; y: number }
|
||||
data?: Record<string, any>
|
||||
parentId?: string
|
||||
extent?: 'parent'
|
||||
}
|
||||
|
||||
interface ImportResult {
|
||||
blocks: ImportedBlock[]
|
||||
edges: ImportedEdge[]
|
||||
errors: string[]
|
||||
warnings: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse YAML content and validate its structure
|
||||
*/
|
||||
export function parseWorkflowYaml(yamlContent: string): {
|
||||
data: YamlWorkflow | null
|
||||
errors: string[]
|
||||
} {
|
||||
const errors: string[] = []
|
||||
|
||||
try {
|
||||
const data = yamlParse(yamlContent) as unknown
|
||||
|
||||
// Validate top-level structure
|
||||
if (!data || typeof data !== 'object') {
|
||||
errors.push('Invalid YAML: Root must be an object')
|
||||
return { data: null, errors }
|
||||
}
|
||||
|
||||
// Type guard to check if data has the expected structure
|
||||
const parsedData = data as Record<string, unknown>
|
||||
|
||||
if (!parsedData.version) {
|
||||
errors.push('Missing required field: version')
|
||||
}
|
||||
|
||||
if (!parsedData.blocks || typeof parsedData.blocks !== 'object') {
|
||||
errors.push('Missing or invalid field: blocks')
|
||||
return { data: null, errors }
|
||||
}
|
||||
|
||||
// Validate blocks structure
|
||||
const blocks = parsedData.blocks as Record<string, unknown>
|
||||
Object.entries(blocks).forEach(([blockId, block]: [string, unknown]) => {
|
||||
if (!block || typeof block !== 'object') {
|
||||
errors.push(`Invalid block definition for '${blockId}': must be an object`)
|
||||
return
|
||||
}
|
||||
|
||||
const blockData = block as Record<string, unknown>
|
||||
|
||||
if (!blockData.type || typeof blockData.type !== 'string') {
|
||||
errors.push(`Invalid block '${blockId}': missing or invalid 'type' field`)
|
||||
}
|
||||
|
||||
if (!blockData.name || typeof blockData.name !== 'string') {
|
||||
errors.push(`Invalid block '${blockId}': missing or invalid 'name' field`)
|
||||
}
|
||||
|
||||
if (blockData.inputs && typeof blockData.inputs !== 'object') {
|
||||
errors.push(`Invalid block '${blockId}': 'inputs' must be an object`)
|
||||
}
|
||||
|
||||
if (blockData.preceding && !Array.isArray(blockData.preceding)) {
|
||||
errors.push(`Invalid block '${blockId}': 'preceding' must be an array`)
|
||||
}
|
||||
|
||||
if (blockData.following && !Array.isArray(blockData.following)) {
|
||||
errors.push(`Invalid block '${blockId}': 'following' must be an array`)
|
||||
}
|
||||
})
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { data: null, errors }
|
||||
}
|
||||
|
||||
return { data: parsedData as unknown as YamlWorkflow, errors: [] }
|
||||
} catch (error) {
|
||||
errors.push(`YAML parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
return { data: null, errors }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that block types exist and are valid
|
||||
*/
|
||||
function validateBlockTypes(yamlWorkflow: YamlWorkflow): { errors: string[]; warnings: string[] } {
|
||||
const errors: string[] = []
|
||||
const warnings: string[] = []
|
||||
|
||||
Object.entries(yamlWorkflow.blocks).forEach(([blockId, block]) => {
|
||||
// Use shared structure validation
|
||||
const { errors: structureErrors, warnings: structureWarnings } = validateBlockStructure(
|
||||
blockId,
|
||||
block
|
||||
)
|
||||
errors.push(...structureErrors)
|
||||
warnings.push(...structureWarnings)
|
||||
|
||||
// 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)
|
||||
if (!subBlockConfig) {
|
||||
warnings.push(
|
||||
`Block '${blockId}' has unknown input '${inputKey}' for type '${block.type}'`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return { errors, warnings }
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate positions for blocks based on their connections
|
||||
* Uses a simple layered approach similar to the auto-layout algorithm
|
||||
*/
|
||||
function calculateBlockPositions(
|
||||
yamlWorkflow: YamlWorkflow
|
||||
): Record<string, { x: number; y: number }> {
|
||||
const positions: Record<string, { x: number; y: number }> = {}
|
||||
const blockIds = Object.keys(yamlWorkflow.blocks)
|
||||
|
||||
// Find starter blocks (no incoming connections)
|
||||
const starterBlocks = blockIds.filter((id) => {
|
||||
const block = yamlWorkflow.blocks[id]
|
||||
return !block.connections?.incoming || block.connections.incoming.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<string>()
|
||||
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.connections?.outgoing) {
|
||||
block.connections.outgoing.forEach((connection) => {
|
||||
if (!visited.has(connection.target)) {
|
||||
queue.push(connection.target)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLayer.length > 0) {
|
||||
layers.push(currentLayer)
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining blocks as isolated layer
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string>()
|
||||
const visiting = new Set<string>() // Track blocks currently being processed to detect cycles
|
||||
|
||||
// Create a map for quick lookup
|
||||
const blockMap = new Map<string, ImportedBlock>()
|
||||
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
|
||||
*/
|
||||
export function convertYamlToWorkflow(yamlWorkflow: YamlWorkflow): ImportResult {
|
||||
const errors: string[] = []
|
||||
const warnings: string[] = []
|
||||
const blocks: ImportedBlock[] = []
|
||||
const edges: ImportedEdge[] = []
|
||||
|
||||
// Validate block references
|
||||
const referenceErrors = validateBlockReferences(yamlWorkflow.blocks)
|
||||
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 }
|
||||
|
||||
// Expand condition inputs from clean format to internal format
|
||||
const processedInputs =
|
||||
yamlBlock.type === 'condition'
|
||||
? expandConditionInputs(blockId, yamlBlock.inputs || {})
|
||||
: yamlBlock.inputs || {}
|
||||
|
||||
const importedBlock: ImportedBlock = {
|
||||
id: blockId,
|
||||
type: yamlBlock.type,
|
||||
name: yamlBlock.name,
|
||||
inputs: processedInputs,
|
||||
position,
|
||||
}
|
||||
|
||||
// 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)
|
||||
})
|
||||
|
||||
// Convert edges from connections using shared parser
|
||||
Object.entries(yamlWorkflow.blocks).forEach(([blockId, yamlBlock]) => {
|
||||
const {
|
||||
edges: blockEdges,
|
||||
errors: connectionErrors,
|
||||
warnings: connectionWarnings,
|
||||
} = parseBlockConnections(blockId, yamlBlock.connections, yamlBlock.type)
|
||||
|
||||
edges.push(...blockEdges)
|
||||
errors.push(...connectionErrors)
|
||||
warnings.push(...connectionWarnings)
|
||||
})
|
||||
|
||||
// Sort blocks to ensure parents are created before children
|
||||
const sortedBlocks = sortBlocksByParentChildOrder(blocks)
|
||||
|
||||
return { blocks: sortedBlocks, edges, errors, warnings }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create smart ID mapping that preserves existing block IDs and generates new ones for new blocks
|
||||
*/
|
||||
function createSmartIdMapping(
|
||||
yamlBlocks: ImportedBlock[],
|
||||
existingBlocks: Record<string, any>,
|
||||
activeWorkflowId: string,
|
||||
forceNewIds = false
|
||||
): Map<string, string> {
|
||||
const yamlIdToActualId = new Map<string, string>()
|
||||
const existingBlockIds = new Set(Object.keys(existingBlocks))
|
||||
|
||||
logger.info('Creating smart ID mapping', {
|
||||
activeWorkflowId,
|
||||
yamlBlockCount: yamlBlocks.length,
|
||||
existingBlockCount: Object.keys(existingBlocks).length,
|
||||
existingBlockIds: Array.from(existingBlockIds),
|
||||
yamlBlockIds: yamlBlocks.map((b) => b.id),
|
||||
forceNewIds,
|
||||
})
|
||||
|
||||
for (const block of yamlBlocks) {
|
||||
if (forceNewIds || !existingBlockIds.has(block.id)) {
|
||||
// Force new ID or block ID doesn't exist in current workflow - generate new UUID
|
||||
const newId = uuidv4()
|
||||
yamlIdToActualId.set(block.id, newId)
|
||||
logger.info(
|
||||
`🆕 Mapping new block: ${block.id} -> ${newId} (${forceNewIds ? 'forced new ID' : `not found in workflow ${activeWorkflowId}`})`
|
||||
)
|
||||
} else {
|
||||
// Block ID exists in current workflow - preserve it
|
||||
yamlIdToActualId.set(block.id, block.id)
|
||||
logger.info(
|
||||
`✅ Preserving existing block ID: ${block.id} (exists in workflow ${activeWorkflowId})`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Smart ID mapping completed', {
|
||||
mappings: Array.from(yamlIdToActualId.entries()),
|
||||
preservedCount: Array.from(yamlIdToActualId.entries()).filter(([old, new_]) => old === new_)
|
||||
.length,
|
||||
newCount: Array.from(yamlIdToActualId.entries()).filter(([old, new_]) => old !== new_).length,
|
||||
})
|
||||
|
||||
return yamlIdToActualId
|
||||
}
|
||||
Reference in New Issue
Block a user