From 5ba893454887aa2e19d9273388528b6a2bd555e1 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Wed, 6 Sep 2023 10:12:29 +0200 Subject: [PATCH] fix: parse the ou filter correctly for all ou types (#2691) Fixes DHIS2-14544 If path exists, then parse it. Otherwise use id, which will be the case for groups, levels and UER_ORGUNITS) --- .../common/view/add_a_FILTERTYPE_filter.js | 39 +++++-- .../integration/view/dashboard_filter.feature | 8 +- .../view/dashboard_filter/create_dashboard.js | 87 +++++++++----- .../view/dashboard_filter/dashboard_filter.js | 17 +++ cypress/support/commands.js | 25 +++++ cypress/support/index.js | 1 + src/components/Item/AppItem/Item.js | 16 +-- .../AppItem/__tests__/getIframeSrc.spec.js | 106 ++++++++++++++++++ src/components/Item/AppItem/getIframeSrc.js | 15 +++ 9 files changed, 260 insertions(+), 54 deletions(-) create mode 100644 cypress/support/commands.js create mode 100644 src/components/Item/AppItem/__tests__/getIframeSrc.spec.js create mode 100644 src/components/Item/AppItem/getIframeSrc.js diff --git a/cypress/integration/common/view/add_a_FILTERTYPE_filter.js b/cypress/integration/common/view/add_a_FILTERTYPE_filter.js index dea5155bb..76f1c1dca 100644 --- a/cypress/integration/common/view/add_a_FILTERTYPE_filter.js +++ b/cypress/integration/common/view/add_a_FILTERTYPE_filter.js @@ -13,18 +13,35 @@ const FACILITY_TYPE = 'Clinic' When('I add a {string} filter', (dimensionType) => { cy.contains('Add filter').click() - // open the dimensions modal - cy.get(filterDimensionsPanelSel).contains(dimensionType).click() - // select an item in the modal - if (dimensionType === 'Period') { - cy.get(unselectedItemsSel).contains(PERIOD).dblclick() - } else if (dimensionType === 'Organisation unit') { - cy.get(orgUnitTreeSel, EXTENDED_TIMEOUT) - .find('[type="checkbox"]', EXTENDED_TIMEOUT) - .check(OU_ID) - } else { - cy.get(unselectedItemsSel).contains(FACILITY_TYPE).dblclick() + switch (dimensionType) { + case 'Period': + cy.get(filterDimensionsPanelSel).contains(dimensionType).click() + cy.get(unselectedItemsSel).contains(PERIOD).dblclick() + break + case 'Organisation unit': + cy.get(filterDimensionsPanelSel).contains(dimensionType).click() + cy.get(orgUnitTreeSel, EXTENDED_TIMEOUT) + .find('[type="checkbox"]', EXTENDED_TIMEOUT) + .check(OU_ID) + break + case 'Org unit group': + cy.get(filterDimensionsPanelSel) + .contains('Organisation unit') + .click() + cy.getByDataTest('org-unit-group-select').click() + cy.getByDataTest('dhis2-uicore-select-menu-menuwrapper') + .contains('District') + .click() + // close the popup + cy.getByDataTest('dhis2-uicore-select-menu-menuwrapper') + .closest('[data-test="dhis2-uicore-layer"]') + .click('topLeft') + break + + default: + cy.get(filterDimensionsPanelSel).contains(dimensionType).click() + cy.get(unselectedItemsSel).contains(FACILITY_TYPE).dblclick() } // confirm to apply the filter diff --git a/cypress/integration/view/dashboard_filter.feature b/cypress/integration/view/dashboard_filter.feature index b40594040..dba070671 100644 --- a/cypress/integration/view/dashboard_filter.feature +++ b/cypress/integration/view/dashboard_filter.feature @@ -2,7 +2,7 @@ Feature: Dashboard filter Scenario: I add a Period filter When I start a new dashboard - And I add a MAP and a CHART and save + And I add items and save Then the dashboard displays in view mode When I add a "Period" filter Then the Period filter is applied to the dashboard @@ -19,6 +19,12 @@ Feature: Dashboard filter When I add a "Facility Type" filter Then the Facility Type filter is applied to the dashboard + Scenario: I add a Org unit group filter + Given I open existing dashboard + Then the dashboard displays in view mode + When I add a "Org unit group" filter + Then the Org unit group filter is applied to the dashboard + Scenario: I can access the dimensions modal from the filter badge Given I open existing dashboard When I add a "Period" filter diff --git a/cypress/integration/view/dashboard_filter/create_dashboard.js b/cypress/integration/view/dashboard_filter/create_dashboard.js index 95ff07b15..29d24c6bc 100644 --- a/cypress/integration/view/dashboard_filter/create_dashboard.js +++ b/cypress/integration/view/dashboard_filter/create_dashboard.js @@ -9,6 +9,7 @@ import { dashboardChipSel, dashboardTitleSel, } from '../../../elements/viewDashboard.js' +import { getApiBaseUrl } from '../../../support/server/utils.js' import { EXTENDED_TIMEOUT, createDashboardTitle, @@ -16,41 +17,70 @@ import { const TEST_DASHBOARD_TITLE = createDashboardTitle('af') -When('I add a MAP and a CHART and save', () => { - //add the title - cy.get('[data-test="dashboard-title-input"]').type(TEST_DASHBOARD_TITLE) +const customApp = { + name: 'Users-Role-Monitor-Widget', + id: '5e43908a-3105-4baa-9a00-87a94ebdc034', +} - // add items - cy.get('[data-test="item-search"]').click() - cy.get('[data-test="item-search"]') - .find('input') - .type('Inpatient', { force: true }) +When('I add items and save', () => { + // first install a custom app + cy.request('POST', `${getApiBaseUrl()}/api/appHub/${customApp.id}`).then( + (response) => { + expect(response.status).to.eq(204) - //chart - cy.get( - '[data-test="menu-item-Inpatient: BMI this year by districts"]' - ).click() + //add the dashboard title + cy.get('[data-test="dashboard-title-input"]').type( + TEST_DASHBOARD_TITLE + ) - cy.get('[data-test="dhis2-uicore-layer"]').click('topLeft') + // open item selector + cy.get('[data-test="item-search"]').click() + cy.get('[data-test="item-search"]') + .find('input') + .type('Inpatient', { force: true }) - cy.get('[data-test="item-search"]').click() - cy.get('[data-test="item-search"]') - .find('input') - .type('ipt 2', { force: true }) + //CHART + cy.get( + '[data-test="menu-item-Inpatient: BMI this year by districts"]' + ).click() - //map - cy.get('[data-test="menu-item-ANC: IPT 2 Coverage this year"]').click() + cy.get('[data-test="dhis2-uicore-layer"]').click('topLeft') - cy.get('[data-test="dhis2-uicore-layer"]').click('topLeft') + cy.get('[data-test="item-search"]').click() + cy.get('[data-test="item-search"]') + .find('input') + .type('ipt 2', { force: true }) - //move things so the dashboard is more compact - cy.get(`${gridItemSel}.MAP`) - .trigger('mousedown') - .trigger('mousemove', { clientX: 650 }) - .trigger('mouseup') + //MAP + cy.get( + '[data-test="menu-item-ANC: IPT 2 Coverage this year"]' + ).click() - //save - cy.get('button').contains('Save changes', EXTENDED_TIMEOUT).click() + // close the item selector + cy.get('[data-test="dhis2-uicore-layer"]').click('topLeft') + + //add a custom app item + cy.get('[data-test="item-search"]').click() + cy.get('[data-test="item-search"]') + .find('input') + .type('Role Monitor', { force: true }) + + cy.contains('Role Monitor Widget').click() + + // close the item selector + cy.get('[data-test="dhis2-uicore-layer"]').click('topLeft') + + //move things so the dashboard is more compact + // eslint-disable-next-line cypress/unsafe-to-chain-command + cy.get(`${gridItemSel}.MAP`) + .trigger('mousedown') + .trigger('mousemove', { clientX: 650 }) + .trigger('mouseup') + + //save + cy.get('button').contains('Save changes', EXTENDED_TIMEOUT).click() + } + ) }) Given('I open existing dashboard', () => { @@ -79,4 +109,7 @@ Then('different dashboard displays in view mode', () => { cy.get(dashboardTitleSel) .should('be.visible') .and('not.contain', TEST_DASHBOARD_TITLE) + + // remove the custom app + cy.request('DELETE', `${getApiBaseUrl()}/api/apps/${customApp.name}`) }) diff --git a/cypress/integration/view/dashboard_filter/dashboard_filter.js b/cypress/integration/view/dashboard_filter/dashboard_filter.js index 79470891d..78124d50e 100644 --- a/cypress/integration/view/dashboard_filter/dashboard_filter.js +++ b/cypress/integration/view/dashboard_filter/dashboard_filter.js @@ -81,6 +81,23 @@ Then('the Facility Type filter is applied to the dashboard', () => { .should('be.visible') }) +Then('the Org unit group filter is applied to the dashboard', () => { + // check that the filter badge is correct + cy.get(filterBadgeSel) + .contains('Organisation unit: District') + .should('be.visible') + + // check that the custom app is loaded (see ticket DHIS2-14544) + cy.get('iframe') + .invoke('attr', 'title') + .contains('Role Monitor Widget') + .scrollIntoView() + cy.get('iframe') + .invoke('attr', 'title') + .contains('Role Monitor Widget') + .should('be.visible') +}) + Then('the filter modal is opened', () => { cy.get(dimensionsModalSel, EXTENDED_TIMEOUT).should('be.visible') }) diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 000000000..bc732e6d7 --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,25 @@ +Cypress.Commands.add('getByDataTest', (selector, ...args) => + cy.get(`[data-test=${selector}]`, ...args) +) +Cypress.Commands.add( + 'findByDataTest', + { + prevSubject: true, + }, + (subject, selector, ...args) => + cy.wrap(subject).find(`[data-test="${selector}"]`, ...args) +) + +Cypress.Commands.add( + 'containsExact', + { + prevSubject: 'optional', + }, + (subject, selector) => + cy.wrap(subject).contains( + new RegExp( + `^${selector.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}$`, //eslint-disable-line no-useless-escape + 'gm' + ) + ) +) diff --git a/cypress/support/index.js b/cypress/support/index.js index 1b2720c19..356da91ea 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -1,6 +1,7 @@ import { enableAutoLogin } from '@dhis2/cypress-commands' import { enableNetworkShim } from './server/index.js' import { getDefaultMode, isStubMode } from './server/utils.js' +import './commands.js' enableNetworkShim() diff --git a/src/components/Item/AppItem/Item.js b/src/components/Item/AppItem/Item.js index 7681c5d00..00b0ee3d4 100644 --- a/src/components/Item/AppItem/Item.js +++ b/src/components/Item/AppItem/Item.js @@ -3,27 +3,13 @@ import { Divider, colors, spacers, IconQuestion24 } from '@dhis2/ui' import PropTypes from 'prop-types' import React from 'react' import { connect } from 'react-redux' -import { FILTER_ORG_UNIT } from '../../../actions/itemFilters.js' import { EDIT, isEditMode } from '../../../modules/dashboardModes.js' import { sGetItemFiltersRoot, DEFAULT_STATE_ITEM_FILTERS, } from '../../../reducers/itemFilters.js' import ItemHeader from '../ItemHeader/ItemHeader.js' - -const getIframeSrc = (appDetails, item, itemFilters) => { - let iframeSrc = `${appDetails.launchUrl}?dashboardItemId=${item.id}` - - if (itemFilters[FILTER_ORG_UNIT] && itemFilters[FILTER_ORG_UNIT].length) { - const ouIds = itemFilters[FILTER_ORG_UNIT].map( - (ouFilter) => ouFilter.path.split('/').slice(-1)[0] - ) - - iframeSrc += `&userOrgUnit=${ouIds.join(',')}` - } - - return iframeSrc -} +import { getIframeSrc } from './getIframeSrc.js' const AppItem = ({ dashboardMode, item, itemFilters }) => { const { d2 } = useD2() diff --git a/src/components/Item/AppItem/__tests__/getIframeSrc.spec.js b/src/components/Item/AppItem/__tests__/getIframeSrc.spec.js new file mode 100644 index 000000000..fd85d6037 --- /dev/null +++ b/src/components/Item/AppItem/__tests__/getIframeSrc.spec.js @@ -0,0 +1,106 @@ +import { getIframeSrc } from '../getIframeSrc.js' + +const appDetails = { launchUrl: 'debug/dev' } +const dashboardItem = { id: 'rainbowdashitem' } +const expectedSrc = `${appDetails.launchUrl}?dashboardItemId=${dashboardItem.id}` + +describe('getIframeSrc', () => { + it('no ou filter', () => { + const ouFilter = [] + + const src = getIframeSrc(appDetails, dashboardItem, { ou: ouFilter }) + expect(src).toEqual(expectedSrc) + }) + + it('org units chosen from the tree', () => { + const ouFilter = [ + { + id: 'fdc6uOvgoji', + path: '/ImspTQPwCqd/fdc6uOvgoji', + name: 'Bombali', + }, + { + id: 'lc3eMKXaEfw', + path: '/ImspTQPwCqd/lc3eMKXaEfw', + name: 'Bonthe', + }, + ] + + const src = getIframeSrc(appDetails, dashboardItem, { ou: ouFilter }) + expect(src).toEqual( + `${expectedSrc}&userOrgUnit=fdc6uOvgoji,lc3eMKXaEfw` + ) + }) + + it('org unit group and org unit from tree', () => { + const ouFilter = [ + { + id: 'OU_GROUP-b0EsAxm8Nge', + name: 'Western Area', + }, + { + id: 'lc3eMKXaEfw', + path: '/ImspTQPwCqd/lc3eMKXaEfw', + name: 'Bonthe', + }, + ] + + const src = getIframeSrc(appDetails, dashboardItem, { ou: ouFilter }) + expect(src).toEqual( + `${expectedSrc}&userOrgUnit=OU_GROUP-b0EsAxm8Nge,lc3eMKXaEfw` + ) + }) + + it('org unit level and org unit from tree', () => { + const ouFilter = [ + { + id: 'LEVEL-m9lBJogzE95', + name: 'Facility', + }, + { + id: 'fdc6uOvgoji', + path: '/ImspTQPwCqd/fdc6uOvgoji', + name: 'Bombali', + }, + ] + + const src = getIframeSrc(appDetails, dashboardItem, { ou: ouFilter }) + expect(src).toEqual( + `${expectedSrc}&userOrgUnit=LEVEL-m9lBJogzE95,fdc6uOvgoji` + ) + }) + + it('user org unit', () => { + const ouFilter = [ + { + id: 'USER_ORGUNIT', + displayName: 'User organisation unit', + }, + ] + + const src = getIframeSrc(appDetails, dashboardItem, { ou: ouFilter }) + expect(src).toEqual(`${expectedSrc}&userOrgUnit=USER_ORGUNIT`) + }) + + it('all user org units', () => { + const ouFilter = [ + { + id: 'USER_ORGUNIT_CHILDREN', + displayName: 'User sub-units', + }, + { + id: 'USER_ORGUNIT_GRANDCHILDREN', + displayName: 'User sub-x2-units', + }, + { + id: 'USER_ORGUNIT', + displayName: 'User organisation unit', + }, + ] + + const src = getIframeSrc(appDetails, dashboardItem, { ou: ouFilter }) + expect(src).toEqual( + `${expectedSrc}&userOrgUnit=USER_ORGUNIT_CHILDREN,USER_ORGUNIT_GRANDCHILDREN,USER_ORGUNIT` + ) + }) +}) diff --git a/src/components/Item/AppItem/getIframeSrc.js b/src/components/Item/AppItem/getIframeSrc.js new file mode 100644 index 000000000..ae2b1c2c8 --- /dev/null +++ b/src/components/Item/AppItem/getIframeSrc.js @@ -0,0 +1,15 @@ +import { FILTER_ORG_UNIT } from '../../../actions/itemFilters.js' + +export const getIframeSrc = (appDetails, item, itemFilters) => { + let iframeSrc = `${appDetails.launchUrl}?dashboardItemId=${item.id}` + + if (itemFilters[FILTER_ORG_UNIT] && itemFilters[FILTER_ORG_UNIT].length) { + const ouIds = itemFilters[FILTER_ORG_UNIT].map(({ id, path }) => + path ? path.split('/').slice(-1)[0] : id + ) + + iframeSrc += `&userOrgUnit=${ouIds.join(',')}` + } + + return iframeSrc +}