Skip to content

Commit c7b77bd

Browse files
committed
Yaml language basics
1 parent c0b8e1a commit c7b77bd

File tree

6 files changed

+589
-3
lines changed

6 files changed

+589
-3
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { Download, FileText } from 'lucide-react'
5+
import { Button } from '@/components/ui/button'
6+
import {
7+
DropdownMenu,
8+
DropdownMenuContent,
9+
DropdownMenuItem,
10+
DropdownMenuSeparator,
11+
DropdownMenuTrigger,
12+
} from '@/components/ui/dropdown-menu'
13+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
14+
import { createLogger } from '@/lib/logs/console-logger'
15+
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
16+
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
17+
import { useWorkflowYamlStore } from '@/stores/workflows/yaml/store'
18+
19+
const logger = createLogger('ExportControls')
20+
21+
interface ExportControlsProps {
22+
disabled?: boolean
23+
}
24+
25+
export function ExportControls({ disabled = false }: ExportControlsProps) {
26+
const [isExporting, setIsExporting] = useState(false)
27+
const workflowState = useWorkflowStore()
28+
const { workflows, activeWorkflowId } = useWorkflowRegistry()
29+
const getYaml = useWorkflowYamlStore(state => state.getYaml)
30+
31+
const currentWorkflow = activeWorkflowId ? workflows[activeWorkflowId] : null
32+
33+
const downloadFile = (content: string, filename: string, mimeType: string) => {
34+
try {
35+
const blob = new Blob([content], { type: mimeType })
36+
const url = URL.createObjectURL(blob)
37+
const a = document.createElement('a')
38+
a.href = url
39+
a.download = filename
40+
document.body.appendChild(a)
41+
a.click()
42+
document.body.removeChild(a)
43+
URL.revokeObjectURL(url)
44+
} catch (error) {
45+
logger.error('Failed to download file:', error)
46+
}
47+
}
48+
49+
const handleExportJson = async () => {
50+
if (!currentWorkflow || !activeWorkflowId) {
51+
logger.warn('No active workflow to export')
52+
return
53+
}
54+
55+
setIsExporting(true)
56+
try {
57+
const exportData = {
58+
workflow: {
59+
id: activeWorkflowId,
60+
name: currentWorkflow.name,
61+
description: currentWorkflow.description,
62+
color: currentWorkflow.color,
63+
},
64+
state: {
65+
blocks: workflowState.blocks,
66+
edges: workflowState.edges,
67+
loops: workflowState.loops,
68+
parallels: workflowState.parallels,
69+
},
70+
exportedAt: new Date().toISOString(),
71+
version: '1.0'
72+
}
73+
74+
const jsonContent = JSON.stringify(exportData, null, 2)
75+
const filename = `${currentWorkflow.name.replace(/[^a-z0-9]/gi, '_')}_workflow.json`
76+
77+
downloadFile(jsonContent, filename, 'application/json')
78+
logger.info('Workflow exported as JSON')
79+
} catch (error) {
80+
logger.error('Failed to export workflow as JSON:', error)
81+
} finally {
82+
setIsExporting(false)
83+
}
84+
}
85+
86+
const handleExportYaml = async () => {
87+
if (!currentWorkflow || !activeWorkflowId) {
88+
logger.warn('No active workflow to export')
89+
return
90+
}
91+
92+
setIsExporting(true)
93+
try {
94+
const yamlContent = getYaml()
95+
const filename = `${currentWorkflow.name.replace(/[^a-z0-9]/gi, '_')}_workflow.yaml`
96+
97+
downloadFile(yamlContent, filename, 'text/yaml')
98+
logger.info('Workflow exported as YAML')
99+
} catch (error) {
100+
logger.error('Failed to export workflow as YAML:', error)
101+
} finally {
102+
setIsExporting(false)
103+
}
104+
}
105+
106+
return (
107+
<DropdownMenu>
108+
<Tooltip>
109+
<TooltipTrigger asChild>
110+
<DropdownMenuTrigger asChild>
111+
<Button
112+
variant='ghost'
113+
size='icon'
114+
disabled={disabled || isExporting || !currentWorkflow}
115+
className='hover:text-foreground'
116+
>
117+
<Download className='h-5 w-5' />
118+
<span className='sr-only'>Export Workflow</span>
119+
</Button>
120+
</DropdownMenuTrigger>
121+
</TooltipTrigger>
122+
<TooltipContent>
123+
{disabled
124+
? 'Export not available'
125+
: !currentWorkflow
126+
? 'No workflow to export'
127+
: 'Export Workflow'
128+
}
129+
</TooltipContent>
130+
</Tooltip>
131+
132+
<DropdownMenuContent align='end' className='w-48'>
133+
<DropdownMenuItem
134+
onClick={handleExportJson}
135+
disabled={isExporting || !currentWorkflow}
136+
className='flex items-center gap-2 cursor-pointer'
137+
>
138+
<FileText className='h-4 w-4' />
139+
<div className='flex flex-col'>
140+
<span>Export as JSON</span>
141+
<span className='text-muted-foreground text-xs'>
142+
Full workflow data
143+
</span>
144+
</div>
145+
</DropdownMenuItem>
146+
147+
<DropdownMenuSeparator />
148+
149+
<DropdownMenuItem
150+
onClick={handleExportYaml}
151+
disabled={isExporting || !currentWorkflow}
152+
className='flex items-center gap-2 cursor-pointer'
153+
>
154+
<FileText className='h-4 w-4' />
155+
<div className='flex flex-col'>
156+
<span>Export as YAML</span>
157+
<span className='text-muted-foreground text-xs'>
158+
Condensed workflow language
159+
</span>
160+
</div>
161+
</DropdownMenuItem>
162+
</DropdownMenuContent>
163+
</DropdownMenu>
164+
)
165+
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
} from '../../../hooks/use-keyboard-shortcuts'
5757
import { useWorkflowExecution } from '../../hooks/use-workflow-execution'
5858
import { DeploymentControls } from './components/deployment-controls/deployment-controls'
59+
import { ExportControls } from './components/export-controls/export-controls'
5960
import { HistoryDropdownItem } from './components/history-dropdown-item/history-dropdown-item'
6061
import { MarketplaceModal } from './components/marketplace-modal/marketplace-modal'
6162
import { NotificationDropdownItem } from './components/notification-dropdown-item/notification-dropdown-item'
@@ -1287,6 +1288,7 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
12871288
{renderDuplicateButton()}
12881289
{renderAutoLayoutButton()}
12891290
{renderDebugModeToggle()}
1291+
<ExportControls disabled={!userPermissions.canRead} />
12901292
{/* {renderPublishButton()} */}
12911293
{renderDeployButton()}
12921294
{renderRunButton()}

apps/sim/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"@radix-ui/react-tooltip": "^1.1.6",
6666
"@react-email/components": "^0.0.34",
6767
"@sentry/nextjs": "^9.15.0",
68+
"@types/js-yaml": "4.0.9",
6869
"@types/three": "0.177.0",
6970
"@vercel/og": "^0.6.5",
7071
"@vercel/speed-insights": "^1.2.0",
@@ -86,6 +87,7 @@
8687
"input-otp": "^1.4.2",
8788
"ioredis": "^5.6.0",
8889
"jose": "6.0.11",
90+
"js-yaml": "4.1.0",
8991
"jwt-decode": "^4.0.0",
9092
"lenis": "^1.2.3",
9193
"lucide-react": "^0.479.0",
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Workflow YAML Store
2+
3+
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.
4+
5+
## Features
6+
7+
- **Dynamic Input Extraction**: Automatically reads input values from block configurations and subblock stores
8+
- **Connection Mapping**: Determines preceding and following blocks from workflow edges
9+
- **Type-Aware Processing**: Handles different input types (text, numbers, booleans, objects) appropriately
10+
- **Auto-Refresh**: Automatically updates when workflow state or input values change
11+
- **Clean Format**: Generates well-formatted YAML with proper indentation
12+
13+
## YAML Structure
14+
15+
```yaml
16+
version: "1.0"
17+
blocks:
18+
block-id-1:
19+
type: "starter"
20+
name: "Start"
21+
inputs:
22+
startWorkflow: "manual"
23+
following:
24+
- "block-id-2"
25+
26+
block-id-2:
27+
type: "agent"
28+
name: "AI Agent"
29+
inputs:
30+
systemPrompt: "You are a helpful assistant"
31+
userPrompt: "Process the input data"
32+
model: "gpt-4"
33+
temperature: 0.7
34+
preceding:
35+
- "block-id-1"
36+
following:
37+
- "block-id-3"
38+
```
39+
40+
## Usage
41+
42+
### Basic Usage
43+
44+
```typescript
45+
import { useWorkflowYamlStore } from '@/stores/workflows/yaml/store'
46+
47+
function WorkflowYamlViewer() {
48+
const yaml = useWorkflowYamlStore(state => state.getYaml())
49+
50+
return (
51+
<pre>
52+
<code>{yaml}</code>
53+
</pre>
54+
)
55+
}
56+
```
57+
58+
### Manual Refresh
59+
60+
```typescript
61+
import { useWorkflowYamlStore } from '@/stores/workflows/yaml/store'
62+
63+
function WorkflowControls() {
64+
const refreshYaml = useWorkflowYamlStore(state => state.refreshYaml)
65+
66+
return (
67+
<button onClick={refreshYaml}>
68+
Refresh YAML
69+
</button>
70+
)
71+
}
72+
```
73+
74+
### Advanced Usage
75+
76+
```typescript
77+
import { useWorkflowYamlStore } from '@/stores/workflows/yaml/store'
78+
79+
function WorkflowExporter() {
80+
const { yaml, lastGenerated, generateYaml } = useWorkflowYamlStore()
81+
82+
const exportToFile = () => {
83+
const blob = new Blob([yaml], { type: 'text/yaml' })
84+
const url = URL.createObjectURL(blob)
85+
const a = document.createElement('a')
86+
a.href = url
87+
a.download = 'workflow.yaml'
88+
a.click()
89+
URL.revokeObjectURL(url)
90+
}
91+
92+
return (
93+
<div>
94+
<p>Last generated: {lastGenerated ? new Date(lastGenerated).toLocaleString() : 'Never'}</p>
95+
<button onClick={generateYaml}>Regenerate</button>
96+
<button onClick={exportToFile}>Export YAML</button>
97+
</div>
98+
)
99+
}
100+
```
101+
102+
## Input Types Handled
103+
104+
The store intelligently processes different subblock input types:
105+
106+
- **Text Inputs** (`short-input`, `long-input`): Trimmed strings
107+
- **Dropdowns/Combobox** (`dropdown`, `combobox`): Selected values
108+
- **Tables** (`table`): Arrays of objects (only if non-empty)
109+
- **Code Blocks** (`code`): Preserves formatting for strings and objects
110+
- **Switches** (`switch`): Boolean values
111+
- **Sliders** (`slider`): Numeric values
112+
- **Checkbox Lists** (`checkbox-list`): Arrays of selected items
113+
114+
## Auto-Refresh Behavior
115+
116+
The store automatically refreshes in these scenarios:
117+
118+
1. **Workflow Structure Changes**: When blocks are added, removed, or connections change
119+
2. **Input Value Changes**: When any subblock input values are modified
120+
3. **Debounced Updates**: Changes are debounced to prevent excessive regeneration
121+
122+
## Performance
123+
124+
- **Lazy Generation**: YAML is only generated when requested
125+
- **Caching**: Results are cached and only regenerated when data changes
126+
- **Debouncing**: Rapid changes are debounced to improve performance
127+
- **Selective Updates**: Only regenerates when meaningful changes occur
128+
129+
## Error Handling
130+
131+
If YAML generation fails, the store returns an error message in YAML comment format:
132+
133+
```yaml
134+
# Error generating YAML: [error message]
135+
```
136+
137+
## Dependencies
138+
139+
- `js-yaml`: For YAML serialization
140+
- `zustand`: For state management
141+
- `@/blocks`: For block configuration access
142+
- `@/stores/workflows/workflow/store`: For workflow state
143+
- `@/stores/workflows/subblock/store`: For input values

0 commit comments

Comments
 (0)