From ebb78c1ce5818c3d78743b3d78067f98975ee5d6 Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Mon, 22 Jan 2024 17:29:56 +0800 Subject: [PATCH] feat: add data element group New and Edit views --- i18n/en.pot | 10 ++- src/pages/dataElementGroups/Edit.tsx | 1 - .../edit/createJsonPatchOperations.spec.ts | 49 ++++++++++++++ .../edit/createJsonPatchOperations.ts | 20 ++++++ src/pages/dataElementGroups/edit/index.ts | 1 + .../dataElementGroups/fields/CodeField.tsx | 25 ++++++++ .../fields/DescriptionField.tsx | 28 ++++++++ .../dataElementGroups/fields/NameField.tsx | 64 +++++++++++++++++++ .../fields/ShortNameField.tsx | 62 ++++++++++++++++++ src/pages/dataElementGroups/form/validate.ts | 27 ++++++++ 10 files changed, 284 insertions(+), 3 deletions(-) create mode 100644 src/pages/dataElementGroups/edit/createJsonPatchOperations.spec.ts create mode 100644 src/pages/dataElementGroups/edit/createJsonPatchOperations.ts create mode 100644 src/pages/dataElementGroups/edit/index.ts create mode 100644 src/pages/dataElementGroups/fields/CodeField.tsx create mode 100644 src/pages/dataElementGroups/fields/DescriptionField.tsx create mode 100644 src/pages/dataElementGroups/fields/NameField.tsx create mode 100644 src/pages/dataElementGroups/fields/ShortNameField.tsx create mode 100644 src/pages/dataElementGroups/form/validate.ts diff --git a/i18n/en.pot b/i18n/en.pot index e2a7c817..d7f19c0c 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-03-05T09:21:50.129Z\n" -"PO-Revision-Date: 2024-03-05T09:21:50.129Z\n" +"POT-Creation-Date: 2024-03-05T09:22:03.031Z\n" +"PO-Revision-Date: 2024-03-05T09:22:03.031Z\n" msgid "schemas" msgstr "schemas" @@ -672,6 +672,12 @@ msgstr "Refresh list" msgid "Add new" msgstr "Add new" +msgid "Explain the purpose of this data element and how it's measured." +msgstr "Explain the purpose of this data element and how it's measured." + +msgid "A data element name should be concise and easy to recognize." +msgstr "A data element name should be concise and easy to recognize." + msgid "Basic information" msgstr "Basic information" diff --git a/src/pages/dataElementGroups/Edit.tsx b/src/pages/dataElementGroups/Edit.tsx index 2c94f6b3..5cc5a026 100644 --- a/src/pages/dataElementGroups/Edit.tsx +++ b/src/pages/dataElementGroups/Edit.tsx @@ -6,7 +6,6 @@ import { withTypes } from 'react-final-form' import { useNavigate, useParams } from 'react-router-dom' import { Loader } from '../../components' import { - CustomAttributes, DefaultFormContents, useCustomAttributesQuery, } from '../../components/form' diff --git a/src/pages/dataElementGroups/edit/createJsonPatchOperations.spec.ts b/src/pages/dataElementGroups/edit/createJsonPatchOperations.spec.ts new file mode 100644 index 00000000..422cf33c --- /dev/null +++ b/src/pages/dataElementGroups/edit/createJsonPatchOperations.spec.ts @@ -0,0 +1,49 @@ +import { createJsonPatchOperations } from './createJsonPatchOperations' + +describe('createJsonPatchOperations', () => { + describe('createJsonPatchOperations', () => { + it('should return an empty array if no dirty fields', () => { + const actual = createJsonPatchOperations({ + dirtyFields: {}, + originalValue: { id: 'foo' }, + values: {}, + }) + expect(actual).toEqual([]) + }) + + it('should return a json-patch payload for a single field', () => { + const actual = createJsonPatchOperations({ + dirtyFields: { name: true }, + originalValue: { + id: 'foo', + name: 'bar', + }, + values: { name: 'baz' }, + }) + const expected = [ + { + op: 'replace', + path: '/name', + value: 'baz', + }, + ] + expect(actual).toEqual(expected) + }) + + it('should return a json-patch payload with add if value does not exist in originalValue', () => { + const actual = createJsonPatchOperations({ + dirtyFields: { name: true }, + originalValue: { id: 'foo' }, + values: { name: 'baz' }, + }) + const expected = [ + { + op: 'add', + path: '/name', + value: 'baz', + }, + ] + expect(actual).toEqual(expected) + }) + }) +}) diff --git a/src/pages/dataElementGroups/edit/createJsonPatchOperations.ts b/src/pages/dataElementGroups/edit/createJsonPatchOperations.ts new file mode 100644 index 00000000..bfdb1dea --- /dev/null +++ b/src/pages/dataElementGroups/edit/createJsonPatchOperations.ts @@ -0,0 +1,20 @@ +import get from 'lodash/fp/get' +import { JsonPatchOperation } from '../../../types' + +interface FormatFormValuesArgs { + originalValue: unknown + dirtyFields: { [key in keyof FormValues]?: boolean } + values: FormValues +} + +export function createJsonPatchOperations({ + dirtyFields, + originalValue, + values, +}: FormatFormValuesArgs): JsonPatchOperation[] { + return Object.keys(dirtyFields).map((name) => ({ + op: get(name, originalValue) ? 'replace' : 'add', + path: `/${name.replace(/[.]/g, '/')}`, + value: get(name, values) || '', + })) +} diff --git a/src/pages/dataElementGroups/edit/index.ts b/src/pages/dataElementGroups/edit/index.ts new file mode 100644 index 00000000..0069ca37 --- /dev/null +++ b/src/pages/dataElementGroups/edit/index.ts @@ -0,0 +1 @@ +export { createJsonPatchOperations } from './createJsonPatchOperations' diff --git a/src/pages/dataElementGroups/fields/CodeField.tsx b/src/pages/dataElementGroups/fields/CodeField.tsx new file mode 100644 index 00000000..fedc4eb8 --- /dev/null +++ b/src/pages/dataElementGroups/fields/CodeField.tsx @@ -0,0 +1,25 @@ +import i18n from '@dhis2/d2-i18n' +import { InputFieldFF } from '@dhis2/ui' +import React from 'react' +import { Field as FieldRFF } from 'react-final-form' +import { useCheckMaxLengthFromSchema } from '../../../lib' +import type { SchemaName } from '../../../types' + +export function CodeField() { + const validate = useCheckMaxLengthFromSchema( + 'dataElement' as SchemaName, + 'code' + ) + + return ( + + ) +} diff --git a/src/pages/dataElementGroups/fields/DescriptionField.tsx b/src/pages/dataElementGroups/fields/DescriptionField.tsx new file mode 100644 index 00000000..c11298b8 --- /dev/null +++ b/src/pages/dataElementGroups/fields/DescriptionField.tsx @@ -0,0 +1,28 @@ +import i18n from '@dhis2/d2-i18n' +import { TextAreaFieldFF } from '@dhis2/ui' +import React from 'react' +import { Field as FieldRFF } from 'react-final-form' +import { useCheckMaxLengthFromSchema } from '../../../lib' +import type { SchemaName } from '../../../types' + +export function DescriptionField() { + const validate = useCheckMaxLengthFromSchema( + 'dataElement' as SchemaName, + 'formName' + ) + + return ( + + ) +} diff --git a/src/pages/dataElementGroups/fields/NameField.tsx b/src/pages/dataElementGroups/fields/NameField.tsx new file mode 100644 index 00000000..b6c38724 --- /dev/null +++ b/src/pages/dataElementGroups/fields/NameField.tsx @@ -0,0 +1,64 @@ +import i18n from '@dhis2/d2-i18n' +import { InputFieldFF } from '@dhis2/ui' +import React, { useMemo } from 'react' +import { Field as FieldRFF, useField } from 'react-final-form' +import { useParams } from 'react-router-dom' +import { + composeAsyncValidators, + required, + useCheckMaxLengthFromSchema, + useIsFieldValueUnique, +} from '../../../lib' +import { SchemaName } from '../../../types' +import type { FormValues } from '../form' + +function useValidator() { + const params = useParams() + const dataElementId = params.id as string + const checkIsValueTaken = useIsFieldValueUnique({ + model: 'dataElements', + field: 'name', + id: dataElementId, + }) + + const checkMaxLength = useCheckMaxLengthFromSchema( + SchemaName.dataElement, + 'name' + ) + + return useMemo( + () => + composeAsyncValidators([ + checkIsValueTaken, + checkMaxLength, + required, + ]), + [checkIsValueTaken, checkMaxLength] + ) +} + +export function NameField() { + const validator = useValidator() + const { meta } = useField('name', { + subscription: { validating: true }, + }) + + return ( + + loading={meta.validating} + component={InputFieldFF} + dataTest="dataelementsformfields-name" + required + inputWidth="400px" + label={i18n.t('{{fieldLabel}} (required)', { + fieldLabel: i18n.t('Name'), + })} + name="name" + helpText={i18n.t( + 'A data element name should be concise and easy to recognize.' + )} + validate={(name?: string) => validator(name)} + validateFields={[]} + /> + ) +} diff --git a/src/pages/dataElementGroups/fields/ShortNameField.tsx b/src/pages/dataElementGroups/fields/ShortNameField.tsx new file mode 100644 index 00000000..7e1845d5 --- /dev/null +++ b/src/pages/dataElementGroups/fields/ShortNameField.tsx @@ -0,0 +1,62 @@ +import i18n from '@dhis2/d2-i18n' +import { InputFieldFF } from '@dhis2/ui' +import React, { useMemo } from 'react' +import { Field as FieldRFF, useField } from 'react-final-form' +import { useParams } from 'react-router-dom' +import { + composeAsyncValidators, + required, + useCheckMaxLengthFromSchema, + useIsFieldValueUnique, +} from '../../../lib' +import type { SchemaName } from '../../../types' +import type { FormValues } from '../form' + +function useValidator() { + const params = useParams() + const dataElementId = params.id as string + const checkIsValueTaken = useIsFieldValueUnique({ + model: 'dataElements', + field: 'name', + id: dataElementId, + }) + + const checkMaxLength = useCheckMaxLengthFromSchema( + 'dataElement' as SchemaName, + 'formName' + ) + + return useMemo( + () => + composeAsyncValidators([ + checkIsValueTaken, + checkMaxLength, + required, + ]), + [checkIsValueTaken, checkMaxLength] + ) +} + +export function ShortNameField() { + const validator = useValidator() + const { meta } = useField('shortName', { + subscription: { validating: true }, + }) + + return ( + + loading={meta.validating} + component={InputFieldFF} + dataTest="dataelementsformfields-shortname" + required + inputWidth="400px" + label={i18n.t('{{fieldLabel}} (required)', { + fieldLabel: i18n.t('Short name'), + })} + name="shortName" + helpText={i18n.t('Often used in reports where space is limited')} + validate={(name?: string) => validator(name)} + validateFields={[]} + /> + ) +} diff --git a/src/pages/dataElementGroups/form/validate.ts b/src/pages/dataElementGroups/form/validate.ts new file mode 100644 index 00000000..b9b9fcd5 --- /dev/null +++ b/src/pages/dataElementGroups/form/validate.ts @@ -0,0 +1,27 @@ +import { setIn } from 'final-form' +import { dataElementGroupSchema } from './dataElementGroupSchema' +import type { FormValues } from './types' + +// @TODO: Figure out if there's a utility for this? I couldn't find one +function segmentsToPath(segments: Array) { + return segments.reduce((path, segment) => { + return typeof segment === 'number' + ? `${path}[${segment}]` + : `${path}.${segment}` + }) as string +} + +export function validate(values: FormValues) { + const zodResult = dataElementGroupSchema.safeParse(values) + + if (zodResult.success !== false) { + return undefined + } + + const allFormErrors = zodResult.error.issues.reduce((formErrors, error) => { + const errorPath = segmentsToPath(error.path) + return setIn(formErrors, errorPath, error.message) + }, {}) + + return allFormErrors +}