Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/336 dynamically render form fields #357

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3367103
feat(DynamicFormFieldsRender): Initial setup with mocked data
g-saracca Mar 20, 2024
e15c6e4
feat(DesignSystem): using Accordion own props and extending Accordion…
g-saracca Mar 21, 2024
969823b
feat(DynamicFormFieldsRender): filter blocks and fields
g-saracca Mar 21, 2024
2f4af31
feat(DynamicFormFieldsRender): remove custom styles from accordion an…
g-saracca Mar 22, 2024
faa52da
feat(DynamicFormFieldsRender): detecting different fields
g-saracca Mar 22, 2024
349b444
feat(DynamicFormFieldsRender): Different fields and wrapper
g-saracca Mar 25, 2024
8d1a15f
feat(DynamicFormFieldsRender): Update models and mock
g-saracca Mar 25, 2024
3fee0b6
feat(DynamicFormFieldsRender): Refactor to avoid for now multiple act…
g-saracca Mar 25, 2024
5566900
feat: change styles for checbox group
g-saracca Mar 26, 2024
57632ec
feat: add names to form fields
g-saracca Mar 26, 2024
01acf84
Merge branch 'feature/316-create-functional-create-dataset-form' into…
g-saracca Mar 26, 2024
f1a6d37
feat(DynamicFormFieldsRender): Add metadta block form fields skeleton
g-saracca Mar 26, 2024
d18f576
feat(design system): Clone through fragments recursively
g-saracca Mar 27, 2024
c788028
feat: Allow form fields to inherit props like required and withinMult…
g-saracca Mar 27, 2024
fc731a1
test(Dynamic Fields): Add component test for dynamic fields
g-saracca Mar 27, 2024
d5bea19
test(Dynamic Fields): Refactor Fields
g-saracca Mar 28, 2024
501434f
test(Dynamic Fields): Multiple refactor to use mocked data response
g-saracca Mar 28, 2024
8378a4c
feat(Dynamic Fields): Add stories and its mocks repositories
g-saracca Mar 28, 2024
3a4291e
feat(Dynamic Fields): Add more tests
g-saracca Mar 28, 2024
4215794
feat(Dynamic Fields): Skip test of submiting valid form
g-saracca Mar 28, 2024
9857e9b
feat(Dynamic Fields): Add test for rendering childrens trough react f…
g-saracca Mar 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading