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

IQSS / dataverse-frontend / 18140405254

30 Sep 2025 06:55PM UTC coverage: 98.052% (+0.7%) from 97.333%
18140405254

push

github

web-flow
Merge pull request #844 from IQSS/jggautier-patch-1

Adding "Featured Items" to changes section of README

1582 of 1638 branches covered (96.58%)

Branch coverage included in aggregate %.

3804 of 3855 relevant lines covered (98.68%)

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

1,761✔
42
    for (const block of metadataBlocksCopy) {
8,277✔
43
      if (block.metadataFields) {
8,277✔
44
        this.metadataBlocksInfoDotReplacer(block.metadataFields)
45
      }
46
    }
1,761✔
47
    return metadataBlocksCopy
48
  }
49

50
  private static metadataBlocksInfoDotReplacer(metadataFields: Record<string, MetadataField>) {
40,346✔
51
    for (const key in metadataFields) {
215,872✔
52
      const field = metadataFields[key]
215,872✔
53
      const fieldReplacedKey = this.replaceDotWithSlash(key)
25,244✔
54
      if (fieldReplacedKey !== key) {
55
        // Change the key in the object only if it has changed (i.e., it had a dot)
215,872✔
56
        metadataFields[fieldReplacedKey] = field
32,069✔
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
    }
170✔
66
  }
170✔
67

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

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

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

82
      dataWithoutKeysWithDots.push(newBlock)
83
    }
84

85
    return dataWithoutKeysWithDots
86
  }
322✔
87

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

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

170✔
96
      const value = datasetMetadataFields[key]
358✔
97

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

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

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

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

131
    return datasetMetadataFieldsNormalized
132
  }
1,173✔
133

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

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

10,827✔
142
      for (const field of Object.values(block.metadataFields)) {
4,895✔
143
        const fieldName = field.name
144
        const fieldValue = field.value
4,895✔
145

4,895✔
146
        if (field.typeClass === 'compound') {
16,057✔
147
          const childFieldsWithEmptyValues: Record<string, string> = {}
14,254✔
148

149
          if (field.childMetadataFields) {
150
            for (const childField of Object.values(field.childMetadataFields)) {
16,057✔
151
              if (childField.typeClass === 'primitive') {
1,803✔
152
                childFieldsWithEmptyValues[childField.name] = ''
153
              }
154

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

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

596✔
166
            let fieldValues: ComposedFieldValues
167

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

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

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

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

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

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

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

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

222
    return formDefaultValues
223
  }
5,194✔
224

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

294
              composedField[newNestedFieldName] = nestedFieldValue
295
            })
296

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

73✔
303
    return formattedNewObject
304
  }
73✔
305

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

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

319
      Object.entries(metadataBlockFormValues).forEach(([fieldName, fieldValue]) => {
780✔
320
        if (this.isPrimitiveFieldValue(fieldValue)) {
90✔
321
          if (fieldValue !== '' || mode === 'edit') {
58✔
322
            formattedMetadataBlock.fields[fieldName] = fieldValue
58✔
323
            return
324
          }
32✔
325
          return
326
        }
327
        if (this.isVocabularyMultipleFieldValue(fieldValue)) {
690✔
328
          if (fieldValue.length > 0 || mode === 'edit') {
185✔
329
            formattedMetadataBlock.fields[fieldName] = fieldValue
237✔
330
            return
237✔
331
          }
332
          return
185✔
333
        }
52✔
334

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

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

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

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

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

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

380
          return
381
        }
382
      })
383

384
      metadataBlocks.push(formattedMetadataBlock)
385
    }
386
    return { licence: defaultLicense, metadataBlocks }
168✔
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(
395
      normalizedMetadataBlocksInfo
396
    )
168✔
397

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

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

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

168✔
411
          if (this.replaceDotWithSlash(fieldName) in currentBlockValues) {
412
            field.value = currentBlockValues[this.replaceDotWithSlash(fieldName)]
413
          }
32,527✔
414
        })
3,086✔
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
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) {
2,436✔
438
      return `${metadataBlockName}.${name}.${fieldsArrayIndex}.value`
439
    }
4,611✔
440
    if (fieldsArrayIndex !== undefined && compoundParentName) {
441
      return `${metadataBlockName}.${compoundParentName}.${fieldsArrayIndex}.${name}`
442
    }
174✔
443

2,503✔
444
    if (compoundParentName) {
445
      return `${metadataBlockName}.${compoundParentName}.${name}`
174✔
446
    }
447
    return `${metadataBlockName}.${name}`
448
  }
1,386✔
449

450
  private static isPrimitiveFieldValue = (value: unknown): value is string => {
174✔
451
    return typeof value === 'string'
452
  }
453
  private static isPrimitiveMultipleFieldValue = (
1,568✔
454
    value: unknown
455
  ): value is PrimitiveMultipleFormValue => {
174✔
456
    return Array.isArray(value) && value.every((v) => typeof v === 'object' && 'value' in v)
457
  }
458
  private static isVocabularyMultipleFieldValue = (
1,012✔
459
    value: unknown
460
  ): value is VocabularyMultipleFormValue => {
174✔
461
    return Array.isArray(value) && value.every((v) => typeof v === 'string')
462
  }
463
  private static isComposedSingleFieldValue = (
933✔
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

© 2025 Coveralls, Inc