mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
Move upload button
This commit is contained in:
@@ -1,340 +0,0 @@
|
||||
'use client'
|
||||
|
||||
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 {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
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 { createLogger } from '@/lib/logs/console-logger'
|
||||
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'
|
||||
|
||||
const logger = createLogger('ImportControls')
|
||||
|
||||
interface ImportControlsProps {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function ImportControls({ disabled = false }: ImportControlsProps) {
|
||||
const [isImporting, setIsImporting] = useState(false)
|
||||
const [showYamlDialog, setShowYamlDialog] = useState(false)
|
||||
const [yamlContent, setYamlContent] = useState('')
|
||||
const [importResult, setImportResult] = useState<{
|
||||
success: boolean
|
||||
errors: string[]
|
||||
warnings: string[]
|
||||
summary?: string
|
||||
} | null>(null)
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
|
||||
// Stores and hooks
|
||||
const { createWorkflow } = useWorkflowRegistry()
|
||||
const { collaborativeAddBlock, collaborativeAddEdge, collaborativeSetSubblockValue } =
|
||||
useCollaborativeWorkflow()
|
||||
const subBlockStore = useSubBlockStore()
|
||||
|
||||
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const content = await file.text()
|
||||
setYamlContent(content)
|
||||
setShowYamlDialog(true)
|
||||
} catch (error) {
|
||||
logger.error('Failed to read file:', error)
|
||||
setImportResult({
|
||||
success: false,
|
||||
errors: [
|
||||
`Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
],
|
||||
warnings: [],
|
||||
})
|
||||
}
|
||||
|
||||
// Reset file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleYamlImport = async () => {
|
||||
if (!yamlContent.trim()) {
|
||||
setImportResult({
|
||||
success: false,
|
||||
errors: ['YAML content is required'],
|
||||
warnings: [],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setIsImporting(true)
|
||||
setImportResult(null)
|
||||
|
||||
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: [],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new workflow
|
||||
|
||||
const newWorkflowId = await createWorkflow({
|
||||
name: `Imported Workflow - ${new Date().toLocaleString()}`,
|
||||
description: 'Workflow imported from YAML',
|
||||
workspaceId,
|
||||
})
|
||||
|
||||
// Import the YAML into the new workflow BEFORE navigation (creates complete state and saves directly to DB)
|
||||
// This avoids timing issues with workflow reload during navigation
|
||||
const result = await importWorkflowFromYaml(
|
||||
yamlContent,
|
||||
{
|
||||
addBlock: collaborativeAddBlock,
|
||||
addEdge: collaborativeAddEdge,
|
||||
applyAutoLayout: () => {
|
||||
// Trigger auto layout
|
||||
window.dispatchEvent(new CustomEvent('trigger-auto-layout'))
|
||||
},
|
||||
setSubBlockValue: (blockId: string, subBlockId: string, value: any) => {
|
||||
// Use the collaborative function - the same one called when users type into fields
|
||||
collaborativeSetSubblockValue(blockId, subBlockId, value)
|
||||
},
|
||||
getExistingBlocks: () => {
|
||||
// For a new workflow, we'll get the starter block from the server
|
||||
return {}
|
||||
},
|
||||
},
|
||||
newWorkflowId
|
||||
) // Pass the new workflow ID to import into
|
||||
|
||||
// Navigate to the new workflow AFTER import is complete
|
||||
if (result.success) {
|
||||
logger.info('Navigating to imported workflow')
|
||||
router.push(`/workspace/${workspaceId}/w/${newWorkflowId}`)
|
||||
}
|
||||
|
||||
setImportResult(result)
|
||||
|
||||
if (result.success) {
|
||||
setYamlContent('')
|
||||
setShowYamlDialog(false)
|
||||
logger.info('YAML import completed successfully')
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to import YAML workflow:', error)
|
||||
setImportResult({
|
||||
success: false,
|
||||
errors: [`Import failed: ${error instanceof Error ? error.message : 'Unknown error'}`],
|
||||
warnings: [],
|
||||
})
|
||||
} finally {
|
||||
setIsImporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenYamlDialog = () => {
|
||||
setYamlContent('')
|
||||
setImportResult(null)
|
||||
setShowYamlDialog(true)
|
||||
}
|
||||
|
||||
const isDisabled = disabled || isImporting
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
{isDisabled ? (
|
||||
<div className='inline-flex h-10 w-10 cursor-not-allowed items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm opacity-50 ring-offset-background transition-colors [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0'>
|
||||
<Plus className='h-4 w-4' />
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='hover:text-primary'
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<Plus className='h-4 w-4' />
|
||||
<span className='sr-only'>Import Workflow</span>
|
||||
</Button>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' className='w-64'>
|
||||
<DropdownMenuItem
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isDisabled}
|
||||
className='flex cursor-pointer items-center gap-2'
|
||||
>
|
||||
<Upload className='h-4 w-4' />
|
||||
<div className='flex flex-col'>
|
||||
<span>Upload YAML File</span>
|
||||
<span className='text-muted-foreground text-xs'>
|
||||
Import from .yaml or .yml file
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={handleOpenYamlDialog}
|
||||
disabled={isDisabled}
|
||||
className='flex cursor-pointer items-center gap-2'
|
||||
>
|
||||
<FileText className='h-4 w-4' />
|
||||
<div className='flex flex-col'>
|
||||
<span>Paste YAML</span>
|
||||
<span className='text-muted-foreground text-xs'>Import from YAML text</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isDisabled
|
||||
? isImporting
|
||||
? 'Importing workflow...'
|
||||
: 'Cannot import workflow'
|
||||
: 'Import Workflow from YAML'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Hidden file input */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type='file'
|
||||
accept='.yaml,.yml'
|
||||
onChange={handleFileUpload}
|
||||
className='hidden'
|
||||
/>
|
||||
|
||||
{/* YAML Import Dialog */}
|
||||
<Dialog open={showYamlDialog} onOpenChange={setShowYamlDialog}>
|
||||
<DialogContent className='flex max-h-[80vh] max-w-4xl flex-col'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Import Workflow from YAML</DialogTitle>
|
||||
<DialogDescription>
|
||||
Paste your workflow YAML content below. This will create a new workflow with the
|
||||
blocks and connections defined in the YAML.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className='flex-1 space-y-4 overflow-hidden'>
|
||||
<Textarea
|
||||
placeholder={`version: "1.0"
|
||||
blocks:
|
||||
start:
|
||||
type: "starter"
|
||||
name: "Start"
|
||||
inputs:
|
||||
startWorkflow: "manual"
|
||||
following:
|
||||
- "process"
|
||||
|
||||
process:
|
||||
type: "agent"
|
||||
name: "Process Data"
|
||||
inputs:
|
||||
systemPrompt: "You are a helpful assistant"
|
||||
userPrompt: "Process the data"
|
||||
model: "gpt-4"
|
||||
preceding:
|
||||
- "start"`}
|
||||
value={yamlContent}
|
||||
onChange={(e) => setYamlContent(e.target.value)}
|
||||
className='min-h-[300px] font-mono text-sm'
|
||||
disabled={isImporting}
|
||||
/>
|
||||
|
||||
{/* Import Result */}
|
||||
{importResult && (
|
||||
<div className='space-y-2'>
|
||||
{importResult.success ? (
|
||||
<Alert>
|
||||
<CheckCircle className='h-4 w-4' />
|
||||
<AlertDescription>
|
||||
<div className='font-medium text-green-700'>Import Successful!</div>
|
||||
{importResult.summary && (
|
||||
<div className='mt-1 text-sm'>{importResult.summary}</div>
|
||||
)}
|
||||
{importResult.warnings.length > 0 && (
|
||||
<div className='mt-2'>
|
||||
<div className='font-medium text-sm'>Warnings:</div>
|
||||
<ul className='mt-1 space-y-1 text-sm'>
|
||||
{importResult.warnings.map((warning, index) => (
|
||||
<li key={index} className='text-yellow-700'>
|
||||
• {warning}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert variant='destructive'>
|
||||
<AlertCircle className='h-4 w-4' />
|
||||
<AlertDescription>
|
||||
<div className='font-medium'>Import Failed</div>
|
||||
{importResult.errors.length > 0 && (
|
||||
<ul className='mt-2 space-y-1 text-sm'>
|
||||
{importResult.errors.map((error, index) => (
|
||||
<li key={index}>• {error}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() => setShowYamlDialog(false)}
|
||||
disabled={isImporting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleYamlImport} disabled={isImporting || !yamlContent.trim()}>
|
||||
{isImporting ? 'Importing...' : 'Import Workflow'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -58,7 +58,7 @@ import { useWorkflowExecution } from '../../hooks/use-workflow-execution'
|
||||
import { DeploymentControls } from './components/deployment-controls/deployment-controls'
|
||||
import { ExportControls } from './components/export-controls/export-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'
|
||||
@@ -1289,7 +1289,7 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
|
||||
{renderDuplicateButton()}
|
||||
{renderAutoLayoutButton()}
|
||||
{renderDebugModeToggle()}
|
||||
<ImportControls disabled={!userPermissions.canEdit} />
|
||||
|
||||
<ExportControls disabled={!userPermissions.canRead} />
|
||||
{/* <WorkflowTextEditorModal disabled={!userPermissions.canEdit} /> */}
|
||||
{/* {renderPublishButton()} */}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState, useRef } from 'react'
|
||||
import { logger } from '@sentry/nextjs'
|
||||
import { File, Folder, Plus } from 'lucide-react'
|
||||
import { ChevronRight, File, Folder, Plus, Upload } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useFolderStore } from '@/stores/folders/store'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/w/components/providers/workspace-permissions-provider'
|
||||
import { ImportControls, ImportControlsRef } from './import-controls'
|
||||
|
||||
interface CreateMenuProps {
|
||||
onCreateWorkflow: (folderId?: string) => void
|
||||
@@ -27,10 +30,15 @@ export function CreateMenu({
|
||||
const [folderName, setFolderName] = useState('')
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [isHoverOpen, setIsHoverOpen] = useState(false)
|
||||
const [isImportSubmenuOpen, setIsImportSubmenuOpen] = useState(false)
|
||||
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
const { createFolder } = useFolderStore()
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
|
||||
// Ref for the file input that will be used by ImportControls
|
||||
const importControlsRef = useRef<ImportControlsRef>(null)
|
||||
|
||||
const handleCreateWorkflow = () => {
|
||||
setIsHoverOpen(false)
|
||||
@@ -42,6 +50,13 @@ export function CreateMenu({
|
||||
setShowFolderDialog(true)
|
||||
}
|
||||
|
||||
const handleUploadYaml = () => {
|
||||
setIsHoverOpen(false)
|
||||
setIsImportSubmenuOpen(false)
|
||||
// Trigger the file upload from ImportControls component
|
||||
importControlsRef.current?.triggerFileUpload()
|
||||
}
|
||||
|
||||
const handleFolderSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!folderName.trim() || !workspaceId) return
|
||||
@@ -99,7 +114,7 @@ export function CreateMenu({
|
||||
'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
'z-50 animate-in overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
|
||||
'data-[state=closed]:animate-out',
|
||||
'w-40'
|
||||
'w-48'
|
||||
)}
|
||||
onMouseEnter={() => setIsHoverOpen(true)}
|
||||
onMouseLeave={() => setIsHoverOpen(false)}
|
||||
@@ -119,6 +134,7 @@ export function CreateMenu({
|
||||
<File className='h-4 w-4' />
|
||||
{isCreatingWorkflow ? 'Creating...' : 'New Workflow'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className='flex w-full cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground'
|
||||
onClick={handleCreateFolder}
|
||||
@@ -126,9 +142,63 @@ export function CreateMenu({
|
||||
<Folder className='h-4 w-4' />
|
||||
New Folder
|
||||
</button>
|
||||
|
||||
{userPermissions.canEdit && (
|
||||
<>
|
||||
<Separator className='my-1' />
|
||||
|
||||
<Popover open={isImportSubmenuOpen} onOpenChange={setIsImportSubmenuOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className='flex w-full cursor-default select-none items-center justify-between rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground'
|
||||
onMouseEnter={() => setIsImportSubmenuOpen(true)}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Upload className='h-4 w-4' />
|
||||
<span>Import Workflow</span>
|
||||
</div>
|
||||
<ChevronRight className='h-3 w-3' />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side='right'
|
||||
align='start'
|
||||
sideOffset={4}
|
||||
className='w-48 p-1'
|
||||
onMouseEnter={() => setIsImportSubmenuOpen(true)}
|
||||
onMouseLeave={() => setIsImportSubmenuOpen(false)}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<button
|
||||
className='flex w-full cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground'
|
||||
onClick={handleUploadYaml}
|
||||
>
|
||||
<Upload className='h-4 w-4' />
|
||||
<div className='flex flex-col items-start'>
|
||||
<span>YAML</span>
|
||||
<span className='text-muted-foreground text-xs'>
|
||||
.yaml or .yml
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Import Controls Component - handles all import functionality */}
|
||||
<ImportControls
|
||||
ref={importControlsRef}
|
||||
disabled={!userPermissions.canEdit}
|
||||
onClose={() => {
|
||||
setIsHoverOpen(false)
|
||||
setIsImportSubmenuOpen(false)
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Folder creation dialog */}
|
||||
<Dialog open={showFolderDialog} onOpenChange={setShowFolderDialog}>
|
||||
<DialogContent className='sm:max-w-[425px]'>
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'
|
||||
import { AlertCircle, CheckCircle, FileText, Upload } from 'lucide-react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
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'
|
||||
|
||||
const logger = createLogger('ImportControls')
|
||||
|
||||
interface ImportControlsProps {
|
||||
disabled?: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export interface ImportControlsRef {
|
||||
triggerFileUpload: () => void
|
||||
}
|
||||
|
||||
export const ImportControls = forwardRef<ImportControlsRef, ImportControlsProps>(
|
||||
({ disabled = false, onClose }, ref) => {
|
||||
const [isImporting, setIsImporting] = useState(false)
|
||||
const [showYamlDialog, setShowYamlDialog] = useState(false)
|
||||
const [yamlContent, setYamlContent] = useState('')
|
||||
const [importResult, setImportResult] = useState<{
|
||||
success: boolean
|
||||
errors: string[]
|
||||
warnings: string[]
|
||||
summary?: string
|
||||
} | null>(null)
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
|
||||
// Stores and hooks
|
||||
const { createWorkflow } = useWorkflowRegistry()
|
||||
const { collaborativeAddBlock, collaborativeAddEdge, collaborativeSetSubblockValue } =
|
||||
useCollaborativeWorkflow()
|
||||
const subBlockStore = useSubBlockStore()
|
||||
|
||||
// Expose methods to parent component
|
||||
useImperativeHandle(ref, () => ({
|
||||
triggerFileUpload: () => {
|
||||
fileInputRef.current?.click()
|
||||
},
|
||||
}))
|
||||
|
||||
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const content = await file.text()
|
||||
setYamlContent(content)
|
||||
setShowYamlDialog(true)
|
||||
onClose?.()
|
||||
} catch (error) {
|
||||
logger.error('Failed to read file:', error)
|
||||
setImportResult({
|
||||
success: false,
|
||||
errors: [
|
||||
`Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
],
|
||||
warnings: [],
|
||||
})
|
||||
}
|
||||
|
||||
// Reset file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleYamlImport = async () => {
|
||||
if (!yamlContent.trim()) {
|
||||
setImportResult({
|
||||
success: false,
|
||||
errors: ['YAML content is required'],
|
||||
warnings: [],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setIsImporting(true)
|
||||
setImportResult(null)
|
||||
|
||||
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: [],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new workflow
|
||||
const newWorkflowId = await createWorkflow({
|
||||
name: `Imported Workflow - ${new Date().toLocaleString()}`,
|
||||
description: 'Workflow imported from YAML',
|
||||
workspaceId,
|
||||
})
|
||||
|
||||
// Import the YAML into the new workflow BEFORE navigation (creates complete state and saves directly to DB)
|
||||
// This avoids timing issues with workflow reload during navigation
|
||||
const result = await importWorkflowFromYaml(
|
||||
yamlContent,
|
||||
{
|
||||
addBlock: collaborativeAddBlock,
|
||||
addEdge: collaborativeAddEdge,
|
||||
applyAutoLayout: () => {
|
||||
// Trigger auto layout
|
||||
window.dispatchEvent(new CustomEvent('trigger-auto-layout'))
|
||||
},
|
||||
setSubBlockValue: (blockId: string, subBlockId: string, value: any) => {
|
||||
// Use the collaborative function - the same one called when users type into fields
|
||||
collaborativeSetSubblockValue(blockId, subBlockId, value)
|
||||
},
|
||||
getExistingBlocks: () => {
|
||||
// For a new workflow, we'll get the starter block from the server
|
||||
return {}
|
||||
},
|
||||
},
|
||||
newWorkflowId
|
||||
) // Pass the new workflow ID to import into
|
||||
|
||||
// Navigate to the new workflow AFTER import is complete
|
||||
if (result.success) {
|
||||
logger.info('Navigating to imported workflow')
|
||||
router.push(`/workspace/${workspaceId}/w/${newWorkflowId}`)
|
||||
}
|
||||
|
||||
setImportResult(result)
|
||||
|
||||
if (result.success) {
|
||||
setYamlContent('')
|
||||
setShowYamlDialog(false)
|
||||
logger.info('YAML import completed successfully')
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to import YAML workflow:', error)
|
||||
setImportResult({
|
||||
success: false,
|
||||
errors: [`Import failed: ${error instanceof Error ? error.message : 'Unknown error'}`],
|
||||
warnings: [],
|
||||
})
|
||||
} finally {
|
||||
setIsImporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const isDisabled = disabled || isImporting
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Hidden file input */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type='file'
|
||||
accept='.yaml,.yml'
|
||||
onChange={handleFileUpload}
|
||||
className='hidden'
|
||||
/>
|
||||
|
||||
{/* YAML Import Dialog */}
|
||||
<Dialog open={showYamlDialog} onOpenChange={setShowYamlDialog}>
|
||||
<DialogContent className='flex max-h-[80vh] max-w-4xl flex-col'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Import Workflow from YAML</DialogTitle>
|
||||
<DialogDescription>
|
||||
Review the YAML content below and click "Import Workflow" to create a new workflow with the
|
||||
blocks and connections defined in the YAML.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className='flex-1 space-y-4 overflow-hidden'>
|
||||
<Textarea
|
||||
placeholder={`version: "1.0"
|
||||
blocks:
|
||||
start:
|
||||
type: "starter"
|
||||
name: "Start"
|
||||
inputs:
|
||||
startWorkflow: "manual"
|
||||
following:
|
||||
- "process"
|
||||
|
||||
process:
|
||||
type: "agent"
|
||||
name: "Process Data"
|
||||
inputs:
|
||||
systemPrompt: "You are a helpful assistant"
|
||||
userPrompt: "Process the data"
|
||||
model: "gpt-4"
|
||||
preceding:
|
||||
- "start"`}
|
||||
value={yamlContent}
|
||||
onChange={(e) => setYamlContent(e.target.value)}
|
||||
className='min-h-[300px] font-mono text-sm'
|
||||
disabled={isImporting}
|
||||
/>
|
||||
|
||||
{/* Import Result */}
|
||||
{importResult && (
|
||||
<div className='space-y-2'>
|
||||
{importResult.success ? (
|
||||
<Alert>
|
||||
<CheckCircle className='h-4 w-4' />
|
||||
<AlertDescription>
|
||||
<div className='font-medium text-green-700'>Import Successful!</div>
|
||||
{importResult.summary && (
|
||||
<div className='mt-1 text-sm'>{importResult.summary}</div>
|
||||
)}
|
||||
{importResult.warnings.length > 0 && (
|
||||
<div className='mt-2'>
|
||||
<div className='font-medium text-sm'>Warnings:</div>
|
||||
<ul className='mt-1 space-y-1 text-sm'>
|
||||
{importResult.warnings.map((warning, index) => (
|
||||
<li key={index} className='text-yellow-700'>
|
||||
• {warning}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert variant='destructive'>
|
||||
<AlertCircle className='h-4 w-4' />
|
||||
<AlertDescription>
|
||||
<div className='font-medium'>Import Failed</div>
|
||||
{importResult.errors.length > 0 && (
|
||||
<ul className='mt-2 space-y-1 text-sm'>
|
||||
{importResult.errors.map((error, index) => (
|
||||
<li key={index}>• {error}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() => setShowYamlDialog(false)}
|
||||
disabled={isImporting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleYamlImport} disabled={isImporting || !yamlContent.trim()}>
|
||||
{isImporting ? 'Importing...' : 'Import Workflow'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
ImportControls.displayName = 'ImportControls'
|
||||
Reference in New Issue
Block a user