@@ -13,11 +13,32 @@ import {
1313 type ExportWorkflowState ,
1414 sanitizeForExport ,
1515} from '@/lib/workflows/sanitization/json-sanitizer'
16+ import { sanitizeMalformedSubBlocks } from '@/lib/workflows/sanitization/subblocks'
1617import { regenerateWorkflowIds } from '@/stores/workflows/utils'
1718import type { Variable , WorkflowState } from '@/stores/workflows/workflow/types'
1819
1920const logger = createLogger ( 'WorkflowImportExport' )
2021
22+ function isRecord ( value : unknown ) : value is Record < string , unknown > {
23+ return typeof value === 'object' && value !== null && ! Array . isArray ( value )
24+ }
25+
26+ function unwrapWorkflowExportEnvelope ( data : unknown ) : unknown {
27+ if ( ! isRecord ( data ) ) {
28+ return data
29+ }
30+
31+ const envelopeData = data . data
32+ if (
33+ isRecord ( envelopeData ) &&
34+ ( envelopeData . state || envelopeData . version || envelopeData . workflow )
35+ ) {
36+ return envelopeData
37+ }
38+
39+ return data
40+ }
41+
2142async function getJSZip ( ) {
2243 const { default : JSZip } = await import ( 'jszip' )
2344 return JSZip
@@ -338,7 +359,7 @@ export interface WorkspaceImportMetadata {
338359
339360function extractSortOrder ( content : string ) : number | undefined {
340361 try {
341- const parsed = JSON . parse ( content )
362+ const parsed = unwrapWorkflowExportEnvelope ( JSON . parse ( content ) ) as Record < string , any >
342363 return parsed . state ?. metadata ?. sortOrder ?? parsed . metadata ?. sortOrder
343364 } catch {
344365 return undefined
@@ -418,11 +439,15 @@ export async function extractWorkflowsFromFiles(files: File[]): Promise<Imported
418439
419440export function extractWorkflowName ( content : string , filename : string ) : string {
420441 try {
421- const parsed = JSON . parse ( content )
442+ const parsed = unwrapWorkflowExportEnvelope ( JSON . parse ( content ) ) as Record < string , any >
422443
423444 if ( parsed . state ?. metadata ?. name && typeof parsed . state . metadata . name === 'string' ) {
424445 return parsed . state . metadata . name . trim ( )
425446 }
447+
448+ if ( parsed . workflow ?. name && typeof parsed . workflow . name === 'string' ) {
449+ return parsed . workflow . name . trim ( )
450+ }
426451 } catch {
427452 // JSON parse failed, fall through to filename
428453 }
@@ -441,14 +466,12 @@ export function extractWorkflowName(content: string, filename: string): string {
441466}
442467
443468/**
444- * Normalize subblock values by converting empty strings to null and filtering out invalid subblocks.
469+ * Normalize subblock values by converting empty strings to null and repairing invalid subblocks.
445470 * This provides backwards compatibility for workflows exported before the null sanitization fix,
446471 * preventing Zod validation errors like "Expected array, received string".
447472 *
448- * Also filters out malformed subBlocks that may have been created by bugs in previous exports:
449- * - SubBlocks with key "undefined" (caused by assigning to undefined key)
450- * - SubBlocks missing required fields like `id`
451- * - SubBlocks with `type: "unknown"` (indicates malformed data)
473+ * Also filters out subBlocks with the literal key "undefined", which cannot be associated
474+ * with a stable block field.
452475 */
453476function normalizeSubblockValues ( blocks : Record < string , any > ) : Record < string , any > {
454477 const normalizedBlocks : Record < string , any > = { }
@@ -457,47 +480,14 @@ function normalizeSubblockValues(blocks: Record<string, any>): Record<string, an
457480 const normalizedBlock = { ...block }
458481
459482 if ( block . subBlocks ) {
460- const normalizedSubBlocks : Record < string , any > = { }
461-
462- Object . entries ( block . subBlocks ) . forEach ( ( [ subBlockId , subBlock ] : [ string , any ] ) => {
463- // Skip subBlocks with invalid keys (literal "undefined" string)
464- if ( subBlockId === 'undefined' ) {
465- logger . warn ( `Skipping malformed subBlock with key "undefined" in block ${ blockId } ` )
466- return
467- }
468-
469- // Skip subBlocks that are null or not objects
470- if ( ! subBlock || typeof subBlock !== 'object' ) {
471- logger . warn ( `Skipping invalid subBlock ${ subBlockId } in block ${ blockId } : not an object` )
472- return
473- }
474-
475- // Skip subBlocks with type "unknown" (malformed data)
476- if ( subBlock . type === 'unknown' ) {
477- logger . warn (
478- `Skipping malformed subBlock ${ subBlockId } in block ${ blockId } : type is "unknown"`
479- )
480- return
481- }
482-
483- // Skip subBlocks missing required id field
484- if ( ! subBlock . id ) {
485- logger . warn (
486- `Skipping malformed subBlock ${ subBlockId } in block ${ blockId } : missing id field`
487- )
488- return
489- }
490-
491- const normalizedSubBlock = { ...subBlock }
492-
493- // Convert empty strings to null for consistency
494- if ( normalizedSubBlock . value === '' ) {
495- normalizedSubBlock . value = null
496- }
497-
498- normalizedSubBlocks [ subBlockId ] = normalizedSubBlock
499- } )
500-
483+ const { subBlocks : normalizedSubBlocks } = sanitizeMalformedSubBlocks (
484+ {
485+ id : typeof block . id === 'string' ? block . id : blockId ,
486+ type : typeof block . type === 'string' ? block . type : '' ,
487+ subBlocks : block . subBlocks ,
488+ } ,
489+ { convertEmptyStringToNull : true }
490+ )
501491 normalizedBlock . subBlocks = normalizedSubBlocks
502492 }
503493
@@ -538,6 +528,8 @@ export function parseWorkflowJson(
538528 return { data : null , errors }
539529 }
540530
531+ data = unwrapWorkflowExportEnvelope ( data )
532+
541533 // Handle new export format (version/exportedAt/state) or old format (blocks/edges at root)
542534 let workflowData : any
543535 if ( data . version && data . state ) {
0 commit comments