Skip to content

Commit

Permalink
feat: custom calculations for thematic layer (DHIS2-15474) (#2745)
Browse files Browse the repository at this point in the history
  • Loading branch information
turban authored Sep 12, 2023
1 parent f40bbcb commit b757770
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 19 deletions.
81 changes: 80 additions & 1 deletion cypress/integration/layers/thematiclayer.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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}`
)
})
})
})
15 changes: 15 additions & 0 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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"

Expand All @@ -436,6 +448,9 @@ msgstr "Event data items"
msgid "Program indicators"
msgstr "Program indicators"

msgid "Calculations"
msgstr "Calculations"

msgid "Item type"
msgstr "Item type"

Expand Down
62 changes: 62 additions & 0 deletions src/components/calculations/CalculationSelect.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.calculationSelect}>
<SelectField
label={i18n.t('Calculation')}
loading={loading}
items={items}
value={value}
onChange={(dataItem) => onChange(dataItem, 'calculation')}
className={className}
emptyText={i18n.t('No calculations found')}
errorText={
error?.message ||
(!calculation && errorText ? errorText : null)
}
filterable={true}
dataTest="calculationselect"
/>
<Help>
{i18n.t(
'Calculations can be created in the Data Visualizer app.'
)}
</Help>
</div>
)
}

CalculationSelect.propTypes = {
onChange: PropTypes.func.isRequired,
calculation: PropTypes.object,
className: PropTypes.string,
errorText: PropTypes.string,
}

export default CalculationSelect
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.calculationSelect {
margin-bottom: var(--spacers-dp16);
}
6 changes: 6 additions & 0 deletions src/components/core/SelectField.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import styles from './styles/InputField.module.css'
const SelectField = (props) => {
const {
dense = true,
emptyText,
errorText,
helpText,
warning,
Expand All @@ -25,6 +26,7 @@ const SelectField = (props) => {
prefix,
loading,
multiple,
filterable,
disabled,
onChange,
className,
Expand Down Expand Up @@ -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}
>
Expand All @@ -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({
Expand Down
18 changes: 18 additions & 0 deletions src/components/edit/thematic/ThematicDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -255,6 +256,7 @@ class ThematicDialog extends Component {
dataElementError,
dataSetError,
programError,
calculationError,
eventDataItemError,
programIndicatorError,
periodTypeError,
Expand Down Expand Up @@ -408,6 +410,14 @@ class ThematicDialog extends Component {
/>
),
]}
{valueType === dimConf.calculation.objectName && (
<CalculationSelect
calculation={dataItem}
onChange={setDataItem}
className={styles.select}
errorText={calculationError}
/>
)}
<AggregationTypeSelect className={styles.select} />
<CompletedOnlyCheckbox valueType={valueType} />
</div>
Expand Down Expand Up @@ -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',
Expand Down
40 changes: 22 additions & 18 deletions src/components/edit/thematic/ValueTypeSelect.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,44 @@
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 =
value === dimConf.operand.objectName
? 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 (
<SelectField
label={i18n.t('Item type')}
items={items}
value={type}
onChange={(valueType) => onChange(valueType.id)}
className={className}
dataTest="thematic-layer-value-type-select"
/>
)
}
Expand Down
6 changes: 6 additions & 0 deletions src/constants/dimension.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit b757770

Please sign in to comment.