Skip to content

Commit

Permalink
refactor: dry up forms
Browse files Browse the repository at this point in the history
  • Loading branch information
Birkbjo committed Feb 29, 2024
1 parent cff43ba commit ef40a0b
Show file tree
Hide file tree
Showing 19 changed files with 471 additions and 151 deletions.
15 changes: 15 additions & 0 deletions src/components/form/DefaultFormContents.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.form {
background: var(--colors-white);
padding: var(--spacers-dp16);
padding-bottom: var(--spacers-dp32);
}

.formActions {
position: fixed;
left: 0;
bottom: 0;
width: 100vw;
padding: var(--spacers-dp16);
box-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
background: var(--colors-white);
}
58 changes: 58 additions & 0 deletions src/components/form/DefaultFormContents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import i18n from '@dhis2/d2-i18n'
import { NoticeBox } from '@dhis2/ui'
import React, { useEffect, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { getSectionPath } from '../../lib'
import { ModelSection } from '../../types'
import { StandardFormSection, StandardFormActions } from '../standardForm'
import classes from './DefaultFormContents.module.css'

export function DefaultFormContents({
children,
section,
submitError,
submitting,
}: {
children: React.ReactNode
section: ModelSection
submitting: boolean
submitError?: string
}) {
const formErrorRef = useRef<HTMLDivElement | null>(null)
const navigate = useNavigate()

const listPath = getSectionPath(section)
useEffect(() => {
if (submitError) {
formErrorRef.current?.scrollIntoView({ behavior: 'smooth' })
}
}, [submitError])

return (
<>
<div className={classes.form}>{children}</div>
{submitError && (
<StandardFormSection>
<div ref={formErrorRef}>
<NoticeBox
error
title={i18n.t(
'Something went wrong when submitting the form'
)}
>
{submitError}
</NoticeBox>
</div>
</StandardFormSection>
)}
<div className={classes.formActions}>
<StandardFormActions
cancelLabel={i18n.t('Cancel')}
submitLabel={i18n.t('Save and close')}
submitting={submitting}
onCancelClick={() => navigate(listPath)}
/>
</div>
</>
)
}
103 changes: 103 additions & 0 deletions src/components/form/attributes/CustomAttributes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import i18n from '@dhis2/d2-i18n'
import { InputFieldFF, SingleSelectFieldFF, TextAreaFieldFF } from '@dhis2/ui'
import * as React from 'react'
import { Field as FieldRFF, useFormState } from 'react-final-form'
import { StandardFormSection } from '../..'
import { Attribute, AttributeValue } from '../../../types/generated'

const inputWidth = '440px'

type ValuesWithAttributes = {
attributeValues: AttributeValue[]
}

type CustomAttributeProps = {
attribute: Attribute
index: number
}

function CustomAttribute({ attribute, index }: CustomAttributeProps) {
const name = `attributeValues[${index}].value`
const required = attribute.mandatory

if (attribute.optionSet?.options) {
const options = attribute.optionSet?.options.map(
({ code, displayName }) => ({
value: code,
label: displayName,
})
)

if (!required) {
options.unshift({ value: '', label: i18n.t('<No value>') })
}

return (
<StandardFormSection key={attribute.id}>
<FieldRFF
component={SingleSelectFieldFF}
required={required}
inputWidth={inputWidth}
label={attribute.displayFormName}
name={name}
options={options}
/>
</StandardFormSection>
)
}

if (attribute.valueType === 'TEXT') {
return (
<StandardFormSection key={attribute.id}>
<FieldRFF
component={InputFieldFF}
required={required}
inputWidth={inputWidth}
label={attribute.displayFormName}
name={name}
/>
</StandardFormSection>
)
}

if (attribute.valueType === 'LONG_TEXT') {
return (
<StandardFormSection key={attribute.id}>
<FieldRFF
component={TextAreaFieldFF}
required={required}
inputWidth={inputWidth}
label={attribute.displayFormName}
name={name}
/>
</StandardFormSection>
)
}

// @TODO: Verify that all value types have been covered!
throw new Error(`Implement value type "${attribute.valueType}"!`)
}

export function CustomAttributes() {
const formState = useFormState<ValuesWithAttributes>({
subscription: { initialValues: true },
})

const customAttributes = formState.initialValues.attributeValues?.map(
(av) => av.attribute
)

return (
<>
{customAttributes?.map((customAttribute, index) => {
return (
<CustomAttribute
key={customAttribute.id}
attribute={customAttribute}
index={index}
/>
)
})}
</>
)
}
1 change: 1 addition & 0 deletions src/components/form/attributes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CustomAttributes } from './CustomAttributes'
44 changes: 44 additions & 0 deletions src/components/form/attributes/useCustomAttributesQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useDataQuery } from '@dhis2/app-runtime'
import { useMemo } from 'react'
import { useSchemaSectionHandleOrThrow } from '../../../lib'
import { Attribute } from '../../../types/generated'

const CUSTOM_ATTRIBUTES_QUERY = {
attributes: {
resource: 'attributes',
params: ({ modelName }: Record<string, string>) => ({
fields: [
'id',
'mandatory',
'displayFormName',
'valueType',
'optionSet[options[id,displayName,name,code]]',
],
paging: false,
filter: `${modelName}Attribute:eq:true`,
}),
},
}

interface QueryResponse {
attributes: {
attributes: Attribute[]
}
}

export function useCustomAttributesQuery() {
const schemaSection = useSchemaSectionHandleOrThrow()

const customAttributes = useDataQuery<QueryResponse>(
CUSTOM_ATTRIBUTES_QUERY,
{ variables: { modelName: schemaSection.name } }
)

return useMemo(
() => ({
...customAttributes,
data: customAttributes.data?.attributes.attributes || [],
}),
[customAttributes]
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import i18n from '@dhis2/d2-i18n'
import { InputFieldFF } from '@dhis2/ui'
import React from 'react'
import { Field as FieldRFF } from 'react-final-form'
import { SchemaSection, useCheckMaxLengthFromSchema } from '../../lib'
import { SchemaSection, useCheckMaxLengthFromSchema } from '../../../lib'

export function CodeField({ schemaSection }: { schemaSection: SchemaSection }) {
const validate = useCheckMaxLengthFromSchema(schemaSection.name, 'code')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import { useSchemaSectionHandleOrThrow } from '../../lib'
import { StandardFormField } from '../standardForm'
import { useSchemaSectionHandleOrThrow } from '../../../lib'
import { StandardFormField } from '../../standardForm'
import { CodeField } from './CodeField'
import { NameField } from './NameField'
import { ShortNameField } from './ShortNameField'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import i18n from '@dhis2/d2-i18n'
import { TextAreaFieldFF } from '@dhis2/ui'
import React from 'react'
import { Field as FieldRFF } from 'react-final-form'
import { SchemaSection, useCheckMaxLengthFromSchema } from '../../lib'
import { SchemaSection, useCheckMaxLengthFromSchema } from '../../../lib'

export function DescriptionField({
helpText,
Expand All @@ -13,17 +13,14 @@ export function DescriptionField({
}) {
const validate = useCheckMaxLengthFromSchema(schemaSection.name, 'formName')

const helpString =
helpText || i18n.t("Explain the purpose of this and how it's measured.")

return (
<FieldRFF
component={TextAreaFieldFF}
dataTest="formfields-description"
inputWidth="400px"
name="description"
label={i18n.t('Description')}
helpText={helpString}
helpText={helpText}
validate={validate}
validateFields={[]}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
useCheckMaxLengthFromSchema,
useIsFieldValueUnique,
SchemaSection,
} from '../../lib'
} from '../../../lib'

function useValidator({ schemaSection }: { schemaSection: SchemaSection }) {
const params = useParams()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
required,
useCheckMaxLengthFromSchema,
useIsFieldValueUnique,
} from '../../lib'
} from '../../../lib'

function useValidator({ schemaSection }: { schemaSection: SchemaSection }) {
const params = useParams()
Expand Down
File renamed without changes.
3 changes: 3 additions & 0 deletions src/components/form/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './fields'
export { DefaultFormContents } from './DefaultFormContents'
export * from './attributes'
74 changes: 74 additions & 0 deletions src/lib/form/createJsonPatchOperations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import get from 'lodash/fp/get'
import { JsonPatchOperation } from '../../types'
import {
Attribute,
AttributeValue,
IdentifiableObject,
} from './../../types/generated/models'

type PatchAttribute = {
id: Attribute['id']
}

type PatchAttributeValue = {
attribute: PatchAttribute
value: AttributeValue['value']
}

type ModelWithAttributeValues = IdentifiableObject & {
attributeValues: PatchAttributeValue[]
}

interface FormatFormValuesArgs<FormValues extends ModelWithAttributeValues> {
originalValue: unknown
dirtyFields: { [key in keyof FormValues]?: boolean }
values: FormValues
}

// these are removed from the dirtyKeys
// attributeValues is an array in the form, thus the key will be attributeValues[0] etc
// remove these, and replace with 'attributeValues'
// style.code should post to style, not style.code, because it's a complex object
const complexKeys = ['attributeValues', 'style'] as const
export const sanitizeDirtyValueKeys = (dirtyKeys: string[]) => {
const complexChanges = complexKeys.filter((complexKey) =>
dirtyKeys.some((dirtyKey) => dirtyKey.startsWith(complexKey))
)

const dirtyKeysWithoutComplexKeys = dirtyKeys.filter(
(dirtyKey) =>
!complexChanges.some((complexKey) =>
dirtyKey.startsWith(complexKey)
)
)

return dirtyKeysWithoutComplexKeys.concat(complexChanges)
}

export function createJsonPatchOperations<
FormValues extends ModelWithAttributeValues
>({
dirtyFields,
originalValue,
values: unsanitizedValues,
}: FormatFormValuesArgs<FormValues>): JsonPatchOperation[] {
// Remove attribute values without a value
const values = {
...unsanitizedValues,
attributeValues: unsanitizedValues.attributeValues
.filter(({ value }) => !!value)
.map((value) => ({
value: value.value,
attribute: { id: value.attribute.id },
})),
}

const dirtyFieldsKeys = Object.keys(dirtyFields)
const adjustedDirtyFieldsKeys = sanitizeDirtyValueKeys(dirtyFieldsKeys)

return adjustedDirtyFieldsKeys.map((name) => ({
op: get(name, originalValue) ? 'replace' : 'add',
path: `/${name.replace(/[.]/g, '/')}`,
value: get(name, values) || '',
}))
}
1 change: 1 addition & 0 deletions src/lib/form/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { usePatchModel } from './usePatchModel'
export { composeAsyncValidators } from './composeAsyncValidators'
export type { FormFieldValidator } from './composeAsyncValidators'
export { required } from './validators'
Expand Down
Loading

0 comments on commit ef40a0b

Please sign in to comment.