@@ -8,6 +8,11 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
88import { createLogger } from '@/lib/logs/console/logger'
99import { generateFolderName } from '@/lib/naming'
1010import { cn } from '@/lib/utils'
11+ import {
12+ extractWorkflowName ,
13+ extractWorkflowsFromFiles ,
14+ extractWorkflowsFromZip ,
15+ } from '@/lib/workflows/import-export'
1116import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
1217import { useFolderStore } from '@/stores/folders/store'
1318import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
@@ -114,105 +119,202 @@ export function CreateMenu({ onCreateWorkflow, isCreatingWorkflow = false }: Cre
114119 }
115120 } , [ createFolder , workspaceId , isCreating ] )
116121
117- const handleDirectImport = useCallback (
118- async ( content : string , filename ?: string ) => {
119- if ( ! content . trim ( ) ) {
120- logger . error ( 'JSON content is required' )
121- return
122- }
123-
124- setIsImporting ( true )
125-
126- try {
127- // First validate the JSON without importing
128- const { data : workflowData , errors : parseErrors } = parseWorkflowJson ( content )
129-
130- if ( ! workflowData || parseErrors . length > 0 ) {
131- logger . error ( 'Failed to parse JSON:' , { errors : parseErrors } )
132- return
133- }
134-
135- // Generate workflow name from filename or fallback to time-based name
136- const getWorkflowName = ( ) => {
137- if ( filename ) {
138- // Remove file extension and use the filename
139- const nameWithoutExtension = filename . replace ( / \. j s o n $ / i, '' )
140- return (
141- nameWithoutExtension . trim ( ) || `Imported Workflow - ${ new Date ( ) . toLocaleString ( ) } `
142- )
143- }
144- return `Imported Workflow - ${ new Date ( ) . toLocaleString ( ) } `
145- }
146-
147- // Clear workflow diff store when creating a new workflow from import
148- const { clearDiff } = useWorkflowDiffStore . getState ( )
149- clearDiff ( )
150-
151- // Create a new workflow
152- const newWorkflowId = await createWorkflow ( {
153- name : getWorkflowName ( ) ,
154- description : 'Workflow imported from JSON' ,
155- workspaceId,
156- } )
157-
158- // Save workflow state to database first
159- const response = await fetch ( `/api/workflows/${ newWorkflowId } /state` , {
160- method : 'PUT' ,
161- headers : {
162- 'Content-Type' : 'application/json' ,
163- } ,
164- body : JSON . stringify ( workflowData ) ,
165- } )
166-
167- if ( ! response . ok ) {
168- logger . error ( 'Failed to persist imported workflow to database' )
169- throw new Error ( 'Failed to save workflow' )
170- }
171-
172- logger . info ( 'Imported workflow persisted to database' )
173-
174- // Pre-load the workflow state before navigating
175- const { setActiveWorkflow } = useWorkflowRegistry . getState ( )
176- await setActiveWorkflow ( newWorkflowId )
177-
178- // Navigate to the new workflow (replace to avoid history entry)
179- router . replace ( `/workspace/${ workspaceId } /w/${ newWorkflowId } ` )
180-
181- logger . info ( 'Workflow imported successfully from JSON' )
182- } catch ( error ) {
183- logger . error ( 'Failed to import workflow:' , { error } )
184- } finally {
185- setIsImporting ( false )
186- }
187- } ,
188- [ createWorkflow , workspaceId , router ]
189- )
190-
191122 const handleImportWorkflow = useCallback ( ( ) => {
192123 setIsOpen ( false )
193124 fileInputRef . current ?. click ( )
194125 } , [ ] )
195126
196127 const handleFileChange = useCallback (
197128 async ( event : React . ChangeEvent < HTMLInputElement > ) => {
198- const file = event . target . files ?. [ 0 ]
199- if ( ! file ) return
129+ const files = event . target . files
130+ if ( ! files || files . length === 0 ) return
131+
132+ setIsImporting ( true )
200133
201134 try {
202- const content = await file . text ( )
135+ const fileArray = Array . from ( files )
136+ const hasZip = fileArray . some ( ( f ) => f . name . toLowerCase ( ) . endsWith ( '.zip' ) )
137+ const jsonFiles = fileArray . filter ( ( f ) => f . name . toLowerCase ( ) . endsWith ( '.json' ) )
138+
139+ let importedWorkflows : Array < { content : string ; name : string ; folderPath : string [ ] } > = [ ]
140+
141+ if ( hasZip && fileArray . length === 1 ) {
142+ const zipFile = fileArray [ 0 ]
143+ const { workflows : extractedWorkflows , metadata } = await extractWorkflowsFromZip ( zipFile )
144+ importedWorkflows = extractedWorkflows
145+
146+ const { createFolder } = useFolderStore . getState ( )
147+ const folderName = metadata ?. workspaceName || zipFile . name . replace ( / \. z i p $ / i, '' )
148+ const importFolder = await createFolder ( {
149+ name : folderName ,
150+ workspaceId,
151+ } )
152+
153+ const folderMap = new Map < string , string > ( )
154+
155+ for ( const workflow of importedWorkflows ) {
156+ try {
157+ const { data : workflowData , errors : parseErrors } = parseWorkflowJson (
158+ workflow . content
159+ )
160+
161+ if ( ! workflowData || parseErrors . length > 0 ) {
162+ logger . warn ( `Failed to parse ${ workflow . name } :` , parseErrors )
163+ continue
164+ }
165+
166+ let targetFolderId = importFolder . id
167+
168+ if ( workflow . folderPath . length > 0 ) {
169+ const folderPathKey = workflow . folderPath . join ( '/' )
170+
171+ if ( ! folderMap . has ( folderPathKey ) ) {
172+ let parentId = importFolder . id
173+
174+ for ( let i = 0 ; i < workflow . folderPath . length ; i ++ ) {
175+ const pathSegment = workflow . folderPath . slice ( 0 , i + 1 ) . join ( '/' )
176+
177+ if ( ! folderMap . has ( pathSegment ) ) {
178+ const subFolder = await createFolder ( {
179+ name : workflow . folderPath [ i ] ,
180+ workspaceId,
181+ parentId,
182+ } )
183+ folderMap . set ( pathSegment , subFolder . id )
184+ parentId = subFolder . id
185+ } else {
186+ parentId = folderMap . get ( pathSegment ) !
187+ }
188+ }
189+ }
190+
191+ targetFolderId = folderMap . get ( folderPathKey ) !
192+ }
193+
194+ const workflowName = extractWorkflowName ( workflow . content )
195+ const { clearDiff } = useWorkflowDiffStore . getState ( )
196+ clearDiff ( )
197+
198+ const newWorkflowId = await createWorkflow ( {
199+ name : workflowName ,
200+ description : 'Imported from workspace export' ,
201+ workspaceId,
202+ folderId : targetFolderId ,
203+ } )
204+
205+ const response = await fetch ( `/api/workflows/${ newWorkflowId } /state` , {
206+ method : 'PUT' ,
207+ headers : {
208+ 'Content-Type' : 'application/json' ,
209+ } ,
210+ body : JSON . stringify ( workflowData ) ,
211+ } )
212+
213+ if ( ! response . ok ) {
214+ logger . error ( `Failed to save imported workflow ${ newWorkflowId } ` )
215+ continue
216+ }
217+
218+ if ( workflowData . variables && workflowData . variables . length > 0 ) {
219+ const variablesPayload = workflowData . variables . map ( ( v : any ) => ( {
220+ id : typeof v . id === 'string' && v . id . trim ( ) ? v . id : crypto . randomUUID ( ) ,
221+ workflowId : newWorkflowId ,
222+ name : v . name ,
223+ type : v . type ,
224+ value : v . value ,
225+ } ) )
226+
227+ await fetch ( `/api/workflows/${ newWorkflowId } /variables` , {
228+ method : 'POST' ,
229+ headers : {
230+ 'Content-Type' : 'application/json' ,
231+ } ,
232+ body : JSON . stringify ( { variables : variablesPayload } ) ,
233+ } )
234+ }
235+
236+ logger . info ( `Imported workflow: ${ workflowName } ` )
237+ } catch ( error ) {
238+ logger . error ( `Failed to import ${ workflow . name } :` , error )
239+ }
240+ }
241+ } else if ( jsonFiles . length > 0 ) {
242+ importedWorkflows = await extractWorkflowsFromFiles ( jsonFiles )
243+
244+ for ( const workflow of importedWorkflows ) {
245+ try {
246+ const { data : workflowData , errors : parseErrors } = parseWorkflowJson (
247+ workflow . content
248+ )
249+
250+ if ( ! workflowData || parseErrors . length > 0 ) {
251+ logger . warn ( `Failed to parse ${ workflow . name } :` , parseErrors )
252+ continue
253+ }
254+
255+ const workflowName = extractWorkflowName ( workflow . content )
256+ const { clearDiff } = useWorkflowDiffStore . getState ( )
257+ clearDiff ( )
258+
259+ const newWorkflowId = await createWorkflow ( {
260+ name : workflowName ,
261+ description : 'Imported from JSON' ,
262+ workspaceId,
263+ } )
264+
265+ const response = await fetch ( `/api/workflows/${ newWorkflowId } /state` , {
266+ method : 'PUT' ,
267+ headers : {
268+ 'Content-Type' : 'application/json' ,
269+ } ,
270+ body : JSON . stringify ( workflowData ) ,
271+ } )
272+
273+ if ( ! response . ok ) {
274+ logger . error ( `Failed to save imported workflow ${ newWorkflowId } ` )
275+ continue
276+ }
277+
278+ if ( workflowData . variables && workflowData . variables . length > 0 ) {
279+ const variablesPayload = workflowData . variables . map ( ( v : any ) => ( {
280+ id : typeof v . id === 'string' && v . id . trim ( ) ? v . id : crypto . randomUUID ( ) ,
281+ workflowId : newWorkflowId ,
282+ name : v . name ,
283+ type : v . type ,
284+ value : v . value ,
285+ } ) )
286+
287+ await fetch ( `/api/workflows/${ newWorkflowId } /variables` , {
288+ method : 'POST' ,
289+ headers : {
290+ 'Content-Type' : 'application/json' ,
291+ } ,
292+ body : JSON . stringify ( { variables : variablesPayload } ) ,
293+ } )
294+ }
295+
296+ logger . info ( `Imported workflow: ${ workflowName } ` )
297+ } catch ( error ) {
298+ logger . error ( `Failed to import ${ workflow . name } :` , error )
299+ }
300+ }
301+ }
203302
204- // Import directly with filename
205- await handleDirectImport ( content , file . name )
206- } catch ( error ) {
207- logger . error ( 'Failed to read file:' , { error } )
208- }
303+ const { loadWorkflows } = useWorkflowRegistry . getState ( )
304+ await loadWorkflows ( workspaceId )
209305
210- // Reset file input
211- if ( fileInputRef . current ) {
212- fileInputRef . current . value = ''
306+ const { fetchFolders } = useFolderStore . getState ( )
307+ await fetchFolders ( workspaceId )
308+ } catch ( error ) {
309+ logger . error ( 'Failed to import workflows:' , error )
310+ } finally {
311+ setIsImporting ( false )
312+ if ( fileInputRef . current ) {
313+ fileInputRef . current . value = ''
314+ }
213315 }
214316 } ,
215- [ handleDirectImport ]
317+ [ workspaceId , createWorkflow ]
216318 )
217319
218320 // Button event handlers
@@ -360,7 +462,7 @@ export function CreateMenu({ onCreateWorkflow, isCreatingWorkflow = false }: Cre
360462 >
361463 < Download className = { iconClassName } />
362464 < span className = { textClassName } >
363- { isImporting ? 'Importing...' : 'Import workflow ' }
465+ { isImporting ? 'Importing...' : 'Import Workflows ' }
364466 </ span >
365467 </ button >
366468 </ PopoverContent >
@@ -369,7 +471,8 @@ export function CreateMenu({ onCreateWorkflow, isCreatingWorkflow = false }: Cre
369471 < input
370472 ref = { fileInputRef }
371473 type = 'file'
372- accept = '.json'
474+ accept = '.json,.zip'
475+ multiple
373476 style = { { display : 'none' } }
374477 onChange = { handleFileChange }
375478 />
0 commit comments