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

IQSS / dataverse-frontend / 18320269166

07 Oct 2025 05:03PM UTC coverage: 97.207% (-0.2%) from 97.446%
18320269166

push

github

web-flow
Merge pull request #852 from IQSS/release/0.2.0

Release/0.2.0

3871 of 4053 branches covered (95.51%)

Branch coverage included in aggregate %.

915 of 954 new or added lines in 80 files covered. (95.91%)

6 existing lines in 6 files now uncovered.

7752 of 7904 relevant lines covered (98.08%)

11141.11 hits per line

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

98.67
/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
1,761✔
40
    const metadataBlocksCopy: MetadataBlockInfo[] = structuredClone(metadataBlocks)
4,144✔
41

1,761✔
42
    for (const block of metadataBlocksCopy) {
12,421✔
43
      if (block.metadataFields) {
19,020✔
44
        this.metadataBlocksInfoDotReplacer(block.metadataFields)
10,743✔
45
      }
46
    }
1,761✔
47
    return metadataBlocksCopy
4,144✔
48
  }
49

50
  private static metadataBlocksInfoDotReplacer(metadataFields: Record<string, MetadataField>) {
40,346✔
51
    for (const key in metadataFields) {
273,709✔
52
      const field = metadataFields[key]
513,828✔
53
      const fieldReplacedKey = this.replaceDotWithSlash(key)
323,200✔
54
      if (fieldReplacedKey !== key) {
297,956✔
55
        // Change the key in the object only if it has changed (i.e., it had a dot)
215,872✔
56
        metadataFields[fieldReplacedKey] = field
58,536✔
57
        delete metadataFields[key]
26,467✔
58
      }
59
      if (field.name.includes('.')) {
297,956✔
60
        field.name = this.replaceDotWithSlash(field.name)
26,467✔
61
      }
62
      if (field.childMetadataFields) {
297,956✔
63
        this.metadataBlocksInfoDotReplacer(field.childMetadataFields)
47,094✔
64
      }
65
    }
170✔
66
  }
170✔
67

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

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

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

82
      dataWithoutKeysWithDots.push(newBlock)
441✔
83
    }
84

85
    return dataWithoutKeysWithDots
247✔
86
  }
322✔
87

88
  private static datasetMetadataBlocksCurrentValuesDotReplacer(
322✔
89
    datasetMetadataFields: DatasetMetadataFields
1,790✔
90
  ): DatasetMetadataFields {
91
    const datasetMetadataFieldsNormalized: DatasetMetadataFields = {}
2,231✔
92

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

170✔
96
      const value = datasetMetadataFields[key]
2,720✔
97

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

170✔
105
          acc[newNestedKey] = nestedValue
2,085✔
106
          return acc
465✔
107
        }, {} as DatasetMetadataSubField)
868✔
108

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

492✔
121
            acc[newNestedKey] = nestedValue
2,301✔
122
            return acc
3,429✔
123
          }, {} as DatasetMetadataSubField)
124
        })
125
        datasetMetadataFieldsNormalized[newKey] = nestedKeysMapped
651✔
126
      } else {
322✔
127
        datasetMetadataFieldsNormalized[newKey] = value
1,489✔
128
      }
129
    }
130

131
    return datasetMetadataFieldsNormalized
441✔
132
  }
1,173✔
133

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

10,827✔
139
    for (const block of metadataBlocks) {
11,670✔
140
      const blockValues: MetadataBlockFormValues = {}
1,708✔
141

10,827✔
142
      for (const field of Object.values(block.metadataFields)) {
6,603✔
143
        const fieldName = field.name
15,005✔
144
        const fieldValue = field.value
19,900✔
145

4,895✔
146
        if (field.typeClass === 'compound') {
31,062✔
147
          const childFieldsWithEmptyValues: Record<string, string> = {}
21,102✔
148

149
          if (field.childMetadataFields) {
6,848✔
150
            for (const childField of Object.values(field.childMetadataFields)) {
22,905✔
151
              if (childField.typeClass === 'primitive') {
24,354✔
152
                childFieldsWithEmptyValues[childField.name] = ''
19,972✔
153
              }
154

155
              if (childField.typeClass === 'controlledVocabulary') {
22,551✔
156
                childFieldsWithEmptyValues[childField.name] = ''
7,474✔
157
              }
612✔
158
            }
159
          }
160

161
          if (fieldValue) {
6,848✔
162
            const castedFieldValue = fieldValue as
804✔
163
              | DatasetMetadataSubField
612✔
164
              | DatasetMetadataSubField[]
451✔
165

596✔
166
            let fieldValues: ComposedFieldValues
167

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

179
                return {
785✔
180
                  ...childFieldsWithEmptyValues,
451✔
181
                  ...fieldsValueNormalized
182
                }
161✔
183
              })
184

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

196
              fieldValues = {
209✔
197
                ...childFieldsWithEmptyValues,
612✔
198
                ...fieldsValueNormalized
199
              }
4,283✔
200
            }
201

202
            blockValues[fieldName] = fieldValues
804✔
203
          } else {
204
            blockValues[fieldName] = field.multiple
6,044✔
205
              ? [childFieldsWithEmptyValues]
10,827✔
206
              : childFieldsWithEmptyValues
5,194✔
207
          }
208
        }
209

10,827✔
210
        if (field.typeClass === 'primitive') {
15,743✔
211
          blockValues[fieldName] = this.getPrimitiveFieldDefaultFormValue(field)
7,123✔
212
        }
213

214
        if (field.typeClass === 'controlledVocabulary') {
16,196✔
215
          blockValues[fieldName] = this.getControlledVocabFieldDefaultFormValue(field)
1,034✔
216
        }
217
      }
1,173✔
218

219
      formDefaultValues[block.name] = blockValues
1,708✔
220
    }
221

222
    return formDefaultValues
843✔
223
  }
5,194✔
224

1,126✔
225
  private static getPrimitiveFieldDefaultFormValue(
226
    field: MetadataFieldWithMaybeValue
1,126✔
227
  ): string | PrimitiveMultipleFormValue {
228
    if (field.multiple) {
7,251✔
229
      const castedFieldValue = field.value as string[] | undefined
1,454✔
230

4,068✔
231
      if (!castedFieldValue) return [{ value: '' }]
5,522✔
232

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

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

25✔
245
      if (!castedFieldValue) return []
1,030✔
246

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

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

119✔
256
    for (const key in obj) {
117✔
257
      const blockKey = this.replaceSlashWithDot(key)
311✔
258
      const metadataBlockFormValues = obj[key]
1,451✔
259

260
      formattedNewObject[blockKey] = {}
1,451✔
261

2,743✔
262
      Object.entries(metadataBlockFormValues).forEach(([fieldName, fieldValue]) => {
192✔
263
        const newFieldName = this.replaceSlashWithDot(fieldName)
2,064✔
264

265
        if (
2,816✔
266
          this.isPrimitiveFieldValue(fieldValue) ||
752✔
267
          this.isVocabularyMultipleFieldValue(fieldValue) ||
268
          this.isPrimitiveMultipleFieldValue(fieldValue)
269
        ) {
507✔
270
          formattedNewObject[blockKey][newFieldName] = fieldValue
1,265✔
271
          return
1,265✔
272
        }
222✔
273

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

282
            parentOfNestedField[newNestedFieldName] = nestedFieldValue
799✔
283
          })
448✔
284
          return
559✔
285
        }
286

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

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

294
              composedField[newNestedFieldName] = nestedFieldValue
2,533✔
295
            })
296

297
            return composedField
800✔
298
          })
74✔
299
        }
300
      })
301
    }
302

73✔
303
    return formattedNewObject
117✔
304
  }
73✔
305

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

118✔
312
    for (const metadataBlockName in formValues) {
1,365✔
313
      const formattedMetadataBlock: DatasetMetadataBlockValuesDTO = {
660✔
314
        name: metadataBlockName,
157✔
315
        fields: {}
157✔
316
      }
317
      const metadataBlockFormValues = formValues[metadataBlockName]
503✔
318

319
      Object.entries(metadataBlockFormValues).forEach(([fieldName, fieldValue]) => {
976✔
320
        if (this.isPrimitiveFieldValue(fieldValue)) {
2,214✔
321
          if (fieldValue !== '' || mode === 'edit') {
843✔
322
            formattedMetadataBlock.fields[fieldName] = fieldValue
672✔
323
            return
614✔
324
          }
32✔
325
          return
171✔
326
        }
327
        if (this.isVocabularyMultipleFieldValue(fieldValue)) {
2,029✔
328
          if (fieldValue.length > 0 || mode === 'edit') {
336✔
329
            formattedMetadataBlock.fields[fieldName] = fieldValue
381✔
330
            return
381✔
331
          }
332
          return
192✔
333
        }
52✔
334

52✔
335
        if (this.isPrimitiveMultipleFieldValue(fieldValue)) {
1,188✔
336
          const primitiveMultipleFieldValues = fieldValue
455✔
337
            .map((primitiveField) => primitiveField.value)
406✔
338
            .filter((v) => v !== '')
406✔
339

505✔
340
          if (primitiveMultipleFieldValues.length > 0 || mode === 'edit') {
380✔
341
            formattedMetadataBlock.fields[fieldName] = primitiveMultipleFieldValues
322✔
342
            return
380✔
343
          }
219✔
344
          return
103✔
345
        }
346

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

58✔
350
          Object.entries(fieldValue).forEach(([nestedFieldName, nestedFieldValue]) => {
96✔
351
            if (nestedFieldValue !== '' || mode === 'edit') {
363✔
352
              formattedMetadataChildFieldValue[nestedFieldName] = nestedFieldValue
240✔
353
            }
354
          })
447✔
355
          if (Object.keys(formattedMetadataChildFieldValue).length > 0) {
543✔
356
            formattedMetadataBlock.fields[fieldName] = formattedMetadataChildFieldValue
96✔
357
            return
543✔
358
          }
466✔
359
          return
466✔
360
        }
1,482✔
361

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

466✔
365
          fieldValue.forEach((composedFieldValues) => {
937✔
366
            const composedField: DatasetMetadataChildFieldValueDTO = {}
804✔
367
            Object.entries(composedFieldValues).forEach(([nestedFieldName, nestedFieldValue]) => {
804✔
368
              if (nestedFieldValue !== '' || mode === 'edit') {
2,996✔
369
                composedField[nestedFieldName] = nestedFieldValue
2,116✔
370
              }
371
            })
372
            if (Object.keys(composedField).length > 0 || mode === 'edit') {
1,251✔
373
              formattedMetadataChildFieldValues.push(composedField)
722✔
374
            }
375
          })
376
          if (formattedMetadataChildFieldValues.length > 0 || mode === 'edit') {
888✔
377
            formattedMetadataBlock.fields[fieldName] = formattedMetadataChildFieldValues
688✔
378
          }
73✔
379

380
          return
770✔
381
        }
382
      })
383

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

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

315✔
398
    const normalizedCurrentValuesMap: Record<string, DatasetMetadataFields> =
399
      normalizedDatasetMetadaBlocksCurrentValues.reduce((map, block) => {
561✔
400
        map[block.name] = block.fields
753✔
401
        return map
6,159✔
402
      }, {} as Record<string, DatasetMetadataFields>)
403

5,719✔
404
    normalizedMetadataBlocksInfoCopy.forEach((block) => {
1,522✔
405
      const currentBlockValues = normalizedCurrentValuesMap[block.name]
468✔
406

407
      if (currentBlockValues) {
468✔
408
        Object.keys(block.metadataFields).forEach((fieldName) => {
429✔
409
          const field = block.metadataFields[fieldName]
7,502✔
410

168✔
411
          if (this.replaceDotWithSlash(fieldName) in currentBlockValues) {
7,502✔
412
            field.value = currentBlockValues[this.replaceDotWithSlash(fieldName)]
1,678✔
413
          }
32,527✔
414
        })
3,086✔
415
      }
416
    })
417

418
    return normalizedMetadataBlocksInfoCopy
246✔
419
  }
420

421
  private static replaceDotWithSlash = (str: string) => str.replace(/\./g, '/')
333,947✔
422
  public static replaceSlashWithDot = (str: string) => str.replace(/\//g, '.')
5,218✔
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
39,402✔
430
   */
6,283✔
431
  public static defineFieldName(
432
    name: string,
33,119✔
433
    metadataBlockName: string,
26,072✔
434
    compoundParentName?: string,
435
    fieldsArrayIndex?: number
436
  ) {
7,047✔
437
    if (fieldsArrayIndex !== undefined && !compoundParentName) {
55,658✔
438
      return `${metadataBlockName}.${name}.${fieldsArrayIndex}.value`
6,592✔
439
    }
4,611✔
440
    if (fieldsArrayIndex !== undefined && compoundParentName) {
46,630✔
441
      return `${metadataBlockName}.${compoundParentName}.${fieldsArrayIndex}.${name}`
36,716✔
442
    }
174✔
443

2,503✔
444
    if (compoundParentName) {
9,914✔
445
      return `${metadataBlockName}.${compoundParentName}.${name}`
3,948✔
446
    }
447
    return `${metadataBlockName}.${name}`
6,140✔
448
  }
1,386✔
449

450
  private static isPrimitiveFieldValue = (value: unknown): value is string => {
412✔
451
    return typeof value === 'string'
4,188✔
452
  }
453
  private static isPrimitiveMultipleFieldValue = (
1,806✔
454
    value: unknown
455
  ): value is PrimitiveMultipleFormValue => {
174✔
456
    return Array.isArray(value) && value.every((v) => typeof v === 'object' && 'value' in v)
2,352✔
457
  }
458
  private static isVocabularyMultipleFieldValue = (
1,250✔
459
    value: unknown
460
  ): value is VocabularyMultipleFormValue => {
174✔
461
    return Array.isArray(value) && value.every((v) => typeof v === 'string')
2,646✔
462
  }
463
  private static isComposedSingleFieldValue = (
1,171✔
464
    value: unknown
465
  ): value is ComposedSingleFieldValue => {
466
    return typeof value === 'object' && !Array.isArray(value)
1,724✔
467
  }
468
  private static isComposedMultipleFieldValue = (
238✔
469
    value: unknown
470
  ): value is ComposedSingleFieldValue[] => {
471
    return Array.isArray(value) && value.every((v) => typeof v === 'object')
1,604✔
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)
814✔
491

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

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

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

506
      // 2) Add missing fields from the template to the metadata blocks info for create
507
      const metadataBlocksInfoWithAddedFieldsFromTemplate =
508
        this.addFieldsFromTemplateToMetadataBlocksInfoForDisplayOnCreate(
25✔
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(
25✔
516
        metadataBlocksInfoWithAddedFieldsFromTemplate,
517
        normalizedDatasetTemplateMetadataBlocksValues
518
      )
519

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

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

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

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

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

545
      return metadataBlocksInfoOrdered
201✔
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) {
25✔
555
      return metadataBlocksInfoForDisplayOnCreate
7✔
556
    }
557

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

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

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

573
    for (const tBlock of templateBlocks) {
18✔
574
      const blockName = tBlock.name
18✔
575
      const editBlock = editMap[blockName]
18✔
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
18!
579

580
      if (!templateBlockHasFields) continue
18!
581

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

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

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

602
      const createFields = createBlock.metadataFields
18✔
603
      const editFields = editBlock.metadataFields
18✔
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 ?? {}
18!
607
      for (const fieldName of Object.keys(templateBlockFields)) {
18✔
608
        if (createFields[fieldName]) continue
37✔
609

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

616
        const clonedField = structuredClone(fieldFromEdit)
13✔
617

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

622
    return createCopy
18✔
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)
226✔
630

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

636
        const orderedFields: Record<string, MetadataField> = {}
448✔
637
        for (const field of fieldsArray) {
448✔
638
          orderedFields[field.name] = field
7,461✔
639
        }
640
        block.metadataFields = orderedFields
448✔
641
      }
642
    }
643
    return metadataBlocksInfoCopy
226✔
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