diff --git a/CHANGELOG.md b/CHANGELOG.md index cecf7e271a..e90033b62c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# [100.4.0](https://github.com/dhis2/data-visualizer-app/compare/v100.3.1...v100.4.0) (2023-12-14) + + +### Bug Fixes + +* **translations:** sync translations from transifex (dev) ([65441f2](https://github.com/dhis2/data-visualizer-app/commit/65441f2b454a6e0f74567b8b7107cad63d594a04)) +* **translations:** sync translations from transifex (dev) ([005be59](https://github.com/dhis2/data-visualizer-app/commit/005be599f7dd4382f0086861d74064aedc49ac29)) + + +### Features + +* cumulative values in PT (DHIS2-5497) ([#2746](https://github.com/dhis2/data-visualizer-app/issues/2746)) ([bff69ab](https://github.com/dhis2/data-visualizer-app/commit/bff69ab9cec7685adad823197de8508e9ae83636)), closes [#1946](https://github.com/dhis2/data-visualizer-app/issues/1946) + ## [100.3.1](https://github.com/dhis2/data-visualizer-app/compare/v100.3.0...v100.3.1) (2023-11-08) 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 70faa64252..a591ac3402 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -375,6 +375,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" @@ -706,6 +709,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" @@ -999,6 +1005,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..93959761c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-visualizer-app", - "version": "100.3.1", + "version": "100.4.0", "description": "DHIS2 Data Visualizer", "license": "BSD-3-Clause", "private": true, @@ -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"