Skip to content

Commit c435180

Browse files
committed
test: add more tests
1 parent 7b404d6 commit c435180

File tree

13 files changed

+871
-356
lines changed

13 files changed

+871
-356
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"@dnd-kit/sortable": "8.0.0",
1919
"@dnd-kit/utilities": "3.2.2",
2020
"@faker-js/faker": "7.6.0",
21-
"@iqss/dataverse-client-javascript": "2.1.0-pr406.31ac368",
21+
"@iqss/dataverse-client-javascript": "2.0.0-alpha.85",
2222
"@iqss/dataverse-design-system": "*",
2323
"@istanbuljs/nyc-config-typescript": "1.0.2",
2424
"@tanstack/react-table": "8.9.2",

src/dataset/domain/useCases/DTOs/DatasetDTO.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ export interface DatasetMetadataBlockValuesDTO {
1010
fields: DatasetMetadataFieldsDTO
1111
}
1212

13-
type DatasetMetadataFieldsDTO = Record<string, DatasetMetadataFieldValueDTO>
13+
export type DatasetMetadataFieldsDTO = Record<string, DatasetMetadataFieldValueDTO>
1414

15-
type DatasetMetadataFieldValueDTO =
15+
export type DatasetMetadataFieldValueDTO =
1616
| string
1717
| string[]
1818
| DatasetMetadataChildFieldValueDTO

src/sections/shared/form/DatasetMetadataForm/MetadataFieldsHelper.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
import {
88
DatasetDTO,
99
DatasetMetadataBlockValuesDTO,
10-
DatasetMetadataChildFieldValueDTO
10+
DatasetMetadataFieldValueDTO,
11+
DatasetMetadataChildFieldValueDTO,
12+
DatasetMetadataFieldsDTO
1113
} from '../../../../dataset/domain/useCases/DTOs/DatasetDTO'
1214
import {
1315
DatasetMetadataBlock,
@@ -16,6 +18,7 @@ import {
1618
DatasetMetadataSubField,
1719
defaultLicense
1820
} from '../../../../dataset/domain/models/Dataset'
21+
import { TemplateFieldInfo } from '../../../../templates/domain/models/TemplateInfo'
1922

2023
export type DatasetMetadataFormValues = Record<string, MetadataBlockFormValues>
2124

@@ -34,6 +37,21 @@ export type ComposedSingleFieldValue = Record<string, string>
3437

3538
export type DateLikeKind = 'Y' | 'YM' | 'YMD' | 'AD' | 'BC' | 'BRACKET' | 'TIMESTAMP'
3639

40+
type TemplateFieldValuePayload =
41+
| string
42+
| string[]
43+
| TemplateFieldCompoundValue
44+
| TemplateFieldCompoundValue[]
45+
46+
type TemplateFieldCompoundValue = Record<string, TemplateFieldCompoundChildValue>
47+
48+
type TemplateFieldCompoundChildValue = {
49+
value: string | string[]
50+
typeName: string
51+
multiple: boolean
52+
typeClass: string
53+
}
54+
3755
/** Stable error codes for i18n mapping */
3856
export const dateKeyMessageErrorMap = {
3957
E_EMPTY: 'field.invalid.date.empty',
@@ -409,6 +427,86 @@ export class MetadataFieldsHelper {
409427
return { licence: defaultLicense, metadataBlocks }
410428
}
411429

430+
public static buildTemplateFieldsFromMetadataValues(
431+
fieldValues: DatasetMetadataFieldsDTO,
432+
metadataFields: Record<string, MetadataField>
433+
): TemplateFieldInfo[] {
434+
const templateFields: TemplateFieldInfo[] = []
435+
436+
Object.entries(fieldValues).forEach(([fieldName, fieldValue]) => {
437+
const fieldInfo = metadataFields[fieldName]
438+
if (!fieldInfo) return
439+
440+
if (fieldInfo.typeClass === 'primitive' || fieldInfo.typeClass === 'controlledVocabulary') {
441+
if (fieldValue === '' || fieldValue === undefined || fieldValue === null) return
442+
443+
const valuePayload =
444+
fieldInfo.multiple && Array.isArray(fieldValue) ? fieldValue : (fieldValue as string)
445+
446+
templateFields.push({
447+
typeName: fieldInfo.name,
448+
multiple: fieldInfo.multiple,
449+
typeClass: fieldInfo.typeClass,
450+
value: valuePayload as unknown as TemplateFieldInfo['value']
451+
})
452+
453+
return
454+
}
455+
456+
if (fieldInfo.typeClass === 'compound') {
457+
const compoundValues = this.buildTemplateCompoundValues(fieldInfo, fieldValue)
458+
if (compoundValues.length === 0) return
459+
460+
const valuePayload = fieldInfo.multiple ? compoundValues : compoundValues[0]
461+
462+
templateFields.push({
463+
typeName: fieldInfo.name,
464+
multiple: fieldInfo.multiple,
465+
typeClass: fieldInfo.typeClass,
466+
value: valuePayload as unknown as TemplateFieldInfo['value']
467+
})
468+
}
469+
})
470+
471+
return templateFields
472+
}
473+
474+
private static buildTemplateCompoundValues(
475+
fieldInfo: MetadataField,
476+
fieldValue: DatasetMetadataFieldValueDTO
477+
): TemplateFieldValuePayload[] {
478+
if (fieldInfo.typeClass !== 'compound') {
479+
return []
480+
}
481+
const valueArray = Array.isArray(fieldValue) ? fieldValue : [fieldValue]
482+
const compoundValues: TemplateFieldValuePayload[] = []
483+
484+
valueArray.forEach((compoundValue) => {
485+
if (!compoundValue || typeof compoundValue !== 'object' || Array.isArray(compoundValue)) {
486+
return
487+
}
488+
const entry: Record<string, TemplateFieldCompoundChildValue> = {}
489+
490+
Object.entries(compoundValue).forEach(([childName, childValue]) => {
491+
const childInfo = fieldInfo.childMetadataFields?.[childName]
492+
if (!childInfo) return
493+
if (childValue === '' || childValue === undefined || childValue === null) return
494+
495+
entry[childInfo.name] = {
496+
value: childValue as string | string[],
497+
typeName: childInfo.name,
498+
multiple: childInfo.multiple,
499+
typeClass: childInfo.typeClass
500+
}
501+
})
502+
if (Object.keys(entry).length > 0) {
503+
compoundValues.push(entry)
504+
}
505+
})
506+
507+
return compoundValues
508+
}
509+
412510
public static addFieldValuesToMetadataBlocksInfo(
413511
normalizedMetadataBlocksInfo: MetadataBlockInfo[],
414512
normalizedDatasetMetadaBlocksCurrentValues: DatasetMetadataBlock[]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@import 'node_modules/bootstrap/scss/functions';
2+
@import 'node_modules/bootstrap/scss/variables';
3+
4+
.template-name-row {
5+
margin-bottom: $spacer;
6+
}
7+
8+
.accordion {
9+
margin-top: $spacer;
10+
}
11+
12+
.form-actions {
13+
margin-top: $spacer;
14+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { useEffect, useMemo, useState } from 'react'
2+
import { useNavigate } from 'react-router-dom'
3+
import { useTranslation } from 'react-i18next'
4+
import { FormProvider, useForm } from 'react-hook-form'
5+
import {
6+
Accordion,
7+
Alert,
8+
Button,
9+
Col,
10+
Form,
11+
RequiredInputSymbol
12+
} from '@iqss/dataverse-design-system'
13+
import {
14+
type MetadataBlockInfo,
15+
type MetadataField
16+
} from '@/metadata-block-info/domain/models/MetadataBlockInfo'
17+
import {
18+
MetadataFieldsHelper,
19+
type DatasetMetadataFormValues
20+
} from '../../DatasetMetadataForm/MetadataFieldsHelper'
21+
import { MetadataBlockFormFields } from '../../DatasetMetadataForm/MetadataForm/MetadataBlockFormFields'
22+
import { RequiredFieldText } from '../../RequiredFieldText/RequiredFieldText'
23+
import { RouteWithParams } from '@/sections/Route.enum'
24+
import { TemplateRepository } from '@/templates/domain/repositories/TemplateRepository'
25+
import { useGetTemplatesByCollectionId } from '@/dataset/domain/hooks/useGetTemplatesByCollectionId'
26+
import { SubmissionStatus, useSubmitTemplate } from '../../useSubmitTemplate'
27+
import { TemplateInfo } from '@/templates/domain/models/TemplateInfo'
28+
import styles from './index.module.scss'
29+
30+
interface TemplateFormProps {
31+
collectionId: string
32+
templateRepository: TemplateRepository
33+
metadataBlocksInfo: MetadataBlockInfo[]
34+
formDefaultValues: DatasetMetadataFormValues
35+
metadataFieldsForMapping: Record<string, Record<string, MetadataField>>
36+
}
37+
38+
export const TemplateForm = ({
39+
collectionId,
40+
templateRepository,
41+
metadataBlocksInfo,
42+
formDefaultValues,
43+
metadataFieldsForMapping
44+
}: TemplateFormProps) => {
45+
const { t } = useTranslation('datasetTemplates')
46+
const navigate = useNavigate()
47+
const [validationError, setValidationError] = useState<string | null>(null)
48+
const [templateName, setTemplateName] = useState('')
49+
const [navigateToTermsPending, setNavigateToTermsPending] = useState(false)
50+
const { submissionStatus, submitError, submitTemplate } = useSubmitTemplate(collectionId)
51+
52+
const { datasetTemplates: templates, fetchDatasetTemplates } = useGetTemplatesByCollectionId({
53+
templateRepository,
54+
collectionIdOrAlias: collectionId
55+
})
56+
57+
const currentTemplateId = useMemo(() => {
58+
const normalizedName = templateName.trim().toLowerCase()
59+
if (!normalizedName) return null
60+
61+
const match = templates.find(
62+
(template) => template.name.trim().toLowerCase() === normalizedName
63+
)
64+
return match?.id ?? null
65+
}, [templates, templateName])
66+
67+
useEffect(() => {
68+
if (!navigateToTermsPending) return
69+
if (currentTemplateId === null) return
70+
71+
setNavigateToTermsPending(false)
72+
navigate(RouteWithParams.TEMPLATES_EDIT_TERMS(collectionId, currentTemplateId), {
73+
state: { fromCreateTemplate: true }
74+
})
75+
}, [collectionId, currentTemplateId, navigate, navigateToTermsPending])
76+
77+
const form = useForm({ mode: 'onChange', defaultValues: formDefaultValues })
78+
79+
useEffect(() => {
80+
form.reset(formDefaultValues)
81+
}, [form, formDefaultValues])
82+
83+
const handleSaveAndAddTerms = () => {
84+
if (!templateName.trim()) {
85+
setValidationError(t('createTemplate.errors.nameRequired'))
86+
return
87+
}
88+
89+
setValidationError(null)
90+
91+
void form.handleSubmit(async (formValues) => {
92+
const formValuesBackToDots = MetadataFieldsHelper.replaceSlashKeysWithDot(formValues)
93+
const datasetDto = MetadataFieldsHelper.formatFormValuesToDatasetDTO(
94+
formValuesBackToDots,
95+
'create'
96+
)
97+
const templateFields = datasetDto.metadataBlocks.flatMap((metadataBlock) =>
98+
MetadataFieldsHelper.buildTemplateFieldsFromMetadataValues(
99+
metadataBlock.fields,
100+
metadataFieldsForMapping[metadataBlock.name] ?? {}
101+
)
102+
)
103+
104+
const templatePayload: TemplateInfo = {
105+
name: templateName.trim(),
106+
fields: templateFields
107+
}
108+
109+
const didSubmit = await submitTemplate(templatePayload)
110+
if (!didSubmit) return
111+
112+
await fetchDatasetTemplates()
113+
setNavigateToTermsPending(true)
114+
})()
115+
}
116+
117+
return (
118+
<FormProvider {...form}>
119+
<form noValidate={true}>
120+
{submissionStatus === SubmissionStatus.SubmitComplete && (
121+
<Alert variant="success" dismissible={false}>
122+
{t('createTemplate.alerts.success')}
123+
</Alert>
124+
)}
125+
{(validationError ?? submitError) && (
126+
<Alert variant="danger" dismissible={false}>
127+
{validationError ?? submitError}
128+
</Alert>
129+
)}
130+
<Form.Group className={styles['template-name-row']} controlId="template-name">
131+
<Form.Group.Label column sm={3}>
132+
{t('createTemplate.templateName')}
133+
<RequiredInputSymbol />
134+
</Form.Group.Label>
135+
<Col sm={9}>
136+
<Form.Group.Input
137+
type="text"
138+
required
139+
isInvalid={Boolean(validationError)}
140+
value={templateName}
141+
onChange={(event) => {
142+
const nextValue = event.target.value
143+
setTemplateName(nextValue)
144+
if (validationError && nextValue.trim()) {
145+
setValidationError(null)
146+
}
147+
}}
148+
/>
149+
{validationError && (
150+
<Form.Group.Feedback type="invalid">{validationError}</Form.Group.Feedback>
151+
)}
152+
</Col>
153+
</Form.Group>
154+
<RequiredFieldText />
155+
<Accordion defaultActiveKey="0" className={styles.accordion}>
156+
{metadataBlocksInfo.map((metadataBlock, index) => (
157+
<Accordion.Item
158+
eventKey={index.toString()}
159+
id={`metadata-block-item-${metadataBlock.name}`}
160+
key={metadataBlock.id}>
161+
<Accordion.Header>{metadataBlock.displayName}</Accordion.Header>
162+
<Accordion.Body>
163+
<MetadataBlockFormFields metadataBlock={metadataBlock} />
164+
</Accordion.Body>
165+
</Accordion.Item>
166+
))}
167+
</Accordion>
168+
<div className={styles['form-actions']}>
169+
<Button
170+
type="button"
171+
onClick={handleSaveAndAddTerms}
172+
disabled={submissionStatus === SubmissionStatus.IsSubmitting}>
173+
{t('createTemplate.saveAddTerms')}
174+
</Button>
175+
<Button
176+
type="button"
177+
variant="secondary"
178+
withSpacing
179+
onClick={() => navigate(RouteWithParams.COLLECTION_TEMPLATES(collectionId))}>
180+
{t('createTemplate.cancel')}
181+
</Button>
182+
</div>
183+
</form>
184+
</FormProvider>
185+
)
186+
}

0 commit comments

Comments
 (0)