diff --git a/i18n/en.pot b/i18n/en.pot index 7f4c9b41..20fe0a5b 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: 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" @@ -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" diff --git a/src/lib/index.ts b/src/lib/index.ts index 9d2ace74..1ac729c4 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -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' diff --git a/src/lib/memoize.ts b/src/lib/memoize.ts new file mode 100644 index 00000000..b6f1f352 --- /dev/null +++ b/src/lib/memoize.ts @@ -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 + } +} diff --git a/src/pages/dataElements/Edit.tsx b/src/pages/dataElements/Edit.tsx index 42600feb..f40508a8 100644 --- a/src/pages/dataElements/Edit.tsx +++ b/src/pages/dataElements/Edit.tsx @@ -151,6 +151,7 @@ export const Component = () => { return (
{ return ( ) } @@ -104,6 +99,7 @@ export function DescriptionField() { helpText={i18n.t( "Explain the purpose of this data element and how it's measured." )} + validateFields={[]} /> ) } @@ -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 ( ) } @@ -174,6 +176,7 @@ export function FormNameField() { helpText={i18n.t( 'An alternative name used in section or automatic data entry forms.' )} + validateFields={[]} /> ) } @@ -186,6 +189,7 @@ export function ZeroIsSignificantField() { name="zeroIsSignificant" label={i18n.t('Store zero data values')} type="checkbox" + validateFields={[]} /> ) } @@ -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 @@ -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') @@ -327,6 +334,7 @@ export function ValueTypeField() { })} helpText={i18n.t('The type of data that will be recorded.')} options={options || []} + validateFields={[]} /> ) } @@ -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') @@ -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') @@ -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') @@ -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: () => { diff --git a/src/pages/dataElements/form/useIsFieldValueUnique.ts b/src/pages/dataElements/form/useIsFieldValueUnique.ts index 53554d65..dfa361d5 100644 --- a/src/pages/dataElements/form/useIsFieldValueUnique.ts +++ b/src/pages/dataElements/form/useIsFieldValueUnique.ts @@ -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 = { @@ -21,35 +23,27 @@ interface QueryResponse { } export function useIsFieldValueUnique(field: string) { - const queryResult = useDataQuery(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 }) }