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 (
)
}
@@ -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 })
}