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

IQSS / dataverse-frontend / 14606716179

22 Apr 2025 11:44PM UTC coverage: 97.863% (+0.4%) from 97.43%
14606716179

push

github

web-flow
Merge pull request #658 from IQSS/634-metadata-display-discrepancies-between-spa-and-jsf-app

Dataset Metadata: update formats of fields

2930 of 3069 branches covered (95.47%)

Branch coverage included in aggregate %.

4 of 4 new or added lines in 1 file covered. (100.0%)

24 existing lines in 14 files now uncovered.

6460 of 6526 relevant lines covered (98.99%)

26057.16 hits per line

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

100.0
/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)
6,998✔
40

41
    for (const block of metadataBlocksCopy) {
6,998✔
42
      if (block.metadataFields) {
32,821✔
43
        this.metadataBlocksInfoDotReplacer(block.metadataFields)
32,821✔
44
      }
45
    }
46
    return metadataBlocksCopy
6,998✔
47
  }
48

49
  private static metadataBlocksInfoDotReplacer(metadataFields: Record<string, MetadataField>) {
50
    for (const key in metadataFields) {
159,982✔
51
      const field = metadataFields[key]
855,784✔
52
      if (field.name.includes('.')) {
855,784✔
53
        field.name = this.replaceDotWithSlash(field.name)
100,016✔
54
      }
55
      if (field.childMetadataFields) {
855,784✔
56
        this.metadataBlocksInfoDotReplacer(field.childMetadataFields)
127,161✔
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)
683✔
66
    const dataWithoutKeysWithDots: DatasetMetadataBlocks = [] as unknown as DatasetMetadataBlocks
683✔
67

68
    for (const block of datasetMetadataBlocksCopy) {
683✔
69
      const newBlockFields: DatasetMetadataFields =
70
        this.datasetMetadataBlocksCurrentValuesDotReplacer(block.fields)
1,291✔
71

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

77
      dataWithoutKeysWithDots.push(newBlock)
1,291✔
78
    }
79

80
    return dataWithoutKeysWithDots
683✔
81
  }
82

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

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

91
      const value = datasetMetadataFields[key]
7,205✔
92

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

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

104
        datasetMetadataFieldsNormalized[newKey] = nestedKeysMapped
683✔
105
      } else if (
6,522✔
106
        Array.isArray(value) &&
9,479✔
107
        (value as readonly (string | DatasetMetadataSubField)[]).every((v) => typeof v === 'object')
3,490✔
108
      ) {
109
        // Case of DatasetMetadataSubField[]
110
        const nestedKeysMapped = value.map((subFields) => {
1,974✔
111
          return Object.entries(subFields).reduce((acc, [nestedKey, nestedValue]) => {
2,507✔
112
            const newNestedKey = nestedKey.includes('.')
6,988✔
113
              ? this.replaceDotWithSlash(nestedKey)
114
              : nestedKey
115

116
            acc[newNestedKey] = nestedValue
6,988✔
117
            return acc
6,988✔
118
          }, {} as DatasetMetadataSubField)
119
        })
120
        datasetMetadataFieldsNormalized[newKey] = nestedKeysMapped
1,974✔
121
      } else {
122
        datasetMetadataFieldsNormalized[newKey] = value
4,548✔
123
      }
124
    }
125

126
    return datasetMetadataFieldsNormalized
1,291✔
127
  }
128

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

134
    for (const block of metadataBlocks) {
4,694✔
135
      const blockValues: MetadataBlockFormValues = {}
4,766✔
136

137
      for (const field of Object.values(block.metadataFields)) {
4,766✔
138
        const fieldName = field.name
43,330✔
139
        const fieldValue = field.value
43,330✔
140

141
        if (field.typeClass === 'compound') {
43,330✔
142
          const childFieldsWithEmptyValues: Record<string, string> = {}
19,571✔
143

144
          if (field.childMetadataFields) {
19,571✔
145
            for (const childField of Object.values(field.childMetadataFields)) {
19,571✔
146
              if (childField.typeClass === 'primitive') {
64,204✔
147
                childFieldsWithEmptyValues[childField.name] = ''
56,992✔
148
              }
149

150
              if (childField.typeClass === 'controlledVocabulary') {
64,204✔
151
                childFieldsWithEmptyValues[childField.name] = ''
7,212✔
152
              }
153
            }
154
          }
155

156
          if (fieldValue) {
19,571✔
157
            const castedFieldValue = fieldValue as
2,450✔
158
              | DatasetMetadataSubField
159
              | DatasetMetadataSubField[]
160

161
            let fieldValues: ComposedFieldValues
162

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

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

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

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

197
            blockValues[fieldName] = fieldValues
2,450✔
198
          } else {
199
            blockValues[fieldName] = field.multiple
17,121✔
200
              ? [childFieldsWithEmptyValues]
201
              : childFieldsWithEmptyValues
202
          }
203
        }
204

205
        if (field.typeClass === 'primitive') {
43,330✔
206
          blockValues[fieldName] = this.getPrimitiveFieldDefaultFormValue(field)
20,801✔
207
        }
208

209
        if (field.typeClass === 'controlledVocabulary') {
43,330✔
210
          blockValues[fieldName] = this.getControlledVocabFieldDefaultFormValue(field)
2,958✔
211
        }
212
      }
213

214
      formDefaultValues[block.name] = blockValues
4,766✔
215
    }
216

217
    return formDefaultValues
4,694✔
218
  }
219

220
  private static getPrimitiveFieldDefaultFormValue(
221
    field: MetadataFieldWithMaybeValue
222
  ): string | PrimitiveMultipleFormValue {
223
    if (field.multiple) {
20,801✔
224
      const castedFieldValue = field.value as string[] | undefined
4,517✔
225

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

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

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

240
      if (!castedFieldValue) return []
2,854✔
241

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

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

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

255
      formattedNewObject[blockKey] = {}
479✔
256

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

260
        if (
5,081✔
261
          this.isPrimitiveFieldValue(fieldValue) ||
11,059✔
262
          this.isVocabularyMultipleFieldValue(fieldValue) ||
263
          this.isPrimitiveMultipleFieldValue(fieldValue)
264
        ) {
265
          formattedNewObject[blockKey][newFieldName] = fieldValue
3,047✔
266
          return
3,047✔
267
        }
268

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

277
            parentOfNestedField[newNestedFieldName] = nestedFieldValue
897✔
278
          })
279
          return
239✔
280
        }
281

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

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

289
              composedField[newNestedFieldName] = nestedFieldValue
5,956✔
290
            })
291

292
            return composedField
1,871✔
293
          })
294
        }
295
      })
296
    }
297

298
    return formattedNewObject
299✔
299
  }
300

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

307
    for (const metadataBlockName in formValues) {
81✔
308
      const formattedMetadataBlock: DatasetMetadataBlockValuesDTO = {
126✔
309
        name: metadataBlockName,
354✔
310
        fields: {}
311
      }
354✔
312
      const metadataBlockFormValues = formValues[metadataBlockName]
3,858✔
313

1,392✔
314
      Object.entries(metadataBlockFormValues).forEach(([fieldName, fieldValue]) => {
597✔
315
        if (this.isPrimitiveFieldValue(fieldValue)) {
1,835✔
316
          if (fieldValue !== '' || mode === 'edit') {
520✔
317
            formattedMetadataBlock.fields[fieldName] = fieldValue
1,330✔
318
            return
409✔
319
          }
2,340✔
320
          return
381✔
321
        }
174✔
322
        if (this.isVocabularyMultipleFieldValue(fieldValue)) {
1,018✔
323
          if (fieldValue.length > 0 || mode === 'edit') {
106✔
324
            formattedMetadataBlock.fields[fieldName] = fieldValue
195✔
325
            return
99✔
326
          }
327
          return
2,077✔
328
        }
555✔
329

711✔
330
        if (this.isPrimitiveMultipleFieldValue(fieldValue)) {
1,449✔
331
          const primitiveMultipleFieldValues = fieldValue
217✔
332
            .map((primitiveField) => primitiveField.value)
856✔
333
            .filter((v) => v !== '')
457✔
334

156✔
335
          if (primitiveMultipleFieldValues.length > 0 || mode === 'edit') {
217✔
336
            formattedMetadataBlock.fields[fieldName] = primitiveMultipleFieldValues
616✔
337
            return
217✔
338
          }
339
          return
1,515✔
340
        }
174✔
341

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

309✔
345
          Object.entries(fieldValue).forEach(([nestedFieldName, nestedFieldValue]) => {
66✔
346
            if (nestedFieldValue !== '' || mode === 'edit') {
243✔
347
              formattedMetadataChildFieldValue[nestedFieldName] = nestedFieldValue
339✔
348
            }
174✔
349
          })
174✔
350
          if (Object.keys(formattedMetadataChildFieldValue).length > 0) {
66✔
UNCOV
351
            formattedMetadataBlock.fields[fieldName] = formattedMetadataChildFieldValue
66✔
352
            return
66✔
353
          }
354
          return
1,341✔
355
        }
1,341✔
356

357
        if (this.isComposedMultipleFieldValue(fieldValue)) {
1,796✔
358
          const formattedMetadataChildFieldValues: DatasetMetadataChildFieldValueDTO[] = []
1,853✔
359

1,398✔
360
          fieldValue.forEach((composedFieldValues) => {
4,901✔
361
            const composedField: DatasetMetadataChildFieldValueDTO = {}
1,452✔
362
            Object.entries(composedFieldValues).forEach(([nestedFieldName, nestedFieldValue]) => {
474✔
363
              if (nestedFieldValue !== '' || mode === 'edit') {
1,514✔
364
                composedField[nestedFieldName] = nestedFieldValue
2,541✔
365
              }
501✔
366
            })
367
            if (Object.keys(composedField).length > 0 || mode === 'edit') {
474✔
368
              formattedMetadataChildFieldValues.push(composedField)
1,763✔
369
            }
444✔
370
          })
371
          if (formattedMetadataChildFieldValues.length > 0 || mode === 'edit') {
455✔
372
            formattedMetadataBlock.fields[fieldName] = formattedMetadataChildFieldValues
1,744✔
373
          }
374

375
          return
455✔
376
        }
354✔
377
      })
378

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

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

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

399
    normalizedMetadataBlocksInfoCopy.forEach((block) => {
1,115✔
400
      const currentBlockValues = normalizedCurrentValuesMap[block.name]
1,255✔
401

17,157✔
402
      if (currentBlockValues) {
316✔
403
        Object.keys(block.metadataFields).forEach((fieldName) => {
17,471✔
404
          const field = block.metadataFields[fieldName]
9,554✔
405

406
          if (this.replaceDotWithSlash(fieldName) in currentBlockValues) {
5,726✔
407
            field.value = currentBlockValues[this.replaceDotWithSlash(fieldName)]
1,314✔
408
          }
409
        })
410
      }
504✔
411
    })
412

413
    return normalizedMetadataBlocksInfoCopy
97,751✔
414
  }
9,258✔
415

416
  private static replaceDotWithSlash = (str: string) => str.replace(/\./g, '/')
31,660✔
417
  private static replaceSlashWithDot = (str: string) => str.replace(/\//g, '.')
3,155✔
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,
118,206✔
430
    fieldsArrayIndex?: number
18,849✔
431
  ) {
432
    if (fieldsArrayIndex !== undefined && !compoundParentName) {
138,594✔
433
      return `${metadataBlockName}.${name}.${fieldsArrayIndex}.value`
84,464✔
434
    }
435
    if (fieldsArrayIndex !== undefined && compoundParentName) {
32,989✔
436
      return `${metadataBlockName}.${compoundParentName}.${fieldsArrayIndex}.${name}`
47,101✔
437
    }
7,308✔
438

439
    if (compoundParentName) {
20,862✔
440
      return `${metadataBlockName}.${compoundParentName}.${name}`
2,432✔
441
    }
442
    return `${metadataBlockName}.${name}`
5,119✔
443
  }
7,509✔
444

445
  private static isPrimitiveFieldValue = (value: unknown): value is string => {
736✔
446
    return typeof value === 'string'
2,668✔
447
  }
448
  private static isPrimitiveMultipleFieldValue = (
4,372✔
449
    value: unknown
450
  ): value is PrimitiveMultipleFormValue => {
522✔
451
    return Array.isArray(value) && value.every((v) => typeof v === 'object' && 'value' in v)
1,476✔
452
  }
453
  private static isVocabularyMultipleFieldValue = (
4,918✔
454
    value: unknown
455
  ): value is VocabularyMultipleFormValue => {
522✔
456
    return Array.isArray(value) && value.every((v) => typeof v === 'string')
1,656✔
457
  }
458
  private static isComposedSingleFieldValue = (
3,250✔
459
    value: unknown
460
  ): value is ComposedSingleFieldValue => {
522✔
461
    return typeof value === 'object' && !Array.isArray(value)
1,034✔
462
  }
463
  private static isComposedMultipleFieldValue = (
3,013✔
464
    value: unknown
465
  ): value is ComposedSingleFieldValue[] => {
466
    return Array.isArray(value) && value.every((v) => typeof v === 'object')
944✔
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

© 2025 Coveralls, Inc