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/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/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/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/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/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 8195b7435..8d9826663 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}}”已保存。" @@ -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 "条目类型" @@ -1276,7 +1261,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 "读取图层数据时发生未知错误" 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/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/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 77b089376..d074437c0 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' @@ -255,7 +254,6 @@ class ThematicDialog extends Component { dataElementError, dataSetError, programError, - calculationError, eventDataItemError, programIndicatorError, periodTypeError, @@ -409,14 +407,6 @@ class ThematicDialog extends Component { /> ), ]} - {valueType === dimConf.calculation.objectName && ( - - )}
@@ -618,14 +608,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/interpretations/Interpretations.js b/src/components/interpretations/Interpretations.js index ef7cb361c..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,12 +13,7 @@ const Interpretations = ({ renderCount }) => { useEffect(() => { if (isMapLoaded) { - const urlInterpretationId = getUrlParameter('interpretationid') - - if (urlInterpretationId) { - dispatch(setInterpretation(urlInterpretationId)) - dispatch(openInterpretationsPanel()) - } + dispatch(openInterpretationsPanel()) } }, [isMapLoaded, dispatch]) 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 cfc89b3e8..2fdd0f826 100644 --- a/src/util/periods.js +++ b/src/util/periods.js @@ -6,43 +6,25 @@ 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 = (includeRelativePeriods, hiddenPeriods = []) => periodTypes(includeRelativePeriods).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 = []) => @@ -66,7 +48,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) } 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"