• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IQSS / dataverse-frontend / 18139366710

30 Sep 2025 06:13PM UTC coverage: 98.05% (+0.7%) from 97.333%
18139366710

Pull #815

github

g-saracca
chore: unsync deps fix
Pull Request #815: External Tools SPA Integration

1582 of 1638 branches covered (96.58%)

Branch coverage included in aggregate %.

20 of 20 new or added lines in 8 files covered. (100.0%)

23 existing lines in 12 files now uncovered.

3798 of 3849 relevant lines covered (98.67%)

21749.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.62
/src/sections/shared/form/DatasetMetadataForm/MetadataFieldsHelper.ts
1
import {
2
  MetadataBlockInfo,
3
  MetadataBlockInfoWithMaybeValues,
4
  MetadataField,
5
  MetadataFieldWithMaybeValue
6
} from '../../../../metadata-block-info/domain/models/MetadataBlockInfo'
7
import {
8
  DatasetDTO,
9
  DatasetMetadataBlockValuesDTO,
10
  DatasetMetadataChildFieldValueDTO
11
} from '../../../../dataset/domain/useCases/DTOs/DatasetDTO'
12
import {
13
  DatasetMetadataBlock,
14
  DatasetMetadataBlocks,
15
  DatasetMetadataFields,
16
  DatasetMetadataSubField,
17
  defaultLicense
18
} from '../../../../dataset/domain/models/Dataset'
19

20
export type DatasetMetadataFormValues = Record<string, MetadataBlockFormValues>
21

22
export type MetadataBlockFormValues = Record<
23
  string,
24
  string | PrimitiveMultipleFormValue | VocabularyMultipleFormValue | ComposedFieldValues
25
>
26

27
type VocabularyMultipleFormValue = string[]
28

29
type PrimitiveMultipleFormValue = { value: string }[]
30

31
type ComposedFieldValues = ComposedSingleFieldValue | ComposedSingleFieldValue[]
32

33
export type ComposedSingleFieldValue = Record<string, string>
34

35
export class MetadataFieldsHelper {
36
  public static replaceMetadataBlocksInfoDotNamesKeysWithSlash(
37
    metadataBlocks: MetadataBlockInfo[]
38
  ): MetadataBlockInfo[] {
39
    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
3,522✔
40
    const metadataBlocksCopy: MetadataBlockInfo[] = structuredClone(metadataBlocks)
41

3,522✔
42
    for (const block of metadataBlocksCopy) {
16,554✔
43
      if (block.metadataFields) {
16,554✔
44
        this.metadataBlocksInfoDotReplacer(block.metadataFields)
45
      }
46
    }
3,522✔
47
    return metadataBlocksCopy
48
  }
49

50
  private static metadataBlocksInfoDotReplacer(metadataFields: Record<string, MetadataField>) {
80,692✔
51
    for (const key in metadataFields) {
431,744✔
52
      const field = metadataFields[key]
431,744✔
53
      const fieldReplacedKey = this.replaceDotWithSlash(key)
50,488✔
54
      if (fieldReplacedKey !== key) {
55
        // Change the key in the object only if it has changed (i.e., it had a dot)
431,744✔
56
        metadataFields[fieldReplacedKey] = field
64,138✔
57
        delete metadataFields[key]
58
      }
59
      if (field.name.includes('.')) {
60
        field.name = this.replaceDotWithSlash(field.name)
61
      }
62
      if (field.childMetadataFields) {
63
        this.metadataBlocksInfoDotReplacer(field.childMetadataFields)
64
      }
65
    }
340✔
66
  }
340✔
67

68
  public static replaceDatasetMetadataBlocksDotKeysWithSlash(
340✔
69
    datasetMetadataBlocks: DatasetMetadataBlock[]
70
  ): DatasetMetadataBlock[] {
644✔
71
    const dataWithoutKeysWithDots: DatasetMetadataBlock[] = [] as unknown as DatasetMetadataBlock[]
72

644✔
73
    for (const block of datasetMetadataBlocks) {
74
      const newBlockFields: DatasetMetadataFields =
75
        this.datasetMetadataBlocksCurrentValuesDotReplacer(block.fields)
76

77
      const newBlock = {
644✔
78
        name: block.name,
79
        fields: newBlockFields
80
      }
340✔
81

82
      dataWithoutKeysWithDots.push(newBlock)
83
    }
84

85
    return dataWithoutKeysWithDots
86
  }
644✔
87

88
  private static datasetMetadataBlocksCurrentValuesDotReplacer(
644✔
89
    datasetMetadataFields: DatasetMetadataFields
3,580✔
90
  ): DatasetMetadataFields {
91
    const datasetMetadataFieldsNormalized: DatasetMetadataFields = {}
3,580✔
92

93
    for (const key in datasetMetadataFields) {
94
      const newKey = key.includes('.') ? this.replaceDotWithSlash(key) : key
3,580✔
95

340✔
96
      const value = datasetMetadataFields[key]
716✔
97

98
      // Case of DatasetMetadataSubField
99
      if (typeof value === 'object' && !Array.isArray(value)) {
100
        const nestedKeysMapped = Object.entries(value).reduce((acc, [nestedKey, nestedValue]) => {
716✔
101
          const newNestedKey = nestedKey.includes('.')
716✔
102
            ? this.replaceDotWithSlash(nestedKey)
103
            : nestedKey
104

340✔
105
          acc[newNestedKey] = nestedValue
3,240✔
106
          return acc
4,708✔
107
        }, {} as DatasetMetadataSubField)
1,736✔
108

109
        datasetMetadataFieldsNormalized[newKey] = nestedKeysMapped
110
      } else if (
984✔
111
        Array.isArray(value) &&
1,252✔
112
        (value as readonly (string | DatasetMetadataSubField)[]).every((v) => typeof v === 'object')
3,488✔
113
      ) {
114
        // Case of DatasetMetadataSubField[]
115
        const nestedKeysMapped = value.map((subFields) => {
116
          return Object.entries(subFields).reduce((acc, [nestedKey, nestedValue]) => {
3,488✔
117
            const newNestedKey = nestedKey.includes('.')
3,488✔
118
              ? this.replaceDotWithSlash(nestedKey)
119
              : nestedKey
120

984✔
121
            acc[newNestedKey] = nestedValue
122
            return acc
2,256✔
123
          }, {} as DatasetMetadataSubField)
124
        })
125
        datasetMetadataFieldsNormalized[newKey] = nestedKeysMapped
126
      } else {
644✔
127
        datasetMetadataFieldsNormalized[newKey] = value
128
      }
129
    }
130

131
    return datasetMetadataFieldsNormalized
132
  }
2,346✔
133

134
  public static getFormDefaultValues(
2,346✔
135
    metadataBlocks: MetadataBlockInfoWithMaybeValues[]
2,382✔
136
  ): DatasetMetadataFormValues {
137
    const formDefaultValues: DatasetMetadataFormValues = {}
2,382✔
138

21,654✔
139
    for (const block of metadataBlocks) {
21,654✔
140
      const blockValues: MetadataBlockFormValues = {}
141

21,654✔
142
      for (const field of Object.values(block.metadataFields)) {
9,790✔
143
        const fieldName = field.name
144
        const fieldValue = field.value
9,790✔
145

9,790✔
146
        if (field.typeClass === 'compound') {
32,114✔
147
          const childFieldsWithEmptyValues: Record<string, string> = {}
28,508✔
148

149
          if (field.childMetadataFields) {
150
            for (const childField of Object.values(field.childMetadataFields)) {
32,114✔
151
              if (childField.typeClass === 'primitive') {
3,606✔
152
                childFieldsWithEmptyValues[childField.name] = ''
153
              }
154

155
              if (childField.typeClass === 'controlledVocabulary') {
156
                childFieldsWithEmptyValues[childField.name] = ''
9,790✔
157
              }
1,224✔
158
            }
159
          }
160

161
          if (fieldValue) {
162
            const castedFieldValue = fieldValue as
163
              | DatasetMetadataSubField
1,224✔
164
              | DatasetMetadataSubField[]
902✔
165

1,192✔
166
            let fieldValues: ComposedFieldValues
167

168
            if (Array.isArray(castedFieldValue)) {
3,318✔
169
              const subFieldsWithValuesPlusEmptyOnes = castedFieldValue.map((subFields) => {
3,318✔
170
                const fieldsValueNormalized: Record<string, string> = Object.entries(
171
                  subFields
3,318✔
172
                ).reduce((acc, [key, value]) => {
173
                  if (value !== undefined) {
174
                    acc[key] = value
1,192✔
175
                  }
176
                  return acc
177
                }, {} as Record<string, string>)
178

179
                return {
180
                  ...childFieldsWithEmptyValues,
902✔
181
                  ...fieldsValueNormalized
182
                }
322✔
183
              })
184

185
              fieldValues = subFieldsWithValuesPlusEmptyOnes
676✔
186
            } else {
676✔
187
              const fieldsValueNormalized: Record<string, string> = Object.entries(
188
                castedFieldValue
676✔
189
              ).reduce((acc, [key, value]) => {
190
                if (value !== undefined) {
191
                  acc[key] = value
322✔
192
                }
193
                return acc
194
              }, {} as Record<string, string>)
195

196
              fieldValues = {
197
                ...childFieldsWithEmptyValues,
1,224✔
198
                ...fieldsValueNormalized
199
              }
8,566✔
200
            }
201

202
            blockValues[fieldName] = fieldValues
203
          } else {
204
            blockValues[fieldName] = field.multiple
205
              ? [childFieldsWithEmptyValues]
21,654✔
206
              : childFieldsWithEmptyValues
10,388✔
207
          }
208
        }
209

21,654✔
210
        if (field.typeClass === 'primitive') {
1,476✔
211
          blockValues[fieldName] = this.getPrimitiveFieldDefaultFormValue(field)
212
        }
213

214
        if (field.typeClass === 'controlledVocabulary') {
2,382✔
215
          blockValues[fieldName] = this.getControlledVocabFieldDefaultFormValue(field)
216
        }
217
      }
2,346✔
218

219
      formDefaultValues[block.name] = blockValues
220
    }
221

222
    return formDefaultValues
223
  }
10,388✔
224

2,252✔
225
  private static getPrimitiveFieldDefaultFormValue(
226
    field: MetadataFieldWithMaybeValue
2,252✔
227
  ): string | PrimitiveMultipleFormValue {
228
    if (field.multiple) {
256✔
229
      const castedFieldValue = field.value as string[] | undefined
230

8,136✔
231
      if (!castedFieldValue) return [{ value: '' }]
8,136✔
232

233
      return castedFieldValue.map((stringValue) => ({ value: stringValue }))
234
    }
235
    const castedFieldValue = field.value as string | undefined
236
    return castedFieldValue ?? ''
237
  }
1,476✔
238

1,426✔
239
  private static getControlledVocabFieldDefaultFormValue(
240
    field: MetadataFieldWithMaybeValue
1,426✔
241
  ): string | VocabularyMultipleFormValue {
242
    if (field.multiple) {
322✔
243
      const castedFieldValue = field.value as string[] | undefined
244

50✔
245
      if (!castedFieldValue) return []
50✔
246

247
      return castedFieldValue
248
    }
249
    const castedFieldValue = field.value as string | undefined
148✔
250
    return castedFieldValue ?? ''
251
  }
148✔
252

238✔
253
  public static replaceSlashKeysWithDot(obj: DatasetMetadataFormValues): DatasetMetadataFormValues {
238✔
254
    const formattedNewObject: DatasetMetadataFormValues = {}
255

238✔
256
    for (const key in obj) {
257
      const blockKey = this.replaceSlashWithDot(key)
238✔
258
      const metadataBlockFormValues = obj[key]
2,518✔
259

260
      formattedNewObject[blockKey] = {}
2,518✔
261

5,486✔
262
      Object.entries(metadataBlockFormValues).forEach(([fieldName, fieldValue]) => {
263
        const newFieldName = this.replaceSlashWithDot(fieldName)
264

265
        if (
1,504✔
266
          this.isPrimitiveFieldValue(fieldValue) ||
1,504✔
267
          this.isVocabularyMultipleFieldValue(fieldValue) ||
268
          this.isPrimitiveMultipleFieldValue(fieldValue)
269
        ) {
1,014✔
270
          formattedNewObject[blockKey][newFieldName] = fieldValue
118✔
271
          return
118✔
272
        }
444✔
273

444✔
274
        if (this.isComposedSingleFieldValue(fieldValue)) {
275
          formattedNewObject[blockKey][newFieldName] = {}
276
          Object.entries(fieldValue).forEach(([nestedFieldName, nestedFieldValue]) => {
277
            const newNestedFieldName = this.replaceSlashWithDot(nestedFieldName)
444✔
278
            const parentOfNestedField = formattedNewObject[blockKey][
279
              newFieldName
118✔
280
            ] as ComposedSingleFieldValue
281

282
            parentOfNestedField[newNestedFieldName] = nestedFieldValue
896✔
283
          })
896✔
284
          return
934✔
285
        }
286

934✔
287
        if (this.isComposedMultipleFieldValue(fieldValue)) {
2,972✔
288
          formattedNewObject[blockKey][newFieldName] = fieldValue.map((composedFieldValues) => {
289
            const composedField: ComposedSingleFieldValue = {}
2,972✔
290

291
            Object.entries(composedFieldValues).forEach(([nestedFieldName, nestedFieldValue]) => {
292
              const newNestedFieldName = this.replaceSlashWithDot(nestedFieldName)
934✔
293

294
              composedField[newNestedFieldName] = nestedFieldValue
295
            })
296

297
            return composedField
298
          })
148✔
299
        }
300
      })
301
    }
302

146✔
303
    return formattedNewObject
304
  }
146✔
305

236✔
306
  public static formatFormValuesToDatasetDTO(
307
    formValues: DatasetMetadataFormValues,
308
    mode: 'create' | 'edit'
309
  ): DatasetDTO {
236✔
310
    const metadataBlocks: DatasetDTO['metadataBlocks'] = []
311

236✔
312
    for (const metadataBlockName in formValues) {
2,488✔
313
      const formattedMetadataBlock: DatasetMetadataBlockValuesDTO = {
928✔
314
        name: metadataBlockName,
314✔
315
        fields: {}
314✔
316
      }
317
      const metadataBlockFormValues = formValues[metadataBlockName]
614✔
318

319
      Object.entries(metadataBlockFormValues).forEach(([fieldName, fieldValue]) => {
1,560✔
320
        if (this.isPrimitiveFieldValue(fieldValue)) {
180✔
321
          if (fieldValue !== '' || mode === 'edit') {
116✔
322
            formattedMetadataBlock.fields[fieldName] = fieldValue
116✔
323
            return
324
          }
64✔
325
          return
326
        }
327
        if (this.isVocabularyMultipleFieldValue(fieldValue)) {
1,380✔
328
          if (fieldValue.length > 0 || mode === 'edit') {
370✔
329
            formattedMetadataBlock.fields[fieldName] = fieldValue
474✔
330
            return
474✔
331
          }
332
          return
370✔
333
        }
104✔
334

104✔
335
        if (this.isPrimitiveMultipleFieldValue(fieldValue)) {
336
          const primitiveMultipleFieldValues = fieldValue
266✔
337
            .map((primitiveField) => primitiveField.value)
338
            .filter((v) => v !== '')
339

1,010✔
340
          if (primitiveMultipleFieldValues.length > 0 || mode === 'edit') {
116✔
341
            formattedMetadataBlock.fields[fieldName] = primitiveMultipleFieldValues
342
            return
116✔
343
          }
438✔
344
          return
206✔
345
        }
346

347
        if (this.isComposedSingleFieldValue(fieldValue)) {
116✔
348
          const formattedMetadataChildFieldValue: DatasetMetadataChildFieldValueDTO = {}
116✔
349

116✔
350
          Object.entries(fieldValue).forEach(([nestedFieldName, nestedFieldValue]) => {
UNCOV
351
            if (nestedFieldValue !== '' || mode === 'edit') {
×
352
              formattedMetadataChildFieldValue[nestedFieldName] = nestedFieldValue
353
            }
354
          })
894✔
355
          if (Object.keys(formattedMetadataChildFieldValue).length > 0) {
894✔
356
            formattedMetadataBlock.fields[fieldName] = formattedMetadataChildFieldValue
357
            return
894✔
358
          }
932✔
359
          return
932✔
360
        }
2,964✔
361

652✔
362
        if (this.isComposedMultipleFieldValue(fieldValue)) {
363
          const formattedMetadataChildFieldValues: DatasetMetadataChildFieldValueDTO[] = []
364

932✔
365
          fieldValue.forEach((composedFieldValues) => {
334✔
366
            const composedField: DatasetMetadataChildFieldValueDTO = {}
367
            Object.entries(composedFieldValues).forEach(([nestedFieldName, nestedFieldValue]) => {
368
              if (nestedFieldValue !== '' || mode === 'edit') {
894✔
369
                composedField[nestedFieldName] = nestedFieldValue
296✔
370
              }
371
            })
372
            if (Object.keys(composedField).length > 0 || mode === 'edit') {
894✔
373
              formattedMetadataChildFieldValues.push(composedField)
374
            }
375
          })
376
          if (formattedMetadataChildFieldValues.length > 0 || mode === 'edit') {
236✔
377
            formattedMetadataBlock.fields[fieldName] = formattedMetadataChildFieldValues
378
          }
146✔
379

380
          return
381
        }
382
      })
383

384
      metadataBlocks.push(formattedMetadataBlock)
385
    }
386
    return { licence: defaultLicense, metadataBlocks }
336✔
387
  }
388

389
  public static addFieldValuesToMetadataBlocksInfo(
390
    normalizedMetadataBlocksInfo: MetadataBlockInfo[],
391
    normalizedDatasetMetadaBlocksCurrentValues: DatasetMetadataBlock[]
336✔
392
  ): MetadataBlockInfoWithMaybeValues[] {
638✔
393
    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
638✔
394
    const normalizedMetadataBlocksInfoCopy: MetadataBlockInfoWithMaybeValues[] = structuredClone(
395
      normalizedMetadataBlocksInfo
396
    )
336✔
397

630✔
398
    const normalizedCurrentValuesMap: Record<string, DatasetMetadataFields> =
399
      normalizedDatasetMetadaBlocksCurrentValues.reduce((map, block) => {
630✔
400
        map[block.name] = block.fields
626✔
401
        return map
11,438✔
402
      }, {} as Record<string, DatasetMetadataFields>)
403

11,438✔
404
    normalizedMetadataBlocksInfoCopy.forEach((block) => {
2,552✔
405
      const currentBlockValues = normalizedCurrentValuesMap[block.name]
406

407
      if (currentBlockValues) {
408
        Object.keys(block.metadataFields).forEach((fieldName) => {
409
          const field = block.metadataFields[fieldName]
410

336✔
411
          if (this.replaceDotWithSlash(fieldName) in currentBlockValues) {
412
            field.value = currentBlockValues[this.replaceDotWithSlash(fieldName)]
413
          }
65,054✔
414
        })
6,172✔
415
      }
416
    })
417

418
    return normalizedMetadataBlocksInfoCopy
419
  }
420

421
  private static replaceDotWithSlash = (str: string) => str.replace(/\./g, '/')
422
  public static replaceSlashWithDot = (str: string) => str.replace(/\//g, '.')
423

424
  /*
425
   * To define the field name that will be used to register the field in the form
426
   * Most basic could be: metadataBlockName.name eg: citation.title
427
   * If the field is part of a compound field, the name will be: metadataBlockName.compoundParentName.name eg: citation.author.authorName
428
   * If the field is part of an array of fields, the name will be: metadataBlockName.fieldsArrayIndex.name.value eg: citation.alternativeTitle.0.value
429
   * If the field is part of a compound field that is part of an array of fields, the name will be: metadataBlockName.compoundParentName.fieldsArrayIndex.name eg: citation.author.0.authorName
78,804✔
430
   */
12,566✔
431
  public static defineFieldName(
432
    name: string,
66,238✔
433
    metadataBlockName: string,
52,144✔
434
    compoundParentName?: string,
435
    fieldsArrayIndex?: number
436
  ) {
14,094✔
437
    if (fieldsArrayIndex !== undefined && !compoundParentName) {
4,872✔
438
      return `${metadataBlockName}.${name}.${fieldsArrayIndex}.value`
439
    }
9,222✔
440
    if (fieldsArrayIndex !== undefined && compoundParentName) {
441
      return `${metadataBlockName}.${compoundParentName}.${fieldsArrayIndex}.${name}`
442
    }
348✔
443

5,006✔
444
    if (compoundParentName) {
445
      return `${metadataBlockName}.${compoundParentName}.${name}`
348✔
446
    }
447
    return `${metadataBlockName}.${name}`
448
  }
2,772✔
449

450
  private static isPrimitiveFieldValue = (value: unknown): value is string => {
348✔
451
    return typeof value === 'string'
452
  }
453
  private static isPrimitiveMultipleFieldValue = (
3,136✔
454
    value: unknown
455
  ): value is PrimitiveMultipleFormValue => {
348✔
456
    return Array.isArray(value) && value.every((v) => typeof v === 'object' && 'value' in v)
457
  }
458
  private static isVocabularyMultipleFieldValue = (
2,024✔
459
    value: unknown
460
  ): value is VocabularyMultipleFormValue => {
348✔
461
    return Array.isArray(value) && value.every((v) => typeof v === 'string')
462
  }
463
  private static isComposedSingleFieldValue = (
1,866✔
464
    value: unknown
465
  ): value is ComposedSingleFieldValue => {
466
    return typeof value === 'object' && !Array.isArray(value)
467
  }
468
  private static isComposedMultipleFieldValue = (
469
    value: unknown
470
  ): value is ComposedSingleFieldValue[] => {
471
    return Array.isArray(value) && value.every((v) => typeof v === 'object')
472
  }
473

474
  /**
475
   * To define the metadata blocks info that will be used to render the form.
476
   * In create mode, if a template is provided, it adds the fields and values from the template to the metadata blocks info.
477
   * In edit mode, it adds the current dataset values to the metadata blocks info.
478
   * Normalizes field names by replacing dots with slashes to avoid issues with react-hook-form. (e.g. coverage.Spectral.MinimumWavelength -> coverage/Spectral/MinimumWavelength)
479
   * Finally, it orders the fields by display order.
480
   */
481
  public static defineMetadataBlockInfo(
482
    mode: 'create' | 'edit',
483
    metadataBlocksInfoForDisplayOnCreate: MetadataBlockInfo[],
484
    metadataBlocksInfoForDisplayOnEdit: MetadataBlockInfo[],
485
    datasetMetadaBlocksCurrentValues: DatasetMetadataBlocks | undefined,
486
    templateMetadataBlocks: DatasetMetadataBlock[] | undefined
487
  ): MetadataBlockInfo[] {
488
    // Replace field names with dots to slashes, to avoid issues with the form library react-hook-form
489
    const normalizedMetadataBlocksInfoForDisplayOnCreate =
490
      this.replaceMetadataBlocksInfoDotNamesKeysWithSlash(metadataBlocksInfoForDisplayOnCreate)
491

492
    const normalizedMetadataBlocksInfoForDisplayOnEdit =
493
      this.replaceMetadataBlocksInfoDotNamesKeysWithSlash(metadataBlocksInfoForDisplayOnEdit)
494

495
    // CREATE MODE
496
    if (mode === 'create') {
497
      // If we have no template, we just return the metadata blocks info for create with normalized field names
498
      if (!templateMetadataBlocks) {
499
        return normalizedMetadataBlocksInfoForDisplayOnCreate
500
      }
501

502
      // 1) Normalize dataset template fields
503
      const normalizedDatasetTemplateMetadataBlocksValues =
504
        this.replaceDatasetMetadataBlocksDotKeysWithSlash(templateMetadataBlocks)
505

506
      // 2) Add missing fields from the template to the metadata blocks info for create
507
      const metadataBlocksInfoWithAddedFieldsFromTemplate =
508
        this.addFieldsFromTemplateToMetadataBlocksInfoForDisplayOnCreate(
509
          normalizedMetadataBlocksInfoForDisplayOnCreate,
510
          normalizedMetadataBlocksInfoForDisplayOnEdit,
511
          normalizedDatasetTemplateMetadataBlocksValues
512
        )
513

514
      // 3) Add the values from the template to the metadata blocks info for create
515
      const metadataBlocksInfoWithValuesFromTemplate = this.addFieldValuesToMetadataBlocksInfo(
516
        metadataBlocksInfoWithAddedFieldsFromTemplate,
517
        normalizedDatasetTemplateMetadataBlocksValues
518
      )
519

520
      // 4) Order fields by display order
521
      const metadataBlocksInfoOrdered = this.orderFieldsByDisplayOrder(
522
        metadataBlocksInfoWithValuesFromTemplate
523
      )
524

525
      return metadataBlocksInfoOrdered
526
    } else {
527
      // EDIT MODE
528
      const datasetCurrentValues = datasetMetadaBlocksCurrentValues as DatasetMetadataBlocks // In edit mode we always have current values
529

530
      // 1) Normalize dataset current values
531
      const normalizedDatasetMetadaBlocksCurrentValues =
532
        this.replaceDatasetMetadataBlocksDotKeysWithSlash(datasetCurrentValues)
533

534
      // 2) Add current values to the metadata blocks info for edit
535
      const metadataBlocksInfoWithCurrentValues = this.addFieldValuesToMetadataBlocksInfo(
536
        normalizedMetadataBlocksInfoForDisplayOnEdit,
537
        normalizedDatasetMetadaBlocksCurrentValues
538
      )
539

540
      // 3) Order fields by display order
541
      const metadataBlocksInfoOrdered = this.orderFieldsByDisplayOrder(
542
        metadataBlocksInfoWithCurrentValues
543
      )
544

545
      return metadataBlocksInfoOrdered
546
    }
547
  }
548

549
  public static addFieldsFromTemplateToMetadataBlocksInfoForDisplayOnCreate(
550
    metadataBlocksInfoForDisplayOnCreate: MetadataBlockInfo[],
551
    metadataBlocksInfoForDisplayOnEdit: MetadataBlockInfo[],
552
    templateBlocks: DatasetMetadataBlock[] | undefined
553
  ): MetadataBlockInfo[] {
554
    if (!templateBlocks || templateBlocks.length === 0) {
555
      return metadataBlocksInfoForDisplayOnCreate
556
    }
557

558
    const createCopy: MetadataBlockInfo[] = structuredClone(metadataBlocksInfoForDisplayOnCreate)
559

560
    const createMap = createCopy.reduce<Record<string, MetadataBlockInfo>>((acc, block) => {
561
      acc[block.name] = block
562
      return acc
563
    }, {})
564

565
    const editMap = metadataBlocksInfoForDisplayOnEdit.reduce<Record<string, MetadataBlockInfo>>(
566
      (acc, block) => {
567
        acc[block.name] = block
568
        return acc
569
      },
570
      {}
571
    )
572

573
    for (const tBlock of templateBlocks) {
574
      const blockName = tBlock.name
575
      const editBlock = editMap[blockName]
576

577
      // Could be the case that the template block is returned from the API but it has no fields, so we skip it.
578
      const templateBlockHasFields: boolean = Object.keys(tBlock.fields ?? {}).length > 0
579

580
      if (!templateBlockHasFields) continue
581

582
      if (!editBlock) {
583
        // We don't know how this block looks in "edit", we can't copy its shape. So we skip it.
584
        continue
585
      }
586

587
      // We ensure the block exists in the "create" array
588
      let createBlock = createMap[blockName]
589

590
      if (!createBlock) {
591
        createBlock = {
592
          id: editBlock.id,
593
          name: editBlock.name,
594
          displayName: editBlock.displayName,
595
          metadataFields: {},
596
          displayOnCreate: editBlock.displayOnCreate
597
        }
598
        createMap[blockName] = createBlock
599
        createCopy.push(createBlock)
600
      }
601

602
      const createFields = createBlock.metadataFields
603
      const editFields = editBlock.metadataFields
604

605
      // For each field that the template brings with value, if it doesn't exist in "create", we copy it from "edit"
606
      const templateBlockFields = tBlock.fields ?? {}
607
      for (const fieldName of Object.keys(templateBlockFields)) {
608
        if (createFields[fieldName]) continue
609

610
        const fieldFromEdit = editFields[fieldName]
611
        if (!fieldFromEdit) {
612
          // The field doesn't exist in "edit" either: there's no way to know its shape; we skip it
613
          continue
614
        }
615

616
        const clonedField = structuredClone(fieldFromEdit)
617

618
        createFields[fieldName] = clonedField
619
      }
620
    }
621

622
    return createCopy
623
  }
624

625
  private static orderFieldsByDisplayOrder(
626
    metadataBlocksInfo: MetadataBlockInfo[]
627
  ): MetadataBlockInfo[] {
628
    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
629
    const metadataBlocksInfoCopy: MetadataBlockInfo[] = structuredClone(metadataBlocksInfo)
630

631
    for (const block of metadataBlocksInfoCopy) {
632
      if (block.metadataFields) {
633
        const fieldsArray = Object.values(block.metadataFields)
634
        fieldsArray.sort((a, b) => a.displayOrder - b.displayOrder)
635

636
        const orderedFields: Record<string, MetadataField> = {}
637
        for (const field of fieldsArray) {
638
          orderedFields[field.name] = field
639
        }
640
        block.metadataFields = orderedFields
641
      }
642
    }
643
    return metadataBlocksInfoCopy
644
  }
645
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc