From bff69ab9cec7685adad823197de8508e9ae83636 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Thu, 14 Dec 2023 16:20:32 +0100 Subject: [PATCH] feat: cumulative values in PT (DHIS2-5497) (#2746) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): bump tar from 4.4.13 to 4.4.19 (#1946) Bumps [tar](https://github.com/npm/node-tar) from 4.4.13 to 4.4.19. - [Release notes](https://github.com/npm/node-tar/releases) - [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/npm/node-tar/compare/v4.4.13...v4.4.19) --- updated-dependencies: - dependency-name: tar dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: support non-gregorian fixed periods (#2233) * feat: support non-gregorian fixed periods * test: fix test failing due to change in single value behaviour --------- Co-authored-by: Jan Henrik Ă˜verland * fix: dependency updates (#2243) * fix: update cli-app-scripts and analytics deps * chore: configure continuous delivery workflows (#2254) There are 4 workflows: verify PR (dhis2-verify-app.yml) -- build, lint, test, e2e-prod verify commit to dev (dhis2-verify-app.yml) -- build, lint, test, e2e-prod, report-failure-to-slack verify commit to master (dhis2-verify-app.yml) -- build, lint, test, e2e-prod, release, report-failure-to-slack nightly (nightly.yml) -- e2e-dev, report-failure In addition: * e2e-prod and e2e-dev are reusable workflows and are called from dhis2-verify-app and nightly. * removed uses: c-hive/gha-yarn-cache@v1 since it is deprecated and setup-node handles that work. * updated action versions and node versions * cypress test version tagging for features and bugs has been added (copied from line-list) * feat: single value background color change based upon legend (DHIS2-13702) (#2223) * feat: implement data icon option for SV visualization (DHIS2-10496) (#2236) * fix: do not pass a boolean for icons when saving If the option is not set, and thus its value is the same as the default (false) remove it from the current object to avoid sending a boolean value that the backend does not expect. This was causing any save to return 500. * fix: hide icon from visualization when option is toggled This didn't work before because the icons option was removed from the options object, but when the current object was merged with the new options, it retained the original value. * fix: fetch data element icon and pass it in extraOptions This is needed for SV visualizations when the "Show data item icon" options is checked and an icon is assigned to the dx dimension in the maintenance app. In that case the icon's SVG is fetched from the API and passed to the visualization API in the extraOptions object. The SVG generator embeds the icon in the SVG so it appears on the side of the value. * chore: manually bump deps (#2543) * chore: upgrade cypress to v12 and adjust project to it * chore: remove videos * chore: switch test server to debug (test.e2e is broken/slow) * chore: revert the test server changes and move to a separate PR * test: refactor clickCheckbox to check/uncheckCheckbox * test: add helper functions for totals options * test: add tests for cumulativeValues option in PT * refactor: allow non toggleable select to be disabled * refactor: allow checkbox options to be disabled * feat: disable option when cumulativeValues is checked in PT DHiS2-15728 * refactor: add helper text when used in PT DHIS2-15727 * feat: disabled options based on cumulativeValues in PT DHIS2-15728 * chore: update pot file * feat: handle disabled option in Redux store * refactor: revert changes to option components * feat: handle disabled and helpText props for disabled options * refactor: avoid extra prop and detect visType internally * feat: remove disabled before passing object to visualization generator * refactor: remove unnecessary code * refactor: streamlined code * fix: set disabledOptions on AO loading and vis type switching * fix: fix bug which cause current to loose props * chore: update pot file * refactor: rename variable for clarity * chore: fix rebase conflict resolutions * fix: avoid crash when visualization object is empty (ie. New) * refactor: allow disabled to be passed as prop * refactor: allow legend related option to be disabled * feat: disable legend option when cumulative values is enabled * refactor: add styles for titles of disabled sections * chore: regenerate pot file * fix: avoid visualization flashing when changing options Move the filtering of disabled options in the plugin, which is needed anyway to have the visualization looking the same also in dashboards. * refactor: simplify code for passing displayProperty * refactor: avoid involuntary changes to current in Redux store We still need to clone the object, and use it also for the various checks on options. * test: enhance tests, options and sorting * test: use current year instead of hardcode it * chore: use alpha version of analytics * docs: add PT to the list of vis types for cumulative values * chore: fix linting error * chore(analytics): add support for cumulative values --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mozafar Co-authored-by: Jan Henrik Ă˜verland Co-authored-by: Jen Jones Arnesen Co-authored-by: Martin Co-authored-by: HendrikThePendric --- cypress/elements/common.js | 9 +- cypress/elements/optionsModal/index.js | 8 +- cypress/elements/optionsModal/lines.js | 8 +- cypress/elements/optionsModal/outliers.js | 4 +- cypress/elements/optionsModal/totals.js | 100 ++++++++ cypress/elements/pivotTable.js | 7 + .../options/cumulativeValues.cy.js | 221 ++++++++++++++++++ cypress/integration/options/fontStyles.cy.js | 8 +- cypress/integration/options/icon.cy.js | 6 +- cypress/integration/options/lines.cy.js | 4 +- cypress/integration/visTypes/scatter.cy.js | 4 +- docs/data-visualizer.md | 2 +- i18n/en.pot | 13 +- package.json | 2 +- src/actions/ui.js | 21 ++ src/components/Visualization/Visualization.js | 16 +- .../Options/CheckboxBaseOption.js | 20 +- .../Options/ColSubTotals.js | 1 + .../VisualizationOptions/Options/ColTotals.js | 1 + .../Options/CumulativeValues.js | 29 ++- .../VisualizationOptions/Options/Legend.js | 36 ++- .../Options/LegendDisplayStrategy.js | 7 +- .../Options/LegendDisplayStyle.js | 8 +- .../VisualizationOptions/Options/LegendSet.js | 7 +- .../Options/NumberType.js | 1 + .../Options/RadioBaseOption.js | 9 +- .../Options/RowSubTotals.js | 1 + .../VisualizationOptions/Options/RowTotals.js | 1 + .../Options/SelectBaseOption.js | 11 +- .../Options/ShowLegendKey.js | 8 +- .../Options/TextBaseOption.js | 6 +- .../VisualizationOptionsManager.js | 6 + .../styles/VisualizationOptions.style.js | 6 + .../VisualizationPlugin.js | 78 ++++--- src/modules/disabledOptions.js | 34 +++ src/modules/options/config.js | 3 + src/modules/options/pivotTableConfig.js | 9 +- .../options/sections/templates/totals.js | 5 +- src/modules/ui.js | 68 ++++-- src/reducers/ui.js | 12 + yarn.lock | 8 +- 41 files changed, 680 insertions(+), 128 deletions(-) create mode 100644 cypress/elements/optionsModal/totals.js create mode 100644 cypress/integration/options/cumulativeValues.cy.js create mode 100644 src/modules/disabledOptions.js diff --git a/cypress/elements/common.js b/cypress/elements/common.js index 7b245a202f..2ff7ba0562 100644 --- a/cypress/elements/common.js +++ b/cypress/elements/common.js @@ -12,9 +12,16 @@ const loadingEl = 'dhis2-uicore-circularloader' export const expectAppToNotBeLoading = () => cy.getBySel(loadingEl, { timeout: 15000 }).should('not.exist') -export const clickCheckbox = (target) => +export const checkCheckbox = (target) => cy.getBySel(target).click().find('[type="checkbox"]').should('be.checked') +export const uncheckCheckbox = (target) => + cy + .getBySel(target) + .click() + .find('[type="checkbox"]') + .should('not.be.checked') + export const typeInput = (target, text) => cy.getBySel(target).find('input').type(text) diff --git a/cypress/elements/optionsModal/index.js b/cypress/elements/optionsModal/index.js index 1f8f7f6bdd..b1a74c55fa 100644 --- a/cypress/elements/optionsModal/index.js +++ b/cypress/elements/optionsModal/index.js @@ -51,19 +51,19 @@ export { } from './axes.js' export { - clickTrendLineCheckbox, + checkTrendLineCheckbox, selectTrendLineType, - clickTargetLineCheckbox, + checkTargetLineCheckbox, setTargetLineValue, setTargetLineLabel, - clickBaseLineCheckbox, + checkBaseLineCheckbox, setBaseLineLabel, setBaseLineValue, } from './lines.js' export { setCustomSubtitle } from './subtitle.js' -export { clickOutliersCheckbox } from './outliers.js' +export { checkOutliersCheckbox } from './outliers.js' export { setItemToAxis, setItemToType } from './series.js' diff --git a/cypress/elements/optionsModal/lines.js b/cypress/elements/optionsModal/lines.js index d7368fa505..6402fa1534 100644 --- a/cypress/elements/optionsModal/lines.js +++ b/cypress/elements/optionsModal/lines.js @@ -1,4 +1,4 @@ -import { clickCheckbox, typeInput } from '../common.js' +import { checkCheckbox, typeInput } from '../common.js' const trendLineCheckboxEl = 'option-trend-line-checkbox' const trendLineSelectEl = 'option-trend-line-select' @@ -10,14 +10,14 @@ const baseLineCheckboxEl = 'option-base-line-checkbox' const baseLineValueInputEl = 'option-base-line-value-input' const baseLineLabelInputEl = 'option-base-line-label-input' -export const clickTrendLineCheckbox = () => clickCheckbox(trendLineCheckboxEl) +export const checkTrendLineCheckbox = () => checkCheckbox(trendLineCheckboxEl) export const selectTrendLineType = (optionName) => { cy.getBySel(trendLineSelectEl).findBySel('dhis2-uicore-select').click() cy.getBySel(trendLineSelectOptionEl).contains(optionName).click() } -export const clickTargetLineCheckbox = () => clickCheckbox(targetLineCheckboxEl) +export const checkTargetLineCheckbox = () => checkCheckbox(targetLineCheckboxEl) export const setTargetLineValue = (text) => typeInput(targetLineValueInputEl, text) @@ -25,7 +25,7 @@ export const setTargetLineValue = (text) => export const setTargetLineLabel = (text) => typeInput(targetLineLabelInputEl, text) -export const clickBaseLineCheckbox = () => clickCheckbox(baseLineCheckboxEl) +export const checkBaseLineCheckbox = () => checkCheckbox(baseLineCheckboxEl) export const setBaseLineValue = (text) => typeInput(baseLineValueInputEl, text) diff --git a/cypress/elements/optionsModal/outliers.js b/cypress/elements/optionsModal/outliers.js index 93db0ea56b..678a4e2e18 100644 --- a/cypress/elements/optionsModal/outliers.js +++ b/cypress/elements/optionsModal/outliers.js @@ -1,5 +1,5 @@ -import { clickCheckbox } from '../common.js' +import { checkCheckbox } from '../common.js' const outliersCheckboxEl = 'option-outliers-enabled-checkbox' -export const clickOutliersCheckbox = () => clickCheckbox(outliersCheckboxEl) +export const checkOutliersCheckbox = () => checkCheckbox(outliersCheckboxEl) diff --git a/cypress/elements/optionsModal/totals.js b/cypress/elements/optionsModal/totals.js new file mode 100644 index 0000000000..bbe931be43 --- /dev/null +++ b/cypress/elements/optionsModal/totals.js @@ -0,0 +1,100 @@ +export const colTotalsOptionEl = 'option-col-totals' +const colSubTotalsOptionEl = 'option-col-subtotals' +const rowTotalsOptionEl = 'option-row-totals' +const rowSubTotalsOptionEl = 'option-row-subtotals' + +export const expectColumnsTotalsToBeDisabled = () => + cy + .getBySel(colTotalsOptionEl) + .find('[type="checkbox"]') + .should('be.disabled') + +export const expectColumnsTotalsToBeEnabled = () => + cy + .getBySel(colTotalsOptionEl) + .find('[type="checkbox"]') + .should('not.be.disabled') + +export const expectColumnsTotalsToBeChecked = () => + cy + .getBySel(colTotalsOptionEl) + .find('[type="checkbox"]') + .should('be.checked') + +export const expectColumnsTotalsToBeUnchecked = () => + cy + .getBySel(colTotalsOptionEl) + .find('[type="checkbox"]') + .should('not.be.checked') + +export const expectColumnsSubTotalsToBeDisabled = () => + cy + .getBySel(colSubTotalsOptionEl) + .find('[type="checkbox"]') + .should('be.disabled') + +export const expectColumnsSubTotalsToBeEnabled = () => + cy + .getBySel(colSubTotalsOptionEl) + .find('[type="checkbox"]') + .should('be.enabled') + +export const expectColumnsSubTotalsToBeChecked = () => + cy + .getBySel(colSubTotalsOptionEl) + .find('[type="checkbox"]') + .should('be.checked') + +export const expectColumnsSubTotalsToBeUnchecked = () => + cy + .getBySel(colSubTotalsOptionEl) + .find('[type="checkbox"]') + .should('not.be.checked') + +export const expectRowsTotalsToBeDisabled = () => + cy + .getBySel(rowTotalsOptionEl) + .find('[type="checkbox"]') + .should('be.disabled') + +export const expectRowsTotalsToBeEnabled = () => + cy + .getBySel(rowTotalsOptionEl) + .find('[type="checkbox"]') + .should('be.enabled') + +export const expectRowsTotalsToBeChecked = () => + cy + .getBySel(rowTotalsOptionEl) + .find('[type="checkbox"]') + .should('be.checked') + +export const expectRowsTotalsToBeUnchecked = () => + cy + .getBySel(rowTotalsOptionEl) + .find('[type="checkbox"]') + .should('not.be.checked') + +export const expectRowsSubTotalsToBeDisabled = () => + cy + .getBySel(rowSubTotalsOptionEl) + .find('[type="checkbox"]') + .should('be.disabled') + +export const expectRowsSubTotalsToBeEnabled = () => + cy + .getBySel(rowSubTotalsOptionEl) + .find('[type="checkbox"]') + .should('be.enabled') + +export const expectRowsSubTotalsToBeChecked = () => + cy + .getBySel(rowSubTotalsOptionEl) + .find('[type="checkbox"]') + .should('be.checked') + +export const expectRowsSubTotalsToBeUnchecked = () => + cy + .getBySel(rowSubTotalsOptionEl) + .find('[type="checkbox"]') + .should('not.be.checked') diff --git a/cypress/elements/pivotTable.js b/cypress/elements/pivotTable.js index e92d64a714..3b41d3982d 100644 --- a/cypress/elements/pivotTable.js +++ b/cypress/elements/pivotTable.js @@ -1,7 +1,14 @@ const valueCellEl = 'visualization-value-cell' +const headerCellEl = 'visualization-column-header' export const clickTableValueCell = (index) => cy.getBySel(valueCellEl).eq(index).click() export const expectTableValueCellsToHaveLength = (length) => cy.getBySel(valueCellEl).should('have.length', length) + +export const expectTableValueCellToContainValue = (index, value) => + cy.getBySel(valueCellEl).eq(index).contains(value) + +export const clickTableHeaderCell = (name) => + cy.getBySel(headerCellEl).contains(name).click() diff --git a/cypress/integration/options/cumulativeValues.cy.js b/cypress/integration/options/cumulativeValues.cy.js new file mode 100644 index 0000000000..0334f46e7e --- /dev/null +++ b/cypress/integration/options/cumulativeValues.cy.js @@ -0,0 +1,221 @@ +import { + AXIS_ID_COLUMNS, + AXIS_ID_ROWS, + DIMENSION_ID_DATA, + DIMENSION_ID_PERIOD, + VIS_TYPE_PIVOT_TABLE, + visTypeDisplayNames, +} from '@dhis2/analytics' +import { + clickNewCalculationButton, + clickSaveButton, + inputCalculationLabel, + selectOperatorFromListByDoubleClick, + typeInNumberField, +} from '../../elements/calculationsModal.js' +import { checkCheckbox, uncheckCheckbox } from '../../elements/common.js' +import { + clickDimensionModalHideButton, + clickDimensionModalUpdateButton, + selectDataElements, + selectFixedPeriods, + unselectAllItemsByButton, +} from '../../elements/dimensionModal/index.js' +import { openDimension } from '../../elements/dimensionsPanel.js' +import { clickContextMenuMove, openContextMenu } from '../../elements/layout.js' +import { openOptionsModal } from '../../elements/menuBar.js' +import { + OPTIONS_TAB_DATA, + OPTIONS_TAB_LEGEND, + clickOptionsModalHideButton, + clickOptionsModalUpdateButton, + clickOptionsTab, +} from '../../elements/optionsModal/index.js' +import { + colTotalsOptionEl, + expectColumnsTotalsToBeChecked, + expectColumnsTotalsToBeDisabled, + expectColumnsSubTotalsToBeDisabled, + expectRowsTotalsToBeDisabled, + expectRowsSubTotalsToBeDisabled, + expectColumnsTotalsToBeEnabled, + expectColumnsSubTotalsToBeEnabled, + expectRowsTotalsToBeEnabled, + expectRowsSubTotalsToBeEnabled, +} from '../../elements/optionsModal/totals.js' +import { + expectTableValueCellToContainValue, + clickTableHeaderCell, +} from '../../elements/pivotTable.js' +import { goToStartPage } from '../../elements/startScreen.js' +import { changeVisType } from '../../elements/visualizationTypeSelector.js' +import { TEST_DATA_ELEMENTS } from '../../utils/data.js' + +const cumulativeValuesOptionEl = 'option-cumulative-values' + +describe('Options - Cumulative values', () => { + describe('Interaction with other options (only for PT)', () => { + beforeEach(() => { + goToStartPage() + changeVisType(visTypeDisplayNames[VIS_TYPE_PIVOT_TABLE]) + }) + + it('disables/enables Totals, Number type and Legend options when cumulativeValues is checked/unchecked', () => { + openOptionsModal(OPTIONS_TAB_DATA) + checkCheckbox(cumulativeValuesOptionEl) + + // Totals + expectColumnsTotalsToBeDisabled() + expectColumnsSubTotalsToBeDisabled() + expectRowsTotalsToBeDisabled() + expectRowsSubTotalsToBeDisabled() + + // Number type + cy.getBySel('option-number-type-select') + .should('contain', 'Not supported when using cumulative values') + .find('[data-test="dhis2-uicore-select-input"]') + .should('have.class', 'disabled') + + // Legend + clickOptionsTab(OPTIONS_TAB_LEGEND) + cy.getBySel('option-legend') + .should('contain', 'Not supported when using cumulative values') + .find('[type="checkbox"]') + .should('be.disabled') + + clickOptionsTab(OPTIONS_TAB_DATA) + uncheckCheckbox(cumulativeValuesOptionEl) + + // Totals + expectColumnsTotalsToBeEnabled() + expectColumnsSubTotalsToBeEnabled() + expectRowsTotalsToBeEnabled() + expectRowsSubTotalsToBeEnabled() + + // Number type + cy.getBySel('option-number-type-select') + .should( + 'not.contain', + 'Not supported when using cumulative values' + ) + .find('[data-test="dhis2-uicore-select-input"]') + .should('not.have.class', 'disabled') + + // Legend + clickOptionsTab(OPTIONS_TAB_LEGEND) + cy.getBySel('option-legend') + .should( + 'not.contain', + 'Not supported when using cumulative values' + ) + .find('[type="checkbox"]') + .should('not.be.disabled') + + clickOptionsModalHideButton() + }) + + it('disables/enables a total option preserving its state', () => { + openOptionsModal(OPTIONS_TAB_DATA) + checkCheckbox(colTotalsOptionEl) + + expectColumnsTotalsToBeChecked() + + checkCheckbox(cumulativeValuesOptionEl) + + expectColumnsTotalsToBeDisabled() + expectColumnsTotalsToBeChecked() + + uncheckCheckbox(cumulativeValuesOptionEl) + + expectColumnsTotalsToBeEnabled() + expectColumnsTotalsToBeChecked() + + clickOptionsModalHideButton() + }) + }) + + describe('Applying cumulativeValues: Pivot table', () => { + beforeEach(() => { + goToStartPage() + changeVisType(visTypeDisplayNames[VIS_TYPE_PIVOT_TABLE]) + }) + + it('correctly shows the cumulative values', () => { + openContextMenu(DIMENSION_ID_DATA) + clickContextMenuMove(DIMENSION_ID_DATA, AXIS_ID_ROWS) + openContextMenu(DIMENSION_ID_PERIOD) + clickContextMenuMove(DIMENSION_ID_PERIOD, AXIS_ID_COLUMNS) + + // create a calculation to facilitate testing the cumulative values + openDimension(DIMENSION_ID_DATA) + clickNewCalculationButton() + selectOperatorFromListByDoubleClick('Number') + typeInNumberField(1, 1) + inputCalculationLabel('test data for cumulativeValues') + clickSaveButton() + + clickDimensionModalUpdateButton() + + openOptionsModal(OPTIONS_TAB_DATA) + checkCheckbox(cumulativeValuesOptionEl) + clickOptionsModalUpdateButton() + + Array.from({ length: 12 }, (_, i) => i).forEach((i) => + expectTableValueCellToContainValue(i, i + 1) + ) + }) + + it('correctly sort a column with cumulative values', () => { + openContextMenu(DIMENSION_ID_DATA) + clickContextMenuMove(DIMENSION_ID_DATA, AXIS_ID_ROWS) + openContextMenu(DIMENSION_ID_PERIOD) + clickContextMenuMove(DIMENSION_ID_PERIOD, AXIS_ID_COLUMNS) + + const year = new Date().getFullYear().toString() + + openDimension(DIMENSION_ID_PERIOD) + unselectAllItemsByButton() + selectFixedPeriods( + [`October ${year}`, `November ${year}`, `December ${year}`], + 'Monthly' + ) + clickDimensionModalHideButton() + + // create a calculation to facilitate testing the cumulative values + openDimension(DIMENSION_ID_DATA) + clickNewCalculationButton() + selectOperatorFromListByDoubleClick('Number') + typeInNumberField(1, 6000) + inputCalculationLabel( + 'test data for sorting cumulative values sorting' + ) + clickSaveButton() + + selectDataElements([TEST_DATA_ELEMENTS[4].name]) + + clickDimensionModalUpdateButton() + + // sort before cumulative + expectTableValueCellToContainValue(2, '6 000') + expectTableValueCellToContainValue(5, '5 266') + + clickTableHeaderCell(`December ${year}`) + + expectTableValueCellToContainValue(2, '5 266') + expectTableValueCellToContainValue(5, '6 000') + + // sort after cumulative + openOptionsModal(OPTIONS_TAB_DATA) + checkCheckbox(cumulativeValuesOptionEl) + clickOptionsModalUpdateButton() + + expectTableValueCellToContainValue(2, '18 000') + expectTableValueCellToContainValue(5, '18 488') + + clickTableHeaderCell(`December ${year}`) + + expectTableValueCellToContainValue(2, '18 000') + expectTableValueCellToContainValue(5, '18 488') + }) + }) +}) diff --git a/cypress/integration/options/fontStyles.cy.js b/cypress/integration/options/fontStyles.cy.js index 15408d334f..2cbee29b55 100644 --- a/cypress/integration/options/fontStyles.cy.js +++ b/cypress/integration/options/fontStyles.cy.js @@ -20,10 +20,10 @@ import { OPTIONS_TAB_STYLE, OPTIONS_TAB_DATA, OPTIONS_TAB_AXES, - clickTargetLineCheckbox, + checkTargetLineCheckbox, setTargetLineValue, setTargetLineLabel, - clickBaseLineCheckbox, + checkBaseLineCheckbox, setBaseLineLabel, setBaseLineValue, setAxisTitleText, @@ -221,7 +221,7 @@ describe('Options - Font styles', () => { }) it('sets target line', () => { cy.log(`Test value: ${TEST_VALUE}`) - clickTargetLineCheckbox() + checkTargetLineCheckbox() setTargetLineLabel(TEST_LABEL) setTargetLineValue(TEST_VALUE) }) @@ -280,7 +280,7 @@ describe('Options - Font styles', () => { }) it('sets base line', () => { cy.log(`Test value: ${TEST_VALUE}`) - clickBaseLineCheckbox() + checkBaseLineCheckbox() setBaseLineLabel(TEST_LABEL) setBaseLineValue(TEST_VALUE) }) diff --git a/cypress/integration/options/icon.cy.js b/cypress/integration/options/icon.cy.js index 541e586eac..ea90015372 100644 --- a/cypress/integration/options/icon.cy.js +++ b/cypress/integration/options/icon.cy.js @@ -9,7 +9,7 @@ import { visTypeDisplayNames, } from '@dhis2/analytics' import { expectVisualizationToBeVisible } from '../../elements/chart.js' -import { clickCheckbox } from '../../elements/common.js' +import { checkCheckbox } from '../../elements/common.js' import { expectSelectableDataItemsAmountToBeLeast, switchDataTypeTo, @@ -98,7 +98,7 @@ describe('Icon', () => { it(`icon shows when option is enabled for ${type}`, () => { // enable the icon openOptionsModal(OPTIONS_TAB_STYLE) - clickCheckbox('option-show-data-item-icon') + checkCheckbox('option-show-data-item-icon') clickOptionsModalHideButton() // find the data item @@ -129,7 +129,7 @@ describe('Icon', () => { it.skip('icon gets correct color when a legend is in use', () => { // enable the icon openOptionsModal(OPTIONS_TAB_STYLE) - clickCheckbox('option-show-data-item-icon') + checkCheckbox('option-show-data-item-icon') // enable the legend clickOptionsTab(OPTIONS_TAB_LEGEND) diff --git a/cypress/integration/options/lines.cy.js b/cypress/integration/options/lines.cy.js index ea8fef0357..3a332aeb84 100644 --- a/cypress/integration/options/lines.cy.js +++ b/cypress/integration/options/lines.cy.js @@ -11,7 +11,7 @@ import { openDimension } from '../../elements/dimensionsPanel.js' import { openOptionsModal } from '../../elements/menuBar.js' import { clickOptionsModalUpdateButton, - clickTrendLineCheckbox, + checkTrendLineCheckbox, OPTIONS_TAB_DATA, selectTrendLineType, } from '../../elements/optionsModal/index.js' @@ -54,7 +54,7 @@ describe('Options - Lines', () => { }) if (index === 0) { it('enables trendline', () => { - clickTrendLineCheckbox() + checkTrendLineCheckbox() }) } it('selects trendline type', () => { diff --git a/cypress/integration/visTypes/scatter.cy.js b/cypress/integration/visTypes/scatter.cy.js index 6fba12f094..d35eba4e66 100644 --- a/cypress/integration/visTypes/scatter.cy.js +++ b/cypress/integration/visTypes/scatter.cy.js @@ -41,7 +41,7 @@ import { } from '../../elements/menuBar.js' import { clickOptionsModalUpdateButton, - clickOutliersCheckbox, + checkOutliersCheckbox, OPTIONS_TAB_AXES, OPTIONS_TAB_OUTLIERS, setAxisRangeMaxValue, @@ -180,7 +180,7 @@ describe('using a Scatter chart', () => { }) it('Options -> Outliers -> enables outliers', () => { openOptionsModal(OPTIONS_TAB_OUTLIERS) - clickOutliersCheckbox() + checkOutliersCheckbox() // TODO: Set more outlier options clickOptionsModalUpdateButton() expectVisualizationToBeVisible(VIS_TYPE_SCATTER) diff --git a/docs/data-visualizer.md b/docs/data-visualizer.md index 2978d41418..3dc516c7a4 100644 --- a/docs/data-visualizer.md +++ b/docs/data-visualizer.md @@ -165,7 +165,7 @@ The display of a visualization can be changed by enabling/disabling and configur | Base line | Displays a horizontal line at the given domain value. Useful for example when you want to visualize how your performance has evolved since the beginning of a process. | | Column sub-totals | Displays sub-totals in a Pivot table for each dimension.
If you only select one dimension, sub-totals will be hidden for those columns. This is because the values will be equal to the sub-totals. | | Column totals | Displays total values in a Pivot table for each column, as well as a total for all values in the table. | -| Cumulative values | Displays cumulative values in Column, Stacked column, Bar, Stacked bar, Line and Area visualizations | +| Cumulative values | Displays cumulative values in Column, Stacked column, Bar, Stacked bar, Line, Area and Pivot Table visualizations | | Custom sort order | Controls the sort order of the values. | | Dimension labels | Shows the dimension names as part of a Pivot table. | | Hide empty categories | Hides the category items with no data from the visualization.
**Before first**: hides missing values only before the first value
**After last**: hides missing values only after the last value
**Before first and after last**: hides missing values only before the first value and after the last value
**All**: hides all missing values
This is useful for example when you create Column and Bar visualizations. | diff --git a/i18n/en.pot b/i18n/en.pot index 8e76313004..ec2d6dac34 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2023-09-06T13:41:14.540Z\n" -"PO-Revision-Date: 2023-09-06T13:41:14.540Z\n" +"POT-Creation-Date: 2023-11-13T12:11:28.959Z\n" +"PO-Revision-Date: 2023-11-13T12:11:28.959Z\n" msgid "All items" msgstr "All items" @@ -381,6 +381,9 @@ msgstr "Include cumulative" msgid "Cumulative values" msgstr "Cumulative values" +msgid "Accumulate cell values along rows" +msgstr "Accumulate cell values along rows" + msgid "Show data item icon" msgstr "Show data item icon" @@ -709,6 +712,9 @@ msgstr "Open as Map" msgid "Visually plot data on a world map. Data elements use separate map layers." msgstr "Visually plot data on a world map. Data elements use separate map layers." +msgid "Not supported when using cumulative values" +msgstr "Not supported when using cumulative values" + msgid "No data available" msgstr "No data available" @@ -1002,6 +1008,9 @@ msgstr "Lines" msgid "Totals" msgstr "Totals" +msgid "Totals are not supported when using cumulative values" +msgstr "Totals are not supported when using cumulative values" + msgid "Vertical (y) axis {{axisId}}" msgstr "Vertical (y) axis {{axisId}}" diff --git a/package.json b/package.json index 1427db228d..7901abb430 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "start-server-and-test": "^2.0.0" }, "dependencies": { - "@dhis2/analytics": "^26.1.6", + "@dhis2/analytics": "^26.2.0", "@dhis2/app-runtime": "^3.7.0", "@dhis2/app-runtime-adapter-d2": "^1.1.0", "@dhis2/app-service-datastore": "^1.0.0-beta.3", diff --git a/src/actions/ui.js b/src/actions/ui.js index fe3e3fcdfe..df704580f7 100644 --- a/src/actions/ui.js +++ b/src/actions/ui.js @@ -1,8 +1,10 @@ +import { getDisabledOptions } from '../modules/disabledOptions.js' import { SET_UI, CLEAR_UI, SET_UI_FROM_VISUALIZATION, SET_UI_TYPE, + SET_UI_DISABLED_OPTIONS, SET_UI_OPTIONS, SET_UI_LAYOUT, ADD_UI_LAYOUT_DIMENSIONS, @@ -21,6 +23,8 @@ import { UPDATE_UI_SERIES_ITEM, SET_UI_OPTION, SET_UI_OPTION_FONT_STYLE, + sGetUiType, + sGetUiOptions, } from '../reducers/ui.js' export const acSetUi = (value) => ({ @@ -43,6 +47,11 @@ export const acSetUiType = (value) => ({ value, }) +export const acSetUiDisabledOptions = (value) => ({ + type: SET_UI_DISABLED_OPTIONS, + value, +}) + export const acSetUiOptions = (value) => ({ type: SET_UI_OPTIONS, value, @@ -129,3 +138,15 @@ export const acSetUiRightSidebarOpen = () => ({ export const acClearSeriesType = () => ({ type: CLEAR_SERIES_TYPE, }) + +export const tSetUiOptionAndDisabledOptions = + (option) => (dispatch, getState) => { + dispatch(acSetUiOption(option)) + + const visType = sGetUiType(getState()) + const options = sGetUiOptions(getState()) + + dispatch( + acSetUiDisabledOptions(getDisabledOptions({ visType, options })) + ) + } diff --git a/src/components/Visualization/Visualization.js b/src/components/Visualization/Visualization.js index 2d67a2bef0..0b67d8a528 100644 --- a/src/components/Visualization/Visualization.js +++ b/src/components/Visualization/Visualization.js @@ -3,7 +3,6 @@ import debounce from 'lodash-es/debounce' import PropTypes from 'prop-types' import React, { Component, Fragment } from 'react' import { connect } from 'react-redux' -import { createSelector } from 'reselect' import { acSetChart } from '../../actions/chart.js' import { tSetCurrentFromUi } from '../../actions/current.js' import { acSetLoadError, acSetPluginLoading } from '../../actions/loader.js' @@ -183,7 +182,7 @@ export class UnconnectedVisualization extends Component { render() { const { visualization, - userSettings, + displayProperty, error, isLoading, onLoadingComplete, @@ -208,7 +207,7 @@ export class UnconnectedVisualization extends Component { onError={this.onError} onDrill={this.onDrill} style={styles.chartCanvas} - displayProperty={userSettings.displayProperty} + displayProperty={displayProperty} /> ) @@ -218,6 +217,7 @@ export class UnconnectedVisualization extends Component { UnconnectedVisualization.propTypes = { addMetadata: PropTypes.func, addParentGraphMap: PropTypes.func, + displayProperty: PropTypes.string, error: PropTypes.object, isLoading: PropTypes.bool, rightSidebarOpen: PropTypes.bool, @@ -225,24 +225,16 @@ UnconnectedVisualization.propTypes = { setCurrent: PropTypes.func, setLoadError: PropTypes.func, setUiItems: PropTypes.func, - userSettings: PropTypes.object, visualization: PropTypes.object, onLoadingComplete: PropTypes.func, } -export const userSettingsSelector = createSelector( - [sGetSettingsDisplayProperty], - (displayProperty) => ({ - displayProperty, - }) -) - const mapStateToProps = (state) => ({ visualization: sGetCurrent(state), rightSidebarOpen: sGetUiRightSidebarOpen(state), error: sGetLoadError(state), isLoading: sGetIsPluginLoading(state), - userSettings: userSettingsSelector(state), + displayProperty: sGetSettingsDisplayProperty(state), }) const mapDispatchToProps = (dispatch) => ({ diff --git a/src/components/VisualizationOptions/Options/CheckboxBaseOption.js b/src/components/VisualizationOptions/Options/CheckboxBaseOption.js index 829dcdf651..4e7ba0e712 100644 --- a/src/components/VisualizationOptions/Options/CheckboxBaseOption.js +++ b/src/components/VisualizationOptions/Options/CheckboxBaseOption.js @@ -2,8 +2,8 @@ import { CheckboxField } from '@dhis2/ui' import PropTypes from 'prop-types' import React from 'react' import { connect } from 'react-redux' -import { acSetUiOption } from '../../../actions/ui.js' -import { sGetUiOption } from '../../../reducers/ui.js' +import { tSetUiOptionAndDisabledOptions } from '../../../actions/ui.js' +import { sGetUiOption, sGetUiDisabledOption } from '../../../reducers/ui.js' import { tabSectionOption, tabSectionOptionToggleable, @@ -19,6 +19,7 @@ export const UnconnectedCheckboxBaseOption = ({ inverted, fontStyleKey, dataTest, + disabled, }) => (
onChange(inverted ? !checked : checked)} dense dataTest={dataTest} + disabled={disabled} /> {((!inverted && value) || (inverted && !value)) && fontStyleKey ? (
@@ -43,6 +45,7 @@ export const UnconnectedCheckboxBaseOption = ({ UnconnectedCheckboxBaseOption.propTypes = { dataTest: PropTypes.string, + disabled: PropTypes.bool, fontStyleKey: PropTypes.string, helpText: PropTypes.string, inverted: PropTypes.bool, @@ -53,18 +56,25 @@ UnconnectedCheckboxBaseOption.propTypes = { } const mapStateToProps = (state, ownProps) => ({ + disabled: Boolean( + sGetUiDisabledOption(state, ownProps.option) ?? ownProps.disabled + ), + helpText: + sGetUiDisabledOption(state, ownProps.option)?.helpText || + ownProps.helpText, value: sGetUiOption(state, ownProps.option) || false, }) const mapDispatchToProps = (dispatch, ownProps) => ({ - onChange: (value) => + onChange: (value) => { dispatch( - acSetUiOption({ + tSetUiOptionAndDisabledOptions({ optionId: ownProps.option.id || ownProps.option.name, axisId: ownProps.option.axisId, value, }) - ), + ) + }, }) export const CheckboxBaseOption = connect( diff --git a/src/components/VisualizationOptions/Options/ColSubTotals.js b/src/components/VisualizationOptions/Options/ColSubTotals.js index bcb36c01d7..1d5e922278 100644 --- a/src/components/VisualizationOptions/Options/ColSubTotals.js +++ b/src/components/VisualizationOptions/Options/ColSubTotals.js @@ -8,6 +8,7 @@ const ColSubTotals = () => ( option={{ name: 'colSubTotals', }} + dataTest="option-col-subtotals" /> ) diff --git a/src/components/VisualizationOptions/Options/ColTotals.js b/src/components/VisualizationOptions/Options/ColTotals.js index c67b8650e2..322a1dcfbd 100644 --- a/src/components/VisualizationOptions/Options/ColTotals.js +++ b/src/components/VisualizationOptions/Options/ColTotals.js @@ -8,6 +8,7 @@ const ColTotals = () => ( option={{ name: 'colTotals', }} + dataTest="option-col-totals" /> ) diff --git a/src/components/VisualizationOptions/Options/CumulativeValues.js b/src/components/VisualizationOptions/Options/CumulativeValues.js index b4cb4fa7ac..630cc17e90 100644 --- a/src/components/VisualizationOptions/Options/CumulativeValues.js +++ b/src/components/VisualizationOptions/Options/CumulativeValues.js @@ -1,14 +1,27 @@ +import { VIS_TYPE_PIVOT_TABLE } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' import React from 'react' +import { useSelector } from 'react-redux' +import { sGetUiType } from '../../../reducers/ui.js' import { CheckboxBaseOption } from './CheckboxBaseOption.js' -const CumulativeValues = () => ( - -) +const CumulativeValues = () => { + const visType = useSelector(sGetUiType) + + return ( + + ) +} export default CumulativeValues diff --git a/src/components/VisualizationOptions/Options/Legend.js b/src/components/VisualizationOptions/Options/Legend.js index 30e4825be1..03923b2979 100644 --- a/src/components/VisualizationOptions/Options/Legend.js +++ b/src/components/VisualizationOptions/Options/Legend.js @@ -4,7 +4,7 @@ import { LEGEND_DISPLAY_STYLE_FILL, } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' -import { Checkbox, FieldSet, Legend as UiCoreLegend } from '@dhis2/ui' +import { Checkbox, FieldSet, Help, Legend as UiCoreLegend } from '@dhis2/ui' import cx from 'classnames' import PropTypes from 'prop-types' import React, { useState } from 'react' @@ -15,22 +15,27 @@ import { OPTION_LEGEND_DISPLAY_STYLE, OPTION_LEGEND_SET, } from '../../../modules/options.js' -import { sGetUiOption } from '../../../reducers/ui.js' +import { sGetUiOption, sGetUiDisabledOption } from '../../../reducers/ui.js' import { tabSectionOptionToggleable, tabSectionOption, tabSectionTitle, + tabSectionTitleDisabled, tabSectionTitleMargin, } from '../styles/VisualizationOptions.style.js' import LegendDisplayStrategy from './LegendDisplayStrategy.js' import LegendDisplayStyle from './LegendDisplayStyle.js' import ShowLegendKey from './ShowLegendKey.js' +const optionName = 'legend' + const Legend = ({ legendSet, legendDisplayStrategy, onChange, + helpText, hideStyleOptions, + disabled, }) => { const [legendEnabled, setLegendEnabled] = useState( !( @@ -69,9 +74,11 @@ const Legend = ({ } return ( -
+
{i18n.t('Legend style')}
- +
@@ -104,33 +115,44 @@ const Legend = ({ className={cx(tabSectionTitle.className, { [tabSectionTitleMargin.className]: hideStyleOptions, + [tabSectionTitleDisabled.className]: + disabled, })} > {i18n.t('Legend type')}
- +
- +
) : null} + {helpText && ( + + {helpText} + + )}
) } Legend.propTypes = { onChange: PropTypes.func.isRequired, + disabled: PropTypes.bool, + helpText: PropTypes.string, hideStyleOptions: PropTypes.bool, legendDisplayStrategy: PropTypes.string, legendSet: PropTypes.object, } const mapStateToProps = (state) => ({ + disabled: Boolean(sGetUiDisabledOption(state, { name: optionName })), + helpText: sGetUiDisabledOption(state, { name: optionName })?.helpText, legendSet: sGetUiOption(state, { id: OPTION_LEGEND_SET }), legendDisplayStrategy: sGetUiOption(state, { id: OPTION_LEGEND_DISPLAY_STRATEGY, diff --git a/src/components/VisualizationOptions/Options/LegendDisplayStrategy.js b/src/components/VisualizationOptions/Options/LegendDisplayStrategy.js index 44ee0b5a33..8f3d16ce0c 100644 --- a/src/components/VisualizationOptions/Options/LegendDisplayStrategy.js +++ b/src/components/VisualizationOptions/Options/LegendDisplayStrategy.js @@ -13,13 +13,14 @@ import { sGetUiOption } from '../../../reducers/ui.js' import { tabSectionOptionToggleable } from '../styles/VisualizationOptions.style.js' import LegendSet from './LegendSet.js' -const LegendDisplayStrategy = ({ value, onChange }) => ( +const LegendDisplayStrategy = ({ value, onChange, disabled }) => ( ( key={LEGEND_DISPLAY_STRATEGY_FIXED} label={i18n.t('Select a legend')} value={LEGEND_DISPLAY_STRATEGY_FIXED} + disabled={disabled} checked={value === LEGEND_DISPLAY_STRATEGY_FIXED} onChange={onChange} dense @@ -37,7 +39,7 @@ const LegendDisplayStrategy = ({ value, onChange }) => ( {value === LEGEND_DISPLAY_STRATEGY_FIXED ? (
- +
) : null}
@@ -46,6 +48,7 @@ const LegendDisplayStrategy = ({ value, onChange }) => ( LegendDisplayStrategy.propTypes = { value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, + disabled: PropTypes.bool, } const mapStateToProps = (state) => ({ diff --git a/src/components/VisualizationOptions/Options/LegendDisplayStyle.js b/src/components/VisualizationOptions/Options/LegendDisplayStyle.js index bd7699f782..819fdaa481 100644 --- a/src/components/VisualizationOptions/Options/LegendDisplayStyle.js +++ b/src/components/VisualizationOptions/Options/LegendDisplayStyle.js @@ -3,11 +3,12 @@ import { LEGEND_DISPLAY_STYLE_TEXT, } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' +import PropTypes from 'prop-types' import React from 'react' import { OPTION_LEGEND_DISPLAY_STYLE } from '../../../modules/options.js' import { default as RadioBaseOption } from './RadioBaseOption.js' -const LegendDisplayStyle = () => ( +const LegendDisplayStyle = ({ disabled }) => ( ( }, ], }} + disabled={disabled} dataTest={'legend-display-style'} /> ) +LegendDisplayStyle.propTypes = { + disabled: PropTypes.bool, +} + export default LegendDisplayStyle diff --git a/src/components/VisualizationOptions/Options/LegendSet.js b/src/components/VisualizationOptions/Options/LegendSet.js index 2fa17a0ec7..307c62afe2 100644 --- a/src/components/VisualizationOptions/Options/LegendSet.js +++ b/src/components/VisualizationOptions/Options/LegendSet.js @@ -29,10 +29,12 @@ const LegendSetSelect = ({ onFocus, onChange, dataTest, + disabled, }) => ( { +const LegendSet = ({ value, onChange, disabled, dataTest }) => { const engine = useDataEngine() const [options, setOptions] = useState([]) @@ -103,6 +106,7 @@ const LegendSet = ({ value, onChange, dataTest }) => { return ( { LegendSet.propTypes = { onChange: PropTypes.func.isRequired, dataTest: PropTypes.string, + disabled: PropTypes.bool, value: PropTypes.object, } diff --git a/src/components/VisualizationOptions/Options/NumberType.js b/src/components/VisualizationOptions/Options/NumberType.js index b69ee1047b..82e24c1800 100644 --- a/src/components/VisualizationOptions/Options/NumberType.js +++ b/src/components/VisualizationOptions/Options/NumberType.js @@ -6,6 +6,7 @@ const NumberType = () => ( ({ + disabled: Boolean( + sGetUiDisabledOption(state, ownProps.option) ?? ownProps.disabled + ), value: ownProps.option.id ? sGetUiOption(state, { id: ownProps.option.id }) : sGetUiOptions(state)[ownProps.option.name], diff --git a/src/components/VisualizationOptions/Options/RowSubTotals.js b/src/components/VisualizationOptions/Options/RowSubTotals.js index 38c1c53f46..f886ec6ee8 100644 --- a/src/components/VisualizationOptions/Options/RowSubTotals.js +++ b/src/components/VisualizationOptions/Options/RowSubTotals.js @@ -8,6 +8,7 @@ const RowSubTotals = () => ( option={{ name: 'rowSubTotals', }} + dataTest="option-row-subtotals" /> ) diff --git a/src/components/VisualizationOptions/Options/RowTotals.js b/src/components/VisualizationOptions/Options/RowTotals.js index ddf7028871..5daa6f2597 100644 --- a/src/components/VisualizationOptions/Options/RowTotals.js +++ b/src/components/VisualizationOptions/Options/RowTotals.js @@ -8,6 +8,7 @@ const RowTotals = () => ( option={{ name: 'rowTotals', }} + dataTest="option-row-totals" /> ) diff --git a/src/components/VisualizationOptions/Options/SelectBaseOption.js b/src/components/VisualizationOptions/Options/SelectBaseOption.js index af8420046e..d6d6c94abb 100644 --- a/src/components/VisualizationOptions/Options/SelectBaseOption.js +++ b/src/components/VisualizationOptions/Options/SelectBaseOption.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import React, { useState } from 'react' import { connect } from 'react-redux' import { acSetUiOptions } from '../../../actions/ui.js' -import { sGetUiOptions } from '../../../reducers/ui.js' +import { sGetUiOption, sGetUiDisabledOption } from '../../../reducers/ui.js' import { tabSectionOption, tabSectionOptionToggleable, @@ -41,7 +41,7 @@ export const UnconnectedSelectBaseOption = ({ dataTest={`${dataTest}-checkbox`} /> ) : null} - {(!toggleable || checked) && !disabled ? ( + {!toggleable || checked ? (
{option.items.map(({ value, label }) => ( ({ - value: sGetUiOptions(state)[ownProps.option.name], + disabled: Boolean(sGetUiDisabledOption(state, ownProps.option)), + helpText: + sGetUiDisabledOption(state, ownProps.option)?.helpText || + ownProps.helpText, + value: sGetUiOption(state, ownProps.option), }) const mapDispatchToProps = (dispatch, ownProps) => ({ diff --git a/src/components/VisualizationOptions/Options/ShowLegendKey.js b/src/components/VisualizationOptions/Options/ShowLegendKey.js index a5211b697e..bb999582fb 100644 --- a/src/components/VisualizationOptions/Options/ShowLegendKey.js +++ b/src/components/VisualizationOptions/Options/ShowLegendKey.js @@ -1,16 +1,22 @@ import i18n from '@dhis2/d2-i18n' +import PropTypes from 'prop-types' import React from 'react' import { OPTION_SHOW_LEGEND_KEY } from '../../../modules/options.js' import { CheckboxBaseOption } from './CheckboxBaseOption.js' -const ShowLegendKey = () => ( +const ShowLegendKey = ({ disabled }) => ( ) +ShowLegendKey.propTypes = { + disabled: PropTypes.bool, +} + export default ShowLegendKey diff --git a/src/components/VisualizationOptions/Options/TextBaseOption.js b/src/components/VisualizationOptions/Options/TextBaseOption.js index 2fac2d5aaf..ac4c4795d7 100644 --- a/src/components/VisualizationOptions/Options/TextBaseOption.js +++ b/src/components/VisualizationOptions/Options/TextBaseOption.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import React from 'react' import { connect } from 'react-redux' import { acSetUiOption } from '../../../actions/ui.js' -import { sGetUiOption } from '../../../reducers/ui.js' +import { sGetUiOption, sGetUiDisabledOption } from '../../../reducers/ui.js' import { tabSectionOption, tabSectionOptionToggleable, @@ -112,6 +112,10 @@ UnconnectedTextBaseOption.propTypes = { } const mapStateToProps = (state, ownProps) => ({ + disabled: Boolean(sGetUiDisabledOption(state, ownProps.option)), + helpText: + sGetUiDisabledOption(state, ownProps.option)?.helpText || + ownProps.helpText, value: sGetUiOption(state, ownProps.option) || '', checked: sGetUiOption(state, { diff --git a/src/components/VisualizationOptions/VisualizationOptionsManager.js b/src/components/VisualizationOptions/VisualizationOptionsManager.js index 3bc6608ef0..bae0102753 100644 --- a/src/components/VisualizationOptions/VisualizationOptionsManager.js +++ b/src/components/VisualizationOptions/VisualizationOptionsManager.js @@ -8,6 +8,7 @@ import { HoverMenuList, HoverMenuListItem, VisualizationOptions, + VIS_TYPE_PIVOT_TABLE, } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' import PropTypes from 'prop-types' @@ -28,6 +29,7 @@ const VisualizationOptionsManager = ({ rowDimensionItems, columns, series, + cumulativeValues, }) => { const [selectedOptionConfigKey, setSelectedOptionConfigKey] = useState(null) const onOptionsUpdate = (handler) => { @@ -54,6 +56,8 @@ const VisualizationOptionsManager = ({ : [0], hasDimensionItemsInColumns: Boolean(columnDimensionItems.length), hasDimensionItemsInRows: Boolean(rowDimensionItems.length), + hasCumulativeValuesInPt: + visualizationType === VIS_TYPE_PIVOT_TABLE && cumulativeValues, }) return ( @@ -94,6 +98,7 @@ VisualizationOptionsManager.propTypes = { visualizationType: PropTypes.string.isRequired, columnDimensionItems: PropTypes.array, columns: PropTypes.array, + cumulativeValues: PropTypes.bool, rowDimensionItems: PropTypes.array, series: PropTypes.array, } @@ -104,6 +109,7 @@ const mapStateToProps = (state) => ({ rowDimensionItems: sGetDimensionItemsByAxis(state, AXIS_ID_ROWS), series: sGetUiOptions(state).series, columns: sGetUiLayout(state).columns, + cumulativeValues: sGetUiOptions(state).cumulativeValues, }) export default connect(mapStateToProps)(VisualizationOptionsManager) diff --git a/src/components/VisualizationOptions/styles/VisualizationOptions.style.js b/src/components/VisualizationOptions/styles/VisualizationOptions.style.js index 897f544d1a..bedbf42c60 100644 --- a/src/components/VisualizationOptions/styles/VisualizationOptions.style.js +++ b/src/components/VisualizationOptions/styles/VisualizationOptions.style.js @@ -42,6 +42,12 @@ export const tabSectionTitle = css.resolve` } ` +export const tabSectionTitleDisabled = css.resolve` + span { + color: ${colors.grey600}; + } +` + export const tabSectionTitleMargin = css.resolve` span { margin-top: ${spacers.dp8}; diff --git a/src/components/VisualizationPlugin/VisualizationPlugin.js b/src/components/VisualizationPlugin/VisualizationPlugin.js index ec8d731b46..5875e674ae 100644 --- a/src/components/VisualizationPlugin/VisualizationPlugin.js +++ b/src/components/VisualizationPlugin/VisualizationPlugin.js @@ -13,17 +13,20 @@ import { import { useDataEngine } from '@dhis2/app-runtime' import { Button, IconLegend24, Layer } from '@dhis2/ui' import cx from 'classnames' +import cloneDeep from 'lodash-es/cloneDeep' import PropTypes from 'prop-types' import React, { useEffect, useState, useCallback } from 'react' import { apiFetchLegendSets } from '../../api/legendSets.js' +import { getDisabledOptions } from '../../modules/disabledOptions.js' import { fetchData } from '../../modules/fetchData.js' +import { getOptionsFromVisualization } from '../../modules/options.js' import ChartPlugin from './ChartPlugin.js' import ContextualMenu from './ContextualMenu.js' import PivotPlugin from './PivotPlugin.js' import styles from './styles/VisualizationPlugin.module.css' export const VisualizationPlugin = ({ - visualization, + visualization: originalVisualization, displayProperty, filters, forDashboard, @@ -36,6 +39,7 @@ export const VisualizationPlugin = ({ onDrill, }) => { const engine = useDataEngine() + const [visualization, setVisualization] = useState(undefined) const [ouLevels, setOuLevels] = useState(undefined) const [fetchResult, setFetchResult] = useState(null) const [contextualMenuRef, setContextualMenuRef] = useState(undefined) @@ -121,28 +125,24 @@ export const VisualizationPlugin = ({ onDrill(args) } - const doFetchData = useCallback(async () => { - const result = await fetchData({ - dataEngine: engine, - visualization, - filters, - forDashboard, - displayProperty, - }) + const doFetchData = useCallback( + async (visualization, filters, forDashboard) => { + const result = await fetchData({ + dataEngine: engine, + visualization, + filters, + forDashboard, + displayProperty, + }) - if (result.responses.length) { - onResponsesReceived(result.responses) - } + if (result.responses.length) { + onResponsesReceived(result.responses) + } - return result - }, [ - engine, - filters, - forDashboard, - displayProperty, - onResponsesReceived, - visualization, - ]) + return result + }, + [engine, displayProperty, onResponsesReceived] + ) const doFetchLegendSets = useCallback( async (legendSetIds) => { @@ -170,9 +170,23 @@ export const VisualizationPlugin = ({ useEffect(() => { setFetchResult(null) + // filter out disabled options + const disabledOptions = getDisabledOptions({ + visType: originalVisualization.type, + options: getOptionsFromVisualization(originalVisualization), + }) + + const filteredVisualization = cloneDeep(originalVisualization) + + Object.keys(disabledOptions).forEach( + (option) => delete filteredVisualization[option] + ) + + setVisualization(filteredVisualization) + const doFetchAll = async () => { const { responses, extraOptions } = await doFetchData( - visualization, + filteredVisualization, filters, forDashboard ) @@ -186,7 +200,7 @@ export const VisualizationPlugin = ({ // DHIS2-10496: show icon on the side of the single value if an icon is assigned in Maintenance app and // the "Show data item icon" option is set in DV options if ( - Boolean(visualization.icons?.length) && + Boolean(filteredVisualization.icons?.length) && dxIds[0] && responses[0].metaData.items[dxIds[0]]?.style?.icon ) { @@ -207,10 +221,10 @@ export const VisualizationPlugin = ({ const legendSetIds = [] - switch (visualization.legend?.strategy) { + switch (filteredVisualization.legend?.strategy) { case LEGEND_DISPLAY_STRATEGY_FIXED: - if (visualization.legend?.set?.id) { - legendSetIds.push(visualization.legend.set.id) + if (filteredVisualization.legend?.set?.id) { + legendSetIds.push(filteredVisualization.legend.set.id) } break case LEGEND_DISPLAY_STRATEGY_BY_DATA_ITEM: { @@ -230,12 +244,12 @@ export const VisualizationPlugin = ({ const legendSets = await doFetchLegendSets(legendSetIds) setFetchResult({ - visualization, + visualization: filteredVisualization, legendSets, responses, extraOptions, }) - setShowLegendKey(visualization.legend?.showKey) + setShowLegendKey(filteredVisualization.legend?.showKey) onLoadingComplete() } @@ -244,7 +258,7 @@ export const VisualizationPlugin = ({ }) /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [visualization, filters, forDashboard]) + }, [originalVisualization, filters, forDashboard]) if (!fetchResult || !ouLevels) { return null @@ -257,7 +271,7 @@ export const VisualizationPlugin = ({ : null let legendSets = [] - switch (visualization.legend?.strategy) { + switch (fetchResult.visualization.legend?.strategy) { case LEGEND_DISPLAY_STRATEGY_BY_DATA_ITEM: { if ( @@ -281,7 +295,9 @@ export const VisualizationPlugin = ({ legendSet: item.legendSet, })) - const unsupportedDimensions = (visualization.series || []) + const unsupportedDimensions = ( + fetchResult.visualization.series || [] + ) .filter((serie) => serie.type === VIS_TYPE_LINE) .map((item) => item.dimensionItem) diff --git a/src/modules/disabledOptions.js b/src/modules/disabledOptions.js new file mode 100644 index 0000000000..495c3b6603 --- /dev/null +++ b/src/modules/disabledOptions.js @@ -0,0 +1,34 @@ +import { VIS_TYPE_PIVOT_TABLE } from '@dhis2/analytics' +import i18n from '@dhis2/d2-i18n' + +export const getDisabledOptions = ({ visType, options }) => { + const disabledOptions = {} + + for (const [option, value] of Object.entries(options)) { + switch (option) { + case 'cumulativeValues': { + const helpText = i18n.t( + 'Not supported when using cumulative values' + ) + + // when checked, disabled totals and numberType options + if (visType === VIS_TYPE_PIVOT_TABLE && value) { + disabledOptions.colTotals = {} + disabledOptions.colSubTotals = {} + disabledOptions.rowTotals = {} + disabledOptions.rowSubTotals = {} + disabledOptions.numberType = { + helpText, + } + disabledOptions.legend = { + helpText, + } + } + + break + } + } + } + + return disabledOptions +} diff --git a/src/modules/options/config.js b/src/modules/options/config.js index 194e291094..b55b1c9d23 100644 --- a/src/modules/options/config.js +++ b/src/modules/options/config.js @@ -20,6 +20,7 @@ import singleValueConfig from './singleValueConfig.js' export const getOptionsByType = ({ type, + hasCumulativeValuesInPt, hasDimensionItemsInColumns, hasDimensionItemsInRows, hasDisabledSections, @@ -33,6 +34,7 @@ export const getOptionsByType = ({ const isVertical = isVerticalType(type) const defaultProps = { + hasCumulativeValuesInPt, hasDisabledSections, isStacked, isColumnBased, @@ -52,6 +54,7 @@ export const getOptionsByType = ({ return singleValueConfig() case VIS_TYPE_PIVOT_TABLE: return pivotTableConfig({ + hasCumulativeValuesInPt, hasDimensionItemsInColumns, hasDimensionItemsInRows, }) diff --git a/src/modules/options/pivotTableConfig.js b/src/modules/options/pivotTableConfig.js index 6e95b5ff87..3b36cfe3d8 100644 --- a/src/modules/options/pivotTableConfig.js +++ b/src/modules/options/pivotTableConfig.js @@ -7,6 +7,7 @@ import ColSubTotals from '../../components/VisualizationOptions/Options/ColSubTo import ColTotals from '../../components/VisualizationOptions/Options/ColTotals.js' import CompletedOnly from '../../components/VisualizationOptions/Options/CompletedOnly.js' import Cumulative from '../../components/VisualizationOptions/Options/Cumulative.js' +import CumulativeValues from '../../components/VisualizationOptions/Options/CumulativeValues.js' import DigitGroupSeparator from '../../components/VisualizationOptions/Options/DigitGroupSeparator.js' import DisplayDensity from '../../components/VisualizationOptions/Options/DisplayDensity.js' import FixColumnHeaders from '../../components/VisualizationOptions/Options/FixColumnHeaders.js' @@ -37,15 +38,21 @@ import getLimitValuesTab from './tabs/limitValues.js' import getSeriesTab from './tabs/series.js' import getStyleTab from './tabs/style.js' -export default ({ hasDimensionItemsInColumns, hasDimensionItemsInRows }) => [ +export default ({ + hasCumulativeValuesInPt, + hasDimensionItemsInColumns, + hasDimensionItemsInRows, +}) => [ getDataTab([ getDisplayTemplate({ content: React.Children.toArray([ + , , , ]), }), getTotalsTemplate({ + hasCumulativeValuesInPt, content: React.Children.toArray([ , , diff --git a/src/modules/options/sections/templates/totals.js b/src/modules/options/sections/templates/totals.js index 30484f3d2e..0d8d6fad07 100644 --- a/src/modules/options/sections/templates/totals.js +++ b/src/modules/options/sections/templates/totals.js @@ -1,7 +1,10 @@ import i18n from '@dhis2/d2-i18n' -export default ({ content }) => ({ +export default ({ hasCumulativeValuesInPt, content }) => ({ key: 'data-totals', label: i18n.t('Totals'), + helpText: hasCumulativeValuesInPt + ? i18n.t('Totals are not supported when using cumulative values') + : null, content, }) diff --git a/src/modules/ui.js b/src/modules/ui.js index 3af241b280..39b6cc6f10 100644 --- a/src/modules/ui.js +++ b/src/modules/ui.js @@ -14,6 +14,7 @@ import { VIS_TYPE_GAUGE, VIS_TYPE_SINGLE_VALUE, } from '@dhis2/analytics' +import { getDisabledOptions } from './disabledOptions.js' import { BASE_FIELD_YEARLY_SERIES } from './fields/baseFields.js' import { getInverseLayout } from './layout.js' import { getOptionsFromVisualization } from './options.js' @@ -25,24 +26,30 @@ export const ITEM_ATTRIBUTE_VERTICAL = 'VERTICAL' export const ITEM_ATTRIBUTE_HORIZONTAL = 'HORIZONTAL' // Transform from backend model to store.ui format -export const getUiFromVisualization = (vis, currentState = {}) => ({ - ...currentState, - type: vis.type || defaultVisType, - options: getOptionsFromVisualization(vis), - layout: layoutGetAxisIdDimensionIdsObject(vis), - itemsByDimension: layoutGetDimensionIdItemIdsObject(vis), - parentGraphMap: - vis.parentGraphMap || - getParentGraphMapFromVisualization(vis) || - currentState.parentGraphMap, - yearOverYearSeries: - isYearOverYear(vis.type) && vis[BASE_FIELD_YEARLY_SERIES] - ? vis[BASE_FIELD_YEARLY_SERIES] - : currentState.yearOverYearSeries, - yearOverYearCategory: isYearOverYear(vis.type) - ? vis.rows[0].items.map((item) => item.id) - : currentState.yearOverYearCategory, -}) +export const getUiFromVisualization = (vis, currentState = {}) => { + const visType = vis.type || defaultVisType + const options = getOptionsFromVisualization(vis) + + return { + ...currentState, + type: visType, + options, + disabledOptions: getDisabledOptions({ visType, options }), + layout: layoutGetAxisIdDimensionIdsObject(vis), + itemsByDimension: layoutGetDimensionIdItemIdsObject(vis), + parentGraphMap: + vis.parentGraphMap || + getParentGraphMapFromVisualization(vis) || + currentState.parentGraphMap, + yearOverYearSeries: + isYearOverYear(vis.type) && vis[BASE_FIELD_YEARLY_SERIES] + ? vis[BASE_FIELD_YEARLY_SERIES] + : currentState.yearOverYearSeries, + yearOverYearCategory: isYearOverYear(vis.type) + ? vis.rows[0].items.map((item) => item.id) + : currentState.yearOverYearCategory, + } +} // Transform from store.ui to default format const defaultUiAdapter = (ui) => ({ @@ -100,20 +107,35 @@ const scatterUiAdapter = (ui) => { } export const getAdaptedUiByType = (ui) => { + let adaptedUi + switch (ui.type) { case VIS_TYPE_YEAR_OVER_YEAR_LINE: case VIS_TYPE_YEAR_OVER_YEAR_COLUMN: { - return yearOverYearUiAdapter(ui) + adaptedUi = yearOverYearUiAdapter(ui) + break } case VIS_TYPE_PIVOT_TABLE: - return ui + adaptedUi = ui + break case VIS_TYPE_GAUGE: case VIS_TYPE_SINGLE_VALUE: - return singleValueUiAdapter(ui) + adaptedUi = singleValueUiAdapter(ui) + break case VIS_TYPE_SCATTER: - return scatterUiAdapter(ui) + adaptedUi = scatterUiAdapter(ui) + break default: - return defaultUiAdapter(ui) + adaptedUi = defaultUiAdapter(ui) + break + } + + return { + ...adaptedUi, + disabledOptions: getDisabledOptions({ + visType: adaptedUi.type, + options: adaptedUi.options, + }), } } diff --git a/src/reducers/ui.js b/src/reducers/ui.js index db5726fd84..d6c7113e53 100644 --- a/src/reducers/ui.js +++ b/src/reducers/ui.js @@ -59,6 +59,7 @@ import { export const SET_UI = 'SET_UI' export const SET_UI_FROM_VISUALIZATION = 'SET_UI_FROM_VISUALIZATION' export const SET_UI_TYPE = 'SET_UI_TYPE' +export const SET_UI_DISABLED_OPTIONS = 'SET_UI_DISABLED_OPTIONS' export const SET_UI_OPTIONS = 'SET_UI_OPTIONS' export const SET_UI_OPTION = 'SET_UI_OPTION' export const SET_UI_OPTION_FONT_STYLE = 'SET_UI_OPTION_FONT_STYLE' @@ -82,6 +83,7 @@ export const UPDATE_UI_SERIES_ITEM = 'UPDATE_UI_SERIES_ITEM' export const DEFAULT_UI = { type: defaultVisType, options: getOptionsForUi(), + disabledOptions: {}, layout: { columns: [DIMENSION_ID_DATA], rows: [DIMENSION_ID_PERIOD], @@ -150,6 +152,12 @@ export default (state = DEFAULT_UI, action) => { type: action.value, } } + case SET_UI_DISABLED_OPTIONS: { + return { + ...state, + disabledOptions: action.value, + } + } case SET_UI_OPTIONS: { return { ...state, @@ -665,6 +673,7 @@ export default (state = DEFAULT_UI, action) => { export const sGetUi = (state) => state.ui export const sGetUiType = (state) => sGetUi(state).type +export const sGetUiDisabledOptions = (state) => sGetUi(state).disabledOptions export const sGetUiOptions = (state) => sGetUi(state).options export const sGetUiLayout = (state) => sGetUi(state).layout export const sGetUiLayoutRows = (state) => sGetUi(state).layout.rows @@ -728,6 +737,9 @@ export const sGetAxisSetup = (state) => { : [] } +export const sGetUiDisabledOption = (state, option) => + sGetUiDisabledOptions(state)[option.name] + export const sGetUiOption = (state, option) => { const options = sGetUi(state).options const [axisType, axisIndex] = (option.axisId || '').split('_') diff --git a/yarn.lock b/yarn.lock index 943e3ca86f..f36c919257 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2028,10 +2028,10 @@ classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2/analytics@^26.1.6": - version "26.1.6" - resolved "https://registry.yarnpkg.com/@dhis2/analytics/-/analytics-26.1.6.tgz#d76d7aa40c4538fae6afbf7d8d5e7cfbad81efb5" - integrity sha512-XIoe2/mUjIlxzMrmA1iVeSH3zydZG7LC1LqZJQK8TBrneC7IrLhVFka+0zaGvTyA/2P35c5xFxSie7gKC8h8Og== +"@dhis2/analytics@^26.2.0": + version "26.2.0" + resolved "https://registry.yarnpkg.com/@dhis2/analytics/-/analytics-26.2.0.tgz#36a7f258ac96ddab90f4001e62257e2cc64f202e" + integrity sha512-YcJu6EHnor6pbHmwXKYumLRVy/9TxuLtBDv9JIzjt9/APZa8kbak6sT2/53pnWDnbUjzDwR8EV1UIz24vAX+ig== dependencies: "@dhis2/d2-ui-rich-text" "^7.4.1" "@dhis2/multi-calendar-dates" "1.0.0"