Skip to content

Commit

Permalink
Merge pull request #351 from IQSS/337-multiple-values-field-type
Browse files Browse the repository at this point in the history
337 multiple values field type
  • Loading branch information
GPortas authored Apr 16, 2024
2 parents 3aa4685 + 8191241 commit dd47171
Show file tree
Hide file tree
Showing 13 changed files with 242 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ interface FormInputProps extends React.HTMLAttributes<FormInputElement> {
isValid?: boolean
isInvalid?: boolean
disabled?: boolean
value?: string | number
required?: boolean
}

export function FormInput({
Expand All @@ -22,6 +24,8 @@ export function FormInput({
isInvalid,
disabled,
withinMultipleFieldsGroup,
value,
required,
...props
}: FormInputProps) {
return (
Expand All @@ -37,6 +41,8 @@ export function FormInput({
isValid={isValid}
isInvalid={isInvalid}
disabled={disabled}
value={value}
required={required}
{...props}
/>
</FormElementLayout>
Expand Down
4 changes: 3 additions & 1 deletion public/locales/en/createDataset.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@
"submitting": "Submitting...",
"success": "Form submitted successfully!",
"failed": "Error: Submission failed."
}
},
"addRowButton": "Add",
"deleteRowButton": "Delete"
},
"datasetFormStates": {
"submitting": "Form Submitting",
Expand Down
24 changes: 17 additions & 7 deletions src/dataset/domain/useCases/validateDataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,23 @@ export function validateDataset(dataset: DatasetDTO) {
isValid = false
}

if (
isArrayOfSubfieldValue(dataset.metadataBlocks[0].fields.author) &&
!dataset.metadataBlocks[0].fields.author[0].authorName
) {
if (isArrayOfSubfieldValue(errors.metadataBlocks[0].fields.author)) {
errors.metadataBlocks[0].fields.author[0].authorName = AUTHOR_NAME_REQUIRED
isValid = false
if (isArrayOfSubfieldValue(dataset.metadataBlocks[0].fields.author)) {
for (let i = 0; i < dataset.metadataBlocks[0].fields.author.length; i++) {
if (!dataset.metadataBlocks[0].fields.author[i].authorName) {
if (isArrayOfSubfieldValue(errors.metadataBlocks[0].fields.author)) {
// Check if the errors array has enough elements
if (i < errors.metadataBlocks[0].fields.author.length) {
errors.metadataBlocks[0].fields.author[i].authorName = AUTHOR_NAME_REQUIRED
console.log('invalid author name')
isValid = false
} else {
// If the errors array does not have enough elements, add a new one
errors.metadataBlocks[0].fields.author.push({ authorName: AUTHOR_NAME_REQUIRED })
console.log('invalid author name')
isValid = false
}
}
}
}
}

Expand Down
96 changes: 96 additions & 0 deletions src/sections/create-dataset/AuthorFormGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Form } from '@iqss/dataverse-design-system'
import { useTranslation } from 'react-i18next'
import { SubmissionStatus } from './useCreateDatasetForm'
import { DatasetMetadataSubField } from '../../dataset/domain/models/Dataset'
import { Col, Row } from '@iqss/dataverse-design-system'
import { DynamicFieldsButtons } from './dynamic-fields-buttons/DynamicFieldsButtons'

import { ChangeEvent } from 'react'
import _ from 'lodash'
import { useMultipleFields } from './useMultipleFields'
import { DatasetDTO } from '../../dataset/domain/useCases/DTOs/DatasetDTO'
interface AuthorFormGroupProps {
submissionStatus: SubmissionStatus
initialAuthorFields: DatasetMetadataSubField[]
updateFormData: (name: string, value: string | DatasetMetadataSubField[]) => void
validationErrors: DatasetDTO
}

export function AuthorFormGroup({
submissionStatus,
initialAuthorFields,
updateFormData,
validationErrors
}: AuthorFormGroupProps) {
const { t } = useTranslation('createDataset')
const { multipleFields, setMultipleFields, addField, removeField } =
useMultipleFields(initialAuthorFields)

const isAuthorInvalid = (index: number) => {
const subfieldArray = validationErrors.metadataBlocks[0].fields
.author as DatasetMetadataSubField[]
const retValue = subfieldArray[index] ? !!subfieldArray[index].authorName : false
return retValue
}
const handleFieldChange = (index: number, event: ChangeEvent<HTMLInputElement>) => {
const updatedAuthorFields = _.cloneDeep(multipleFields)
updatedAuthorFields[index].authorName = event.target.value
setMultipleFields(updatedAuthorFields)
updateFormData('metadataBlocks.0.fields.author', updatedAuthorFields)
}
const FIRST_AUTHOR = 0
const initialAuthorFieldState = { authorName: '' }
return (
<>
{multipleFields.map((author, index) => (
<Form.Group controlId="author-name" required key={index}>
<Row>
<Col sm={3}>
{index === FIRST_AUTHOR && (
<Form.Group required controlId={'author-title'} as={Col}>
<Form.Group.Label message={t('datasetForm.fields.authorName.tooltip')}>
{t('datasetForm.fields.authorName.label')}
</Form.Group.Label>
</Form.Group>
)}
</Col>
<Col sm={6}>
<Form.Group.Label required message={t('datasetForm.fields.authorName.tooltip')}>
Name
</Form.Group.Label>
<Form.Group controlId={'author-name'} as={Col} required>
<Form.Group.Input
disabled={submissionStatus === SubmissionStatus.IsSubmitting}
type="text"
name={`metadataBlocks.0.fields.author.${index}.authorName`}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(index, event)
}
isInvalid={isAuthorInvalid(index)}
value={author.authorName}
required
/>
<Form.Group.Feedback type="invalid">
{t('datasetForm.fields.authorName.feedback')}
</Form.Group.Feedback>
</Form.Group>
</Col>
<Col sm={3}>
<Form.Group controlId={'author-button'} as={Col} required>
<DynamicFieldsButtons
originalField={index === FIRST_AUTHOR}
onAddButtonClick={() => {
addField(index, initialAuthorFieldState)
}}
onRemoveButtonClick={() => {
removeField(index)
}}
/>
</Form.Group>
</Col>
</Row>
</Form.Group>
))}
</>
)
}
26 changes: 8 additions & 18 deletions src/sections/create-dataset/CreateDatasetForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Route } from '../Route.enum'
import { useNavigate } from 'react-router-dom'
import { useDatasetValidator } from './useDatasetValidator'
import { DatasetMetadataSubField } from '../../dataset/domain/models/Dataset'
import { AuthorFormGroup } from './AuthorFormGroup'

interface CreateDatasetFormProps {
repository: DatasetRepository
Expand Down Expand Up @@ -85,24 +86,13 @@ export function CreateDatasetForm({ repository }: CreateDatasetFormProps) {
{t('datasetForm.fields.title.feedback')}
</Form.Group.Feedback>
</Form.Group>
<Form.Group controlId="author-name" required>
<Form.Group.Label message={t('datasetForm.fields.authorName.tooltip')}>
{t('datasetForm.fields.authorName.label')}
</Form.Group.Label>
<Form.Group.Input
disabled={submissionStatus === SubmissionStatus.IsSubmitting}
type="text"
name="metadataBlocks.0.fields.author.0.authorName"
onChange={handleFieldChange}
isInvalid={
!!(validationErrors.metadataBlocks[0].fields.author as DatasetMetadataSubField[])[0]
.authorName
}
/>
<Form.Group.Feedback type="invalid">
{t('datasetForm.fields.authorName.feedback')}
</Form.Group.Feedback>
</Form.Group>
<AuthorFormGroup
submissionStatus={submissionStatus}
updateFormData={updateFormData}
initialAuthorFields={
formData.metadataBlocks[0].fields['author'] as DatasetMetadataSubField[]
}
validationErrors={validationErrors}></AuthorFormGroup>
<Form.Group controlId="contact-email" required>
<Form.Group.Label message={t('datasetForm.fields.datasetContactEmail.tooltip')}>
{t('datasetForm.fields.datasetContactEmail.label')}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.container {
display: flex;
margin: 2.5em;
}

.icon {
display: inline-block;
vertical-align: -0.125em;
}

.overlay-container {
width: fit-content;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Button } from '@iqss/dataverse-design-system'
import styles from './DynamicFieldsButtons.module.scss'
import { MouseEvent } from 'react'
import { Dash, Plus } from 'react-bootstrap-icons'
import { Tooltip } from '@iqss/dataverse-design-system'
import { useTranslation } from 'react-i18next'

interface AddFieldButtonsProps {
originalField?: boolean
onAddButtonClick: (event: MouseEvent<HTMLButtonElement>) => void
onRemoveButtonClick: (event: MouseEvent<HTMLButtonElement>) => void
}

export function DynamicFieldsButtons({
originalField,
onAddButtonClick,
onRemoveButtonClick
}: AddFieldButtonsProps) {
const { t } = useTranslation('createDataset')
return (
<div className={styles.container}>
<Tooltip placement="top" overlay={t('datasetForm.addRowButton')}>
<div className={styles['overlay-container']}>
<Button type="button" variant="secondary" onClick={onAddButtonClick}>
<Plus className={styles.icon} title="Add" />
</Button>
</div>
</Tooltip>
{!originalField && (
<Tooltip placement="top" overlay={t('datasetForm.deleteRowButton')}>
<div className={styles['overlay-container']}>
<Button type="button" variant="secondary" withSpacing onClick={onRemoveButtonClick}>
<Dash className={styles.icon} title="Delete" />
</Button>
</div>
</Tooltip>
)}
</div>
)
}
8 changes: 4 additions & 4 deletions src/sections/create-dataset/useDatasetFormData.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { useState } from 'react'
import { DatasetDTO, initialDatasetDTO } from '../../dataset/domain/useCases/DTOs/DatasetDTO'
import _ from 'lodash'
import { DatasetMetadataSubField } from '../../dataset/domain/models/Dataset'

const initialState: DatasetDTO = JSON.parse(JSON.stringify(initialDatasetDTO)) as DatasetDTO
export const useDatasetFormData = (
datasetIsValid: (formData: DatasetDTO) => boolean
): {
formData: DatasetDTO
updateFormData: (name: string, value: string) => void
updateFormData: (name: string, value: string | DatasetMetadataSubField[]) => void
} => {
const [formData, setFormData] = useState(initialState)

const updateFormData = (name: string, value: string) => {
const updateFormData = (name: string, value: string | DatasetMetadataSubField[]) => {
const updatedFormData = _.cloneDeep(getUpdatedFormData(formData, name, value))

setFormData(updatedFormData)
datasetIsValid(updatedFormData)
}

const getUpdatedFormData = (
currentFormData: DatasetDTO,
name: string,
value: string
value: string | DatasetMetadataSubField[]
): DatasetDTO => {
const objectFromPath: DatasetDTO = initialState

Expand Down
21 changes: 21 additions & 0 deletions src/sections/create-dataset/useMultipleFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useState } from 'react'
import _ from 'lodash'
import { DatasetMetadataSubField } from '../../dataset/domain/models/Dataset'

export function useMultipleFields(initialFields: DatasetMetadataSubField[]) {
const [multipleFields, setMultipleFields] = useState(initialFields)

const addField = (index: number, newField: DatasetMetadataSubField) => {
const updatedFields = _.cloneDeep(multipleFields)
updatedFields.splice(index + 1, 0, newField)
setMultipleFields(updatedFields)
}

const removeField = (index: number) => {
const updatedFields = _.cloneDeep(multipleFields)
updatedFields.splice(index, 1)
setMultipleFields(updatedFields)
}

return { multipleFields, setMultipleFields, addField, removeField }
}
29 changes: 29 additions & 0 deletions src/stories/create-dataset/AuthorFormGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { StoryObj, Meta } from '@storybook/react'
import { WithLayout } from '../WithLayout'
import { WithI18next } from '../WithI18next'
import { AuthorFormGroup } from '../../sections/create-dataset/AuthorFormGroup'
import { SubmissionStatus } from '../../sections/create-dataset/useCreateDatasetForm'
import { initialState } from '../../sections/create-dataset/useDatasetValidator'
import { DatasetMetadataSubField } from '../../dataset/domain/models/Dataset'
import { initialDatasetDTO } from '../../../src/dataset/domain/useCases/DTOs/DatasetDTO'

const meta: Meta<typeof AuthorFormGroup> = {
title: 'Sections/Create Dataset Page/AuthorFormGroup',
component: AuthorFormGroup,
decorators: [WithI18next, WithLayout]
}
export default meta
type Story = StoryObj<typeof AuthorFormGroup>

export const Default: Story = {
render: () => (
<AuthorFormGroup
submissionStatus={SubmissionStatus.NotSubmitted}
updateFormData={() => {}}
initialAuthorFields={
initialState.metadataBlocks[0].fields['author'] as DatasetMetadataSubField[]
}
validationErrors={initialDatasetDTO}
/>
)
}
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ describe('Create Dataset', () => {
cy.findByText('Title').children('div').trigger('mouseover')
cy.findByText('The main title of the Dataset').should('exist')

cy.findByLabelText(/Author Name/i)
.should('exist')
.should('have.attr', 'required', 'required')
cy.findByLabelText(/Name/i).should('exist').should('have.attr', 'required', 'required')
cy.findByText('Author Name').children('div').trigger('mouseover')
cy.findByText(
"The name of the author, such as the person's name or the name of an organization"
Expand Down Expand Up @@ -115,9 +113,7 @@ describe('Create Dataset', () => {

cy.findByLabelText(/Title/i).type('Test Dataset Title').and('have.value', 'Test Dataset Title')

cy.findByLabelText(/Author Name/i)
.type('Test author name')
.and('have.value', 'Test author name')
cy.findByLabelText(/Name/i).type('Test author name').and('have.value', 'Test author name')

cy.findByLabelText(/Point of Contact E-mail/i)
.type('[email protected]')
Expand All @@ -140,7 +136,7 @@ describe('Create Dataset', () => {
cy.customMount(<CreateDatasetForm repository={datasetRepository} />)

cy.findByLabelText(/Title/i).type('Test Dataset Title')
cy.findByLabelText(/Author Name/i).type('Test author name')
cy.findByLabelText(/Name/i).type('Test author name')
cy.findByLabelText(/Point of Contact E-mail/i).type('[email protected]')
cy.findByLabelText(/Description Text/i).type('Test description text')
cy.findByLabelText(/Arts and Humanities/i).check()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ describe('Create Dataset', () => {
it('navigates to the new dataset after submitting a valid form', () => {
cy.visit('/spa/datasets/create')

cy.findByLabelText(/Title/i).type('Test Dataset Title')
cy.findByLabelText(/Author Name/i).type('Test author name', { force: true })
cy.findByLabelText(/Title/i).type('Test Dataset Title', { force: true })
cy.findByLabelText(/Name/i).type('Test author name', { force: true })
cy.findByLabelText(/Point of Contact E-mail/i).type('[email protected]')
cy.findByLabelText(/Description Text/i).type('Test description text')
cy.findByLabelText(/Arts and Humanities/i).check()
Expand Down

0 comments on commit dd47171

Please sign in to comment.