Skip to content

Commit

Permalink
fix(de form fields): validate only field that change, only on blur
Browse files Browse the repository at this point in the history
  • Loading branch information
Mohammer5 committed Oct 30, 2023
1 parent bb839b2 commit 903a9a9
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 48 deletions.
8 changes: 4 additions & 4 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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: 2023-10-30T09:38:05.168Z\n"
"PO-Revision-Date: 2023-10-30T09:38:05.168Z\n"
"POT-Creation-Date: 2023-10-30T14:04:02.910Z\n"
"PO-Revision-Date: 2023-10-30T14:04:02.910Z\n"

msgid "schemas"
msgstr "schemas"
Expand Down Expand Up @@ -686,8 +686,8 @@ msgstr "Option set comment"
msgid "Choose a set of predefined comment for data entry"
msgstr "Choose a set of predefined comment for data entry"

msgid "This {{field}} already exists, please choose antoher one"
msgstr "This {{field}} already exists, please choose antoher one"
msgid "This field requires a unique value, please choose another one"
msgstr "This field requires a unique value, please choose another one"

msgid "Metadata management"
msgstr "Metadata management"
Expand Down
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './schemas'
export { useLoadApp } from './useLoadApp'
export type { ModelSchemas, Schema } from './useLoadApp'
export * from './errors'
export { memoize } from './memoize'
export * from './user'
export * from './sections'
export * from './useDebounce'
Expand Down
33 changes: 33 additions & 0 deletions src/lib/memoize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

const doArgsMatch = (prev: any[], next: any[]) => {
if (!prev && !next) {
return true
}

if (prev?.length !== next?.length) {
return false
}

return prev.every((value, index) => value === next[index])
}

// eslint-disable-next-line @typescript-eslint/ban-types
export function memoize(fn: Function) {
let called = false
let args: any
let value: any

return (...nextArgs: any[]) => {
const argsMatch = called && doArgsMatch(args, nextArgs)
called = true

if (argsMatch) {
return value
}

args = nextArgs
value = fn(...nextArgs)
return value
}
}
1 change: 1 addition & 0 deletions src/pages/dataElements/Edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export const Component = () => {

return (
<Form
validateOnBlur
onSubmit={onSubmit}
validate={validate}
initialValues={initialValues}
Expand Down
1 change: 1 addition & 0 deletions src/pages/dataElements/New.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export const Component = () => {

return (
<Form
validateOnBlur
onSubmit={onSubmit}
initialValues={initialValues}
validate={validate}
Expand Down
50 changes: 33 additions & 17 deletions src/pages/dataElements/form/fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,12 @@ import { EditableFieldWrapper } from './EditableFieldWrapper'
import { useIsFieldValueUnique } from './useIsFieldValueUnique'

export function NameField() {
const {
loading,
fetching,
refetch: checkIsValueTaken,
} = useIsFieldValueUnique('name')
const { meta } = useField('name')
const checkIsValueTaken = useIsFieldValueUnique('name')

return (
<FieldRFF
loading={loading || fetching}
loading={meta.validating}
component={InputFieldFF}
dataTest="dataelementsformfields-name"
required
Expand All @@ -55,15 +52,12 @@ export function NameField() {
}

export function ShortNameField() {
const {
loading,
fetching,
refetch: checkIsValueTaken,
} = useIsFieldValueUnique('shortName')
const { meta } = useField('shortName')
const checkIsValueTaken = useIsFieldValueUnique('shortName')

return (
<FieldRFF
loading={loading || fetching}
loading={meta.validating}
component={InputFieldFF}
dataTest="dataelementsformfields-shortname"
required
Expand All @@ -89,6 +83,7 @@ export function CodeField() {
inputWidth="150px"
name="code"
label={i18n.t('Code')}
validateFields={[]}
/>
)
}
Expand All @@ -104,6 +99,7 @@ export function DescriptionField() {
helpText={i18n.t(
"Explain the purpose of this data element and how it's measured."
)}
validateFields={[]}
/>
)
}
Expand All @@ -117,13 +113,18 @@ export function UrlField() {
name="url"
label={i18n.t('Url')}
helpText={i18n.t('A web link that provides extra information')}
validateFields={[]}
/>
)
}

export function ColorAndIconField() {
const { input: colorInput } = useField('style.color')
const { input: iconInput } = useField('style.icon')
const { input: colorInput } = useField('style.color', {
validateFields: [],
})
const { input: iconInput } = useField('style.icon', {
validateFields: [],
})

return (
<Field
Expand Down Expand Up @@ -159,6 +160,7 @@ export function FieldMaskField() {
'Use a pattern to limit what information can be entered.'
)}
placeholder={i18n.t('e.g. 999-000-0000')}
validateFields={[]}
/>
)
}
Expand All @@ -174,6 +176,7 @@ export function FormNameField() {
helpText={i18n.t(
'An alternative name used in section or automatic data entry forms.'
)}
validateFields={[]}
/>
)
}
Expand All @@ -186,6 +189,7 @@ export function ZeroIsSignificantField() {
name="zeroIsSignificant"
label={i18n.t('Store zero data values')}
type="checkbox"
validateFields={[]}
/>
)
}
Expand All @@ -197,11 +201,13 @@ export function DomainField() {
type: 'radio',
value: 'AGGREGATE',
validate,
validateFields: [],
})
const trackerInput = useField(name, {
type: 'radio',
value: 'TRACKER',
validate,
validateFields: [],
})
const touched = aggregateInput.meta.touched || trackerInput.meta.touched
const error = aggregateInput.meta.error || trackerInput.meta.error
Expand Down Expand Up @@ -256,6 +262,7 @@ export function LegendSetField() {
format: (legendSets: { id: string }[]) =>
legendSets?.map((legendSet) => legendSet.id),
parse: (ids: string[]) => ids.map((id) => ({ id })),
validateFields: [],
})

const newLegendSetLink = useHref('/legendSets/new')
Expand Down Expand Up @@ -327,6 +334,7 @@ export function ValueTypeField() {
})}
helpText={i18n.t('The type of data that will be recorded.')}
options={options || []}
validateFields={[]}
/>
)
}
Expand Down Expand Up @@ -354,13 +362,16 @@ export function AggregationTypeField() {
'The default way to aggregate this data element in analytics.'
)}
options={options || []}
validateFields={[]}
/>
)
}

export function CategoryComboField() {
const newCategoryComboLink = useHref('/categoryCombos/new')
const { input, meta } = useField('categoryCombo.id')
const { input, meta } = useField('categoryCombo.id', {
validateFields: [],
})
const categoryComboHandle = useRef({
refetch: () => {
throw new Error('Not initialized')
Expand Down Expand Up @@ -404,7 +415,9 @@ export function CategoryComboField() {

export function OptionSetField() {
const newOptionSetLink = useHref('/optionSets/new')
const { input, meta } = useField('optionSet.id')
const { input, meta } = useField('optionSet.id', {
validateFields: [],
})
const optionSetHandle = useRef({
refetch: () => {
throw new Error('Not initialized')
Expand Down Expand Up @@ -445,7 +458,9 @@ export function OptionSetField() {

export function OptionSetCommentField() {
const newOptionSetLink = useHref('/optionSets/new')
const { input, meta } = useField('commentOptionSet.id')
const { input, meta } = useField('commentOptionSet.id', {
validateFields: [],
})
const optionSetHandle = useRef({
refetch: () => {
throw new Error('Not initialized')
Expand Down Expand Up @@ -490,6 +505,7 @@ export function AggregationLevelsField() {
multiple: true,
format: (levels: number[]) => levels.map((level) => level.toString()),
parse: (levels: string[]) => levels.map((level) => parseInt(level, 10)),
validateFields: [],
})
const aggregationLevelHandle = useRef({
refetch: () => {
Expand Down
48 changes: 21 additions & 27 deletions src/pages/dataElements/form/useIsFieldValueUnique.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useDataQuery } from '@dhis2/app-runtime'
import { useDataEngine } from '@dhis2/app-runtime'
import i18n from '@dhis2/d2-i18n'
import { useMemo } from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { memoize } from '../../../lib'
import { Pager } from '../../../types/generated'

const HAS_FIELD_VALUE_QUERY = {
Expand All @@ -21,35 +23,27 @@ interface QueryResponse {
}

export function useIsFieldValueUnique(field: string) {
const queryResult = useDataQuery<QueryResponse>(HAS_FIELD_VALUE_QUERY, {
lazy: true,
variables: { field, value: '' },
})
const engine = useDataEngine()

return useMemo(
() => ({
...queryResult,
refetch: (value: string) => {
const validate = useMemo(
() =>
memoize(async (value: string) => {
if (!value) {
return
return undefined
}

return queryResult.refetch({ field, value }).then(
// We don't have access to app-runtime's internal type `JsonMap`,
// so we have to ignore the type error
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(data: QueryResponse) => {
if (data.dataElements.pager.total) {
return i18n.t(
'This {{field}} already exists, please choose antoher one',
{ field }
)
}
}
)
},
}),
[field, queryResult]
const data = (await engine.query(HAS_FIELD_VALUE_QUERY, {
variables: { field, value },
})) as unknown as QueryResponse

if (data.dataElements.pager.total > 0) {
return i18n.t(
'This field requires a unique value, please choose another one'
)
}
}),
[field, engine]
)

return useDebouncedCallback(validate, 200, { leading: true })
}

0 comments on commit 903a9a9

Please sign in to comment.