diff --git a/cypress/integration/layers/thematiclayer.cy.js b/cypress/integration/layers/thematiclayer.cy.js
index d9966fc7d..4dacacc0b 100644
--- a/cypress/integration/layers/thematiclayer.cy.js
+++ b/cypress/integration/layers/thematiclayer.cy.js
@@ -6,7 +6,7 @@ import {
expectContextMenuOptions,
} from '../../elements/map_context_menu.js'
import { ThematicLayer } from '../../elements/thematic_layer.js'
-import { CURRENT_YEAR } from '../../support/util.js'
+import { CURRENT_YEAR, getApiBaseUrl } from '../../support/util.js'
const INDICATOR_NAME = 'VCCT post-test counselling rate'
@@ -136,4 +136,83 @@ context('Thematic Layers', () => {
{ name: VIEW_PROFILE },
])
})
+
+ // TODO - update demo database with calculations instead of creating on the fly
+ it('adds a thematic layer with a calculation', () => {
+ const timestamp = new Date().toUTCString().slice(-24, -4)
+ const calculationName = `map calc ${timestamp}`
+
+ // add a calculation
+ cy.request('POST', `${getApiBaseUrl()}/api/expressionDimensionItems`, {
+ name: calculationName,
+ shortName: calculationName,
+ expression: '#{fbfJHSPpUQD}/2',
+ }).then((response) => {
+ expect(response.status).to.eq(201)
+
+ const calculationUid = response.body.response.uid
+
+ // open thematic dialog
+ cy.getByDataTest('add-layer-button').click()
+ cy.getByDataTest('addlayeritem-thematic').click()
+
+ // choose "Calculation" in item type
+ cy.getByDataTest('thematic-layer-value-type-select').click()
+ cy.contains('Calculations').click()
+
+ // assert that the label on the Calculation select is "Calculation"
+ cy.getByDataTest('calculationselect-label').contains('Calculation')
+
+ // click to open the calculation select
+ cy.getByDataTest('calculationselect').click()
+
+ // check search box exists "Type to filter options"
+ cy.getByDataTest('dhis2-uicore-popper')
+ .find('input[type="text"]')
+ .should('have.attr', 'placeholder', 'Type to filter options')
+
+ // search for something that doesn't exist
+ cy.getByDataTest('dhis2-uicore-popper')
+ .find('input[type="text"]')
+ .type('foo')
+
+ cy.getByDataTest('dhis2-uicore-select-menu-menuwrapper')
+ .contains('No options found')
+ .should('be.visible')
+
+ // try search for something that exists
+ cy.getByDataTest('dhis2-uicore-popper')
+ .find('input[type="text"]')
+ .clear()
+
+ cy.getByDataTest('dhis2-uicore-popper')
+ .find('input[type="text"]')
+ .type(calculationName)
+
+ cy.getByDataTest('dhis2-uicore-select-menu-menuwrapper')
+ .contains(calculationName)
+ .should('be.visible')
+
+ // select the calculation and close dialog
+ cy.contains(calculationName).click()
+
+ cy.getByDataTest('dhis2-uicore-modalactions')
+ .contains('Add layer')
+ .click()
+
+ // check the layer card title
+ cy.getByDataTest('layercard')
+ .contains(calculationName, { timeout: 50000 })
+ .should('be.visible')
+
+ // check the map canvas is displayed
+ cy.get('canvas.maplibregl-canvas').should('be.visible')
+
+ // delete the calculation
+ cy.request(
+ 'DELETE',
+ `${getApiBaseUrl()}/api/expressionDimensionItems/${calculationUid}`
+ )
+ })
+ })
})
diff --git a/i18n/en.pot b/i18n/en.pot
index 2ff7557e9..34bbf28dd 100644
--- a/i18n/en.pot
+++ b/i18n/en.pot
@@ -17,6 +17,15 @@ msgstr "Map \"{{- name}}\" is saved."
msgid "Failed to save map: {{message}}"
msgstr "Failed to save map: {{message}}"
+msgid "Calculation"
+msgstr "Calculation"
+
+msgid "No calculations found"
+msgstr "No calculations found"
+
+msgid "Calculations can be created in the Data Visualizer app."
+msgstr "Calculations can be created in the Data Visualizer app."
+
msgid "Classification"
msgstr "Classification"
@@ -421,6 +430,9 @@ msgstr "Event data item is required"
msgid "Program indicator is required"
msgstr "Program indicator is required"
+msgid "Calculation is required"
+msgstr "Calculation is required"
+
msgid "Period is required"
msgstr "Period is required"
@@ -436,6 +448,9 @@ msgstr "Event data items"
msgid "Program indicators"
msgstr "Program indicators"
+msgid "Calculations"
+msgstr "Calculations"
+
msgid "Item type"
msgstr "Item type"
diff --git a/package.json b/package.json
index 90d10a274..b256755ea 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
"@dhis2/cypress-commands": "^10.0.3",
"@dhis2/cypress-plugins": "^10.0.3",
"@testing-library/react": "^12.1.5",
- "cypress": "^12.16.0",
+ "cypress": "^12.17.4",
"cypress-tags": "^1.1.2",
"cypress-wait-until": "^1.7.2",
"d2-i18n-extract": "^1.0.5",
diff --git a/src/components/calculations/CalculationSelect.js b/src/components/calculations/CalculationSelect.js
new file mode 100644
index 000000000..8bdc94ad6
--- /dev/null
+++ b/src/components/calculations/CalculationSelect.js
@@ -0,0 +1,62 @@
+import { useDataQuery } from '@dhis2/app-runtime'
+import i18n from '@dhis2/d2-i18n'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { SelectField, Help } from '../core/index.js'
+import { useUserSettings } from '../UserSettingsProvider.js'
+import styles from './styles/CalculationSelect.module.css'
+
+// Load all calculations
+const CALCULATIONS_QUERY = {
+ calculations: {
+ resource: 'expressionDimensionItems',
+ params: ({ nameProperty }) => ({
+ fields: ['id', `${nameProperty}~rename(name)`],
+ paging: false,
+ }),
+ },
+}
+
+const CalculationSelect = ({ calculation, className, errorText, onChange }) => {
+ const { nameProperty } = useUserSettings()
+ const { loading, error, data } = useDataQuery(CALCULATIONS_QUERY, {
+ variables: { nameProperty },
+ })
+
+ const items = data?.calculations.expressionDimensionItems
+ const value = calculation?.id
+
+ return (
+
+ onChange(dataItem, 'calculation')}
+ className={className}
+ emptyText={i18n.t('No calculations found')}
+ errorText={
+ error?.message ||
+ (!calculation && errorText ? errorText : null)
+ }
+ filterable={true}
+ dataTest="calculationselect"
+ />
+
+ {i18n.t(
+ 'Calculations can be created in the Data Visualizer app.'
+ )}
+
+
+ )
+}
+
+CalculationSelect.propTypes = {
+ onChange: PropTypes.func.isRequired,
+ calculation: PropTypes.object,
+ className: PropTypes.string,
+ errorText: PropTypes.string,
+}
+
+export default CalculationSelect
diff --git a/src/components/calculations/styles/CalculationSelect.module.css b/src/components/calculations/styles/CalculationSelect.module.css
new file mode 100644
index 000000000..d8cf160d2
--- /dev/null
+++ b/src/components/calculations/styles/CalculationSelect.module.css
@@ -0,0 +1,3 @@
+.calculationSelect {
+ margin-bottom: var(--spacers-dp16);
+}
diff --git a/src/components/core/SelectField.js b/src/components/core/SelectField.js
index ecfed175d..fa21e24c9 100644
--- a/src/components/core/SelectField.js
+++ b/src/components/core/SelectField.js
@@ -17,6 +17,7 @@ import styles from './styles/InputField.module.css'
const SelectField = (props) => {
const {
dense = true,
+ emptyText,
errorText,
helpText,
warning,
@@ -25,6 +26,7 @@ const SelectField = (props) => {
prefix,
loading,
multiple,
+ filterable,
disabled,
onChange,
className,
@@ -65,12 +67,14 @@ const SelectField = (props) => {
label={label}
prefix={prefix}
selected={!isLoading ? selected : undefined}
+ filterable={filterable}
disabled={disabled}
loading={isLoading}
error={!!errorText}
warning={!!warning}
validationText={warning ? warning : errorText}
helpText={helpText}
+ empty={emptyText}
onChange={onSelectChange}
dataTest={dataTest}
>
@@ -88,7 +92,9 @@ SelectField.propTypes = {
dataTest: PropTypes.string,
dense: PropTypes.bool,
disabled: PropTypes.bool,
+ emptyText: PropTypes.string, // If set, shows empty text when no options
errorText: PropTypes.string, // If set, shows the error message below the SelectField
+ filterable: PropTypes.bool,
helpText: PropTypes.string, // If set, shows the help text below the SelectField
items: PropTypes.arrayOf(
PropTypes.shape({
diff --git a/src/components/edit/thematic/ThematicDialog.js b/src/components/edit/thematic/ThematicDialog.js
index 263c4120f..7470902a9 100644
--- a/src/components/edit/thematic/ThematicDialog.js
+++ b/src/components/edit/thematic/ThematicDialog.js
@@ -36,6 +36,7 @@ import {
} from '../../../util/analytics.js'
import { isPeriodAvailable } from '../../../util/periods.js'
import { getStartEndDateError } from '../../../util/time.js'
+import CalculationSelect from '../../calculations/CalculationSelect.js'
import NumericLegendStyle from '../../classification/NumericLegendStyle.js'
import { Tab, Tabs } from '../../core/index.js'
import DataElementGroupSelect from '../../dataElement/DataElementGroupSelect.js'
@@ -255,6 +256,7 @@ class ThematicDialog extends Component {
dataElementError,
dataSetError,
programError,
+ calculationError,
eventDataItemError,
programIndicatorError,
periodTypeError,
@@ -408,6 +410,14 @@ class ThematicDialog extends Component {
/>
),
]}
+ {valueType === dimConf.calculation.objectName && (
+
+ )}
@@ -609,6 +619,14 @@ class ThematicDialog extends Component {
}
}
+ if (valueType === dimConf.calculation.objectName && !dataItem) {
+ return this.setErrorState(
+ 'calculationError',
+ i18n.t('Calculation is required'),
+ 'data'
+ )
+ }
+
if (!period && periodType !== START_END_DATES) {
return this.setErrorState(
'periodError',
diff --git a/src/components/edit/thematic/ValueTypeSelect.js b/src/components/edit/thematic/ValueTypeSelect.js
index 018b87ab8..0726f9b86 100644
--- a/src/components/edit/thematic/ValueTypeSelect.js
+++ b/src/components/edit/thematic/ValueTypeSelect.js
@@ -1,11 +1,29 @@
import i18n from '@dhis2/d2-i18n'
import PropTypes from 'prop-types'
-import React from 'react'
+import React, { useMemo } from 'react'
import { dimConf } from '../../../constants/dimension.js'
import { SelectField } from '../../core/index.js'
-const ValueTypeSelect = (props) => {
- const { value, onChange, className } = props
+const getValueTypes = () => [
+ { id: dimConf.indicator.objectName, name: i18n.t('Indicator') },
+ { id: dimConf.dataElement.objectName, name: i18n.t('Data element') },
+ { id: dimConf.dataSet.objectName, name: i18n.t('Reporting rates') },
+ {
+ id: dimConf.eventDataItem.objectName,
+ name: i18n.t('Event data items'),
+ },
+ {
+ id: dimConf.programIndicator.objectName,
+ name: i18n.t('Program indicators'),
+ },
+ {
+ id: dimConf.calculation.objectName,
+ name: i18n.t('Calculations'),
+ },
+]
+
+const ValueTypeSelect = ({ value, onChange, className }) => {
+ const items = useMemo(() => getValueTypes(), [])
// If value type is data element operand, make it data element
const type =
@@ -13,21 +31,6 @@ const ValueTypeSelect = (props) => {
? dimConf.dataElement.objectName
: value
- // TODO: Avoid creating on each render (needs to be created after i18next contains translations
- const items = [
- { id: dimConf.indicator.objectName, name: i18n.t('Indicator') },
- { id: dimConf.dataElement.objectName, name: i18n.t('Data element') },
- { id: dimConf.dataSet.objectName, name: i18n.t('Reporting rates') },
- {
- id: dimConf.eventDataItem.objectName,
- name: i18n.t('Event data items'),
- },
- {
- id: dimConf.programIndicator.objectName,
- name: i18n.t('Program indicators'),
- },
- ]
-
return (
{
value={type}
onChange={(valueType) => onChange(valueType.id)}
className={className}
+ dataTest="thematic-layer-value-type-select"
/>
)
}
diff --git a/src/components/layers/overlays/OverlayCard.js b/src/components/layers/overlays/OverlayCard.js
index 7c1405dff..cfd1e86e3 100644
--- a/src/components/layers/overlays/OverlayCard.js
+++ b/src/components/layers/overlays/OverlayCard.js
@@ -105,7 +105,10 @@ const OverlayCard = ({
await set(currentAO)
// Open it in another app
- window.location.href = `${baseUrl}/${APP_URLS[type]}/#/currentAnalyticalObject`
+ window.open(
+ `${baseUrl}/${APP_URLS[type]}/#/currentAnalyticalObject`,
+ '_blank'
+ )
}
: undefined
}
diff --git a/src/constants/dimension.js b/src/constants/dimension.js
index 16b6d48ae..3834de2b0 100644
--- a/src/constants/dimension.js
+++ b/src/constants/dimension.js
@@ -64,6 +64,12 @@ export const dimConf = {
objectName: 'pi',
itemType: 'PROGRAM_INDICATOR',
},
+ calculation: {
+ value: 'expressionDimensionItems',
+ dimensionName: 'dx',
+ objectName: 'ed', // Created by Bjorn, don't seem to be in use when the map is saved
+ itemType: 'EXPRESSION_DIMENSION_ITEM',
+ },
period: {
id: 'period',
value: 'period',
diff --git a/yarn.lock b/yarn.lock
index b886043cb..934bbb425 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1403,10 +1403,10 @@
through2 "^2.0.0"
watchify "^4.0.0"
-"@cypress/request@^2.88.10":
- version "2.88.11"
- resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.11.tgz#5a4c7399bc2d7e7ed56e92ce5acb620c8b187047"
- integrity sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==
+"@cypress/request@2.88.12":
+ version "2.88.12"
+ resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.12.tgz#ba4911431738494a85e93fb04498cb38bc55d590"
+ integrity sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.8.0"
@@ -1423,7 +1423,7 @@
performance-now "^2.1.0"
qs "~6.10.3"
safe-buffer "^5.1.2"
- tough-cookie "~2.5.0"
+ tough-cookie "^4.1.3"
tunnel-agent "^0.6.0"
uuid "^8.3.2"
@@ -3498,10 +3498,10 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.2.tgz#c076ed1d7b6095078ad3cf21dfeea951842778b1"
integrity sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==
-"@types/node@^14.14.31":
- version "14.18.36"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.36.tgz#c414052cb9d43fab67d679d5f3c641be911f5835"
- integrity sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==
+"@types/node@^16.18.39":
+ version "16.18.50"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.50.tgz#93003cf0251a2ecd26dad6dc757168d648519805"
+ integrity sha512-OiDU5xRgYTJ203v4cprTs0RwOCd5c5Zjv+K5P8KSqfiCsB1W3LcamTUMcnQarpq5kOYbhHfSOgIEJvdPyb5xyw==
"@types/normalize-package-data@^2.4.0":
version "2.4.1"
@@ -6238,14 +6238,14 @@ cypress-wait-until@^1.7.2:
resolved "https://registry.yarnpkg.com/cypress-wait-until/-/cypress-wait-until-1.7.2.tgz#7f534dd5a11c89b65359e7a0210f20d3dfc22107"
integrity sha512-uZ+M8/MqRcpf+FII/UZrU7g1qYZ4aVlHcgyVopnladyoBrpoaMJ4PKZDrdOJ05H5RHbr7s9Tid635X3E+ZLU/Q==
-cypress@^12.16.0:
- version "12.16.0"
- resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.16.0.tgz#d0dcd0725a96497f4c60cf54742242259847924c"
- integrity sha512-mwv1YNe48hm0LVaPgofEhGCtLwNIQEjmj2dJXnAkY1b4n/NE9OtgPph4TyS+tOtYp5CKtRmDvBzWseUXQTjbTg==
+cypress@^12.17.4:
+ version "12.17.4"
+ resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.4.tgz#b4dadf41673058493fa0d2362faa3da1f6ae2e6c"
+ integrity sha512-gAN8Pmns9MA5eCDFSDJXWKUpaL3IDd89N9TtIupjYnzLSmlpVr+ZR+vb4U/qaMp+lB6tBvAmt7504c3Z4RU5KQ==
dependencies:
- "@cypress/request" "^2.88.10"
+ "@cypress/request" "2.88.12"
"@cypress/xvfb" "^1.2.4"
- "@types/node" "^14.14.31"
+ "@types/node" "^16.18.39"
"@types/sinonjs__fake-timers" "8.1.1"
"@types/sizzle" "^2.3.2"
arch "^2.2.0"
@@ -6278,9 +6278,10 @@ cypress@^12.16.0:
minimist "^1.2.8"
ospath "^1.2.2"
pretty-bytes "^5.6.0"
+ process "^0.11.10"
proxy-from-env "1.0.0"
request-progress "^3.0.0"
- semver "^7.3.2"
+ semver "^7.5.3"
supports-color "^8.1.1"
tmp "~0.2.1"
untildify "^4.0.0"
@@ -12692,7 +12693,7 @@ process-nextick-args@^2.0.0, process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
-process@~0.11.0:
+process@^0.11.10, process@~0.11.0:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
@@ -13878,10 +13879,10 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
-semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8:
- version "7.3.8"
- resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
- integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
+semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3:
+ version "7.5.4"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
+ integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
dependencies:
lru-cache "^6.0.0"
@@ -15077,6 +15078,16 @@ tough-cookie@^4.0.0:
universalify "^0.2.0"
url-parse "^1.5.3"
+tough-cookie@^4.1.3:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf"
+ integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==
+ dependencies:
+ psl "^1.1.33"
+ punycode "^2.1.1"
+ universalify "^0.2.0"
+ url-parse "^1.5.3"
+
tough-cookie@~2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"