Skip to content

Commit

Permalink
Merge pull request IQSS#251 from IQSS/feature/231-create-dataset-boil…
Browse files Browse the repository at this point in the history
…erplate

Feature/231 create dataset boilerplate
  • Loading branch information
ekraffmiller authored Jan 29, 2024
2 parents c48c175 + d442286 commit 6ca9ddc
Show file tree
Hide file tree
Showing 27 changed files with 337 additions and 12 deletions.
5 changes: 3 additions & 2 deletions packages/design-system/src/lib/components/form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import { FormGroupWithMultipleFields } from './form-group-multiple-fields/FormGr
import { FormInputGroup } from './form-group/form-input-group/FormInputGroup'

interface FormProps {
className?: string
validated?: boolean
onSubmit?: (event: FormEvent<HTMLFormElement>) => void
}

function Form({ validated, onSubmit, children }: PropsWithChildren<FormProps>) {
function Form({ validated, onSubmit, children, className }: PropsWithChildren<FormProps>) {
return (
<FormBS validated={validated} onSubmit={onSubmit}>
<FormBS validated={validated} onSubmit={onSubmit} className={className}>
{children}
</FormBS>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@ interface FormInputProps extends React.HTMLAttributes<FormInputElement> {
type?: 'text' | 'email' | 'password'
readOnly?: boolean
withinMultipleFieldsGroup?: boolean
name?: string
}

export function FormInput({
type = 'text',
name,
readOnly,
withinMultipleFieldsGroup,
...props
}: FormInputProps) {
return (
<FormElementLayout withinMultipleFieldsGroup={withinMultipleFieldsGroup}>
<FormBS.Control type={type} readOnly={readOnly} plaintext={readOnly} {...props} />
<FormBS.Control name={name} type={type} readOnly={readOnly} plaintext={readOnly} {...props} />
</FormElementLayout>
)
}
1 change: 1 addition & 0 deletions packages/design-system/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export { Icon } from './components/icon/Icon'
export { IconName } from './components/icon/IconName'
export { Tooltip } from './components/tooltip/Tooltip'
export { Pagination } from './components/pagination/Pagination'
export { RequiredInputSymbol } from './components/form/required-input-symbol/RequiredInputSymbol'
22 changes: 22 additions & 0 deletions public/locales/en/createDataset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"pageTitle": "Create Dataset",
"metadataTip": {
"title": "Metadata Tip",
"content": "After adding the dataset, click the Edit Dataset button to add more metadata."
},
"requiredFields": "Asterisks indicate required fields",
"datasetForm": {
"title": "Title",
"status": {
"submitting": "Submitting...",
"success": "Form submitted successfully!",
"failed": "Error: Submission failed."
}
},
"datasetFormStates": {
"submitting": "Form Submitting",
"submissionSuccess": "Form submission successful"
},
"saveButton": "Save Dataset",
"cancelButton": "Cancel"
}
7 changes: 7 additions & 0 deletions src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { Layout } from './sections/layout/Layout'
import { Route } from './sections/Route.enum'
import { DatasetFactory } from './sections/dataset/DatasetFactory'
import { PageNotFound } from './sections/page-not-found/PageNotFound'
import { CreateDatasetFactory } from './sections/create-dataset/CreateDatasetFactory'
import { FileFactory } from './sections/file/FileFactory'
import { HomeFactory } from './sections/home/HomeFactory'

Expand All @@ -10,6 +12,7 @@ const router = createBrowserRouter(
{
path: '/',
element: <Layout />,
errorElement: <PageNotFound />,
children: [
{
path: Route.HOME,
Expand All @@ -19,6 +22,10 @@ const router = createBrowserRouter(
path: Route.DATASETS,
element: DatasetFactory.create()
},
{
path: Route.CREATE_DATASET,
element: CreateDatasetFactory.create()
},
{
path: Route.FILES,
element: FileFactory.create()
Expand Down
File renamed without changes
File renamed without changes.
3 changes: 3 additions & 0 deletions src/dataset/domain/models/DatasetFormFields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface DatasetFormFields {
createDatasetTitle: string
}
6 changes: 6 additions & 0 deletions src/dataset/domain/models/DatasetValidationResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { DatasetFormFields } from '../models/DatasetFormFields'

export interface DatasetValidationResponse {
isValid: boolean
errors: Record<keyof DatasetFormFields, string | undefined>
}
3 changes: 3 additions & 0 deletions src/dataset/domain/repositories/DatasetRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import { Dataset } from '../models/Dataset'
import { TotalDatasetsCount } from '../models/TotalDatasetsCount'
import { DatasetPaginationInfo } from '../models/DatasetPaginationInfo'
import { DatasetPreview } from '../models/DatasetPreview'
import { DatasetFormFields } from '../models/DatasetFormFields'

export interface DatasetRepository {
getByPersistentId: (persistentId: string, version?: string) => Promise<Dataset | undefined>
getByPrivateUrlToken: (privateUrlToken: string) => Promise<Dataset | undefined>
getAll: (paginationInfo: DatasetPaginationInfo) => Promise<DatasetPreview[]>
getTotalDatasetsCount: () => Promise<TotalDatasetsCount>
// Created as placeholder for https://github.com/IQSS/dataverse-frontend/pull/251
createDataset: (fields: DatasetFormFields) => Promise<string>
}
17 changes: 17 additions & 0 deletions src/dataset/domain/useCases/createDataset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { DatasetFormFields } from '../models/DatasetFormFields'
import { DatasetRepository } from '../repositories/DatasetRepository'
import { DatasetJSDataverseRepository } from '../../infrastructure/repositories/DatasetJSDataverseRepository'

const repo = new DatasetJSDataverseRepository()
function createDatasetMockHelper(
datasetRepository: DatasetRepository,
formFieldsToSubmit: DatasetFormFields
): Promise<string> {
return datasetRepository.createDataset(formFieldsToSubmit).catch((error: Error) => {
throw new Error(error.message)
})
}

export function createDataset(fields: DatasetFormFields): Promise<string> {
return createDatasetMockHelper(repo, fields)
}
19 changes: 19 additions & 0 deletions src/dataset/domain/useCases/validateDataset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { DatasetFormFields } from '../models/DatasetFormFields'
import { DatasetValidationResponse } from '../models/DatasetValidationResponse'
const NAME_REQUIRED = 'Name is required'

export function validateDataset(fieldsToSubmit: DatasetFormFields) {
const errors: Record<keyof DatasetFormFields, string | undefined> = {
createDatasetTitle: undefined
}

if (!fieldsToSubmit.createDatasetTitle) {
errors.createDatasetTitle = NAME_REQUIRED
}

const validationResponse: DatasetValidationResponse = {
isValid: Object.values(errors).every((error) => error === undefined),
errors
}
return validationResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { TotalDatasetsCount } from '../../domain/models/TotalDatasetsCount'
import { DatasetPaginationInfo } from '../../domain/models/DatasetPaginationInfo'
import { DatasetPreview } from '../../domain/models/DatasetPreview'
import { JSDatasetPreviewMapper } from '../mappers/JSDatasetPreviewMapper'
import { DatasetFormFields } from '../../domain/models/DatasetFormFields'

const includeDeaccessioned = true

Expand Down Expand Up @@ -135,6 +136,15 @@ export class DatasetJSDataverseRepository implements DatasetRepository {
})
}

createDataset(fields: DatasetFormFields): Promise<string> {
const returnMsg = 'Form Data Submitted: ' + JSON.stringify(fields)
return new Promise((resolve) => {
setTimeout(() => {
resolve(returnMsg)
}, 1000)
})
}

versionToVersionId(version?: string): string | undefined {
if (version === 'DRAFT') {
return ':draft'
Expand Down
1 change: 1 addition & 0 deletions src/sections/Route.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export enum Route {
LOG_IN = '/loginpage.xhtml?redirectPage=%2Fdataverse.xhtml',
LOG_OUT = '/',
DATASETS = '/datasets',
CREATE_DATASET = '/datasets/create',
FILES = '/files'
}
8 changes: 8 additions & 0 deletions src/sections/create-dataset/CreateDatasetFactory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { CreateDatasetForm } from './CreateDatasetForm'
import { ReactElement } from 'react'

export class CreateDatasetFactory {
static create(): ReactElement {
return <CreateDatasetForm />
}
}
87 changes: 87 additions & 0 deletions src/sections/create-dataset/CreateDatasetForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { ChangeEvent, FormEvent, MouseEvent, useEffect } from 'react'
import { Alert, Button, Col, Form, Row } from '@iqss/dataverse-design-system'
import { useTranslation } from 'react-i18next'
import { RequiredFieldText } from '../shared/form/RequiredFieldText/RequiredFieldText'
import { SeparationLine } from '../shared/layout/SeparationLine/SeparationLine'
import { useCreateDatasetForm, SubmissionStatusEnums } from './useCreateDatasetForm'
import styles from '/src/sections/dataset/Dataset.module.scss'
import { useLoading } from '../loading/LoadingContext'

export function CreateDatasetForm() {
const { isLoading, setIsLoading } = useLoading()
const { formErrors, submissionStatus, updateFormData, submitFormData, cancelFormSubmit } =
useCreateDatasetForm()

const { t } = useTranslation('createDataset')

const handleCreateDatasetFieldChange = (event: ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target
updateFormData({ [name]: value })
}

const handleCreateDatasetSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
submitFormData()
}

const handleFormCancel = (event: MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
cancelFormSubmit()
}
useEffect(() => {
setIsLoading(false)
}, [isLoading])

return (
<article>
<header className={styles.header}>
<h1>{t('pageTitle')}</h1>
</header>
<SeparationLine />
<div className={styles.container}>
<RequiredFieldText />
{submissionStatus === SubmissionStatusEnums.IsSubmitting && (
<p>{t('datasetForm.status.submitting')}</p>
)}
{submissionStatus === SubmissionStatusEnums.SubmitComplete && (
<p>{t('datasetForm.status.success')}</p>
)}
{submissionStatus === SubmissionStatusEnums.Errored && (
<p>{t('datasetForm.status.fail')}</p>
)}
<Form
onSubmit={(event: FormEvent<HTMLFormElement>) => {
handleCreateDatasetSubmit(event)
}}
className={'create-dataset-form'}>
<Row>
<Col md={9}>
<Form.Group controlId="createDatasetTitle" required>
<Form.Group.Label>{t('datasetForm.title')}</Form.Group.Label>
<Form.Group.Input
readOnly={submissionStatus === SubmissionStatusEnums.IsSubmitting && true}
type="text"
name="createDatasetTitle"
placeholder="Dataset Title"
onChange={handleCreateDatasetFieldChange}
withinMultipleFieldsGroup={false}
/>
</Form.Group>
{formErrors.createDatasetTitle && <span>{formErrors.createDatasetTitle}</span>}
</Col>
</Row>
<SeparationLine />
<Alert variant={'info'} customHeading={t('metadataTip.title')} dismissible={false}>
{t('metadataTip.content')}
</Alert>
<Button type="submit" disabled={submissionStatus === SubmissionStatusEnums.IsSubmitting}>
{t('saveButton')}
</Button>
<Button withSpacing variant="secondary" type="button" onClick={handleFormCancel}>
{t('cancelButton')}
</Button>
</Form>
</div>
</article>
)
}
71 changes: 71 additions & 0 deletions src/sections/create-dataset/useCreateDatasetForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useState } from 'react'
import { DatasetFormFields } from '../../dataset/domain/models/DatasetFormFields'
import { createDataset } from '../../dataset/domain/useCases/createDataset'
import { validateDataset } from '../../dataset/domain/useCases/validateDataset'
import { DatasetValidationResponse } from '../../dataset/domain/models/DatasetValidationResponse'
import { useNavigate } from 'react-router-dom'
import { Route } from '../Route.enum'
interface FormContextInterface {
fields: DatasetFormFields
}

const defaultFormState: DatasetFormFields = {
createDatasetTitle: ''
}

export enum SubmissionStatusEnums {
NotSubmitted = 'NotSubmitted',
IsSubmitting = 'IsSubmitting',
SubmitComplete = 'SubmitComplete',
Errored = 'Errored'
}

export function useCreateDatasetForm() {
const [formState, setFormState] = useState<FormContextInterface>({
fields: defaultFormState
})

const [submissionStatus, setSubmissionStatus] = useState<SubmissionStatusEnums>(
SubmissionStatusEnums.NotSubmitted
)
const [formErrors, setFormErrors] = useState<Record<keyof DatasetFormFields, string | undefined>>(
{ createDatasetTitle: undefined }
)

const updateFormData = (updatedFormData: object) => {
setFormState((prevState) => ({
...prevState,
fields: { ...prevState.fields, ...updatedFormData }
}))
}

const submitFormData = () => {
setSubmissionStatus(SubmissionStatusEnums.IsSubmitting)

const validationResult: DatasetValidationResponse = validateDataset(formState.fields)

if (validationResult.isValid) {
createDataset(formState.fields)
.then(() => setSubmissionStatus(SubmissionStatusEnums.IsSubmitting))
.catch(() => setSubmissionStatus(SubmissionStatusEnums.Errored))
.finally(() => setSubmissionStatus(SubmissionStatusEnums.SubmitComplete))
} else {
setFormErrors(validationResult.errors)
setSubmissionStatus(SubmissionStatusEnums.Errored)
}
}
const navigate = useNavigate()
const cancelFormSubmit = () => {
const path = Route.HOME
navigate(path)
}

return {
formState,
formErrors,
submissionStatus,
updateFormData,
submitFormData,
cancelFormSubmit
}
}
5 changes: 0 additions & 5 deletions src/sections/dataset/Dataset.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,4 @@

.tab-container {
padding: 1em 0;
}

.separation-line {
margin: 1em 0;
border-bottom: 1px solid #dee2e6;
}
3 changes: 2 additions & 1 deletion src/sections/dataset/Dataset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useEffect } from 'react'
import { DatasetAlerts } from './dataset-alerts/DatasetAlerts'
import { useNotImplementedModal } from '../not-implemented/NotImplementedModalContext'
import { NotImplementedModal } from '../not-implemented/NotImplementedModal'
import { SeparationLine } from '../shared/layout/SeparationLine/SeparationLine'

interface DatasetProps {
fileRepository: FileRepository
Expand Down Expand Up @@ -87,7 +88,7 @@ export function Dataset({ fileRepository }: DatasetProps) {
</div>
</Tabs.Tab>
</Tabs>
<div className={styles['separation-line']}></div>
<SeparationLine />
</div>
</article>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/sections/layout/Layout.module.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@import "src/sections/assets/variables";
@import "src/assets/variables";

.body-container {
min-height: $body-available-height;
Expand Down
2 changes: 1 addition & 1 deletion src/sections/layout/header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import logo from '../../assets/logo.svg'
import logo from '../../../assets/logo.svg'
import { useTranslation } from 'react-i18next'
import { Navbar } from '@iqss/dataverse-design-system'
import { Route } from '../../Route.enum'
Expand Down
Loading

0 comments on commit 6ca9ddc

Please sign in to comment.