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

IQSS / dataverse-frontend / 17953448345

23 Sep 2025 05:01PM UTC coverage: 98.052% (+0.5%) from 97.516%
17953448345

push

github

web-flow
Merge pull request #829 from IQSS/803-environment-docs

Added environments section to 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
  DatasetMetadataBlocks,
14
  DatasetMetadataFields,
15
  DatasetMetadataSubField,
16
  defaultLicense
17
} from '../../../../dataset/domain/models/Dataset'
18

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

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

26
type VocabularyMultipleFormValue = string[]
27

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

30
type ComposedFieldValues = ComposedSingleFieldValue | ComposedSingleFieldValue[]
31

32
type ComposedSingleFieldValue = Record<string, string>
33

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

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

49
  private static metadataBlocksInfoDotReplacer(metadataFields: Record<string, MetadataField>) {
50
    for (const key in metadataFields) {
40,346✔
51
      const field = metadataFields[key]
215,872✔
52
      if (field.name.includes('.')) {
215,872✔
53
        field.name = this.replaceDotWithSlash(field.name)
25,244✔
54
      }
55
      if (field.childMetadataFields) {
215,872✔
56
        this.metadataBlocksInfoDotReplacer(field.childMetadataFields)
32,069✔
57
      }
58
    }
59
  }
60

61
  public static replaceDatasetMetadataBlocksCurrentValuesDotKeysWithSlash(
62
    datasetMetadataBlocks: DatasetMetadataBlocks
63
  ): DatasetMetadataBlocks {
64
    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
65
    const datasetMetadataBlocksCopy: DatasetMetadataBlocks = structuredClone(datasetMetadataBlocks)
170✔
66
    const dataWithoutKeysWithDots: DatasetMetadataBlocks = [] as unknown as DatasetMetadataBlocks
170✔
67

68
    for (const block of datasetMetadataBlocksCopy) {
170✔
69
      const newBlockFields: DatasetMetadataFields =
70
        this.datasetMetadataBlocksCurrentValuesDotReplacer(block.fields)
322✔
71

72
      const newBlock = {
322✔
73
        name: block.name,
74
        fields: newBlockFields
75
      }
76

77
      dataWithoutKeysWithDots.push(newBlock)
322✔
78
    }
79

80
    return dataWithoutKeysWithDots
170✔
81
  }
82

83
  private static datasetMetadataBlocksCurrentValuesDotReplacer(
84
    datasetMetadataFields: DatasetMetadataFields
85
  ): DatasetMetadataFields {
86
    const datasetMetadataFieldsNormalized: DatasetMetadataFields = {}
322✔
87

88
    for (const key in datasetMetadataFields) {
322✔
89
      const newKey = key.includes('.') ? this.replaceDotWithSlash(key) : key
1,790✔
90

91
      const value = datasetMetadataFields[key]
1,790✔
92

93
      // Case of DatasetMetadataSubField
94
      if (typeof value === 'object' && !Array.isArray(value)) {
1,790✔
95
        const nestedKeysMapped = Object.entries(value).reduce((acc, [nestedKey, nestedValue]) => {
170✔
96
          const newNestedKey = nestedKey.includes('.')
358✔
97
            ? this.replaceDotWithSlash(nestedKey)
98
            : nestedKey
99

100
          acc[newNestedKey] = nestedValue
358✔
101
          return acc
358✔
102
        }, {} as DatasetMetadataSubField)
103

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

116
            acc[newNestedKey] = nestedValue
1,744✔
117
            return acc
1,744✔
118
          }, {} as DatasetMetadataSubField)
119
        })
120
        datasetMetadataFieldsNormalized[newKey] = nestedKeysMapped
492✔
121
      } else {
122
        datasetMetadataFieldsNormalized[newKey] = value
1,128✔
123
      }
124
    }
125

126
    return datasetMetadataFieldsNormalized
322✔
127
  }
128

129
  public static getFormDefaultValues(
130
    metadataBlocks: MetadataBlockInfoWithMaybeValues[]
131
  ): DatasetMetadataFormValues {
132
    const formDefaultValues: DatasetMetadataFormValues = {}
1,173✔
133

134
    for (const block of metadataBlocks) {
1,173✔
135
      const blockValues: MetadataBlockFormValues = {}
1,191✔
136

137
      for (const field of Object.values(block.metadataFields)) {
1,191✔
138
        const fieldName = field.name
10,827✔
139
        const fieldValue = field.value
10,827✔
140

141
        if (field.typeClass === 'compound') {
10,827✔
142
          const childFieldsWithEmptyValues: Record<string, string> = {}
4,895✔
143

144
          if (field.childMetadataFields) {
4,895✔
145
            for (const childField of Object.values(field.childMetadataFields)) {
4,895✔
146
              if (childField.typeClass === 'primitive') {
16,057✔
147
                childFieldsWithEmptyValues[childField.name] = ''
14,254✔
148
              }
149

150
              if (childField.typeClass === 'controlledVocabulary') {
16,057✔
151
                childFieldsWithEmptyValues[childField.name] = ''
1,803✔
152
              }
153
            }
154
          }
155

156
          if (fieldValue) {
4,895✔
157
            const castedFieldValue = fieldValue as
612✔
158
              | DatasetMetadataSubField
159
              | DatasetMetadataSubField[]
160

161
            let fieldValues: ComposedFieldValues
162

163
            if (Array.isArray(castedFieldValue)) {
612✔
164
              const subFieldsWithValuesPlusEmptyOnes = castedFieldValue.map((subFields) => {
451✔
165
                const fieldsValueNormalized: Record<string, string> = Object.entries(
596✔
166
                  subFields
167
                ).reduce((acc, [key, value]) => {
168
                  if (value !== undefined) {
1,659✔
169
                    acc[key] = value
1,659✔
170
                  }
171
                  return acc
1,659✔
172
                }, {} as Record<string, string>)
173

174
                return {
596✔
175
                  ...childFieldsWithEmptyValues,
176
                  ...fieldsValueNormalized
177
                }
178
              })
179

180
              fieldValues = subFieldsWithValuesPlusEmptyOnes
451✔
181
            } else {
182
              const fieldsValueNormalized: Record<string, string> = Object.entries(
161✔
183
                castedFieldValue
184
              ).reduce((acc, [key, value]) => {
185
                if (value !== undefined) {
338✔
186
                  acc[key] = value
338✔
187
                }
188
                return acc
338✔
189
              }, {} as Record<string, string>)
190

191
              fieldValues = {
161✔
192
                ...childFieldsWithEmptyValues,
193
                ...fieldsValueNormalized
194
              }
195
            }
196

197
            blockValues[fieldName] = fieldValues
612✔
198
          } else {
199
            blockValues[fieldName] = field.multiple
4,283✔
200
              ? [childFieldsWithEmptyValues]
201
              : childFieldsWithEmptyValues
202
          }
203
        }
204

205
        if (field.typeClass === 'primitive') {
10,827✔
206
          blockValues[fieldName] = this.getPrimitiveFieldDefaultFormValue(field)
5,194✔
207
        }
208

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

214
      formDefaultValues[block.name] = blockValues
1,191✔
215
    }
216

217
    return formDefaultValues
1,173✔
218
  }
219

220
  private static getPrimitiveFieldDefaultFormValue(
221
    field: MetadataFieldWithMaybeValue
222
  ): string | PrimitiveMultipleFormValue {
223
    if (field.multiple) {
5,194✔
224
      const castedFieldValue = field.value as string[] | undefined
1,126✔
225

226
      if (!castedFieldValue) return [{ value: '' }]
1,126✔
227

228
      return castedFieldValue.map((stringValue) => ({ value: stringValue }))
128✔
229
    }
230
    const castedFieldValue = field.value as string | undefined
4,068✔
231
    return castedFieldValue ?? ''
4,068✔
232
  }
233

234
  private static getControlledVocabFieldDefaultFormValue(
235
    field: MetadataFieldWithMaybeValue
236
  ): string | VocabularyMultipleFormValue {
237
    if (field.multiple) {
738✔
238
      const castedFieldValue = field.value as string[] | undefined
713✔
239

240
      if (!castedFieldValue) return []
713✔
241

242
      return castedFieldValue
161✔
243
    }
244
    const castedFieldValue = field.value as string | undefined
25✔
245
    return castedFieldValue ?? ''
25✔
246
  }
247

248
  public static replaceSlashKeysWithDot(obj: DatasetMetadataFormValues): DatasetMetadataFormValues {
249
    const formattedNewObject: DatasetMetadataFormValues = {}
74✔
250

251
    for (const key in obj) {
74✔
252
      const blockKey = this.replaceSlashWithDot(key)
119✔
253
      const metadataBlockFormValues = obj[key]
119✔
254

255
      formattedNewObject[blockKey] = {}
119✔
256

257
      Object.entries(metadataBlockFormValues).forEach(([fieldName, fieldValue]) => {
119✔
258
        const newFieldName = this.replaceSlashWithDot(fieldName)
1,259✔
259

260
        if (
1,259✔
261
          this.isPrimitiveFieldValue(fieldValue) ||
2,743✔
262
          this.isVocabularyMultipleFieldValue(fieldValue) ||
263
          this.isPrimitiveMultipleFieldValue(fieldValue)
264
        ) {
265
          formattedNewObject[blockKey][newFieldName] = fieldValue
752✔
266
          return
752✔
267
        }
268

269
        if (this.isComposedSingleFieldValue(fieldValue)) {
507✔
270
          formattedNewObject[blockKey][newFieldName] = {}
59✔
271
          Object.entries(fieldValue).forEach(([nestedFieldName, nestedFieldValue]) => {
59✔
272
            const newNestedFieldName = this.replaceSlashWithDot(nestedFieldName)
222✔
273
            const parentOfNestedField = formattedNewObject[blockKey][
222✔
274
              newFieldName
275
            ] as ComposedSingleFieldValue
276

277
            parentOfNestedField[newNestedFieldName] = nestedFieldValue
222✔
278
          })
279
          return
59✔
280
        }
281

282
        if (this.isComposedMultipleFieldValue(fieldValue)) {
448✔
283
          formattedNewObject[blockKey][newFieldName] = fieldValue.map((composedFieldValues) => {
448✔
284
            const composedField: ComposedSingleFieldValue = {}
467✔
285

286
            Object.entries(composedFieldValues).forEach(([nestedFieldName, nestedFieldValue]) => {
467✔
287
              const newNestedFieldName = this.replaceSlashWithDot(nestedFieldName)
1,486✔
288

289
              composedField[newNestedFieldName] = nestedFieldValue
1,486✔
290
            })
291

292
            return composedField
467✔
293
          })
294
        }
295
      })
296
    }
297

298
    return formattedNewObject
74✔
299
  }
300

301
  public static formatFormValuesToDatasetDTO(
302
    formValues: DatasetMetadataFormValues,
73✔
303
    mode: 'create' | 'edit'
304
  ): DatasetDTO {
73✔
305
    const metadataBlocks: DatasetDTO['metadataBlocks'] = []
118✔
306

307
    for (const metadataBlockName in formValues) {
308
      const formattedMetadataBlock: DatasetMetadataBlockValuesDTO = {
309
        name: metadataBlockName,
118✔
310
        fields: {}
311
      }
118✔
312
      const metadataBlockFormValues = formValues[metadataBlockName]
1,244✔
313

464✔
314
      Object.entries(metadataBlockFormValues).forEach(([fieldName, fieldValue]) => {
157✔
315
        if (this.isPrimitiveFieldValue(fieldValue)) {
157✔
316
          if (fieldValue !== '' || mode === 'edit') {
317
            formattedMetadataBlock.fields[fieldName] = fieldValue
307✔
318
            return
319
          }
780✔
320
          return
90✔
321
        }
58✔
322
        if (this.isVocabularyMultipleFieldValue(fieldValue)) {
58✔
323
          if (fieldValue.length > 0 || mode === 'edit') {
324
            formattedMetadataBlock.fields[fieldName] = fieldValue
32✔
325
            return
326
          }
327
          return
690✔
328
        }
185✔
329

237✔
330
        if (this.isPrimitiveMultipleFieldValue(fieldValue)) {
237✔
331
          const primitiveMultipleFieldValues = fieldValue
332
            .map((primitiveField) => primitiveField.value)
185✔
333
            .filter((v) => v !== '')
52✔
334

52✔
335
          if (primitiveMultipleFieldValues.length > 0 || mode === 'edit') {
336
            formattedMetadataBlock.fields[fieldName] = primitiveMultipleFieldValues
133✔
337
            return
338
          }
339
          return
505✔
340
        }
58✔
341

342
        if (this.isComposedSingleFieldValue(fieldValue)) {
58✔
343
          const formattedMetadataChildFieldValue: DatasetMetadataChildFieldValueDTO = {}
219✔
344

103✔
345
          Object.entries(fieldValue).forEach(([nestedFieldName, nestedFieldValue]) => {
346
            if (nestedFieldValue !== '' || mode === 'edit') {
347
              formattedMetadataChildFieldValue[nestedFieldName] = nestedFieldValue
58✔
348
            }
58✔
349
          })
58✔
350
          if (Object.keys(formattedMetadataChildFieldValue).length > 0) {
351
            formattedMetadataBlock.fields[fieldName] = formattedMetadataChildFieldValue
×
352
            return
353
          }
354
          return
447✔
355
        }
447✔
356

357
        if (this.isComposedMultipleFieldValue(fieldValue)) {
447✔
358
          const formattedMetadataChildFieldValues: DatasetMetadataChildFieldValueDTO[] = []
466✔
359

466✔
360
          fieldValue.forEach((composedFieldValues) => {
1,482✔
361
            const composedField: DatasetMetadataChildFieldValueDTO = {}
326✔
362
            Object.entries(composedFieldValues).forEach(([nestedFieldName, nestedFieldValue]) => {
363
              if (nestedFieldValue !== '' || mode === 'edit') {
364
                composedField[nestedFieldName] = nestedFieldValue
466✔
365
              }
167✔
366
            })
367
            if (Object.keys(composedField).length > 0 || mode === 'edit') {
368
              formattedMetadataChildFieldValues.push(composedField)
447✔
369
            }
148✔
370
          })
371
          if (formattedMetadataChildFieldValues.length > 0 || mode === 'edit') {
372
            formattedMetadataBlock.fields[fieldName] = formattedMetadataChildFieldValues
447✔
373
          }
374

375
          return
376
        }
118✔
377
      })
378

73✔
379
      metadataBlocks.push(formattedMetadataBlock)
380
    }
381
    return { licence: defaultLicense, metadataBlocks }
382
  }
383

384
  public static addFieldValuesToMetadataBlocksInfo(
385
    normalizedMetadataBlocksInfo: MetadataBlockInfo[],
386
    normalizedDatasetMetadaBlocksCurrentValues: DatasetMetadataBlocks
168✔
387
  ): MetadataBlockInfoWithMaybeValues[] {
388
    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
389
    const normalizedMetadataBlocksInfoCopy: MetadataBlockInfoWithMaybeValues[] = structuredClone(
390
      normalizedMetadataBlocksInfo
391
    )
168✔
392

319✔
393
    const normalizedCurrentValuesMap: Record<string, DatasetMetadataFields> =
319✔
394
      normalizedDatasetMetadaBlocksCurrentValues.reduce((map, block) => {
395
        map[block.name] = block.fields
396
        return map
168✔
397
      }, {} as Record<string, DatasetMetadataFields>)
315✔
398

399
    normalizedMetadataBlocksInfoCopy.forEach((block) => {
315✔
400
      const currentBlockValues = normalizedCurrentValuesMap[block.name]
313✔
401

5,719✔
402
      if (currentBlockValues) {
403
        Object.keys(block.metadataFields).forEach((fieldName) => {
5,719✔
404
          const field = block.metadataFields[fieldName]
1,276✔
405

406
          if (this.replaceDotWithSlash(fieldName) in currentBlockValues) {
407
            field.value = currentBlockValues[this.replaceDotWithSlash(fieldName)]
408
          }
409
        })
410
      }
168✔
411
    })
412

413
    return normalizedMetadataBlocksInfoCopy
32,527✔
414
  }
3,086✔
415

416
  private static replaceDotWithSlash = (str: string) => str.replace(/\./g, '/')
417
  public static replaceSlashWithDot = (str: string) => str.replace(/\//g, '.')
418

419
  /*
420
   * To define the field name that will be used to register the field in the form
421
   * Most basic could be: metadataBlockName.name eg: citation.title
422
   * If the field is part of a compound field, the name will be: metadataBlockName.compoundParentName.name eg: citation.author.authorName
423
   * If the field is part of an array of fields, the name will be: metadataBlockName.fieldsArrayIndex.name.value eg: citation.alternativeTitle.0.value
424
   * 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
425
   */
426
  public static defineFieldName(
427
    name: string,
428
    metadataBlockName: string,
429
    compoundParentName?: string,
39,402✔
430
    fieldsArrayIndex?: number
6,283✔
431
  ) {
432
    if (fieldsArrayIndex !== undefined && !compoundParentName) {
33,119✔
433
      return `${metadataBlockName}.${name}.${fieldsArrayIndex}.value`
26,072✔
434
    }
435
    if (fieldsArrayIndex !== undefined && compoundParentName) {
436
      return `${metadataBlockName}.${compoundParentName}.${fieldsArrayIndex}.${name}`
7,047✔
437
    }
2,436✔
438

439
    if (compoundParentName) {
4,611✔
440
      return `${metadataBlockName}.${compoundParentName}.${name}`
441
    }
442
    return `${metadataBlockName}.${name}`
174✔
443
  }
2,503✔
444

445
  private static isPrimitiveFieldValue = (value: unknown): value is string => {
174✔
446
    return typeof value === 'string'
447
  }
448
  private static isPrimitiveMultipleFieldValue = (
1,386✔
449
    value: unknown
450
  ): value is PrimitiveMultipleFormValue => {
174✔
451
    return Array.isArray(value) && value.every((v) => typeof v === 'object' && 'value' in v)
452
  }
453
  private static isVocabularyMultipleFieldValue = (
1,568✔
454
    value: unknown
455
  ): value is VocabularyMultipleFormValue => {
174✔
456
    return Array.isArray(value) && value.every((v) => typeof v === 'string')
457
  }
458
  private static isComposedSingleFieldValue = (
1,012✔
459
    value: unknown
460
  ): value is ComposedSingleFieldValue => {
174✔
461
    return typeof value === 'object' && !Array.isArray(value)
462
  }
463
  private static isComposedMultipleFieldValue = (
933✔
464
    value: unknown
465
  ): value is ComposedSingleFieldValue[] => {
466
    return Array.isArray(value) && value.every((v) => typeof v === 'object')
467
  }
468
}
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