Skip to content

Commit

Permalink
Merge pull request #357 from IQSS/feature/336-dynamically-render-form…
Browse files Browse the repository at this point in the history
…-fields

Feature/336 dynamically render form fields
  • Loading branch information
g-saracca authored Apr 1, 2024
2 parents 75fe4fb + 9857e9b commit 7fde401
Show file tree
Hide file tree
Showing 38 changed files with 5,251 additions and 346 deletions.
14 changes: 3 additions & 11 deletions packages/design-system/src/lib/components/accordion/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { ReactNode } from 'react'
import { Accordion as AccordionBS } from 'react-bootstrap'
import { Accordion as AccordionBS, AccordionProps as AccordionPropsBS } from 'react-bootstrap'
import { AccordionItem } from './AccordionItem'
import { AccordionBody } from './AccordionBody'
import { AccordionHeader } from './AccordionHeader'

interface AccordionProps {
defaultActiveKey?: string[] | string
alwaysOpen?: boolean
children: ReactNode
}

function Accordion({ defaultActiveKey, children, alwaysOpen = false }: AccordionProps) {
function Accordion({ alwaysOpen = false, children, ...rest }: AccordionPropsBS) {
return (
<AccordionBS defaultActiveKey={defaultActiveKey} alwaysOpen={alwaysOpen}>
<AccordionBS alwaysOpen={alwaysOpen} {...rest}>
{children}
</AccordionBS>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ReactNode } from 'react'
import { ElementType, ReactNode } from 'react'
import { Accordion as AccordionBS } from 'react-bootstrap'

interface AccordionBodyProps {
interface AccordionBodyProps extends React.HTMLAttributes<HTMLElement> {
children: ReactNode
bsPrefix?: string
as?: ElementType
}

export function AccordionBody({ children }: AccordionBodyProps) {
return <AccordionBS.Body>{children}</AccordionBS.Body>
export function AccordionBody({ children, ...rest }: AccordionBodyProps) {
return <AccordionBS.Body {...rest}>{children}</AccordionBS.Body>
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { ReactNode } from 'react'
import { ElementType, ReactNode } from 'react'
import { Accordion as AccordionBS } from 'react-bootstrap'

interface AccordionHeaderProps {
interface AccordionHeaderProps extends React.HTMLAttributes<HTMLElement> {
children: ReactNode
onClick?: () => void
bsPrefix?: string
as?: ElementType
}

export function AccordionHeader({ children }: AccordionHeaderProps) {
return <AccordionBS.Header>{children}</AccordionBS.Header>
export function AccordionHeader({ children, ...rest }: AccordionHeaderProps) {
return <AccordionBS.Header {...rest}>{children}</AccordionBS.Header>
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ReactNode } from 'react'
import { ElementType, ReactNode } from 'react'
import { Accordion as AccordionBS } from 'react-bootstrap'

interface AccordionItemProps {
export interface AccordionItemProps extends React.HTMLAttributes<HTMLElement> {
eventKey: string
children: ReactNode
bsPrefix?: string
as?: ElementType
}

export function AccordionItem({ eventKey, children }: AccordionItemProps) {
return <AccordionBS.Item eventKey={eventKey}>{children}</AccordionBS.Item>
export function AccordionItem({ children, ...rest }: AccordionItemProps) {
return <AccordionBS.Item {...rest}>{children}</AccordionBS.Item>
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@import "src/lib/assets/styles/design-tokens/typography.module";
@import 'src/lib/assets/styles/design-tokens/typography.module';

.title {
padding-top: calc(0.375rem + 1px);
padding-bottom: calc(0.375rem + 1px);
display: inline-block;
margin-bottom: 0.5rem;
font-weight: $dv-font-weight-bold;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function FormCheckboxGroup({
}: PropsWithChildren<FormCheckboxGroupProps>) {
const validationClass = isInvalid ? 'is-invalid' : isValid ? 'is-valid' : ''
return (
<Row>
<Row className="mb-3">
<Col sm={3}>
<span className={styles.title}>
{title} {required && <RequiredInputSymbol />}{' '}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,21 @@ export function FormGroupWithMultipleFields({
const isFirstField = index == 0

return (
<Row key={index}>
<Row key={index} className="mb-3">
<Col sm={3}>
{isFirstField && <Title title={title} required={required} message={message} />}
</Col>
<Col sm={6}>{field}</Col>
<Col sm={3}>
{withDynamicFields && (
<Col sm={withDynamicFields ? 6 : 9}>{field}</Col>

{withDynamicFields && (
<Col sm={3}>
<DynamicFieldsButtons
originalField={isFirstField}
onAddButtonClick={() => addField(field)}
onRemoveButtonClick={() => removeField(index)}
/>
)}
</Col>
</Col>
)}
</Row>
)
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,7 @@ function FormGroup({
children,
...props
}: PropsWithChildren<FormGroupProps>) {
const childrenWithRequiredProp = React.Children.map(children as JSX.Element, (child) => {
return React.cloneElement(child, {
required: required,
withinMultipleFieldsGroup: as === Col
})
})
const childrenWithRequiredProp = cloneThroughFragments(children, required, as)

return (
<FormBS.Group
Expand All @@ -42,6 +37,29 @@ function FormGroup({
</FormBS.Group>
)
}
function cloneThroughFragments(
children: React.ReactNode,
required?: boolean,
as?: typeof Col | typeof Row
): React.ReactNode {
return React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
if (child.type === React.Fragment) {
const hasChildren = (props: unknown): props is { children: React.ReactNode } =>
typeof props === 'object' && Object.hasOwnProperty.call(props, 'children')

if (hasChildren(child.props)) {
return cloneThroughFragments(child.props.children, required, as)
}
}
return React.cloneElement(child as React.ReactElement, {
required: required,
withinMultipleFieldsGroup: as === Col
})
}
return child
})
}

FormGroup.Label = FormLabel
FormGroup.Input = FormInput
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { FormGroup } from '../../../../src/lib/components/form/form-group/FormGroup'
import { Form } from '../../../../src/lib/components/form/Form'

describe('FormGroup', () => {
it('should render childrens correctly even trough react fragments', () => {
cy.mount(
<Form>
<FormGroup controlId="some-control-id">
<>
<label htmlFor="username">Username</label>
<input type="text" id="username" />
</>
</FormGroup>
</Form>
)

cy.findByText('Username').should('exist')
})
})
101 changes: 101 additions & 0 deletions src/metadata-block-info/domain/models/MetadataBlockInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,104 @@ export interface MetadataFieldInfo {

export const METADATA_FIELD_DISPLAY_FORMAT_PLACEHOLDER = '#VALUE'
export const METADATA_FIELD_DISPLAY_FORMAT_NAME_PLACEHOLDER = '#NAME'

// 👇👇 New types for #336

export interface MetadataBlockInfo2 {
id: number
name: string
displayName: string
metadataFields: Record<string, MetadataField2>
displayOnCreate: boolean // If true, the block will be displayed on the create dataset form
}

export interface MetadataField2 {
name: string
displayName: string
title: string
type: TypeMetadataField
typeClass: TypeClassMetadataField
watermark: WatermarkMetadataField
description: string
multiple: boolean
isControlledVocabulary: boolean
displayFormat: string
isRequired: boolean
displayOrder: number
controlledVocabularyValues?: string[]
childMetadataFields?: Record<string, MetadataField2>
displayOnCreate: boolean // If true, the field will be displayed on the create metadata block
}

export const TypeMetadataFieldOptions = {
Date: 'DATE',
Email: 'EMAIL',
Float: 'FLOAT',
Int: 'INT',
None: 'NONE',
Text: 'TEXT',
Textbox: 'TEXTBOX',
URL: 'URL'
} as const

export type TypeMetadataField =
(typeof TypeMetadataFieldOptions)[keyof typeof TypeMetadataFieldOptions]

// export enum TypeMetadataField {
// Date = 'DATE',
// Email = 'EMAIL',
// Float = 'FLOAT',
// Int = 'INT',
// None = 'NONE',
// Text = 'TEXT',
// Textbox = 'TEXTBOX',
// URL = 'URL'
// }

export const TypeClassMetadataFieldOptions = {
Compound: 'compound',
ControlledVocabulary: 'controlledVocabulary',
Primitive: 'primitive'
} as const

export type TypeClassMetadataField =
(typeof TypeClassMetadataFieldOptions)[keyof typeof TypeClassMetadataFieldOptions]

// export enum TypeClassMetadataField {
// Compound = 'compound',
// ControlledVocabulary = 'controlledVocabulary',
// Primitive = 'primitive'
// }

export const WatermarkMetadataFieldOptions = {
Empty: '',
EnterAFloatingPointNumber: 'Enter a floating-point number.',
EnterAnInteger: 'Enter an integer.',
FamilyNameGivenNameOrOrganization: 'FamilyName, GivenName or Organization',
HTTPS: 'https://',
NameEmailXyz: '[email protected]',
OrganizationXYZ: 'Organization XYZ',
The1FamilyNameGivenNameOr2Organization: '1) FamilyName, GivenName or 2) Organization',
The1FamilyNameGivenNameOr2OrganizationXYZ: '1) Family Name, Given Name or 2) Organization XYZ',
WatermarkEnterAnInteger: 'Enter an integer...',
YYYYOrYYYYMMOrYYYYMMDD: 'YYYY or YYYY-MM or YYYY-MM-DD',
YyyyMmDD: 'YYYY-MM-DD'
} as const

export type WatermarkMetadataField =
(typeof WatermarkMetadataFieldOptions)[keyof typeof WatermarkMetadataFieldOptions]

// export enum WatermarkMetadataField {
// Empty = '',
// EnterAFloatingPointNumber = 'Enter a floating-point number.',
// EnterAnInteger = 'Enter an integer.',
// FamilyNameGivenNameOrOrganization = 'FamilyName, GivenName or Organization',
// HTTPS = 'https://',
// NameEmailXyz = '[email protected]',
// OrganizationXYZ = 'Organization XYZ',
// The1FamilyNameGivenNameOr2Organization = '1) FamilyName, GivenName or 2) Organization',
// The1FamilyNameGivenNameOr2OrganizationXYZ = '1) Family Name, Given Name or 2) Organization XYZ',
// WatermarkEnterAnInteger = 'Enter an integer...',
// YYYYOrYYYYMMOrYYYYMMDD = 'YYYY or YYYY-MM or YYYY-MM-DD',
// YyyyMmDD = 'YYYY-MM-DD'
// }
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MetadataBlockInfo } from '../models/MetadataBlockInfo'
import { MetadataBlockInfo, MetadataBlockInfo2 } from '../models/MetadataBlockInfo'

export interface MetadataBlockInfoRepository {
getByName: (name: string) => Promise<MetadataBlockInfo | undefined>
getByColecctionId: (collectionId: string, create: boolean) => Promise<MetadataBlockInfo2[]>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { MetadataBlockInfo2 } from '../models/MetadataBlockInfo'
import { MetadataBlockInfoRepository } from '../repositories/MetadataBlockInfoRepository'

export async function getMetadataBlockInfoByCollectionId(
metadataBlockInfoRepository: MetadataBlockInfoRepository,
collectionId: string,
create: boolean
): Promise<MetadataBlockInfo2[]> {
return metadataBlockInfoRepository
.getByColecctionId(collectionId, create)
.catch((error: Error) => {
throw new Error(error.message)
})
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { MetadataBlockInfoRepository } from '../../domain/repositories/MetadataBlockInfoRepository'
import { MetadataBlockInfo } from '../../domain/models/MetadataBlockInfo'
import { MetadataBlockInfo, MetadataBlockInfo2 } from '../../domain/models/MetadataBlockInfo'
import {
getMetadataBlockByName,
MetadataBlock as JSMetadataBlockInfo,
ReadError
} from '@iqss/dataverse-client-javascript'
import { JSMetadataBlockInfoMapper } from '../mappers/JSMetadataBlockInfoMapper'
import { MetadataBlockInfoMother } from '../../../../tests/component/metadata-block-info/domain/models/MetadataBlockInfoMother'

export class MetadataBlockInfoJSDataverseRepository implements MetadataBlockInfoRepository {
getByName(name: string): Promise<MetadataBlockInfo | undefined> {
Expand All @@ -18,4 +19,12 @@ export class MetadataBlockInfoJSDataverseRepository implements MetadataBlockInfo
throw new Error(error.message)
})
}

getByColecctionId(_collectionId: string, _create: boolean): Promise<MetadataBlockInfo2[]> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(MetadataBlockInfoMother.getByCollectionIdFullResponse())
}, 1_000)
})
}
}
11 changes: 10 additions & 1 deletion src/sections/create-dataset/CreateDatasetFactory.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { CreateDatasetForm } from './CreateDatasetForm'
import { ReactElement } from 'react'
import { DatasetJSDataverseRepository } from '../../dataset/infrastructure/repositories/DatasetJSDataverseRepository'
import { MetadataBlockInfoJSDataverseRepository } from '../../metadata-block-info/infrastructure/repositories/MetadataBlockInfoJSDataverseRepository'
import { MetadataBlockInfoMockRepository } from '../../stories/create-dataset/MetadataBlockInfoMockRepository'

const repository = new DatasetJSDataverseRepository()
const _metadataBlockInfoRepository = new MetadataBlockInfoJSDataverseRepository()
const metadataBlockInfoMockRepository = new MetadataBlockInfoMockRepository()

export class CreateDatasetFactory {
static create(): ReactElement {
return <CreateDatasetForm repository={repository} />
return (
<CreateDatasetForm
repository={repository}
metadataBlockInfoRepository={metadataBlockInfoMockRepository}
/>
)
}
}
7 changes: 7 additions & 0 deletions src/sections/create-dataset/CreateDatasetForm.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.header {
margin: 0.5em 0;
}

.container {
// margin: 0.5rem 0;
}
Loading

0 comments on commit 7fde401

Please sign in to comment.