diff --git a/cypress/support/form/helpers/nameToDataTest.js b/cypress/support/form/helpers/nameToDataTest.js
index 2dc649353..0c280535b 100644
--- a/cypress/support/form/helpers/nameToDataTest.js
+++ b/cypress/support/form/helpers/nameToDataTest.js
@@ -21,6 +21,21 @@ export const nameToDataTest = (name) => {
case 'idScheme':
return 'input-id-scheme'
+ case 'categoryIdScheme':
+ return 'input-category-id-scheme'
+
+ case 'categoryOptionIdScheme':
+ return 'input-category-option-id-scheme'
+
+ case 'categoryOptionComboIdScheme':
+ return 'input-category-option-combo-id-scheme'
+
+ case 'dataSetIdScheme':
+ return 'input-data-set-id-scheme'
+
+ case 'attributeOptionComboIdScheme':
+ return 'input-attribute-option-combo-id-scheme'
+
case 'program':
return 'input-program-picker'
diff --git a/i18n/en.pot b/i18n/en.pot
index a80af4321..956367327 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: 2026-09-02T14:17:13.038Z\n"
-"PO-Revision-Date: 2026-09-02T14:17:13.038Z\n"
+"POT-Creation-Date: 2026-09-17T11:35:01.753Z\n"
+"PO-Revision-Date: 2026-09-17T11:35:01.754Z\n"
msgid "Something went wrong when loading the current user!"
msgstr "Something went wrong when loading the current user!"
@@ -29,18 +29,23 @@ msgstr "Invalid duration"
msgid "Format"
msgstr "Format"
-msgid "Something went wrong when loading the additional data element ID schemes"
-msgstr "Something went wrong when loading the additional data element ID schemes"
+msgid "Uid"
+msgstr "Uid"
+
+msgid "Code"
+msgstr "Code"
+
+msgid "Name"
+msgstr "Name"
msgid "Something went wrong when loading the additional ID schemes"
msgstr "Something went wrong when loading the additional ID schemes"
-msgid ""
-"Something went wrong when loading the additional organisation unit ID "
-"schemes"
-msgstr ""
-"Something went wrong when loading the additional organisation unit ID "
-"schemes"
+msgid "(Default)"
+msgstr "(Default)"
+
+msgid "(Undefined)"
+msgstr "(Undefined)"
msgid "Please select a file"
msgstr "Please select a file"
@@ -139,15 +144,6 @@ msgstr "Uncompressed"
msgid "Compression mode"
msgstr "Compression mode"
-msgid "Uid"
-msgstr "Uid"
-
-msgid "Code"
-msgstr "Code"
-
-msgid "Name"
-msgstr "Name"
-
msgid "Data element ID scheme"
msgstr "Data element ID scheme"
@@ -819,6 +815,21 @@ msgstr ""
msgid "Date range to export data for"
msgstr "Date range to export data for"
+msgid "Category ID scheme"
+msgstr "Category ID scheme"
+
+msgid "Category option ID scheme"
+msgstr "Category option ID scheme"
+
+msgid "Category option combo ID scheme"
+msgstr "Category option combo ID scheme"
+
+msgid "Data set ID scheme"
+msgstr "Data set ID scheme"
+
+msgid "Attribute option combo ID scheme"
+msgstr "Attribute option combo ID scheme"
+
msgid "Export data"
msgstr "Export data"
diff --git a/src/components/ElementSchemes/DataElementIdScheme.jsx b/src/components/ElementSchemes/DataElementIdScheme.jsx
deleted file mode 100644
index 51b7c13f6..000000000
--- a/src/components/ElementSchemes/DataElementIdScheme.jsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { useConfig } from '@dhis2/app-runtime'
-import i18n from '@dhis2/d2-i18n'
-import { SingleSelectFieldFF } from '@dhis2/ui'
-import PropTypes from 'prop-types'
-import React, { useEffect, useState } from 'react'
-import { fetchAttributes } from '../../utils/helper.js'
-import { optionsPropType } from '../../utils/options.js'
-import { StyledField } from '../index.js'
-
-const DataElementIdScheme = ({
- name,
- label,
- dataElementIdSchemeOptions,
- dataTest,
-}) => {
- const { baseUrl } = useConfig()
- const [loading, setLoading] = useState(true)
- const [schemes, setSchemes] = useState([])
- const [error, setError] = useState(undefined)
-
- useEffect(() => {
- fetchAttributes(`${baseUrl}/api/`, 'dataElementAttribute')
- .then((attributes) => setSchemes(attributes))
- .catch((error) => setError(error))
- .finally(() => setLoading(false))
- }, [])
-
- const validationText =
- error &&
- `${i18n.t(
- 'Something went wrong when loading the additional data element ID schemes'
- )} : ${error.message}`
-
- const options = [...dataElementIdSchemeOptions, ...schemes]
- return (
-
- )
-}
-
-DataElementIdScheme.propTypes = {
- dataElementIdSchemeOptions: optionsPropType.isRequired,
- dataTest: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
- name: PropTypes.string.isRequired,
-}
-
-export { DataElementIdScheme }
diff --git a/src/components/ElementSchemes/EventIdScheme.jsx b/src/components/ElementSchemes/EventIdScheme.jsx
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/components/ElementSchemes/IdScheme.jsx b/src/components/ElementSchemes/IdScheme.jsx
deleted file mode 100644
index 390013c69..000000000
--- a/src/components/ElementSchemes/IdScheme.jsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import { useConfig } from '@dhis2/app-runtime'
-import i18n from '@dhis2/d2-i18n'
-import { SingleSelectFieldFF } from '@dhis2/ui'
-import PropTypes from 'prop-types'
-import React, { useState, useEffect } from 'react'
-import { fetchAttributes } from '../../utils/helper.js'
-import { optionsPropType } from '../../utils/options.js'
-import { StyledField } from '../index.js'
-
-const attributeFoundIn = (attribute, collection) =>
- !!collection.find(({ value }) => value === attribute.value)
-
-const IdScheme = ({ name, label, idSchemeOptions, dataTest }) => {
- const { baseUrl } = useConfig()
- const [loading, setLoading] = useState(true)
- const [schemes, setSchemes] = useState([])
- const [error, setError] = useState(undefined)
-
- useEffect(() => {
- const f = async () => {
- let err
-
- const dataElementAttributes = await fetchAttributes(
- `${baseUrl}/api/`,
- 'dataElementAttribute'
- ).catch((error) => (err = error))
- const organisationUnitAttributes = await fetchAttributes(
- `${baseUrl}/api/`,
- 'organisationUnitAttribute'
- ).catch((error) => (err = error))
-
- setError(err)
-
- if (!err) {
- const sharedAttributes = dataElementAttributes.reduce(
- (shared, attribute) => {
- const foundInOrgUnits = attributeFoundIn(
- attribute,
- organisationUnitAttributes
- )
- return foundInOrgUnits ? [...shared, attribute] : shared
- },
- []
- )
-
- setSchemes(sharedAttributes)
- }
-
- setLoading(false)
- }
- f()
- }, [])
-
- const validationText =
- error &&
- `${i18n.t(
- 'Something went wrong when loading the additional ID schemes'
- )} : ${error.message}`
-
- const options = [...idSchemeOptions, ...schemes]
- return (
-
- )
-}
-
-IdScheme.propTypes = {
- dataTest: PropTypes.string.isRequired,
- idSchemeOptions: optionsPropType.isRequired,
- label: PropTypes.string.isRequired,
- name: PropTypes.string.isRequired,
-}
-
-export { IdScheme }
diff --git a/src/components/ElementSchemes/IdSchemeSelect.jsx b/src/components/ElementSchemes/IdSchemeSelect.jsx
new file mode 100644
index 000000000..3b337a95c
--- /dev/null
+++ b/src/components/ElementSchemes/IdSchemeSelect.jsx
@@ -0,0 +1,116 @@
+import { useConfig } from '@dhis2/app-runtime'
+import i18n from '@dhis2/d2-i18n'
+import { SingleSelectFieldFF } from '@dhis2/ui'
+import PropTypes from 'prop-types'
+import React, { useEffect, useState } from 'react'
+import { fetchAttributes } from '../../utils/helper.js'
+import { StyledField } from '../index.js'
+
+const ID_SCHEME_OPTIONS = (emptyOptionLabel) => [
+ { value: '', label: emptyOptionLabel },
+ { value: 'UID', label: i18n.t('Uid') },
+ { value: 'CODE', label: i18n.t('Code') },
+ { value: 'NAME', label: i18n.t('Name') },
+]
+
+const NO_ATTRIBUTE_TYPES = []
+
+const intersectByValue = (a, b) =>
+ a.filter((item) => b.some((other) => other.value === item.value))
+
+const IdSchemeSelect = ({
+ name,
+ label,
+ dataTest,
+ attributeTypes = NO_ATTRIBUTE_TYPES,
+ isFallback = false,
+}) => {
+ const { baseUrl } = useConfig()
+ const [loading, setLoading] = useState(attributeTypes.length > 0)
+ const [attributeOptions, setAttributeOptions] = useState([])
+ const [error, setError] = useState(undefined)
+
+ useEffect(() => {
+ if (attributeTypes.length === 0) {
+ return undefined
+ }
+
+ let cancelled = false
+
+ const loadAttributeSchemes = async () => {
+ let err
+
+ const schemesByType = await Promise.all(
+ attributeTypes.map((type) =>
+ fetchAttributes(`${baseUrl}/api/`, type).catch((e) => {
+ err = e
+ return []
+ })
+ )
+ )
+
+ if (cancelled) {
+ return
+ }
+
+ setError(err)
+ if (!err) {
+ const [first, ...rest] = schemesByType
+ setAttributeOptions(
+ rest.reduce(
+ (shared, schemes) => intersectByValue(shared, schemes),
+ first
+ )
+ )
+ }
+ setLoading(false)
+ }
+
+ loadAttributeSchemes()
+
+ return () => {
+ cancelled = true
+ }
+ }, [baseUrl, attributeTypes])
+
+ const validationText =
+ error &&
+ `${i18n.t(
+ 'Something went wrong when loading the additional ID schemes'
+ )} : ${error.message}`
+
+ // The fallback (common) ID scheme dropdown omits its parameter to defer
+ // to the server's own default, so "(Default)" fits there. The other,
+ // per-object-type dropdowns omit theirs to defer to the fallback
+ // dropdown instead - labeling that choice "(Default)" too would imply
+ // it means the same thing in both places, so it's called "(Undefined)".
+ const emptyOptionLabel = isFallback
+ ? i18n.t('(Default)')
+ : i18n.t('(Undefined)')
+
+ return (
+
+ )
+}
+
+IdSchemeSelect.propTypes = {
+ dataTest: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ attributeTypes: PropTypes.arrayOf(PropTypes.string),
+ isFallback: PropTypes.bool,
+}
+
+export { IdSchemeSelect }
diff --git a/src/components/ElementSchemes/IdSchemeSelect.test.jsx b/src/components/ElementSchemes/IdSchemeSelect.test.jsx
new file mode 100644
index 000000000..98fd48923
--- /dev/null
+++ b/src/components/ElementSchemes/IdSchemeSelect.test.jsx
@@ -0,0 +1,73 @@
+import { ReactFinalForm } from '@dhis2/ui'
+import { render, waitFor } from '@testing-library/react'
+import React from 'react'
+import { fetchAttributes } from '../../utils/helper.js'
+import { IdSchemeSelect } from './IdSchemeSelect.jsx'
+
+jest.mock('@dhis2/app-runtime', () => ({
+ ...jest.requireActual('@dhis2/app-runtime'),
+ useConfig: () => ({ baseUrl: 'http://test' }),
+}))
+
+jest.mock('../../utils/helper.js', () => ({
+ ...jest.requireActual('../../utils/helper.js'),
+ fetchAttributes: jest.fn(() => Promise.resolve([])),
+}))
+
+const renderInForm = (children) =>
+ render(
+ {}}>
+ {() => }
+
+ )
+
+beforeEach(() => {
+ fetchAttributes.mockClear()
+})
+
+describe('IdSchemeSelect', () => {
+ it('does not fetch attribute schemes when none are configured', () => {
+ renderInForm(
+
+ )
+
+ expect(fetchAttributes).not.toHaveBeenCalled()
+ })
+
+ it('fetches every configured attribute type', async () => {
+ renderInForm(
+
+ )
+
+ await waitFor(() =>
+ expect(fetchAttributes).toHaveBeenCalledWith(
+ 'http://test/api/',
+ 'dataElementAttribute'
+ )
+ )
+ expect(fetchAttributes).toHaveBeenCalledWith(
+ 'http://test/api/',
+ 'organisationUnitAttribute'
+ )
+ })
+
+ it('renders the field label', () => {
+ const { getByText } = renderInForm(
+
+ )
+
+ expect(getByText('Category ID scheme')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/ElementSchemes/OrgUnitIdScheme.jsx b/src/components/ElementSchemes/OrgUnitIdScheme.jsx
deleted file mode 100644
index 47511c650..000000000
--- a/src/components/ElementSchemes/OrgUnitIdScheme.jsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { useConfig } from '@dhis2/app-runtime'
-import i18n from '@dhis2/d2-i18n'
-import { SingleSelectFieldFF } from '@dhis2/ui'
-import PropTypes from 'prop-types'
-import React, { useEffect, useState } from 'react'
-import { fetchAttributes } from '../../utils/helper.js'
-import { optionsPropType } from '../../utils/options.js'
-import { StyledField } from '../index.js'
-
-const OrgUnitIdScheme = ({ name, label, orgUnitIdSchemeOptions, dataTest }) => {
- const { baseUrl } = useConfig()
- const [loading, setLoading] = useState(true)
- const [schemes, setSchemes] = useState([])
- const [error, setError] = useState(undefined)
-
- useEffect(() => {
- fetchAttributes(`${baseUrl}/api/`, 'organisationUnitAttribute')
- .then((attributes) => setSchemes(attributes))
- .catch((error) => setError(error))
- .finally(() => setLoading(false))
- }, [])
-
- const validationText =
- error &&
- `${i18n.t(
- 'Something went wrong when loading the additional organisation unit ID schemes'
- )} : ${error.message}`
-
- const options = [...orgUnitIdSchemeOptions, ...schemes]
- return (
-
- )
-}
-
-OrgUnitIdScheme.propTypes = {
- dataTest: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
- name: PropTypes.string.isRequired,
- orgUnitIdSchemeOptions: optionsPropType.isRequired,
-}
-
-export { OrgUnitIdScheme }
diff --git a/src/components/ElementSchemes/idSchemeAttributeTypes.js b/src/components/ElementSchemes/idSchemeAttributeTypes.js
new file mode 100644
index 000000000..21ffce5f7
--- /dev/null
+++ b/src/components/ElementSchemes/idSchemeAttributeTypes.js
@@ -0,0 +1,9 @@
+const CATEGORY_ATTRIBUTE_TYPES = ['categoryAttribute']
+const CATEGORY_OPTION_ATTRIBUTE_TYPES = ['categoryOptionAttribute']
+const DATA_SET_ATTRIBUTE_TYPES = ['dataSetAttribute']
+
+export {
+ CATEGORY_ATTRIBUTE_TYPES,
+ CATEGORY_OPTION_ATTRIBUTE_TYPES,
+ DATA_SET_ATTRIBUTE_TYPES,
+}
diff --git a/src/components/Inputs/DataElementIdScheme.jsx b/src/components/Inputs/DataElementIdScheme.jsx
index 5e9085ee8..6b65fe3ef 100644
--- a/src/components/Inputs/DataElementIdScheme.jsx
+++ b/src/components/Inputs/DataElementIdScheme.jsx
@@ -1,24 +1,20 @@
import i18n from '@dhis2/d2-i18n'
import React from 'react'
-import { DataElementIdScheme as DataElementIdSchemeGeneric } from '../index.js'
+import { IdSchemeSelect } from '../index.js'
-const dataElementIdSchemeOptions = [
- { value: 'UID', label: i18n.t('Uid') },
- { value: 'CODE', label: i18n.t('Code') },
- { value: 'NAME', label: i18n.t('Name') },
-]
-const defaultDataElementIdSchemeOption = dataElementIdSchemeOptions[0].value
+const defaultDataElementIdSchemeOption = ''
const NAME = 'dataElementIdScheme'
const DATATEST = 'input-data-element-id-scheme'
const LABEL = i18n.t('Data element ID scheme')
+const ATTRIBUTE_TYPES = ['dataElementAttribute']
const DataElementIdScheme = () => (
-
)
diff --git a/src/components/Inputs/EventIdScheme.jsx b/src/components/Inputs/EventIdScheme.jsx
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/components/Inputs/IdScheme.jsx b/src/components/Inputs/IdScheme.jsx
index 6f88ba78f..90f8b817f 100644
--- a/src/components/Inputs/IdScheme.jsx
+++ b/src/components/Inputs/IdScheme.jsx
@@ -1,24 +1,21 @@
import i18n from '@dhis2/d2-i18n'
import React from 'react'
-import { IdScheme as IdSchemeGeneric } from '../index.js'
+import { IdSchemeSelect } from '../index.js'
-const idSchemeOptions = [
- { value: 'UID', label: i18n.t('Uid') },
- { value: 'CODE', label: i18n.t('Code') },
- { value: 'NAME', label: i18n.t('Name') },
-]
-const defaultIdSchemeOption = idSchemeOptions[0].value
+const defaultIdSchemeOption = ''
const NAME = 'idScheme'
const DATATEST = 'input-id-scheme'
const LABEL = i18n.t('ID scheme')
+const ATTRIBUTE_TYPES = ['dataElementAttribute', 'organisationUnitAttribute']
const IdScheme = () => (
-
)
diff --git a/src/components/Inputs/OrgUnitIdScheme.jsx b/src/components/Inputs/OrgUnitIdScheme.jsx
index b7cfc18fd..1a40d1191 100644
--- a/src/components/Inputs/OrgUnitIdScheme.jsx
+++ b/src/components/Inputs/OrgUnitIdScheme.jsx
@@ -1,24 +1,20 @@
import i18n from '@dhis2/d2-i18n'
import React from 'react'
-import { OrgUnitIdScheme as OrgUnitIdSchemeGeneric } from '../index.js'
+import { IdSchemeSelect } from '../index.js'
-const orgUnitIdSchemeOptions = [
- { value: 'UID', label: i18n.t('Uid') },
- { value: 'CODE', label: i18n.t('Code') },
- { value: 'NAME', label: i18n.t('Name') },
-]
-const defaultOrgUnitIdSchemeOption = orgUnitIdSchemeOptions[0].value
+const defaultOrgUnitIdSchemeOption = ''
const NAME = 'orgUnitIdScheme'
const DATATEST = 'input-org-unit-id-scheme'
const LABEL = i18n.t('Organisation unit ID scheme')
+const ATTRIBUTE_TYPES = ['organisationUnitAttribute']
const OrgUnitIdScheme = () => (
-
)
diff --git a/src/components/index.js b/src/components/index.js
index 713db8ab1..aa0757866 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -13,19 +13,22 @@ export {
TasksIcon,
TEIIcon,
} from './Icon/Icon.jsx'
-export { DataElementIdScheme } from './ElementSchemes/DataElementIdScheme.jsx'
export { DatePicker } from './DatePicker/DatePicker.jsx'
export { DatePickerField } from './DatePicker/DatePickerField.jsx'
export { FileUpload } from './FileUpload/FileUpload.jsx'
export { FormAlerts } from './FormAlerts/FormAlerts.jsx'
export { FormField } from './FormField/FormField.jsx'
-export { IdScheme } from './ElementSchemes/IdScheme.jsx'
+export { IdSchemeSelect } from './ElementSchemes/IdSchemeSelect.jsx'
+export {
+ CATEGORY_ATTRIBUTE_TYPES,
+ CATEGORY_OPTION_ATTRIBUTE_TYPES,
+ DATA_SET_ATTRIBUTE_TYPES,
+} from './ElementSchemes/idSchemeAttributeTypes.js'
export { ImportButtonStrip } from './ImportButtonStrip/ImportButtonStrip.jsx'
export { JobOverview } from './JobOverview/JobOverview.jsx'
export { JobSummary } from './JobSummary/JobSummary.jsx'
export { MiniJobSummary } from './JobSummary/MiniJobSummary/MiniJobSummary.jsx'
export { MoreOptions } from './MoreOptions/MoreOptions.jsx'
-export { OrgUnitIdScheme } from './ElementSchemes/OrgUnitIdScheme.jsx'
export { OrgUnitTreeField } from './OrgUnitTree/OrgUnitTreeField.jsx'
export { Page } from './Page/Page.jsx'
export { ProgramStages } from './ProgramStages/ProgramStages.jsx'
diff --git a/src/pages/DataExport/DataExport.jsx b/src/pages/DataExport/DataExport.jsx
index d0f0b987a..e3eabfd50 100644
--- a/src/pages/DataExport/DataExport.jsx
+++ b/src/pages/DataExport/DataExport.jsx
@@ -9,6 +9,10 @@ import {
SchemeContainer,
DataIcon,
ValidationSummary,
+ IdSchemeSelect,
+ CATEGORY_ATTRIBUTE_TYPES,
+ CATEGORY_OPTION_ATTRIBUTE_TYPES,
+ DATA_SET_ATTRIBUTE_TYPES,
} from '../../components/index.js'
import {
OrgUnitTree,
@@ -63,6 +67,11 @@ const initialValues = {
dataElementIdScheme: defaultDataElementIdSchemeOption,
orgUnitIdScheme: defaultOrgUnitIdSchemeOption,
idScheme: defaultIdSchemeOption,
+ categoryIdScheme: '',
+ categoryOptionIdScheme: '',
+ categoryOptionComboIdScheme: '',
+ dataSetIdScheme: '',
+ attributeOptionComboIdScheme: '',
}
const DataExport = () => {
@@ -106,6 +115,40 @@ const DataExport = () => {
+
+
+
+
+
diff --git a/src/pages/DataExport/form-helper.js b/src/pages/DataExport/form-helper.js
index b0f92e8e2..74938bf68 100644
--- a/src/pages/DataExport/form-helper.js
+++ b/src/pages/DataExport/form-helper.js
@@ -7,23 +7,21 @@ import {
compressionToName,
pathToId,
} from '../../utils/helper.js'
+import { idSchemeParams } from '../../utils/idSchemeParams.js'
-const valuesToParams = ({
- selectedOrgUnits,
- includeChildren,
- selectedDataSets,
- compression,
- startDate,
- endDate,
- includeDeleted,
- dataElementIdScheme,
- orgUnitIdScheme,
- idScheme,
-}) =>
- [
- `dataElementIdScheme=${dataElementIdScheme}`,
- `orgUnitIdScheme=${orgUnitIdScheme}`,
- `idScheme=${idScheme}`,
+const valuesToParams = (values) => {
+ const {
+ selectedOrgUnits,
+ includeChildren,
+ selectedDataSets,
+ compression,
+ startDate,
+ endDate,
+ includeDeleted,
+ } = values
+
+ return [
+ ...idSchemeParams(values, 'dataValueSets'),
`includeDeleted=${includeDeleted}`,
`children=${includeChildren}`,
`startDate=${startDate}`,
@@ -34,6 +32,7 @@ const valuesToParams = ({
]
.filter((s) => s != '')
.join('&')
+}
const onExport = (baseUrl, setExportEnabled) => async (values) => {
setExportEnabled(false)
@@ -60,4 +59,4 @@ const validate = (values) => ({
endDate: DATE_AFTER_VALIDATOR(values.endDate, values.startDate),
})
-export { onExport, validate }
+export { onExport, validate, valuesToParams }
diff --git a/src/pages/DataExport/form-helper.test.js b/src/pages/DataExport/form-helper.test.js
new file mode 100644
index 000000000..a7ca2c1d6
--- /dev/null
+++ b/src/pages/DataExport/form-helper.test.js
@@ -0,0 +1,65 @@
+import { valuesToParams } from './form-helper.js'
+
+const paramKeys = (query) => query.split('&').map((pair) => pair.split('=')[0])
+const paramMap = (query) =>
+ Object.fromEntries(query.split('&').map((pair) => pair.split('=')))
+
+const baseValues = {
+ selectedOrgUnits: ['/ImspTQPwCqd'],
+ includeChildren: true,
+ selectedDataSets: ['BfMAe6Itzgt'],
+ compression: '',
+ startDate: '2020-01-01',
+ endDate: '2020-02-01',
+ includeDeleted: false,
+ dataElementIdScheme: 'UID',
+ orgUnitIdScheme: 'UID',
+ idScheme: 'UID',
+ categoryIdScheme: '',
+ categoryOptionIdScheme: '',
+ categoryOptionComboIdScheme: '',
+ dataSetIdScheme: '',
+ attributeOptionComboIdScheme: '',
+}
+
+describe('DataExport valuesToParams', () => {
+ it('emits the explicitly chosen schemes and omits the "(Undefined)" ones', () => {
+ const params = paramMap(valuesToParams(baseValues))
+
+ expect(params.idScheme).toBe('UID')
+ expect(params.dataElementIdScheme).toBe('UID')
+ expect(params.orgUnitIdScheme).toBe('UID')
+ expect(paramKeys(valuesToParams(baseValues))).not.toContain(
+ 'categoryIdScheme'
+ )
+ expect(paramKeys(valuesToParams(baseValues))).not.toContain(
+ 'dataSetIdScheme'
+ )
+ })
+
+ it('drops idScheme entirely when it is left at "(Default)"', () => {
+ const keys = paramKeys(valuesToParams({ ...baseValues, idScheme: '' }))
+
+ expect(keys).not.toContain('idScheme')
+ })
+
+ it('includes a category scheme once one is chosen', () => {
+ const params = paramMap(
+ valuesToParams({
+ ...baseValues,
+ categoryOptionComboIdScheme: 'CODE',
+ })
+ )
+
+ expect(params.categoryOptionComboIdScheme).toBe('CODE')
+ })
+
+ it('still emits the non-scheme params', () => {
+ const params = paramMap(valuesToParams(baseValues))
+
+ expect(params.includeDeleted).toBe('false')
+ expect(params.children).toBe('true')
+ expect(params.orgUnit).toBe('ImspTQPwCqd')
+ expect(params.dataSet).toBe('BfMAe6Itzgt')
+ })
+})
diff --git a/src/pages/DataImport/DataImport.jsx b/src/pages/DataImport/DataImport.jsx
index 6f42c86dc..01c84ac43 100644
--- a/src/pages/DataImport/DataImport.jsx
+++ b/src/pages/DataImport/DataImport.jsx
@@ -11,6 +11,10 @@ import {
SchemeContainer,
DataIcon,
ValidationSummary,
+ IdSchemeSelect,
+ CATEGORY_ATTRIBUTE_TYPES,
+ CATEGORY_OPTION_ATTRIBUTE_TYPES,
+ DATA_SET_ATTRIBUTE_TYPES,
} from '../../components/index.js'
import {
FileUpload,
@@ -71,6 +75,13 @@ const createInitialValues = (prevJobDetails) => ({
orgUnitIdScheme:
prevJobDetails.orgUnitIdScheme || defaultOrgUnitIdSchemeOption,
idScheme: prevJobDetails.idScheme || defaultIdSchemeOption,
+ categoryIdScheme: prevJobDetails.categoryIdScheme || '',
+ categoryOptionIdScheme: prevJobDetails.categoryOptionIdScheme || '',
+ categoryOptionComboIdScheme:
+ prevJobDetails.categoryOptionComboIdScheme || '',
+ dataSetIdScheme: prevJobDetails.dataSetIdScheme || '',
+ attributeOptionComboIdScheme:
+ prevJobDetails.attributeOptionComboIdScheme || '',
skipExistingCheck: getInitialBoolValue(
prevJobDetails.skipExistingCheck,
defaultSkipExistingCheckOption
@@ -142,6 +153,40 @@ const DataImport = () => {
+
+
+
+
+
diff --git a/src/pages/DataImport/form-helper.js b/src/pages/DataImport/form-helper.js
index b12031ef8..ccf798ac0 100644
--- a/src/pages/DataImport/form-helper.js
+++ b/src/pages/DataImport/form-helper.js
@@ -1,5 +1,6 @@
import { FORM_ERROR, jobStartedMessage } from '../../utils/final-form.js'
import { uploadFile } from '../../utils/helper.js'
+import { idSchemeParams } from '../../utils/idSchemeParams.js'
const isAsync = true
@@ -12,9 +13,6 @@ const onImport =
strategy,
preheatCache,
skipAudit,
- dataElementIdScheme,
- orgUnitIdScheme,
- idScheme,
skipExistingCheck,
format,
firstRowIsHeader,
@@ -29,9 +27,7 @@ const onImport =
`strategy=${strategy}`,
`preheatCache=${preheatCache}`,
`skipAudit=${skipAudit}`,
- `dataElementIdScheme=${dataElementIdScheme}`,
- `orgUnitIdScheme=${orgUnitIdScheme}`,
- `idScheme=${idScheme}`,
+ ...idSchemeParams(values, 'dataValueSets'),
`skipExistingCheck=${skipExistingCheck}`,
format == 'csv' ? `firstRowIsHeader=${firstRowIsHeader}` : '',
]
diff --git a/src/pages/EventExport/form-helper.js b/src/pages/EventExport/form-helper.js
index a9e907e08..94300ac91 100644
--- a/src/pages/EventExport/form-helper.js
+++ b/src/pages/EventExport/form-helper.js
@@ -4,38 +4,26 @@ import {
} from '../../components/DatePicker/DatePickerField.jsx'
import { ALL_VALUE } from '../../hooks/useProgramStages.js'
import { fetchAndDownload, pathToId } from '../../utils/helper.js'
+import { idSchemeParams } from '../../utils/idSchemeParams.js'
-const onExport = (baseUrl, setExportEnabled) => async (values) => {
- setExportEnabled(false)
-
+const valuesToParams = (values) => {
const {
selectedOrgUnits,
selectedPrograms,
programStage,
- format,
- compression,
occurredAfter,
occurredBefore,
includeDeleted,
- dataElementIdScheme,
- orgUnitIdScheme,
- idScheme,
inclusion,
} = values
- // generate URL and redirect
- const apiBaseUrl = `${baseUrl}/api/tracker/`
- const endpoint = `events`
- const endpointExtension = compression ? `${format}.${compression}` : format
- const downloadUrlParams = [
+ return [
'paging=false',
'totalPages=false',
`orgUnit=${pathToId(selectedOrgUnits[0])}`,
`program=${selectedPrograms}`,
`includeDeleted=${includeDeleted}`,
- `dataElementIdScheme=${dataElementIdScheme}`,
- `orgUnitIdScheme=${orgUnitIdScheme}`,
- `idScheme=${idScheme}`,
+ ...idSchemeParams(values, 'tracker'),
`occurredAfter=${occurredAfter}`,
`occurredBefore=${occurredBefore}`,
`orgUnitMode=${inclusion}`,
@@ -43,6 +31,18 @@ const onExport = (baseUrl, setExportEnabled) => async (values) => {
]
.filter((s) => s != '')
.join('&')
+}
+
+const onExport = (baseUrl, setExportEnabled) => async (values) => {
+ setExportEnabled(false)
+
+ const { format, compression } = values
+
+ // generate URL and redirect
+ const apiBaseUrl = `${baseUrl}/api/tracker/`
+ const endpoint = `events`
+ const endpointExtension = compression ? `${format}.${compression}` : format
+ const downloadUrlParams = valuesToParams(values)
const url = `${apiBaseUrl}${endpoint}.${endpointExtension}?${downloadUrlParams}`
try {
@@ -60,4 +60,4 @@ const validate = (values) => ({
endDate: DATE_AFTER_VALIDATOR(values.endDate, values.startDate),
})
-export { onExport, validate }
+export { onExport, validate, valuesToParams }
diff --git a/src/pages/EventExport/form-helper.test.js b/src/pages/EventExport/form-helper.test.js
new file mode 100644
index 000000000..302b80c66
--- /dev/null
+++ b/src/pages/EventExport/form-helper.test.js
@@ -0,0 +1,47 @@
+import { valuesToParams } from './form-helper.js'
+
+const paramKeys = (query) => query.split('&').map((pair) => pair.split('=')[0])
+const paramMap = (query) =>
+ Object.fromEntries(query.split('&').map((pair) => pair.split('=')))
+
+const baseValues = {
+ selectedOrgUnits: ['/ImspTQPwCqd'],
+ selectedPrograms: 'lxAQ7Zs9VYR',
+ programStage: 'ALL',
+ format: 'json',
+ compression: '',
+ occurredAfter: '2020-01-01',
+ occurredBefore: '2020-02-01',
+ includeDeleted: false,
+ dataElementIdScheme: 'UID',
+ orgUnitIdScheme: 'UID',
+ idScheme: 'UID',
+ inclusion: 'SELECTED',
+}
+
+describe('EventExport valuesToParams', () => {
+ it('emits the explicitly chosen schemes', () => {
+ const params = paramMap(valuesToParams(baseValues))
+
+ expect(params.idScheme).toBe('UID')
+ expect(params.dataElementIdScheme).toBe('UID')
+ expect(params.orgUnitIdScheme).toBe('UID')
+ })
+
+ it('drops a scheme left at "(Undefined)"', () => {
+ const keys = paramKeys(
+ valuesToParams({ ...baseValues, dataElementIdScheme: '' })
+ )
+
+ expect(keys).not.toContain('dataElementIdScheme')
+ expect(keys).toContain('idScheme')
+ })
+
+ it('never emits the dataValueSets-only schemes', () => {
+ const keys = paramKeys(
+ valuesToParams({ ...baseValues, categoryIdScheme: 'CODE' })
+ )
+
+ expect(keys).not.toContain('categoryIdScheme')
+ })
+})
diff --git a/src/pages/EventImport/form-helper.js b/src/pages/EventImport/form-helper.js
index 5dd6c3a1f..817c61654 100644
--- a/src/pages/EventImport/form-helper.js
+++ b/src/pages/EventImport/form-helper.js
@@ -1,29 +1,23 @@
import { FORM_ERROR, jobStartedMessage } from '../../utils/final-form.js'
import { uploadFile } from '../../utils/helper.js'
+import { idSchemeParams } from '../../utils/idSchemeParams.js'
const isAsync = true
const onImport =
({ baseUrl, setProgress, addTask, setShowFullSummaryTask }) =>
async (values) => {
- const {
- dryRun,
- files,
- format,
- dataElementIdScheme,
- orgUnitIdScheme,
- idScheme,
- } = values
+ const { dryRun, files, format } = values
// send xhr
const apiBaseUrl = `${baseUrl}/api/tracker`
const params = [
`async=${isAsync}`,
`importMode=${dryRun ? 'validate' : 'commit'}`,
- `dataElementIdScheme=${dataElementIdScheme}`,
- `orgUnitIdScheme=${orgUnitIdScheme}`,
- `idScheme=${idScheme}`,
- ].join('&')
+ ...idSchemeParams(values, 'tracker'),
+ ]
+ .filter((s) => s != '')
+ .join('&')
const url = `${apiBaseUrl}?${params}`
try {
diff --git a/src/pages/TEIExport/form-helper.js b/src/pages/TEIExport/form-helper.js
index ea5d0cdb9..ddde09d85 100644
--- a/src/pages/TEIExport/form-helper.js
+++ b/src/pages/TEIExport/form-helper.js
@@ -5,100 +5,105 @@ import {
} from '../../components/DatePicker/DatePickerField.jsx'
import { OU_MODE_MANUAL_VALUE } from '../../components/Inputs/index.js'
import { fetchAndDownload, pathToId } from '../../utils/helper.js'
-
-// calculate minimum set of parameters based on given filters
-const valuesToParams = ({
- selectedOrgUnits,
- selectedUsers,
- selectedPrograms,
- selectedTETypes,
- orgUnitMode,
- inclusion,
- format,
- includeDeleted,
- dataElementIdScheme,
- orgUnitIdScheme,
- idScheme,
+import { idSchemeEntries } from '../../utils/idSchemeParams.js'
+
+const compact = (obj) =>
+ Object.fromEntries(Object.entries(obj).filter(([, value]) => value))
+
+// include selected org.units only when manual selection is selected
+// orgUnitMode is then stored in the `inclusion` field
+const orgUnitParams = ({ orgUnitMode, inclusion, selectedOrgUnits }) =>
+ orgUnitMode === OU_MODE_MANUAL_VALUE
+ ? {
+ orgUnits: selectedOrgUnits.map((o) => pathToId(o)).join(','),
+ orgUnitMode: inclusion,
+ }
+ : {}
+
+const assignedUserParams = ({
assignedUserModeFilter,
assignedUserMode,
- teiTypeFilter,
- programStatus,
- followUp,
- enrollmentEnrolledAfter,
- enrollmentEnrolledBefore,
- lastUpdatedFilter,
- updatedAfter,
- updatedBefore,
- updatedWithin,
+ selectedUsers,
}) => {
- const minParams = {
- fields: '*,enrollments[*,events[*]]',
- orgUnitMode: orgUnitMode,
- format: format,
- includeDeleted: includeDeleted.toString(),
- dataElementIdScheme: dataElementIdScheme,
- orgUnitIdScheme: orgUnitIdScheme,
- idScheme: idScheme,
- paging: false,
- totalPages: false,
+ if (!assignedUserModeFilter) {
+ return {}
}
-
- // include selected org.units only when manual selection is selected
- // orgUnitMode is then stored in the `inclusion` field
- if (orgUnitMode === OU_MODE_MANUAL_VALUE) {
- minParams.orgUnits = selectedOrgUnits.map((o) => pathToId(o)).join(',')
- minParams.orgUnitMode = inclusion
+ return {
+ assignedUserMode,
+ ...(assignedUserMode === 'PROVIDED'
+ ? { assignedUsers: selectedUsers.join(',') }
+ : {}),
}
+}
- if (assignedUserModeFilter) {
- minParams.assignedUserMode = assignedUserMode
-
- if (assignedUserMode == 'PROVIDED') {
- minParams.assignedUsers = selectedUsers.join(',')
- }
+const programFilterParams = (values) => {
+ if (values.teiTypeFilter !== 'PROGRAM') {
+ return {}
}
-
- if (teiTypeFilter == 'PROGRAM') {
- minParams.program = selectedPrograms
- if (programStatus) {
- // programStatus = ALL is now the same
- // as not providing a value for this param at all
- minParams.programStatus = programStatus
- }
-
- if (followUp !== 'ALL') {
- minParams.followUp = followUp
- }
-
- if (enrollmentEnrolledAfter) {
- minParams.enrollmentEnrolledAfter = enrollmentEnrolledAfter
- }
-
- if (enrollmentEnrolledBefore) {
- minParams.enrollmentEnrolledBefore = enrollmentEnrolledBefore
- }
+ const {
+ selectedPrograms,
+ programStatus,
+ followUp,
+ enrollmentEnrolledAfter,
+ enrollmentEnrolledBefore,
+ } = values
+ return {
+ program: selectedPrograms,
+ // programStatus = ALL is now the same
+ // as not providing a value for this param at all
+ ...compact({
+ programStatus,
+ followUp: followUp === 'ALL' ? '' : followUp,
+ enrollmentEnrolledAfter,
+ enrollmentEnrolledBefore,
+ }),
}
+}
- if (teiTypeFilter == 'TE') {
- minParams.trackedEntityType = selectedTETypes
+const lastUpdatedParams = ({
+ lastUpdatedFilter,
+ updatedAfter,
+ updatedBefore,
+ updatedWithin,
+}) => {
+ if (lastUpdatedFilter === 'DATE') {
+ return compact({ updatedAfter, updatedBefore })
}
-
- if (lastUpdatedFilter == 'DATE') {
- if (updatedAfter) {
- minParams.updatedAfter = updatedAfter
- }
-
- if (updatedBefore) {
- minParams.updatedBefore = updatedBefore
- }
+ if (lastUpdatedFilter === 'DURATION') {
+ return { updatedWithin }
}
+ return {}
+}
+
+// calculate minimum set of parameters based on given filters
+const valuesToParams = (values) => {
+ const {
+ orgUnitMode,
+ format,
+ includeDeleted,
+ teiTypeFilter,
+ selectedTETypes,
+ } = values
- if (lastUpdatedFilter == 'DURATION') {
- minParams.updatedWithin = updatedWithin
+ const minParams = {
+ fields: '*,enrollments[*,events[*]]',
+ orgUnitMode,
+ format,
+ includeDeleted: includeDeleted.toString(),
+ ...Object.fromEntries(idSchemeEntries(values, 'tracker')),
+ paging: false,
+ totalPages: false,
+ ...orgUnitParams(values),
+ ...assignedUserParams(values),
+ ...programFilterParams(values),
+ ...(teiTypeFilter === 'TE'
+ ? { trackedEntityType: selectedTETypes }
+ : {}),
+ ...lastUpdatedParams(values),
}
- return Object.keys(minParams)
- .map((param) => `${param}=${minParams[param]}`)
+ return Object.entries(minParams)
+ .map(([param, value]) => `${param}=${value}`)
.join('&')
}
@@ -169,4 +174,4 @@ const validate = (values) => {
return errors
}
-export { onExport, validate }
+export { onExport, validate, valuesToParams }
diff --git a/src/pages/TEIExport/form-helper.test.js b/src/pages/TEIExport/form-helper.test.js
new file mode 100644
index 000000000..aa2e68b5e
--- /dev/null
+++ b/src/pages/TEIExport/form-helper.test.js
@@ -0,0 +1,45 @@
+import { valuesToParams } from './form-helper.js'
+
+const paramMap = (query) =>
+ Object.fromEntries(query.split('&').map((pair) => pair.split('=')))
+
+const baseValues = {
+ selectedOrgUnits: ['/ImspTQPwCqd'],
+ selectedUsers: [],
+ selectedPrograms: 'lxAQ7Zs9VYR',
+ selectedTETypes: [],
+ orgUnitMode: 'SELECTED',
+ inclusion: 'SELECTED',
+ format: 'json',
+ includeDeleted: false,
+ dataElementIdScheme: 'UID',
+ orgUnitIdScheme: 'UID',
+ idScheme: 'UID',
+ assignedUserModeFilter: false,
+ teiTypeFilter: 'PROGRAM',
+ programStatus: '',
+ followUp: 'ALL',
+ enrollmentEnrolledAfter: '',
+ enrollmentEnrolledBefore: '',
+ lastUpdatedFilter: 'NONE',
+ updatedAfter: '',
+ updatedBefore: '',
+ updatedWithin: '',
+}
+
+describe('TEIExport valuesToParams', () => {
+ it('emits the explicitly chosen schemes', () => {
+ const params = paramMap(valuesToParams(baseValues))
+
+ expect(params.idScheme).toBe('UID')
+ expect(params.dataElementIdScheme).toBe('UID')
+ expect(params.orgUnitIdScheme).toBe('UID')
+ })
+
+ it('drops a scheme left at "(Default)"', () => {
+ const params = paramMap(valuesToParams({ ...baseValues, idScheme: '' }))
+
+ expect(params.idScheme).toBeUndefined()
+ expect(params.dataElementIdScheme).toBe('UID')
+ })
+})
diff --git a/src/pages/TEIImport/form-helper.js b/src/pages/TEIImport/form-helper.js
index 877fffb78..1b32beedc 100644
--- a/src/pages/TEIImport/form-helper.js
+++ b/src/pages/TEIImport/form-helper.js
@@ -1,5 +1,6 @@
import { FORM_ERROR, jobStartedMessage } from '../../utils/final-form.js'
import { uploadFile } from '../../utils/helper.js'
+import { idSchemeParams } from '../../utils/idSchemeParams.js'
const onImport =
({ baseUrl, setProgress, addTask, setShowFullSummaryTask }) =>
@@ -19,9 +20,6 @@ const onImport =
skipValidation,
inclusionStrategy,
isAsync,
- dataElementIdScheme,
- orgUnitIdScheme,
- idScheme,
} = values
// send xhr
@@ -39,9 +37,7 @@ const onImport =
`skipValidation=${skipValidation}`,
`inclusionStrategy=${inclusionStrategy}`,
`async=${isAsync}`,
- `dataElementIdScheme=${dataElementIdScheme}`,
- `orgUnitIdScheme=${orgUnitIdScheme}`,
- `idScheme=${idScheme}`,
+ ...idSchemeParams(values, 'tracker'),
]
.filter((s) => s != '')
.join('&')
diff --git a/src/utils/helper.js b/src/utils/helper.js
index a191a648d..d0dfaef9e 100644
--- a/src/utils/helper.js
+++ b/src/utils/helper.js
@@ -225,8 +225,6 @@ const exportErrorAlert = (source, message) => ({
],
})
-// fetches the export URL, and only triggers the download if the server
-// responded with a success status otherwise returns an error alert
const fetchAndDownload = async (url, source) => {
try {
const response = await fetch(url, { credentials: 'include' })
@@ -237,7 +235,6 @@ const fetchAndDownload = async (url, source) => {
const body = await response.json()
message = body.message || message
} catch (e) {
- // response body wasn't JSON, fall back to the generic message
console.error(
`${source}-export: failed to parse error response`,
e
@@ -275,7 +272,6 @@ const getInitialBoolValue = (prevValue, defaultValue) => {
return prevValue
}
-// adds a digit group separator matching the given locale
const formatNumber = (value, locale) => {
if (value === undefined || value === null || value === '') {
return value
diff --git a/src/utils/idSchemeParams.js b/src/utils/idSchemeParams.js
new file mode 100644
index 000000000..17f34c89e
--- /dev/null
+++ b/src/utils/idSchemeParams.js
@@ -0,0 +1,23 @@
+const ID_SCHEME_PARAMS_BY_ENDPOINT = {
+ dataValueSets: [
+ 'dataElementIdScheme',
+ 'orgUnitIdScheme',
+ 'idScheme',
+ 'categoryIdScheme',
+ 'categoryOptionIdScheme',
+ 'categoryOptionComboIdScheme',
+ 'dataSetIdScheme',
+ 'attributeOptionComboIdScheme',
+ ],
+ tracker: ['dataElementIdScheme', 'orgUnitIdScheme', 'idScheme'],
+}
+
+const idSchemeEntries = (values, endpoint) =>
+ (ID_SCHEME_PARAMS_BY_ENDPOINT[endpoint] || [])
+ .filter((key) => values[key])
+ .map((key) => [key, values[key]])
+
+const idSchemeParams = (values, endpoint) =>
+ idSchemeEntries(values, endpoint).map(([key, value]) => `${key}=${value}`)
+
+export { ID_SCHEME_PARAMS_BY_ENDPOINT, idSchemeEntries, idSchemeParams }
diff --git a/src/utils/idSchemeParams.test.js b/src/utils/idSchemeParams.test.js
new file mode 100644
index 000000000..3e0edc710
--- /dev/null
+++ b/src/utils/idSchemeParams.test.js
@@ -0,0 +1,61 @@
+import { idSchemeEntries, idSchemeParams } from './idSchemeParams.js'
+
+describe('idSchemeParams', () => {
+ it('emits only the params that have a value', () => {
+ const values = {
+ dataElementIdScheme: '',
+ orgUnitIdScheme: 'CODE',
+ idScheme: 'UID',
+ }
+
+ expect(idSchemeParams(values, 'tracker')).toEqual([
+ 'orgUnitIdScheme=CODE',
+ 'idScheme=UID',
+ ])
+ })
+
+ it('emits nothing when every scheme is left at "(Default)"', () => {
+ const values = {
+ dataElementIdScheme: '',
+ orgUnitIdScheme: '',
+ idScheme: '',
+ }
+
+ expect(idSchemeParams(values, 'tracker')).toEqual([])
+ })
+
+ it('supports the category / data set / attribute option combo params for dataValueSets', () => {
+ const values = {
+ categoryIdScheme: 'NAME',
+ dataSetIdScheme: 'CODE',
+ attributeOptionComboIdScheme: 'UID',
+ }
+
+ expect(idSchemeParams(values, 'dataValueSets')).toEqual([
+ 'categoryIdScheme=NAME',
+ 'dataSetIdScheme=CODE',
+ 'attributeOptionComboIdScheme=UID',
+ ])
+ })
+
+ it('ignores params the endpoint does not support even when set', () => {
+ const values = { categoryIdScheme: 'NAME', idScheme: 'UID' }
+
+ expect(idSchemeParams(values, 'tracker')).toEqual(['idScheme=UID'])
+ })
+
+ it('returns [key, value] pairs from idSchemeEntries', () => {
+ const values = { orgUnitIdScheme: 'CODE', idScheme: '' }
+
+ expect(idSchemeEntries(values, 'tracker')).toEqual([
+ ['orgUnitIdScheme', 'CODE'],
+ ])
+ })
+
+ it('returns an empty array for an unknown endpoint instead of throwing', () => {
+ const values = { orgUnitIdScheme: 'CODE' }
+
+ expect(idSchemeParams(values, 'notAnEndpoint')).toEqual([])
+ expect(idSchemeEntries(values, 'notAnEndpoint')).toEqual([])
+ })
+})