From 1ec1b5cac53d2f36a80201a258fe99757523ab15 Mon Sep 17 00:00:00 2001 From: "@dhis2-bot" Date: Tue, 19 Sep 2023 03:37:03 +0200 Subject: [PATCH 1/6] fix(translations): sync translations from transifex (dev) Automatically merged. --- i18n/zh.po | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/i18n/zh.po b/i18n/zh.po index 8195b7435..f05ab5b05 100644 --- a/i18n/zh.po +++ b/i18n/zh.po @@ -19,7 +19,7 @@ msgstr "" "Plural-Forms: nplurals=1; plural=0;\n" msgid "Untitled map, {{date}}" -msgstr "无标题地图,{{日期}}" +msgstr "无标题地图,{{date}}" msgid "Map \"{{- name}}\" is saved." msgstr "地图“{{- name}}”已保存。" @@ -28,13 +28,13 @@ msgid "Failed to save map: {{message}}" msgstr "无法保存地图:{{message}}" msgid "Calculation" -msgstr "" +msgstr "计算" msgid "No calculations found" -msgstr "" +msgstr "没有找到计算结果" msgid "Calculations can be created in the Data Visualizer app." -msgstr "" +msgstr "可以在数据可视化工具应用程序中创建计算。" msgid "Classification" msgstr "分类" @@ -439,7 +439,7 @@ msgid "Program indicator is required" msgstr "需要项目指标" msgid "Calculation is required" -msgstr "" +msgstr "需要计算" msgid "Period is required" msgstr "需要期间" @@ -457,7 +457,7 @@ msgid "Program indicators" msgstr "项目指标" msgid "Calculations" -msgstr "" +msgstr "计算" msgid "Item type" msgstr "条目类型" @@ -1276,7 +1276,7 @@ msgid "You don't have access to this layer data" msgstr "您无权访问此图层数据" msgid "The event filter is not supported" -msgstr "" +msgstr "不支持事件过滤器" msgid "An unknown error occurred while reading layer data" msgstr "读取图层数据时发生未知错误" From c171b3d26189206e546361911e31f887082ee136 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Mon, 25 Sep 2023 09:28:46 +0200 Subject: [PATCH 2/6] fix: accept both lowercase and camelCase interpretationId in url(#2937) Needed for: https://dhis2.atlassian.net/browse/DHIS2-15441 The analytics interpretation component sends a camelcase interpretationId url parameter. This same format is used in DV and LL. Including both lowercase and camelcase ensures that this version of maps isn't dependent on also having the updated dashboard app --- src/components/interpretations/Interpretations.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/interpretations/Interpretations.js b/src/components/interpretations/Interpretations.js index ef7cb361c..c937e5ef9 100644 --- a/src/components/interpretations/Interpretations.js +++ b/src/components/interpretations/Interpretations.js @@ -15,7 +15,10 @@ const Interpretations = ({ renderCount }) => { useEffect(() => { if (isMapLoaded) { - const urlInterpretationId = getUrlParameter('interpretationid') + // analytics interpretation component uses camelcase + const urlInterpretationId = + getUrlParameter('interpretationid') || + getUrlParameter('interpretationId') if (urlInterpretationId) { dispatch(setInterpretation(urlInterpretationId)) From 202fae08f7082dccc18b5589e24ece01e0ae27ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 13:49:48 +0200 Subject: [PATCH 3/6] chore(deps-dev): bump start-server-and-test from 1.15.5 to 2.0.1 (#2980) Bumps [start-server-and-test](https://github.com/bahmutov/start-server-and-test) from 1.15.5 to 2.0.1. - [Release notes](https://github.com/bahmutov/start-server-and-test/releases) - [Commits](https://github.com/bahmutov/start-server-and-test/compare/v1.15.5...v2.0.1) --- updated-dependencies: - dependency-name: start-server-and-test dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 66e6d4cd0..92113ed13 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "patch-package": "^6.5.1", "redux-devtools": "^3.7.0", "redux-mock-store": "^1.5.4", - "start-server-and-test": "^1.15.4" + "start-server-and-test": "^2.0.1" }, "dependencies": { "@dhis2/analytics": "^26.0.17", diff --git a/yarn.lock b/yarn.lock index 11fbd9bda..eb6b06666 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14376,10 +14376,10 @@ stackframe@^1.3.4: resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== -start-server-and-test@^1.15.4: - version "1.15.5" - resolved "https://registry.yarnpkg.com/start-server-and-test/-/start-server-and-test-1.15.5.tgz#5c9103bd87c06678fc62658fbe97d09501714011" - integrity sha512-o3EmkX0++GV+qsvIJ/OKWm3w91fD8uS/bPQVPrh/7loaxkpXSuAIHdnmN/P/regQK9eNAK76aBJcHt+OSTk+nA== +start-server-and-test@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/start-server-and-test/-/start-server-and-test-2.0.1.tgz#e110e0b5a54c80963f65b9689c2c320ab8ac9025" + integrity sha512-8PFo4DLLLCDMuS51/BEEtE1m9CAXw1LNVtZSS1PzkYQh6Qf9JUwM4huYeSoUumaaoAyuwYBwCa9OsrcpMqcOdQ== dependencies: arg "^5.0.2" bluebird "3.7.2" From f532a81463e1842a3e3aa938497505d290be028a Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Mon, 25 Sep 2023 16:25:09 +0200 Subject: [PATCH 4/6] revert: "chore: fixed period select refactor (#2958)" (#2984) This reverts commit 8fdd67eb0b57e3c185cda9da3486d727dc2a64c7. --- .../integration/layers/thematiclayer.cy.js | 81 +------ i18n/en.pot | 15 -- .../calculations/CalculationSelect.js | 62 ----- .../styles/CalculationSelect.module.css | 3 - src/components/core/SelectField.js | 6 - .../edit/thematic/ThematicDialog.js | 18 -- .../edit/thematic/ValueTypeSelect.js | 40 ++-- src/components/layers/overlays/OverlayCard.js | 5 +- src/components/orgunits/OrgUnitData.js | 2 +- src/components/periods/PeriodSelect.js | 217 +++++++++--------- src/constants/dimension.js | 6 - src/util/periods.js | 38 +-- 12 files changed, 142 insertions(+), 351 deletions(-) delete mode 100644 src/components/calculations/CalculationSelect.js delete mode 100644 src/components/calculations/styles/CalculationSelect.module.css diff --git a/cypress/integration/layers/thematiclayer.cy.js b/cypress/integration/layers/thematiclayer.cy.js index 4dacacc0b..d9966fc7d 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, getApiBaseUrl } from '../../support/util.js' +import { CURRENT_YEAR } from '../../support/util.js' const INDICATOR_NAME = 'VCCT post-test counselling rate' @@ -136,83 +136,4 @@ 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 86649bf58..fbce2a3d2 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -17,15 +17,6 @@ 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" @@ -430,9 +421,6 @@ 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" @@ -448,9 +436,6 @@ msgstr "Event data items" msgid "Program indicators" msgstr "Program indicators" -msgid "Calculations" -msgstr "Calculations" - msgid "Item type" msgstr "Item type" diff --git a/src/components/calculations/CalculationSelect.js b/src/components/calculations/CalculationSelect.js deleted file mode 100644 index 8bdc94ad6..000000000 --- a/src/components/calculations/CalculationSelect.js +++ /dev/null @@ -1,62 +0,0 @@ -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 deleted file mode 100644 index d8cf160d2..000000000 --- a/src/components/calculations/styles/CalculationSelect.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.calculationSelect { - margin-bottom: var(--spacers-dp16); -} diff --git a/src/components/core/SelectField.js b/src/components/core/SelectField.js index fa21e24c9..ecfed175d 100644 --- a/src/components/core/SelectField.js +++ b/src/components/core/SelectField.js @@ -17,7 +17,6 @@ import styles from './styles/InputField.module.css' const SelectField = (props) => { const { dense = true, - emptyText, errorText, helpText, warning, @@ -26,7 +25,6 @@ const SelectField = (props) => { prefix, loading, multiple, - filterable, disabled, onChange, className, @@ -67,14 +65,12 @@ 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} > @@ -92,9 +88,7 @@ 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 7470902a9..263c4120f 100644 --- a/src/components/edit/thematic/ThematicDialog.js +++ b/src/components/edit/thematic/ThematicDialog.js @@ -36,7 +36,6 @@ 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' @@ -256,7 +255,6 @@ class ThematicDialog extends Component { dataElementError, dataSetError, programError, - calculationError, eventDataItemError, programIndicatorError, periodTypeError, @@ -410,14 +408,6 @@ class ThematicDialog extends Component { /> ), ]} - {valueType === dimConf.calculation.objectName && ( - - )} @@ -619,14 +609,6 @@ 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 0726f9b86..018b87ab8 100644 --- a/src/components/edit/thematic/ValueTypeSelect.js +++ b/src/components/edit/thematic/ValueTypeSelect.js @@ -1,29 +1,11 @@ import i18n from '@dhis2/d2-i18n' import PropTypes from 'prop-types' -import React, { useMemo } from 'react' +import React from 'react' import { dimConf } from '../../../constants/dimension.js' import { SelectField } from '../../core/index.js' -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(), []) +const ValueTypeSelect = (props) => { + const { value, onChange, className } = props // If value type is data element operand, make it data element const type = @@ -31,6 +13,21 @@ const ValueTypeSelect = ({ value, onChange, className }) => { ? 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 cfd1e86e3..7c1405dff 100644 --- a/src/components/layers/overlays/OverlayCard.js +++ b/src/components/layers/overlays/OverlayCard.js @@ -105,10 +105,7 @@ const OverlayCard = ({ await set(currentAO) // Open it in another app - window.open( - `${baseUrl}/${APP_URLS[type]}/#/currentAnalyticalObject`, - '_blank' - ) + window.location.href = `${baseUrl}/${APP_URLS[type]}/#/currentAnalyticalObject` } : undefined } diff --git a/src/components/orgunits/OrgUnitData.js b/src/components/orgunits/OrgUnitData.js index 4108d34d1..bce650e02 100644 --- a/src/components/orgunits/OrgUnitData.js +++ b/src/components/orgunits/OrgUnitData.js @@ -23,7 +23,7 @@ const ORGUNIT_PROFILE_QUERY = { // Only YEARLY period type is supported in first version const periodType = 'YEARLY' const currentYear = String(new Date().getFullYear()) -const periods = getFixedPeriodsByType({ periodType, currentYear }) +const periods = getFixedPeriodsByType(periodType, currentYear) const defaultPeriod = filterFuturePeriods(periods)[0] || periods[0] /* diff --git a/src/components/periods/PeriodSelect.js b/src/components/periods/PeriodSelect.js index f8261ac88..f020e3ce4 100644 --- a/src/components/periods/PeriodSelect.js +++ b/src/components/periods/PeriodSelect.js @@ -7,8 +7,7 @@ import { } from '@dhis2/ui' import cx from 'classnames' import PropTypes from 'prop-types' -import React, { useState, useMemo, useCallback, useEffect } from 'react' -import usePrevious from '../../hooks/usePrevious.js' +import React, { Component } from 'react' import { getFixedPeriodsByType, filterFuturePeriods, @@ -17,118 +16,124 @@ import { getYear } from '../../util/time.js' import { SelectField } from '../core/index.js' import styles from './styles/PeriodSelect.module.css' -const PeriodSelect = ({ - onChange, - className, - errorText, - firstDate, - lastDate, - period, - periodType, -}) => { - const [year, setYear] = useState(getYear(period?.startDate || lastDate)) - const prevYear = usePrevious(year) - - // Set periods when periodType or year changes - /* eslint-disable react-hooks/exhaustive-deps */ - const periods = useMemo( - () => - periodType - ? getFixedPeriodsByType({ - periodType, - year, - firstDate, - lastDate, - }) - : [period], // saved map period (not included in depency array by design) - [periodType, year, firstDate, lastDate] - ) - /* eslint-enable react-hooks/exhaustive-deps */ - - // Increment/decrement year - const changeYear = useCallback( - (change) => { - const newYear = year + change - - if ( - (!firstDate || newYear >= getYear(firstDate)) && - (!lastDate || newYear <= getYear(lastDate)) - ) { - setYear(newYear) - } - }, - [year, firstDate, lastDate] - ) - - // Autoselect most recent period - useEffect(() => { - if (!period) { - onChange(filterFuturePeriods(periods)[0] || periods[0]) +class PeriodSelect extends Component { + static propTypes = { + onChange: PropTypes.func.isRequired, + className: PropTypes.string, + errorText: PropTypes.string, + period: PropTypes.shape({ + id: PropTypes.string.isRequired, + startDate: PropTypes.string, + }), + periodType: PropTypes.string, + } + + state = { + year: null, + periods: null, + } + + componentDidMount() { + this.setPeriods() + } + + componentDidUpdate(prevProps, prevState) { + const { periodType, period, onChange } = this.props + const { year, periods } = this.state + + if (periodType !== prevProps.periodType) { + this.setPeriods() + } else if (periods && !period) { + onChange(filterFuturePeriods(periods)[0] || periods[0]) // Autoselect most recent period + } + + // Change period if year is changed (but keep period index) + if (period && prevState.periods && year !== prevState.year) { + const periodIndex = prevState.periods.findIndex( + (item) => item.id === period.id + ) + onChange(periods[periodIndex]) + } + } + + render() { + const { periodType, period, onChange, className, errorText } = + this.props + const { periods } = this.state + + if (!periods) { + return null } - }, [period, periods, year, onChange]) - // Keep the same period position when year changes - useEffect(() => { - if (period && !periods.some((p) => p.id === period.id)) { - const periodId = period.id.replace(prevYear, year) + const value = + period && periods.some((p) => p.id === period.id) ? period.id : null + + return ( +
+ + {periodType && ( +
+ +
+ )} +
+ ) + } + + setPeriods() { + const { periodType, period } = this.props + const year = this.state.year || getYear(period && period.startDate) + let periods - onChange(periods.find((p) => p.id === periodId)) + if (periodType) { + periods = getFixedPeriodsByType(periodType, year) + } else if (period) { + periods = [period] // If period is loaded in favorite } - }, [period, periods, year, prevYear, onChange]) - if (!periods) { - return null + this.setState({ periods, year }) } - const value = - period && periods.some((p) => p.id === period.id) ? period.id : null - - return ( -
- - {periodType && ( -
- -
- )} -
- ) -} + nextYear = () => { + this.changeYear(1) + } + + previousYear = () => { + this.changeYear(-1) + } -PeriodSelect.propTypes = { - onChange: PropTypes.func.isRequired, - className: PropTypes.string, - errorText: PropTypes.string, - firstDate: PropTypes.string, - lastDate: PropTypes.string, - period: PropTypes.shape({ - id: PropTypes.string.isRequired, - startDate: PropTypes.string, - }), - periodType: PropTypes.string, + changeYear = (change) => { + const { periodType } = this.props + const year = this.state.year + change + + this.setState({ + year, + periods: getFixedPeriodsByType(periodType, year), + }) + } } export default PeriodSelect diff --git a/src/constants/dimension.js b/src/constants/dimension.js index 3834de2b0..16b6d48ae 100644 --- a/src/constants/dimension.js +++ b/src/constants/dimension.js @@ -64,12 +64,6 @@ 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/src/util/periods.js b/src/util/periods.js index 777ab1bd6..366633e8a 100644 --- a/src/util/periods.js +++ b/src/util/periods.js @@ -6,41 +6,23 @@ import { periodTypes, periodGroups } from '../constants/periods.js' const getYearOffsetFromNow = (year) => year - new Date(Date.now()).getFullYear() -const filterPeriods = (periods, firstDate, lastDate) => - periods.filter( - (p) => - (!firstDate || p.startDate >= firstDate) && - (!lastDate || p.endDate <= lastDate) - ) - export const getPeriodTypes = (hiddenPeriods = []) => periodTypes().filter(({ group }) => !hiddenPeriods.includes(group)) -export const getFixedPeriodsByType = ({ - periodType, - year, - firstDate, - lastDate, -}) => { +export const getFixedPeriodsByType = (periodType, year) => { const period = getFixedPeriodsOptionsById(periodType) const forceDescendingForYearTypes = !!periodType.match(/^FY|YEARLY/) const offset = getYearOffsetFromNow(year) - let periods = period?.getPeriods({ offset, reversePeriods: true }) - - if (!periods) { - return null + const periods = period?.getPeriods({ offset, reversePeriods: true }) || null + if (periods && forceDescendingForYearTypes) { + // TODO: the reverse() is a workaround for a bug in the analytics + // getPeriods function that no longer correctly reverses the order + // for YEARLY and FY period types + return periods.reverse() } - - if (firstDate || lastDate) { - periods = filterPeriods(periods, firstDate, lastDate) - } - - // TODO: the reverse() is a workaround for a bug in the analytics - // getPeriods function that no longer correctly reverses the order - // for YEARLY and FY period types - return forceDescendingForYearTypes ? periods.reverse() : periods + return periods } export const getRelativePeriods = (hiddenPeriods = []) => @@ -64,7 +46,7 @@ export const getPeriodNames = () => ({ }, {}), }) -export const filterFuturePeriods = (periods, date) => { - const now = new Date(date || Date.now()) +export const filterFuturePeriods = (periods) => { + const now = new Date(Date.now()) return periods.filter(({ startDate }) => new Date(startDate) < now) } From af3183cb28928d5a2f862b13a57c45377d136b8b Mon Sep 17 00:00:00 2001 From: "@dhis2-bot" Date: Tue, 26 Sep 2023 03:37:31 +0200 Subject: [PATCH 5/6] fix(translations): sync translations from transifex (dev) Automatically merged. --- i18n/cs.po | 15 --------------- i18n/es.po | 15 --------------- i18n/zh.po | 15 --------------- 3 files changed, 45 deletions(-) diff --git a/i18n/cs.po b/i18n/cs.po index 4e7ea0aad..74f0ce52e 100644 --- a/i18n/cs.po +++ b/i18n/cs.po @@ -24,15 +24,6 @@ msgstr "Mapa \"{{- name}}\" je uložena." msgid "Failed to save map: {{message}}" msgstr "Nepodařilo se uložit mapu: {{message}}" -msgid "Calculation" -msgstr "Výpočet" - -msgid "No calculations found" -msgstr "Nenalezeny žádné výpočty" - -msgid "Calculations can be created in the Data Visualizer app." -msgstr "Výpočty lze vytvářet v aplikaci Data Visualizer." - msgid "Classification" msgstr "Klasifikace" @@ -444,9 +435,6 @@ msgstr "Položka dat události je povinná" msgid "Program indicator is required" msgstr "Je vyžadován indikátor programu" -msgid "Calculation is required" -msgstr "Je vyžadován výpočet" - msgid "Period is required" msgstr "Období je vyžadováno" @@ -462,9 +450,6 @@ msgstr "Položky dat události" msgid "Program indicators" msgstr "Ukazatele programu" -msgid "Calculations" -msgstr "Výpočty" - msgid "Item type" msgstr "Typ položky" diff --git a/i18n/es.po b/i18n/es.po index da70c867b..68929d7f4 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -36,15 +36,6 @@ msgstr "El mapa \"{{- name}}\" está guardado." msgid "Failed to save map: {{message}}" msgstr "Error al guardar el mapa: {{message}}" -msgid "Calculation" -msgstr "Cálculo" - -msgid "No calculations found" -msgstr "" - -msgid "Calculations can be created in the Data Visualizer app." -msgstr "" - msgid "Classification" msgstr "Clasificación" @@ -454,9 +445,6 @@ msgstr "Se requiere el dato de evento" msgid "Program indicator is required" msgstr "Se requiere indicador de programa" -msgid "Calculation is required" -msgstr "" - msgid "Period is required" msgstr "Período requerido" @@ -472,9 +460,6 @@ msgstr "Datos de eventos" msgid "Program indicators" msgstr "Indicadores de programa" -msgid "Calculations" -msgstr "Cálculos" - msgid "Item type" msgstr "Item type" diff --git a/i18n/zh.po b/i18n/zh.po index f05ab5b05..8d9826663 100644 --- a/i18n/zh.po +++ b/i18n/zh.po @@ -27,15 +27,6 @@ msgstr "地图“{{- name}}”已保存。" msgid "Failed to save map: {{message}}" msgstr "无法保存地图:{{message}}" -msgid "Calculation" -msgstr "计算" - -msgid "No calculations found" -msgstr "没有找到计算结果" - -msgid "Calculations can be created in the Data Visualizer app." -msgstr "可以在数据可视化工具应用程序中创建计算。" - msgid "Classification" msgstr "分类" @@ -438,9 +429,6 @@ msgstr "需要事件数据项" msgid "Program indicator is required" msgstr "需要项目指标" -msgid "Calculation is required" -msgstr "需要计算" - msgid "Period is required" msgstr "需要期间" @@ -456,9 +444,6 @@ msgstr "事件数据项" msgid "Program indicators" msgstr "项目指标" -msgid "Calculations" -msgstr "计算" - msgid "Item type" msgstr "条目类型" From 73503ab9dfa20ad261c27ccdd430dd46c71c927d Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Wed, 27 Sep 2023 09:56:38 +0200 Subject: [PATCH 6/6] chore: update the toolbar feature commit with a fix for interpretationId url parameter (#2987) With the changes from the toolbar update, the Interpretations panel might not be loaded by default. So when the url included the interpretationId, no component was listening for it, resulting in the interpretation modal not being opened. The url param listener is thus moved to App. Jest and cypress tests added --- cypress/integration/interpretations.cy.js | 29 ++++ cypress/integration/smoke.cy.js | 42 ++++++ src/components/app/App.js | 10 ++ src/components/app/DetailsPanel.js | 11 +- .../app/__tests__/Detailspanel.spec.js | 142 ++++++++++++++++++ .../__snapshots__/Detailspanel.spec.js.snap | 89 +++++++++++ .../interpretations/Interpretations.js | 12 +- 7 files changed, 317 insertions(+), 18 deletions(-) create mode 100644 src/components/app/__tests__/Detailspanel.spec.js create mode 100644 src/components/app/__tests__/__snapshots__/Detailspanel.spec.js.snap diff --git a/cypress/integration/interpretations.cy.js b/cypress/integration/interpretations.cy.js index 7662150da..4a1e54abe 100644 --- a/cypress/integration/interpretations.cy.js +++ b/cypress/integration/interpretations.cy.js @@ -67,4 +67,33 @@ context('Interpretations', () => { deleteMap() }) + + it('opens and closes the interpretation panel', () => { + cy.intercept({ method: 'POST', url: /dataStatistics/ }).as( + 'postDataStatistics' + ) + cy.visit( + '/?id=ZBjCfSaLSqD&interpretationId=yKqhXZdeJ6a', + EXTENDED_TIMEOUT + ) //ANC: LLITN coverage district and facility + + cy.wait('@postDataStatistics') + .its('response.statusCode') + .should('eq', 201) + + cy.getByDataTest('interpretation-modal') + .find('h1') + .contains( + 'Viewing interpretation: ANC: LLITN coverage district and facility' + ) + .should('be.visible') + + cy.getByDataTest('interpretation-modal') + .findByDataTest('dhis2-modal-close-button') + .click() + + cy.getByDataTest('interpretation-modal').should('not.exist') + + cy.getByDataTest('interpretations-list').should('be.visible') + }) }) diff --git a/cypress/integration/smoke.cy.js b/cypress/integration/smoke.cy.js index 5a7ad16e3..be0d0f82d 100644 --- a/cypress/integration/smoke.cy.js +++ b/cypress/integration/smoke.cy.js @@ -38,4 +38,46 @@ context('Smoke Test', () => { Layer.validateCardTitle('ANC 1 Coverage') cy.get('canvas.maplibregl-canvas').should('be.visible') }) + + it('loads with map id and interpretationid lowercase', () => { + cy.intercept({ method: 'POST', url: /dataStatistics/ }).as( + 'postDataStatistics' + ) + cy.visit( + '/?id=ZBjCfSaLSqD&interpretationid=yKqhXZdeJ6a', + EXTENDED_TIMEOUT + ) //ANC: LLITN coverage district and facility + + cy.wait('@postDataStatistics') + .its('response.statusCode') + .should('eq', 201) + + cy.getByDataTest('interpretation-modal') + .find('h1') + .contains( + 'Viewing interpretation: ANC: LLITN coverage district and facility' + ) + .should('be.visible') + }) + + it('loads with map id and interpretationId uppercase', () => { + cy.intercept({ method: 'POST', url: /dataStatistics/ }).as( + 'postDataStatistics' + ) + cy.visit( + '/?id=ZBjCfSaLSqD&interpretationId=yKqhXZdeJ6a', + EXTENDED_TIMEOUT + ) //ANC: LLITN coverage district and facility + + cy.wait('@postDataStatistics') + .its('response.statusCode') + .should('eq', 201) + + cy.getByDataTest('interpretation-modal') + .find('h1') + .contains( + 'Viewing interpretation: ANC: LLITN coverage district and facility' + ) + .should('be.visible') + }) }) diff --git a/src/components/app/App.js b/src/components/app/App.js index 762d13b8a..7e8f8ca55 100644 --- a/src/components/app/App.js +++ b/src/components/app/App.js @@ -7,6 +7,7 @@ import { useDispatch } from 'react-redux' import { tSetAnalyticalObject } from '../../actions/analyticalObject.js' import { removeBingBasemaps, setBingMapsApiKey } from '../../actions/basemap.js' import { tSetExternalLayers } from '../../actions/externalLayers.js' +import { setInterpretation } from '../../actions/interpretations.js' import { tOpenMap } from '../../actions/map.js' import { CURRENT_AO_KEY } from '../../util/analyticalObject.js' import { getUrlParameter } from '../../util/requests.js' @@ -33,6 +34,15 @@ const App = () => { } else if (getUrlParameter('currentAnalyticalObject') === 'true') { await dispatch(tSetAnalyticalObject(currentAO)) } + + // analytics interpretation component uses camelcase + const interpretationId = + getUrlParameter('interpretationid') || + getUrlParameter('interpretationId') + + if (interpretationId) { + dispatch(setInterpretation(interpretationId)) + } } if (!isEmpty(systemSettings)) { diff --git a/src/components/app/DetailsPanel.js b/src/components/app/DetailsPanel.js index ddb88d10e..a5dee646a 100644 --- a/src/components/app/DetailsPanel.js +++ b/src/components/app/DetailsPanel.js @@ -9,17 +9,14 @@ import styles from './styles/DetailsPanel.module.css' const DetailsPanel = ({ interpretationsRenderCount }) => { const detailsPanelOpen = useSelector((state) => state.ui.rightPanelOpen) const viewOrgUnitProfile = useSelector((state) => state.orgUnitProfile) + const interpretationId = useSelector((state) => state.interpretation?.id) const getContent = () => { - if (!detailsPanelOpen) { - return null + if (interpretationId || (detailsPanelOpen && !viewOrgUnitProfile)) { + return } - return viewOrgUnitProfile ? ( - - ) : ( - - ) + return detailsPanelOpen ? : null } return ( diff --git a/src/components/app/__tests__/Detailspanel.spec.js b/src/components/app/__tests__/Detailspanel.spec.js new file mode 100644 index 000000000..6d6110eef --- /dev/null +++ b/src/components/app/__tests__/Detailspanel.spec.js @@ -0,0 +1,142 @@ +import { render } from '@testing-library/react' +import React from 'react' +import { Provider } from 'react-redux' +import configureMockStore from 'redux-mock-store' +import DetailsPanel from '../DetailsPanel.js' + +const mockStore = configureMockStore() + +jest.mock( + '../../interpretations/Interpretations.js', + () => + function MockInterpretations() { + return
Interpretations
+ } +) + +jest.mock( + '../../orgunits/OrgUnitProfile.js', + () => + function MockOrgUnitProfile() { + return
Org Unit Profile
+ } +) + +describe('DetailsPanel', () => { + test('renders InterpretationsPanel when has interpretationId, has orgUnitProfile and panel is open', () => { + const store = { + interpretation: { id: 'abc123' }, + orgUnitProfile: 'xyzpdq', + ui: { rightPanelOpen: true }, + } + + const { container } = render( + + + + ) + expect(container).toMatchSnapshot() + }) + test('renders InterpretationsPanel when has interpretationId, has orgUnitProfile and panel is closed', () => { + const store = { + interpretation: { id: 'abc123' }, + orgUnitProfile: 'xyzpdq', + ui: { rightPanelOpen: false }, + } + + const { container } = render( + + + + ) + expect(container).toMatchSnapshot() + }) + test('renders InterpretationsPanel when has interpretationId, no orgUnitProfile and panel is open', () => { + const store = { + interpretation: { id: 'abc123' }, + orgUnitProfile: null, + ui: { rightPanelOpen: true }, + } + + const { container } = render( + + + + ) + expect(container).toMatchSnapshot() + }) + + test('renders InterpretationsPanel when has interpretationId, no orgUnitProfile and panel is closed', () => { + const store = { + interpretation: { id: 'abc123' }, + orgUnitProfile: null, + ui: { rightPanelOpen: false }, + } + + const { container } = render( + + + + ) + expect(container).toMatchSnapshot() + }) + + test('renders OrgUnitProfile when no interpretationId, has orgUnitProfile and panel is open', () => { + const store = { + interpretation: {}, + orgUnitProfile: 'xyzpdq', + ui: { rightPanelOpen: true }, + } + + const { container } = render( + + + + ) + expect(container).toMatchSnapshot() + }) + + test('renders null when no interpretationId, has orgUnitProfile, and panel closed', () => { + const store = { + interpretation: {}, + orgUnitProfile: 'xyzpdq', + ui: { rightPanelOpen: false }, + } + + const { container } = render( + + + + ) + expect(container).toMatchSnapshot() + }) + test('renders InterpretationsPanel when no interpretationId, no orgUnitProfile and panel open', () => { + const store = { + interpretation: {}, + orgUnitProfile: null, + ui: { rightPanelOpen: true }, + } + + const { container } = render( + + + + ) + expect(container).toMatchSnapshot() + }) + + test('renders null when no interpretationId, no orgUnitProfile, and panel closed', () => { + const store = { + interpretation: {}, + orgUnitProfile: null, + ui: { rightPanelOpen: false }, + } + + const { container } = render( + + + + ) + expect(container).toMatchSnapshot() + }) +}) diff --git a/src/components/app/__tests__/__snapshots__/Detailspanel.spec.js.snap b/src/components/app/__tests__/__snapshots__/Detailspanel.spec.js.snap new file mode 100644 index 000000000..b45d254ce --- /dev/null +++ b/src/components/app/__tests__/__snapshots__/Detailspanel.spec.js.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DetailsPanel renders InterpretationsPanel when has interpretationId, has orgUnitProfile and panel is closed 1`] = ` +
+ +
+`; + +exports[`DetailsPanel renders InterpretationsPanel when has interpretationId, has orgUnitProfile and panel is open 1`] = ` +
+
+
+ Interpretations +
+
+
+`; + +exports[`DetailsPanel renders InterpretationsPanel when has interpretationId, no orgUnitProfile and panel is closed 1`] = ` +
+ +
+`; + +exports[`DetailsPanel renders InterpretationsPanel when has interpretationId, no orgUnitProfile and panel is open 1`] = ` +
+
+
+ Interpretations +
+
+
+`; + +exports[`DetailsPanel renders InterpretationsPanel when no interpretationId, no orgUnitProfile and panel open 1`] = ` +
+
+
+ Interpretations +
+
+
+`; + +exports[`DetailsPanel renders OrgUnitProfile when no interpretationId, has orgUnitProfile and panel is open 1`] = ` +
+
+
+ Org Unit Profile +
+
+
+`; + +exports[`DetailsPanel renders null when no interpretationId, has orgUnitProfile, and panel closed 1`] = ` +
+ +`; + +exports[`DetailsPanel renders null when no interpretationId, no orgUnitProfile, and panel closed 1`] = ` +
+ +`; diff --git a/src/components/interpretations/Interpretations.js b/src/components/interpretations/Interpretations.js index c937e5ef9..f3cf14087 100644 --- a/src/components/interpretations/Interpretations.js +++ b/src/components/interpretations/Interpretations.js @@ -1,9 +1,7 @@ import PropTypes from 'prop-types' import React, { useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' -import { setInterpretation } from '../../actions/interpretations.js' import { openInterpretationsPanel } from '../../actions/ui.js' -import { getUrlParameter } from '../../util/requests.js' import InterpretationsPanel from './InterpretationsPanel.js' const Interpretations = ({ renderCount }) => { @@ -15,15 +13,7 @@ const Interpretations = ({ renderCount }) => { useEffect(() => { if (isMapLoaded) { - // analytics interpretation component uses camelcase - const urlInterpretationId = - getUrlParameter('interpretationid') || - getUrlParameter('interpretationId') - - if (urlInterpretationId) { - dispatch(setInterpretation(urlInterpretationId)) - dispatch(openInterpretationsPanel()) - } + dispatch(openInterpretationsPanel()) } }, [isMapLoaded, dispatch])