From ef40a0bcb4713881122aad24c986170b5b407ce5 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Thu, 29 Feb 2024 23:02:01 +0100 Subject: [PATCH] refactor: dry up forms --- .../form/DefaultFormContents.module.css | 15 ++ src/components/form/DefaultFormContents.tsx | 58 +++++ .../form/attributes/CustomAttributes.tsx | 103 ++++++++ src/components/form/attributes/index.ts | 1 + .../attributes/useCustomAttributesQuery.ts | 44 ++++ .../{formFields => form/fields}/CodeField.tsx | 2 +- .../fields}/DefaultIdentifibleFIelds.tsx | 4 +- .../fields}/DescriptionField.tsx | 7 +- .../{formFields => form/fields}/NameField.tsx | 2 +- .../fields}/ShortNameField.tsx | 2 +- .../{formFields => form/fields}/index.ts | 0 src/components/form/index.ts | 3 + src/lib/form/createJsonPatchOperations.ts | 74 ++++++ src/lib/form/index.ts | 1 + src/lib/form/usePatchModel.ts | 33 +++ src/lib/models/attributes.ts | 28 ++ src/pages/dataElementGroups/Edit.tsx | 241 ++++++++---------- .../form/DataElementGroupFormFields.tsx | 2 +- .../form/DataElementFormFields.tsx | 2 +- 19 files changed, 471 insertions(+), 151 deletions(-) create mode 100644 src/components/form/DefaultFormContents.module.css create mode 100644 src/components/form/DefaultFormContents.tsx create mode 100644 src/components/form/attributes/CustomAttributes.tsx create mode 100644 src/components/form/attributes/index.ts create mode 100644 src/components/form/attributes/useCustomAttributesQuery.ts rename src/components/{formFields => form/fields}/CodeField.tsx (98%) rename src/components/{formFields => form/fields}/DefaultIdentifibleFIelds.tsx (85%) rename src/components/{formFields => form/fields}/DescriptionField.tsx (83%) rename src/components/{formFields => form/fields}/NameField.tsx (98%) rename src/components/{formFields => form/fields}/ShortNameField.tsx (98%) rename src/components/{formFields => form/fields}/index.ts (100%) create mode 100644 src/components/form/index.ts create mode 100644 src/lib/form/createJsonPatchOperations.ts create mode 100644 src/lib/form/usePatchModel.ts create mode 100644 src/lib/models/attributes.ts diff --git a/src/components/form/DefaultFormContents.module.css b/src/components/form/DefaultFormContents.module.css new file mode 100644 index 00000000..de17044a --- /dev/null +++ b/src/components/form/DefaultFormContents.module.css @@ -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); +} diff --git a/src/components/form/DefaultFormContents.tsx b/src/components/form/DefaultFormContents.tsx new file mode 100644 index 00000000..2f0bd6af --- /dev/null +++ b/src/components/form/DefaultFormContents.tsx @@ -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(null) + const navigate = useNavigate() + + const listPath = getSectionPath(section) + useEffect(() => { + if (submitError) { + formErrorRef.current?.scrollIntoView({ behavior: 'smooth' }) + } + }, [submitError]) + + return ( + <> +
{children}
+ {submitError && ( + +
+ + {submitError} + +
+
+ )} +
+ navigate(listPath)} + /> +
+ + ) +} diff --git a/src/components/form/attributes/CustomAttributes.tsx b/src/components/form/attributes/CustomAttributes.tsx new file mode 100644 index 00000000..c02ba003 --- /dev/null +++ b/src/components/form/attributes/CustomAttributes.tsx @@ -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('') }) + } + + return ( + + + + ) + } + + if (attribute.valueType === 'TEXT') { + return ( + + + + ) + } + + if (attribute.valueType === 'LONG_TEXT') { + return ( + + + + ) + } + + // @TODO: Verify that all value types have been covered! + throw new Error(`Implement value type "${attribute.valueType}"!`) +} + +export function CustomAttributes() { + const formState = useFormState({ + subscription: { initialValues: true }, + }) + + const customAttributes = formState.initialValues.attributeValues?.map( + (av) => av.attribute + ) + + return ( + <> + {customAttributes?.map((customAttribute, index) => { + return ( + + ) + })} + + ) +} diff --git a/src/components/form/attributes/index.ts b/src/components/form/attributes/index.ts new file mode 100644 index 00000000..df64bb9b --- /dev/null +++ b/src/components/form/attributes/index.ts @@ -0,0 +1 @@ +export { CustomAttributes } from './CustomAttributes' diff --git a/src/components/form/attributes/useCustomAttributesQuery.ts b/src/components/form/attributes/useCustomAttributesQuery.ts new file mode 100644 index 00000000..0c01b58b --- /dev/null +++ b/src/components/form/attributes/useCustomAttributesQuery.ts @@ -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) => ({ + 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( + CUSTOM_ATTRIBUTES_QUERY, + { variables: { modelName: schemaSection.name } } + ) + + return useMemo( + () => ({ + ...customAttributes, + data: customAttributes.data?.attributes.attributes || [], + }), + [customAttributes] + ) +} diff --git a/src/components/formFields/CodeField.tsx b/src/components/form/fields/CodeField.tsx similarity index 98% rename from src/components/formFields/CodeField.tsx rename to src/components/form/fields/CodeField.tsx index 379f1a2d..60093076 100644 --- a/src/components/formFields/CodeField.tsx +++ b/src/components/form/fields/CodeField.tsx @@ -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') diff --git a/src/components/formFields/DefaultIdentifibleFIelds.tsx b/src/components/form/fields/DefaultIdentifibleFIelds.tsx similarity index 85% rename from src/components/formFields/DefaultIdentifibleFIelds.tsx rename to src/components/form/fields/DefaultIdentifibleFIelds.tsx index b2f9c8f4..b1604800 100644 --- a/src/components/formFields/DefaultIdentifibleFIelds.tsx +++ b/src/components/form/fields/DefaultIdentifibleFIelds.tsx @@ -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' diff --git a/src/components/formFields/DescriptionField.tsx b/src/components/form/fields/DescriptionField.tsx similarity index 83% rename from src/components/formFields/DescriptionField.tsx rename to src/components/form/fields/DescriptionField.tsx index ea653a45..4cd08751 100644 --- a/src/components/formFields/DescriptionField.tsx +++ b/src/components/form/fields/DescriptionField.tsx @@ -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, @@ -13,9 +13,6 @@ 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 ( diff --git a/src/components/formFields/NameField.tsx b/src/components/form/fields/NameField.tsx similarity index 98% rename from src/components/formFields/NameField.tsx rename to src/components/form/fields/NameField.tsx index 28fdaee9..0b745d68 100644 --- a/src/components/formFields/NameField.tsx +++ b/src/components/form/fields/NameField.tsx @@ -9,7 +9,7 @@ import { useCheckMaxLengthFromSchema, useIsFieldValueUnique, SchemaSection, -} from '../../lib' +} from '../../../lib' function useValidator({ schemaSection }: { schemaSection: SchemaSection }) { const params = useParams() diff --git a/src/components/formFields/ShortNameField.tsx b/src/components/form/fields/ShortNameField.tsx similarity index 98% rename from src/components/formFields/ShortNameField.tsx rename to src/components/form/fields/ShortNameField.tsx index c7a896ef..fa257132 100644 --- a/src/components/formFields/ShortNameField.tsx +++ b/src/components/form/fields/ShortNameField.tsx @@ -9,7 +9,7 @@ import { required, useCheckMaxLengthFromSchema, useIsFieldValueUnique, -} from '../../lib' +} from '../../../lib' function useValidator({ schemaSection }: { schemaSection: SchemaSection }) { const params = useParams() diff --git a/src/components/formFields/index.ts b/src/components/form/fields/index.ts similarity index 100% rename from src/components/formFields/index.ts rename to src/components/form/fields/index.ts diff --git a/src/components/form/index.ts b/src/components/form/index.ts new file mode 100644 index 00000000..9e37a7b4 --- /dev/null +++ b/src/components/form/index.ts @@ -0,0 +1,3 @@ +export * from './fields' +export { DefaultFormContents } from './DefaultFormContents' +export * from './attributes' diff --git a/src/lib/form/createJsonPatchOperations.ts b/src/lib/form/createJsonPatchOperations.ts new file mode 100644 index 00000000..a4b5f09f --- /dev/null +++ b/src/lib/form/createJsonPatchOperations.ts @@ -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 { + 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): 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) || '', + })) +} diff --git a/src/lib/form/index.ts b/src/lib/form/index.ts index cc2fdd0c..917f9475 100644 --- a/src/lib/form/index.ts +++ b/src/lib/form/index.ts @@ -1,3 +1,4 @@ +export { usePatchModel } from './usePatchModel' export { composeAsyncValidators } from './composeAsyncValidators' export type { FormFieldValidator } from './composeAsyncValidators' export { required } from './validators' diff --git a/src/lib/form/usePatchModel.ts b/src/lib/form/usePatchModel.ts new file mode 100644 index 00000000..a7eadd83 --- /dev/null +++ b/src/lib/form/usePatchModel.ts @@ -0,0 +1,33 @@ +import { useDataEngine } from '@dhis2/app-runtime' +import { FORM_ERROR } from 'final-form' +import { useCallback, useState } from 'react' +import { JsonPatchOperation } from '../../types' + +const createPatchQuery = (id: string, resource: string) => { + return { + resource: resource, + id: id, + type: 'json-patch', + data: ({ operations }: Record) => operations, + } as const +} + +export const usePatchModel = (id: string, resource: string) => { + const dataEngine = useDataEngine() + const [query] = useState(() => createPatchQuery(id, resource)) + + const patch = useCallback( + async (operations: JsonPatchOperation[]) => { + try { + await dataEngine.mutate(query, { + variables: { operations }, + }) + } catch (error) { + return { [FORM_ERROR]: (error as Error | string).toString() } + } + }, + [dataEngine, query] + ) + + return patch +} diff --git a/src/lib/models/attributes.ts b/src/lib/models/attributes.ts new file mode 100644 index 00000000..f9d7f210 --- /dev/null +++ b/src/lib/models/attributes.ts @@ -0,0 +1,28 @@ +import type { + IdentifiableObject, + Attribute, + AttributeValue, +} from '../../types/generated' + +type ModelWithAttributes = IdentifiableObject & { + attributeValues: AttributeValue[] +} + +/* Gather all assigned attributes from both model.attributeValues and attributes. + Normally a metadata model will only return attributes with a value. + However, in a form we should still show assigned attributes, so the user can edit them */ +export const getAllAttributeValues = ( + model: ModelWithAttributes, + attributes: Attribute[] +): AttributeValue[] => { + const attributeValuesMap = new Map( + model.attributeValues.map((a) => [a.attribute.id, a]) + ) + + const attributeValues: AttributeValue[] = attributes.map((attribute) => { + const value = attributeValuesMap.get(attribute.id)?.value || '' + return { attribute, value } + }) + + return attributeValues +} diff --git a/src/pages/dataElementGroups/Edit.tsx b/src/pages/dataElementGroups/Edit.tsx index f506ac4c..bfe8a5f2 100644 --- a/src/pages/dataElementGroups/Edit.tsx +++ b/src/pages/dataElementGroups/Edit.tsx @@ -1,20 +1,21 @@ -import { useDataEngine, useDataQuery } from '@dhis2/app-runtime' +import { useDataQuery } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' -import { NoticeBox } from '@dhis2/ui' -import { FORM_ERROR, FormApi } from 'final-form' -import React, { useEffect, useRef } from 'react' +import { FormApi } from 'final-form' +import React from 'react' import { withTypes } from 'react-final-form' import { useNavigate, useParams } from 'react-router-dom' +import { Loader } from '../../components' +import { CustomAttributes, DefaultFormContents } from '../../components/form' import { - Loader, - StandardFormActions, - StandardFormSection, -} from '../../components' -import { SCHEMA_SECTIONS, getSectionPath, validate } from '../../lib' -import { JsonPatchOperation } from '../../types' -import { DataElementGroup } from '../../types/generated' -import { createJsonPatchOperations } from './edit/' -import classes from './Edit.module.css' + SCHEMA_SECTIONS, + getSectionPath, + usePatchModel, + validate, +} from '../../lib' +import { createJsonPatchOperations } from '../../lib/form/createJsonPatchOperations' +import { getAllAttributeValues } from '../../lib/models/attributes' +import { Attribute, DataElementGroup } from '../../types/generated' +import { useCustomAttributesQuery } from '../dataElements/fields' import { DataElementGroupFormFields, dataElementGroupSchema } from './form' import type { FormValues } from './form' @@ -26,168 +27,130 @@ type DataElementGroupQueryResponse = { dataElementGroup: DataElementGroup } -const listPath = `/${getSectionPath(SCHEMA_SECTIONS.dataElementGroup)}` +const section = SCHEMA_SECTIONS.dataElementGroup -function useDataElementGroupQuery(id: string) { - const DATA_ELEMENT_QUERY = { - dataElementGroup: { - resource: `dataElementGroups/${id}`, - params: { - fields: ['*', 'attributeValues[*]'], - }, +const query = { + dataElementGroup: { + resource: `dataElementGroups`, + id: ({ id }: Record) => id, + params: { + fields: ['*', 'attributeValues[*]'], }, - } + }, +} - return useDataQuery(DATA_ELEMENT_QUERY, { +function useDataElementGroupQuery(id: string) { + return useDataQuery(query, { variables: { id }, }) } -function usePatchDirtyFields() { - const dataEngine = useDataEngine() +function computeInitialValues({ + dataElementGroup, + customAttributes, +}: { + dataElementGroup: DataElementGroup + customAttributes: Attribute[] +}) { + if (!dataElementGroup) { + return {} + } - return async ({ - values, - dirtyFields, + // We want to have an array in the state with all attributes, not just the + // ones that are set + const attributeValues = getAllAttributeValues( dataElementGroup, - }: { - values: FormValues - dirtyFields: { [name: string]: boolean } - dataElementGroup: DataElementGroup - }) => { - const jsonPatchPayload = createJsonPatchOperations({ - values, - dirtyFields, - originalValue: dataElementGroup, - }) + customAttributes + ) - // We want the promise so we know when submitting is done. The promise - // returned by the mutation function of useDataMutation will never - // resolve - const ADD_NEW_DATA_ELEMENT_MUTATION = { - resource: 'dataElementGroups', - id: values.id, - type: 'json-patch', - data: ({ operations }: { operations: JsonPatchOperation[] }) => - operations, - } as const - - try { - await dataEngine.mutate(ADD_NEW_DATA_ELEMENT_MUTATION, { - variables: { operations: jsonPatchPayload }, - }) - } catch (e) { - return { [FORM_ERROR]: (e as Error | string).toString() } - } + return { + id: dataElementGroup.id, + name: dataElementGroup.name, + shortName: dataElementGroup.shortName, + code: dataElementGroup.code, + dataElements: dataElementGroup.dataElements, + attributeValues, } } export const Component = () => { - const navigate = useNavigate() const params = useParams() const dataElementGroupId = params.id as string const dataElementGroupQuery = useDataElementGroupQuery(dataElementGroupId) - const patchDirtyFields = usePatchDirtyFields() + const attributesQuery = useCustomAttributesQuery() - async function onSubmit(values: FormValues, form: FinalFormFormApi) { - const errors = await patchDirtyFields({ - values, - dirtyFields: form.getState().dirtyFields, - dataElementGroup: dataElementGroupQuery.data - ?.dataElementGroup as DataElementGroup, - }) - - if (errors) { - return errors - } - - navigate(listPath) - } - - const dataElementGroup = dataElementGroupQuery.data - ?.dataElementGroup as DataElementGroup - const initialValues = dataElementGroup - ? { - id: dataElementGroupId, - name: dataElementGroup.name, - shortName: dataElementGroup.shortName, - code: dataElementGroup.code, - description: dataElementGroup.description, - dataElements: dataElementGroup.dataElements || [], - } - : {} + const dataElementGroup = dataElementGroupQuery.data?.dataElementGroup return ( -
{ - return validate(dataElementGroupSchema, values) - }} - initialValues={initialValues} + - {({ handleSubmit, submitting, submitError }) => ( - - - - )} - + +
) } -function FormContents({ - submitError, - submitting, +function DataElementGroupForm({ + dataElementGroup, + attributes, }: { - submitting: boolean - submitError?: string + dataElementGroup: DataElementGroup + attributes: Attribute[] }) { - const formErrorRef = useRef(null) const navigate = useNavigate() + const patchDirtyFields = usePatchModel( + dataElementGroup.id, + section.namePlural + ) - useEffect(() => { - if (submitError) { - formErrorRef.current?.scrollIntoView({ behavior: 'smooth' }) + async function onSubmit(values: FormValues, form: FinalFormFormApi) { + const jsonPatchOperations = createJsonPatchOperations({ + values, + dirtyFields: form.getState().dirtyFields, + originalValue: dataElementGroup, + }) + const errors = await patchDirtyFields(jsonPatchOperations) + + if (errors) { + return errors } - }, [submitError]) + + navigate(getSectionPath(section)) + } return ( - <> - {submitError && ( - -
- - {submitError} - -
-
+
{ + return validate(dataElementGroupSchema, values) + }} + initialValues={computeInitialValues({ + dataElementGroup, + customAttributes: attributes, + })} + > + {({ handleSubmit, submitting, submitError }) => ( + + + + + +
)} - -
- -
- -
- navigate(listPath)} - /> -
- + ) } diff --git a/src/pages/dataElementGroups/form/DataElementGroupFormFields.tsx b/src/pages/dataElementGroups/form/DataElementGroupFormFields.tsx index 73de35d8..bf40e8e8 100644 --- a/src/pages/dataElementGroups/form/DataElementGroupFormFields.tsx +++ b/src/pages/dataElementGroups/form/DataElementGroupFormFields.tsx @@ -9,7 +9,7 @@ import { import { DefaultIdentifiableFields, DescriptionField, -} from '../../../components/formFields' +} from '../../../components/form' import { SCHEMA_SECTIONS } from '../../../lib' import { DataElementsField } from '../fields' diff --git a/src/pages/dataElements/form/DataElementFormFields.tsx b/src/pages/dataElements/form/DataElementFormFields.tsx index 1b16d631..748d13d1 100644 --- a/src/pages/dataElements/form/DataElementFormFields.tsx +++ b/src/pages/dataElements/form/DataElementFormFields.tsx @@ -11,7 +11,7 @@ import { DescriptionField, NameField, ShortNameField, -} from '../../../components/formFields' +} from '../../../components/form' import { SCHEMA_SECTIONS } from '../../../lib' import { AggregationLevelsField,