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 b8798d18d9..b441acac0b 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 @@ -1291,6 +1291,7 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) { {renderDebugModeToggle()} + {/* */} {/* {renderPublishButton()} */} {renderDeployButton()} {renderRunButton()} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 273c55edd9..9be2783451 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -10,7 +10,6 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { Chat } from './components/chat/chat' import { ChatModal } from './components/chat/components/chat-modal/chat-modal' import { Console } from './components/console/console' -import { Copilot } from './components/copilot/copilot' import { Variables } from './components/variables/variables' export function Panel() { @@ -120,7 +119,7 @@ export function Panel() { > Variables - + */} - {(activeTab === 'console' || activeTab === 'chat' || activeTab === 'copilot') && ( + {(activeTab === 'console' || activeTab === 'chat' /* || activeTab === 'copilot' */) && ( + )} + + + + {isDisabled + ? disabled + ? 'Text editor not available' + : 'No active workflow' + : 'Edit as Text'} + + + + + + Workflow Text Editor + + Edit your workflow as YAML or JSON. Changes will completely replace the current workflow + when you save. + + + +
+ {isLoading ? ( +
+
+
+

Loading workflow content...

+
+
+ ) : ( + + )} +
+ + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-text-editor/workflow-text-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-text-editor/workflow-text-editor.tsx new file mode 100644 index 0000000000..0aba867857 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-text-editor/workflow-text-editor.tsx @@ -0,0 +1,348 @@ +'use client' + +import { useCallback, useEffect, useState } from 'react' +import { dump as yamlDump, load as yamlParse } from 'js-yaml' +import { AlertCircle, Check, FileCode, Save } from 'lucide-react' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Button } from '@/components/ui/button' +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { createLogger } from '@/lib/logs/console-logger' +import { cn } from '@/lib/utils' +import { CodeEditor } from '../workflow-block/components/sub-block/components/tool-input/components/code-editor/code-editor' + +const logger = createLogger('WorkflowTextEditor') + +export type EditorFormat = 'yaml' | 'json' + +interface ValidationError { + line?: number + column?: number + message: string +} + +interface WorkflowTextEditorProps { + initialValue: string + format: EditorFormat + onSave: ( + content: string, + format: EditorFormat + ) => Promise<{ success: boolean; errors?: string[]; warnings?: string[] }> + onFormatChange?: (format: EditorFormat) => void + className?: string + disabled?: boolean +} + +export function WorkflowTextEditor({ + initialValue, + format, + onSave, + onFormatChange, + className, + disabled = false, +}: WorkflowTextEditorProps) { + const [content, setContent] = useState(initialValue) + const [currentFormat, setCurrentFormat] = useState(format) + const [validationErrors, setValidationErrors] = useState([]) + const [isSaving, setIsSaving] = useState(false) + const [saveResult, setSaveResult] = useState<{ + success: boolean + errors?: string[] + warnings?: string[] + } | null>(null) + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) + + // Validate content based on format + const validateContent = useCallback((text: string, fmt: EditorFormat): ValidationError[] => { + const errors: ValidationError[] = [] + + if (!text.trim()) { + return errors // Empty content is valid + } + + try { + if (fmt === 'yaml') { + yamlParse(text) + } else if (fmt === 'json') { + JSON.parse(text) + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Parse error' + + // Extract line/column info if available + const lineMatch = errorMessage.match(/line (\d+)/i) + const columnMatch = errorMessage.match(/column (\d+)/i) + + errors.push({ + line: lineMatch ? Number.parseInt(lineMatch[1], 10) : undefined, + column: columnMatch ? Number.parseInt(columnMatch[1], 10) : undefined, + message: errorMessage, + }) + } + + return errors + }, []) + + // Convert between formats + const convertFormat = useCallback( + (text: string, fromFormat: EditorFormat, toFormat: EditorFormat): string => { + if (fromFormat === toFormat || !text.trim()) { + return text + } + + try { + let parsed: any + + if (fromFormat === 'yaml') { + parsed = yamlParse(text) + } else { + parsed = JSON.parse(text) + } + + if (toFormat === 'yaml') { + return yamlDump(parsed, { + indent: 2, + lineWidth: -1, + noRefs: true, + }) + } + return JSON.stringify(parsed, null, 2) + } catch (error) { + logger.warn(`Failed to convert from ${fromFormat} to ${toFormat}:`, error) + return text // Return original if conversion fails + } + }, + [] + ) + + // Handle content changes + const handleContentChange = useCallback( + (newContent: string) => { + setContent(newContent) + setHasUnsavedChanges(newContent !== initialValue) + + // Validate on change + const errors = validateContent(newContent, currentFormat) + setValidationErrors(errors) + + // Clear save result when editing + setSaveResult(null) + }, + [initialValue, currentFormat, validateContent] + ) + + // Handle format changes + const handleFormatChange = useCallback( + (newFormat: EditorFormat) => { + if (newFormat === currentFormat) return + + // Convert content to new format + const convertedContent = convertFormat(content, currentFormat, newFormat) + + setCurrentFormat(newFormat) + setContent(convertedContent) + + // Validate converted content + const errors = validateContent(convertedContent, newFormat) + setValidationErrors(errors) + + // Notify parent + onFormatChange?.(newFormat) + }, + [content, currentFormat, convertFormat, validateContent, onFormatChange] + ) + + // Handle save + const handleSave = useCallback(async () => { + if (validationErrors.length > 0) { + logger.warn('Cannot save with validation errors') + return + } + + setIsSaving(true) + setSaveResult(null) + + try { + const result = await onSave(content, currentFormat) + setSaveResult(result) + + if (result.success) { + setHasUnsavedChanges(false) + logger.info('Workflow successfully updated from text editor') + } else { + logger.error('Failed to save workflow:', result.errors) + } + } catch (error) { + logger.error('Save failed with exception:', error) + setSaveResult({ + success: false, + errors: [error instanceof Error ? error.message : 'Unknown error'], + }) + } finally { + setIsSaving(false) + } + }, [content, currentFormat, validationErrors, onSave]) + + // Update content when initialValue changes + useEffect(() => { + setContent(initialValue) + setHasUnsavedChanges(false) + setSaveResult(null) + }, [initialValue]) + + // Validation status + const isValid = validationErrors.length === 0 + const canSave = isValid && hasUnsavedChanges && !disabled + + // Get editor language for syntax highlighting + const editorLanguage = currentFormat === 'yaml' ? 'javascript' : 'json' // yaml highlighting not available, use js + + return ( +
+ {/* Header with controls */} +
+
+
+ + Workflow Text Editor +
+
+ handleFormatChange(value as EditorFormat)} + > + + + YAML + + + JSON + + + + + + + + + {!isValid + ? 'Fix validation errors to save' + : !hasUnsavedChanges + ? 'No changes to save' + : disabled + ? 'Editor is disabled' + : 'Save changes to workflow'} + + +
+
+ + {/* Status indicators */} +
+ {isValid ? ( +
+ + Valid {currentFormat.toUpperCase()} +
+ ) : ( +
+ + {validationErrors.length} validation error{validationErrors.length !== 1 ? 's' : ''} +
+ )} + + {hasUnsavedChanges &&
• Unsaved changes
} +
+
+ + {/* Alerts section - fixed height, scrollable if needed */} + {(validationErrors.length > 0 || saveResult) && ( +
+
+ {/* Validation errors */} + {validationErrors.length > 0 && ( + <> + {validationErrors.map((error, index) => ( + + + + {error.line && error.column + ? `Line ${error.line}, Column ${error.column}: ${error.message}` + : error.message} + + + ))} + + )} + + {/* Save result */} + {saveResult && ( + + {saveResult.success ? ( + + ) : ( + + )} + + {saveResult.success ? ( + <> + Workflow updated successfully! + {saveResult.warnings && saveResult.warnings.length > 0 && ( +
+ Warnings: +
    + {saveResult.warnings.map((warning, index) => ( +
  • {warning}
  • + ))} +
+
+ )} + + ) : ( + <> + Failed to update workflow: + {saveResult.errors && ( +
    + {saveResult.errors.map((error, index) => ( +
  • {error}
  • + ))} +
+ )} + + )} +
+
+ )} +
+
+ )} + + {/* Code editor - takes remaining space */} +
+
+ +
+
+
+ ) +}