From 0ee3fadca5fba399a3103b3ddf4f217bd60e13d2 Mon Sep 17 00:00:00 2001 From: braimbault Date: Mon, 18 Nov 2024 17:48:44 +0100 Subject: [PATCH 01/28] feat: use PeriodDimension in ThematicDialog --- src/actions/layerEdit.js | 6 + .../edit/styles/LayerDialog.module.css | 6 + .../edit/thematic/ThematicDialog.js | 148 +++++++++++------- src/constants/actionTypes.js | 1 + src/reducers/layerEdit.js | 12 ++ src/util/analytics.js | 8 + 6 files changed, 121 insertions(+), 60 deletions(-) diff --git a/src/actions/layerEdit.js b/src/actions/layerEdit.js index b810d7895..afae1496d 100644 --- a/src/actions/layerEdit.js +++ b/src/actions/layerEdit.js @@ -185,6 +185,12 @@ export const setPeriodType = (periodType, keepPeriod) => ({ keepPeriod, }) +// Set periods (thematic) +export const setPeriods = (periods) => ({ + type: types.LAYER_EDIT_PERIODS_SET, + periods, +}) + // Set period (event & thematic) export const setPeriod = (period) => ({ type: types.LAYER_EDIT_PERIOD_SET, diff --git a/src/components/edit/styles/LayerDialog.module.css b/src/components/edit/styles/LayerDialog.module.css index 26111ae2e..e4e7cdde5 100644 --- a/src/components/edit/styles/LayerDialog.module.css +++ b/src/components/edit/styles/LayerDialog.module.css @@ -109,6 +109,12 @@ margin: 8px 0 0 -8px; } +/* Periods */ + +.periodDimension { + height: 410px +} + /* Tracked entity */ .teiCheckbox { diff --git a/src/components/edit/thematic/ThematicDialog.js b/src/components/edit/thematic/ThematicDialog.js index 28bde8b66..88ac4f569 100644 --- a/src/components/edit/thematic/ThematicDialog.js +++ b/src/components/edit/thematic/ThematicDialog.js @@ -1,3 +1,5 @@ +import cx from 'classnames' +import { PeriodDimension } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' import PropTypes from 'prop-types' import React, { Component } from 'react' @@ -11,7 +13,7 @@ import { setNoDataColor, setOperand, setOrgUnits, - setPeriod, + setPeriods, setPeriodType, setRenderingStrategy, setProgram, @@ -31,7 +33,7 @@ import { import { getDataItemFromColumns, getOrgUnitsFromRows, - getPeriodFromFilters, + getPeriodsFromFilters, getDimensionsFromFilters, } from '../../../util/analytics.js' import { isPeriodAvailable } from '../../../util/periods.js' @@ -75,7 +77,7 @@ class ThematicDialog extends Component { setNoDataColor: PropTypes.func.isRequired, setOperand: PropTypes.func.isRequired, setOrgUnits: PropTypes.func.isRequired, - setPeriod: PropTypes.func.isRequired, + setPeriods: PropTypes.func.isRequired, setPeriodType: PropTypes.func.isRequired, setProgram: PropTypes.func.isRequired, setRenderingStrategy: PropTypes.func.isRequired, @@ -121,12 +123,20 @@ class ThematicDialog extends Component { startDate, systemSettings, endDate, - setPeriod, + setPeriods, setOrgUnits, } = this.props + console.log( + '🚀 ~ ThematicDialog ~ componentDidMount ~ filters:', + filters + ) const dataItem = getDataItemFromColumns(columns) - const period = getPeriodFromFilters(filters) + const periods = getPeriodsFromFilters(filters) + console.log( + '🚀 ~ ThematicDialog ~ componentDidMount ~ periods:', + periods + ) const { keyAnalysisRelativePeriod: defaultPeriod, hiddenPeriods } = systemSettings @@ -147,17 +157,41 @@ class ThematicDialog extends Component { } } + console.log( + '🚀 ~ ThematicDialog ~ componentDidMount ~ periods?.length == 0:', + periods?.length == 0 + ) + console.log( + '🚀 ~ ThematicDialog ~ componentDidMount ~ !startDate:', + !startDate + ) + console.log( + '🚀 ~ ThematicDialog ~ componentDidMount ~ !endDate:', + !endDate + ) + console.log( + '🚀 ~ ThematicDialog ~ componentDidMount ~ defaultPeriod:', + defaultPeriod + ) + console.log( + '🚀 ~ ThematicDialog ~ componentDidMount ~ isPeriodAvailable(defaultPeriod, hiddenPeriods):', + isPeriodAvailable(defaultPeriod, hiddenPeriods) + ) // Set default period from system settings if ( - !period && + periods?.length == 0 && !startDate && !endDate && defaultPeriod && isPeriodAvailable(defaultPeriod, hiddenPeriods) ) { - setPeriod({ - id: defaultPeriod, - }) + const defaultPeriods = [ + { + id: defaultPeriod, + }, + ] + setPeriods(defaultPeriods) + //setPeriodType(RELATIVE_PERIODS) } // Set default org unit level @@ -186,6 +220,10 @@ class ThematicDialog extends Component { validateLayer, onLayerValidation, } = this.props + console.log( + '🚀 ~ ThematicDialog ~ componentDidUpdate ~ periodType:', + periodType + ) // Set rendering strategy to single if not relative period if ( @@ -235,6 +273,8 @@ class ThematicDialog extends Component { systemSettings, periodsSettings, } = this.props + console.log('🚀 ~ ThematicDialog ~ render ~ filters:', filters) + console.log('🚀 ~ ThematicDialog ~ render ~ periodType:', periodType) const { // Handlers @@ -243,7 +283,7 @@ class ThematicDialog extends Component { setIndicatorGroup, setNoDataColor, setOperand, - setPeriod, + setPeriods, setPeriodType, setRenderingStrategy, setProgram, @@ -267,10 +307,17 @@ class ThematicDialog extends Component { legendSetError, } = this.state - const period = getPeriodFromFilters(filters) + const periods = getPeriodsFromFilters(filters) + console.log('🚀 ~ ThematicDialog ~ render ~ periods:', periods) const dataItem = getDataItemFromColumns(columns) const dimensions = getDimensionsFromFilters(filters) + const setPeriodsFromFilter = (e) => { + console.log('🚀 ~ ThematicDialog ~ setPeriodsFromFilter ~ e:', e) + setPeriods(e.items) + //setPeriodType(RELATIVE_PERIODS) + } + return (
this.setState({ tab })}> @@ -426,55 +473,19 @@ class ThematicDialog extends Component { )} {tab === 'period' && (
- - {periodType === RELATIVE_PERIODS && ( - - )} - {((periodType && - periodType !== RELATIVE_PERIODS && - periodType !== START_END_DATES) || - (!periodType && id)) && ( - - )} - {periodType === START_END_DATES && ( - - )} - {periodType === RELATIVE_PERIODS && ( - - )}
)} {tab === 'orgunits' && ( @@ -548,8 +559,10 @@ class ThematicDialog extends Component { method, legendSet, } = this.props + console.log('🚀 ~ ThematicDialog ~ validate ~ filters:', filters) const dataItem = getDataItemFromColumns(columns) - const period = getPeriodFromFilters(filters) + const periods = getPeriodsFromFilters(filters) + console.log('🚀 ~ ThematicDialog ~ validate ~ periods:', periods) // Indicators if (valueType === dimConf.indicator.objectName) { @@ -567,6 +580,7 @@ class ThematicDialog extends Component { ) } } + console.log('🚀 ~ ThematicDialog ~ validate ~ Indicators: OK') // Data elements if ( @@ -587,6 +601,7 @@ class ThematicDialog extends Component { ) } } + console.log('🚀 ~ ThematicDialog ~ validate ~ Data elements: OK') // Reporting rates if (valueType === dimConf.dataSet.objectName && !dataItem) { @@ -596,6 +611,7 @@ class ThematicDialog extends Component { 'data' ) } + console.log('🚀 ~ ThematicDialog ~ validate ~ Reporting rates: OK') // Event data items / Program indicators if ( @@ -622,7 +638,11 @@ class ThematicDialog extends Component { ) } } + console.log( + '🚀 ~ ThematicDialog ~ validate ~ Event data items / Program indicators: OK' + ) + // Calculation if (valueType === dimConf.calculation.objectName && !dataItem) { return this.setErrorState( 'calculationError', @@ -630,8 +650,15 @@ class ThematicDialog extends Component { 'data' ) } + console.log('🚀 ~ ThematicDialog ~ validate ~ Calculation: OK') - if (!period && periodType !== START_END_DATES) { + console.log('🚀 ~ ThematicDialog ~ validate ~ periodType:', periodType) + console.log( + '🚀 ~ validate ~ periods?.length !== 0:', + periods?.length !== 0 + ) + if (periods?.length === 0) { + //&& periodType !== START_END_DATES) { return this.setErrorState( 'periodError', i18n.t('Period is required'), @@ -644,6 +671,7 @@ class ThematicDialog extends Component { return this.setErrorState('periodError', error, 'period') } } + console.log('🚀 ~ ThematicDialog ~ validate ~ periods: OK') if (!getOrgUnitsFromRows(rows).length) { return this.setErrorState( @@ -681,7 +709,7 @@ export default connect( setNoDataColor, setOperand, setOrgUnits, - setPeriod, + setPeriods, setPeriodType, setRenderingStrategy, setProgram, diff --git a/src/constants/actionTypes.js b/src/constants/actionTypes.js index ba3596a75..a1a1536e7 100644 --- a/src/constants/actionTypes.js +++ b/src/constants/actionTypes.js @@ -57,6 +57,7 @@ export const LAYER_EDIT_PROGRAM_INDICATOR_SET = 'LAYER_EDIT_PROGRAM_INDICATOR_SET' export const LAYER_EDIT_PERIOD_NAME_SET = 'LAYER_EDIT_PERIOD_NAME_SET' export const LAYER_EDIT_PERIOD_SET = 'LAYER_EDIT_PERIOD_SET' +export const LAYER_EDIT_PERIODS_SET = 'LAYER_EDIT_PERIODS_SET' export const LAYER_EDIT_START_DATE_SET = 'LAYER_EDIT_START_DATE_SET' export const LAYER_EDIT_END_DATE_SET = 'LAYER_EDIT_END_DATE_SET' export const LAYER_EDIT_FILTER_ADD = 'LAYER_EDIT_FILTER_ADD' diff --git a/src/reducers/layerEdit.js b/src/reducers/layerEdit.js index 565c843b5..cbe8a3b37 100644 --- a/src/reducers/layerEdit.js +++ b/src/reducers/layerEdit.js @@ -12,6 +12,7 @@ import { import { START_END_DATES } from '../constants/periods.js' import { setFiltersFromPeriod, + setFiltersFromPeriods, setDataItemInColumns, setOrgUnitPathInRows, removePeriodFromFilters, @@ -121,6 +122,17 @@ const layerEdit = (state = null, action) => { : [], } + case types.LAYER_EDIT_PERIODS_SET: + return { + ...state, + filters: !( + action.periods.length === 1 && + action.periods[0].id === START_END_DATES + ) + ? setFiltersFromPeriods(state.filters, action.periods) + : [], + } + case types.LAYER_EDIT_RENDERING_STRATEGY_SET: return { ...state, diff --git a/src/util/analytics.js b/src/util/analytics.js index 1dd2911d0..7c30a3741 100644 --- a/src/util/analytics.js +++ b/src/util/analytics.js @@ -83,6 +83,9 @@ export const setOrgUnitPathInRows = (rows = [], id, path) => { export const getPeriodFromFilters = (filters = []) => getDimensionItems('pe', filters)[0] +export const getPeriodsFromFilters = (filters = []) => + getDimensionItems('pe', filters) + export const removePeriodFromFilters = (filters = []) => [ ...filters.filter((f) => f.dimension !== 'pe'), ] @@ -94,6 +97,11 @@ export const setFiltersFromPeriod = (filters, period) => [ createDimension('pe', [{ ...period }]), ] +export const setFiltersFromPeriods = (filters, periods) => [ + ...removePeriodFromFilters(filters), + createDimension('pe', periods), +] + /* DYNAMIC DIMENSION FILTERS */ export const getDimensionsFromFilters = (filters = []) => From 06d6d7166e4e965220304ec72408700e196339e3 Mon Sep 17 00:00:00 2001 From: braimbault Date: Tue, 19 Nov 2024 16:51:30 +0100 Subject: [PATCH 02/28] feat: support multiple periods --- package.json | 2 +- src/components/layers/LayerCard.js | 5 ++++- .../layers/styles/LayerCard.module.css | 11 +++++++++-- src/loaders/thematicLoader.js | 17 ++++++++++++++--- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 0ef32edb1..bd952b61f 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "start-server-and-test": "^2.0.3" }, "dependencies": { - "@dhis2/analytics": "^26.8.4", + "@dhis2/analytics": "git+https://github.com/d2-ci/analytics.git#feat/DHIS2-15796", "@dhis2/app-runtime": "^3.11.2", "@dhis2/app-runtime-adapter-d2": "^1.1.0", "@dhis2/app-service-datastore": "^1.0.0-beta.3", diff --git a/src/components/layers/LayerCard.js b/src/components/layers/LayerCard.js index e209a0f03..7c6b49436 100644 --- a/src/components/layers/LayerCard.js +++ b/src/components/layers/LayerCard.js @@ -31,7 +31,10 @@ const LayerCard = ({ })} >

{title}

- {subtitle &&

{subtitle}

} + { + // TODO Handle long list of periods + subtitle &&

{subtitle}

+ }
{isOverlay && } diff --git a/src/components/layers/styles/LayerCard.module.css b/src/components/layers/styles/LayerCard.module.css index 0d95ec8eb..99773c073 100644 --- a/src/components/layers/styles/LayerCard.module.css +++ b/src/components/layers/styles/LayerCard.module.css @@ -6,7 +6,7 @@ } .cardHeader { - height: 54px; + min-height: 54px; padding: 2px 8px 0 18px; font-size: 14px; display: flex; @@ -26,15 +26,22 @@ overflow: hidden; text-overflow: ellipsis; font-weight: 500; + margin: 8px 0 0 0; +} + +.title hz3 { line-height: 17px; + font-size: 14px; + font-weight: 400; margin: 0; + color: var(--colors-grey800); } .title h3 { line-height: 17px; font-size: 14px; font-weight: 400; - margin: 0; + margin: 4px 32px 8px 0; color: var(--colors-grey800); } diff --git a/src/loaders/thematicLoader.js b/src/loaders/thematicLoader.js index 03f52b2ea..1467ec14c 100644 --- a/src/loaders/thematicLoader.js +++ b/src/loaders/thematicLoader.js @@ -23,6 +23,7 @@ import { } from '../constants/layers.js' import { getOrgUnitsFromRows, + getPeriodsFromFilters, getPeriodFromFilters, getValidDimensionsFromFilters, getDataItemFromColumns, @@ -98,9 +99,13 @@ const thematicLoader = async ({ config, engine, nameProperty }) => { const isBubbleMap = thematicMapType === THEMATIC_BUBBLE const isSingleColor = config.method === CLASSIFICATION_SINGLE_COLOR const period = getPeriodFromFilters(config.filters) + console.log('🚀 ~ thematicLoader ~ period:', period) + const periodx = getPeriodsFromFilters(config.filters) + console.log('🚀 ~ thematicLoader ~ periodx:', periodx) const periods = getPeriodsFromMetaData(data.metaData) const dimensions = getValidDimensionsFromFilters(config.filters) const names = getApiResponseNames(data) + // TODO Handle multiple maps const valuesByPeriod = !isSingleMap ? getValuesByPeriod(data) : null const valueById = getValueById(data) const valueFeatures = noDataColor @@ -150,13 +155,14 @@ const thematicLoader = async ({ config, engine, nameProperty }) => { const legend = { title: name, period: period - ? names[period.id] || period.id + ? periodx.map((pe) => names[pe.id] || pe.id).join(', ') : formatStartEndDate( getDateArray(config.startDate), getDateArray(config.endDate) ), items: legendItems, } + console.log('🚀 ~ thematicLoader ~ names:', names) if (dimensions && dimensions.length) { legend.filters = dimensions.map( @@ -262,6 +268,7 @@ const thematicLoader = async ({ config, engine, nameProperty }) => { if (noDataColor && Array.isArray(legend.items) && !isBubbleMap) { legend.items.push({ color: noDataColor, name: i18n.t('No data') }) } + console.log('🚀 ~ thematicLoader ~ legend:', legend) return { ...config, @@ -350,6 +357,9 @@ const loadData = async (config, nameProperty) => { } = config const orgUnits = getOrgUnitsFromRows(rows) const period = getPeriodFromFilters(filters) + console.log('🚀 ~ loadData ~ period:', period) + const periodx = getPeriodsFromFilters(filters) + console.log('🚀 ~ loadData ~ periodx:', periodx) const dimensions = getValidDimensionsFromFilters(config.filters) const dataItem = getDataItemFromColumns(columns) || {} const coordinateField = getCoordinateField(config) @@ -371,10 +381,11 @@ const loadData = async (config, nameProperty) => { .withDisplayProperty(nameProperty) if (!isSingleMap) { + // TODO Handle multiple maps analyticsRequest = analyticsRequest.addPeriodDimension(period.id) } else { - analyticsRequest = period - ? analyticsRequest.addPeriodFilter(period.id) + analyticsRequest = periodx + ? analyticsRequest.addPeriodFilter(periodx.map((pe) => pe.id)) : analyticsRequest.withStartDate(startDate).withEndDate(endDate) } From 67ff60df723683b95bdbd3745eaece0bb16e3c34 Mon Sep 17 00:00:00 2001 From: braimbault Date: Wed, 20 Nov 2024 09:58:41 +0100 Subject: [PATCH 03/28] fix: PeriodDimension style --- .../edit/styles/LayerDialog.module.css | 6 --- .../edit/thematic/ThematicDialog.js | 43 +++++++++---------- yarn.lock | 11 +++-- 3 files changed, 26 insertions(+), 34 deletions(-) diff --git a/src/components/edit/styles/LayerDialog.module.css b/src/components/edit/styles/LayerDialog.module.css index e4e7cdde5..26111ae2e 100644 --- a/src/components/edit/styles/LayerDialog.module.css +++ b/src/components/edit/styles/LayerDialog.module.css @@ -109,12 +109,6 @@ margin: 8px 0 0 -8px; } -/* Periods */ - -.periodDimension { - height: 410px -} - /* Tracked entity */ .teiCheckbox { diff --git a/src/components/edit/thematic/ThematicDialog.js b/src/components/edit/thematic/ThematicDialog.js index 88ac4f569..485388873 100644 --- a/src/components/edit/thematic/ThematicDialog.js +++ b/src/components/edit/thematic/ThematicDialog.js @@ -1,4 +1,3 @@ -import cx from 'classnames' import { PeriodDimension } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' import PropTypes from 'prop-types' @@ -51,11 +50,6 @@ import DimensionFilter from '../../dimensions/DimensionFilter.js' import IndicatorGroupSelect from '../../indicator/IndicatorGroupSelect.js' import IndicatorSelect from '../../indicator/IndicatorSelect.js' import OrgUnitSelect from '../../orgunits/OrgUnitSelect.js' -import PeriodSelect from '../../periods/PeriodSelect.js' -import PeriodTypeSelect from '../../periods/PeriodTypeSelect.js' -import RelativePeriodSelect from '../../periods/RelativePeriodSelect.js' -import RenderingStrategy from '../../periods/RenderingStrategy.js' -import StartEndDates from '../../periods/StartEndDates.js' import ProgramIndicatorSelect from '../../program/ProgramIndicatorSelect.js' import ProgramSelect from '../../program/ProgramSelect.js' import Labels from '../shared/Labels.js' @@ -77,8 +71,8 @@ class ThematicDialog extends Component { setNoDataColor: PropTypes.func.isRequired, setOperand: PropTypes.func.isRequired, setOrgUnits: PropTypes.func.isRequired, + //setPeriodType: PropTypes.func.isRequired, setPeriods: PropTypes.func.isRequired, - setPeriodType: PropTypes.func.isRequired, setProgram: PropTypes.func.isRequired, setRenderingStrategy: PropTypes.func.isRequired, setValueType: PropTypes.func.isRequired, @@ -88,7 +82,7 @@ class ThematicDialog extends Component { dataElementGroup: PropTypes.object, endDate: PropTypes.string, filters: PropTypes.array, - id: PropTypes.string, + //id: PropTypes.string, indicatorGroup: PropTypes.object, legendSet: PropTypes.object, method: PropTypes.number, @@ -96,7 +90,7 @@ class ThematicDialog extends Component { operand: PropTypes.bool, orgUnits: PropTypes.object, periodType: PropTypes.string, - periodsSettings: PropTypes.object, + //periodsSettings: PropTypes.object, program: PropTypes.object, radiusHigh: PropTypes.number, radiusLow: PropTypes.number, @@ -126,6 +120,7 @@ class ThematicDialog extends Component { setPeriods, setOrgUnits, } = this.props + console.log('🚀 ~ componentDidMount ~ this.props:', this.props) console.log( '🚀 ~ ThematicDialog ~ componentDidMount ~ filters:', filters @@ -141,6 +136,11 @@ class ThematicDialog extends Component { const { keyAnalysisRelativePeriod: defaultPeriod, hiddenPeriods } = systemSettings + console.log( + '🚀 ~ ThematicDialog ~ componentDidMount ~ systemSettings:', + systemSettings + ) + // Set value type if favorite is loaded if (!valueType) { if (dataItem && dataItem.dimensionItemType) { @@ -259,19 +259,19 @@ class ThematicDialog extends Component { columns, dataElementGroup, filters, - id, + // id, indicatorGroup, noDataColor, operand, periodType, - renderingStrategy, - startDate, - endDate, + // renderingStrategy, + // startDate, + // endDate, program, valueType, thematicMapType, systemSettings, - periodsSettings, + // periodsSettings, } = this.props console.log('🚀 ~ ThematicDialog ~ render ~ filters:', filters) console.log('🚀 ~ ThematicDialog ~ render ~ periodType:', periodType) @@ -284,8 +284,8 @@ class ThematicDialog extends Component { setNoDataColor, setOperand, setPeriods, - setPeriodType, - setRenderingStrategy, + // setPeriodType, + // setRenderingStrategy, setProgram, setValueType, } = this.props @@ -301,8 +301,8 @@ class ThematicDialog extends Component { calculationError, eventDataItemError, programIndicatorError, - periodTypeError, - periodError, + // periodTypeError, + //periodError, orgUnitsError, legendSetError, } = this.state @@ -473,10 +473,7 @@ class ThematicDialog extends Component { )} {tab === 'period' && (
)} diff --git a/yarn.lock b/yarn.lock index 47963db33..fe29bec97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2018,10 +2018,9 @@ classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2/analytics@^26.8.4": - version "26.8.4" - resolved "https://registry.yarnpkg.com/@dhis2/analytics/-/analytics-26.8.4.tgz#e26bb345b729f3e3b27af03d763c9a8c3de4f8ee" - integrity sha512-kQsLcLt0b9FWnzrKNF4m44CSCok7eZft6iGXvl5Kq2Oo0B/k/P3kTosSL/Hx2Oub40CbT+V6Z2bQVrgRqJ49vg== +"@dhis2/analytics@git+https://github.com/d2-ci/analytics.git#feat/DHIS2-15796": + version "26.9.2" + resolved "git+https://github.com/d2-ci/analytics.git#c3860a94bfbb927719fcef4f8eaa57a7950018b2" dependencies: "@dhis2/multi-calendar-dates" "^1.2.2" "@dnd-kit/core" "^6.0.7" @@ -2029,7 +2028,7 @@ "@dnd-kit/utilities" "^3.2.1" "@react-hook/debounce" "^4.0.0" classnames "^2.3.1" - crypto-js "^4.1.1" + crypto-js "^4.2.0" d2-utilizr "^0.2.16" d3-color "^1.2.3" highcharts "^10.3.3" @@ -6747,7 +6746,7 @@ crypto-browserify@^3.0.0: randombytes "^2.0.0" randomfill "^1.0.3" -crypto-js@^4.1.1: +crypto-js@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== From 7b3545a521fc7ac881067a62d3c71d133deebe81 Mon Sep 17 00:00:00 2001 From: braimbault Date: Mon, 25 Nov 2024 16:55:00 +0100 Subject: [PATCH 04/28] fix: get default relative periods labels --- package.json | 2 +- src/components/edit/thematic/ThematicDialog.js | 3 ++- yarn.lock | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index bd952b61f..303107fa5 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "start-server-and-test": "^2.0.3" }, "dependencies": { - "@dhis2/analytics": "git+https://github.com/d2-ci/analytics.git#feat/DHIS2-15796", + "@dhis2/analytics": "git+https://github.com/d2-ci/analytics.git#223ec5f2dd57fc9190aae3882ed8bbb51dde46fa", "@dhis2/app-runtime": "^3.11.2", "@dhis2/app-runtime-adapter-d2": "^1.1.0", "@dhis2/app-service-datastore": "^1.0.0-beta.3", diff --git a/src/components/edit/thematic/ThematicDialog.js b/src/components/edit/thematic/ThematicDialog.js index 485388873..d798c5a2f 100644 --- a/src/components/edit/thematic/ThematicDialog.js +++ b/src/components/edit/thematic/ThematicDialog.js @@ -1,4 +1,4 @@ -import { PeriodDimension } from '@dhis2/analytics' +import { PeriodDimension, getRelativePeriodsMap } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' import PropTypes from 'prop-types' import React, { Component } from 'react' @@ -188,6 +188,7 @@ class ThematicDialog extends Component { const defaultPeriods = [ { id: defaultPeriod, + name: getRelativePeriodsMap()[defaultPeriod], }, ] setPeriods(defaultPeriods) diff --git a/yarn.lock b/yarn.lock index fe29bec97..e61a7f716 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2018,9 +2018,9 @@ classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2/analytics@git+https://github.com/d2-ci/analytics.git#feat/DHIS2-15796": - version "26.9.2" - resolved "git+https://github.com/d2-ci/analytics.git#c3860a94bfbb927719fcef4f8eaa57a7950018b2" +"@dhis2/analytics@git+https://github.com/d2-ci/analytics.git#223ec5f2dd57fc9190aae3882ed8bbb51dde46fa": + version "26.9.3" + resolved "git+https://github.com/d2-ci/analytics.git#223ec5f2dd57fc9190aae3882ed8bbb51dde46fa" dependencies: "@dhis2/multi-calendar-dates" "^1.2.2" "@dnd-kit/core" "^6.0.7" From b2be0edc1aa5dc83877fde51b8fa79ba912caad3 Mon Sep 17 00:00:00 2001 From: braimbault Date: Mon, 2 Dec 2024 17:01:45 +0100 Subject: [PATCH 05/28] feat: use PeriodDimension in ThematicDialog --- i18n/en.pot | 116 +++++++++++- src/components/core/RadioGroup.js | 16 +- .../core/styles/RadioGroup.module.css | 8 + src/components/download/DownloadSettings.js | 3 + src/components/edit/LayerEdit.js | 7 +- .../edit/styles/LayerDialog.module.css | 12 ++ .../edit/thematic/ThematicDialog.js | 179 ++++++++++-------- .../layerSources/ManageLayerSourcesModal.js | 3 + .../layers/overlays/AddLayerPopover.js | 3 + .../overlays/styles/AddLayerButton.module.css | 1 - src/components/map/Map.js | 1 + src/components/periods/PeriodTypeSelect.js | 5 + src/components/periods/RenderingStrategy.js | 5 +- src/components/periods/StartEndDate.js | 61 ++++++ .../periods/StartEndDate.module.css | 15 ++ src/components/periods/StartEndDates.js | 8 +- src/constants/periods.js | 1 + src/constants/settings.js | 16 +- src/hooks/useKeyDown.js | 18 ++ src/loaders/thematicLoader.js | 9 +- src/reducers/layerEdit.js | 11 +- 21 files changed, 395 insertions(+), 103 deletions(-) create mode 100644 src/components/periods/StartEndDate.js create mode 100644 src/components/periods/StartEndDate.module.css create mode 100644 src/hooks/useKeyDown.js diff --git a/i18n/en.pot b/i18n/en.pot index 072bf313f..a132fbb2b 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: 2024-10-23T12:50:51.401Z\n" -"PO-Revision-Date: 2024-10-23T12:50:51.401Z\n" +"POT-Creation-Date: 2024-12-02T09:29:29.011Z\n" +"PO-Revision-Date: 2024-12-02T09:29:29.011Z\n" msgid "2020" msgstr "2020" @@ -461,6 +461,12 @@ msgstr "High radius should be greater than low radius" msgid "Radius should be between {{min}} and {{max}}" msgstr "Radius should be between {{min}} and {{max}}" +msgid "Choose from presets" +msgstr "Choose from presets" + +msgid "Define start - end dates" +msgstr "Define start - end dates" + msgid "Indicator group is required" msgstr "Indicator group is required" @@ -927,8 +933,8 @@ msgstr "Only one timeline is allowed." msgid "Remove other layers to enable split map views." msgstr "Remove other layers to enable split map views." -msgid "Display periods" -msgstr "Display periods" +msgid "Period display mode" +msgstr "Period display mode" msgid "Single (aggregate)" msgstr "Single (aggregate)" @@ -1413,6 +1419,108 @@ msgstr "Financial year (Start July)" msgid "Financial year (Start April)" msgstr "Financial year (Start April)" +msgid "Today" +msgstr "Today" + +msgid "Yesterday" +msgstr "Yesterday" + +msgid "Last 3 days" +msgstr "Last 3 days" + +msgid "Last 7 days" +msgstr "Last 7 days" + +msgid "Last 14 days" +msgstr "Last 14 days" + +msgid "This week" +msgstr "This week" + +msgid "Last week" +msgstr "Last week" + +msgid "Last 4 weeks" +msgstr "Last 4 weeks" + +msgid "Last 12 weeks" +msgstr "Last 12 weeks" + +msgid "Last 52 weeks" +msgstr "Last 52 weeks" + +msgid "Weeks this year" +msgstr "Weeks this year" + +msgid "This month" +msgstr "This month" + +msgid "Last month" +msgstr "Last month" + +msgid "Last 3 months" +msgstr "Last 3 months" + +msgid "Last 6 months" +msgstr "Last 6 months" + +msgid "Last 12 months" +msgstr "Last 12 months" + +msgid "Months this year" +msgstr "Months this year" + +msgid "This bimonth" +msgstr "This bimonth" + +msgid "Last bimonth" +msgstr "Last bimonth" + +msgid "Last 6 bimonths" +msgstr "Last 6 bimonths" + +msgid "Bimonths this year" +msgstr "Bimonths this year" + +msgid "This quarter" +msgstr "This quarter" + +msgid "Last quarter" +msgstr "Last quarter" + +msgid "Last 4 quarters" +msgstr "Last 4 quarters" + +msgid "Quarters this year" +msgstr "Quarters this year" + +msgid "This six-month" +msgstr "This six-month" + +msgid "Last six-month" +msgstr "Last six-month" + +msgid "Last 2 six-months" +msgstr "Last 2 six-months" + +msgid "This financial year" +msgstr "This financial year" + +msgid "Last financial year" +msgstr "Last financial year" + +msgid "Last 5 financial years" +msgstr "Last 5 financial years" + +msgid "This year" +msgstr "This year" + +msgid "Last year" +msgstr "Last year" + +msgid "Last 5 years" +msgstr "Last 5 years" + msgid "Cancelled" msgstr "Cancelled" diff --git a/src/components/core/RadioGroup.js b/src/components/core/RadioGroup.js index f746175e8..2784c4052 100644 --- a/src/components/core/RadioGroup.js +++ b/src/components/core/RadioGroup.js @@ -12,6 +12,8 @@ const RadioGroup = ({ helpText, display, onChange, + boldLabel, + compact, children, dataTest, }) => { @@ -28,10 +30,20 @@ const RadioGroup = ({
+ {label} + + } helpText={helpText} dataTest={dataTest} > @@ -47,6 +59,8 @@ RadioGroup.propTypes = { children: PropTypes.arrayOf(PropTypes.node), dataTest: PropTypes.string, display: PropTypes.string, + boldLabel: PropTypes.bool, + compact: PropTypes.bool, helpText: PropTypes.string, label: PropTypes.string, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), diff --git a/src/components/core/styles/RadioGroup.module.css b/src/components/core/styles/RadioGroup.module.css index 91d4f14b8..65e160d74 100644 --- a/src/components/core/styles/RadioGroup.module.css +++ b/src/components/core/styles/RadioGroup.module.css @@ -10,6 +10,10 @@ height: 36px; } +.radioGroup.compact fieldset>div>div>label { + height: 20px; +} + .row { margin: var(--spacers-dp8) 0; } @@ -21,3 +25,7 @@ .row fieldset > div > div > label { margin-right: var(--spacers-dp16); } + +.boldLabel { + font-weight: bold; +} \ No newline at end of file diff --git a/src/components/download/DownloadSettings.js b/src/components/download/DownloadSettings.js index a6bfd81e7..9a137cabc 100644 --- a/src/components/download/DownloadSettings.js +++ b/src/components/download/DownloadSettings.js @@ -2,6 +2,7 @@ import i18n from '@dhis2/d2-i18n' import { Button, ButtonStrip } from '@dhis2/ui' import React, { useState, useMemo, useCallback, useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' +import useKeyDown from '../../hooks/useKeyDown.js' import { setDownloadConfig } from '../../actions/download.js' import { standardizeFilename } from '../../util/dataDownload.js' import { downloadMapImage, downloadSupport } from '../../util/export-image.js' @@ -105,6 +106,8 @@ const DownloadSettings = () => { } }, [isPushAnalytics]) + useKeyDown('Escape', closeDownloadMode) + const isSupported = downloadSupport() && !error const isSplitView = !!getSplitViewLayer(mapViews) const showMarginsCheckbox = false // Not in use diff --git a/src/components/edit/LayerEdit.js b/src/components/edit/LayerEdit.js index 286909ce6..e260d855a 100644 --- a/src/components/edit/LayerEdit.js +++ b/src/components/edit/LayerEdit.js @@ -9,11 +9,12 @@ import { ButtonStrip, } from '@dhis2/ui' import PropTypes from 'prop-types' -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { connect } from 'react-redux' import { addLayer, updateLayer, cancelLayer } from '../../actions/layers.js' import { EARTH_ENGINE_LAYER } from '../../constants/layers.js' import { useOrgUnits } from '../OrgUnitsProvider.js' +import useKeyDown from '../../hooks/useKeyDown.js' import EarthEngineDialog from './earthEngine/EarthEngineDialog.js' import EventDialog from './event/EventDialog.js' import FacilityDialog from './FacilityDialog.js' @@ -72,6 +73,8 @@ const LayerEdit = ({ layer, addLayer, updateLayer, cancelLayer }) => { } } + useKeyDown('Escape', cancelLayer) + if (!layer) { return null } @@ -94,7 +97,7 @@ const LayerEdit = ({ layer, addLayer, updateLayer, cancelLayer }) => { : i18n.t('Add new {{name}} layer', { name }) return ( - + {title}
diff --git a/src/components/edit/styles/LayerDialog.module.css b/src/components/edit/styles/LayerDialog.module.css index 26111ae2e..def2b7131 100644 --- a/src/components/edit/styles/LayerDialog.module.css +++ b/src/components/edit/styles/LayerDialog.module.css @@ -80,6 +80,11 @@ .error { margin-top: var(--spacers-dp8); color: var(--theme-error); + font-size: 12px; + display: flex; + align-items: center; + line-height: 1; + gap: 8px; } .indent { @@ -109,6 +114,13 @@ margin: 8px 0 0 -8px; } +/* Thematic */ + +.navigation { + flex-grow: 0; + margin-bottom: 8px; +} + /* Tracked entity */ .teiCheckbox { diff --git a/src/components/edit/thematic/ThematicDialog.js b/src/components/edit/thematic/ThematicDialog.js index d798c5a2f..7af8a6d01 100644 --- a/src/components/edit/thematic/ThematicDialog.js +++ b/src/components/edit/thematic/ThematicDialog.js @@ -1,5 +1,6 @@ import { PeriodDimension, getRelativePeriodsMap } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' +import { SegmentedControl, IconErrorFilled24 } from '@dhis2/ui' import PropTypes from 'prop-types' import React, { Component } from 'react' import { connect } from 'react-redux' @@ -27,6 +28,7 @@ import { } from '../../../constants/layers.js' import { RELATIVE_PERIODS, + PREDEFINED_PERIODS, START_END_DATES, } from '../../../constants/periods.js' import { @@ -50,6 +52,7 @@ import DimensionFilter from '../../dimensions/DimensionFilter.js' import IndicatorGroupSelect from '../../indicator/IndicatorGroupSelect.js' import IndicatorSelect from '../../indicator/IndicatorSelect.js' import OrgUnitSelect from '../../orgunits/OrgUnitSelect.js' +import RenderingStrategy from '../../periods/RenderingStrategy.js' import ProgramIndicatorSelect from '../../program/ProgramIndicatorSelect.js' import ProgramSelect from '../../program/ProgramSelect.js' import Labels from '../shared/Labels.js' @@ -60,6 +63,7 @@ import NoDataColor from './NoDataColor.js' import RadiusSelect, { isValidRadius } from './RadiusSelect.js' import ThematicMapTypeSelect from './ThematicMapTypeSelect.js' import ValueTypeSelect from './ValueTypeSelect.js' +import StartEndDate from '../../periods/StartEndDate.js' class ThematicDialog extends Component { static propTypes = { @@ -71,7 +75,7 @@ class ThematicDialog extends Component { setNoDataColor: PropTypes.func.isRequired, setOperand: PropTypes.func.isRequired, setOrgUnits: PropTypes.func.isRequired, - //setPeriodType: PropTypes.func.isRequired, + setPeriodType: PropTypes.func.isRequired, setPeriods: PropTypes.func.isRequired, setProgram: PropTypes.func.isRequired, setRenderingStrategy: PropTypes.func.isRequired, @@ -118,31 +122,23 @@ class ThematicDialog extends Component { systemSettings, endDate, setPeriods, + setPeriodType, setOrgUnits, } = this.props - console.log('🚀 ~ componentDidMount ~ this.props:', this.props) - console.log( - '🚀 ~ ThematicDialog ~ componentDidMount ~ filters:', - filters - ) + console.log('🚀 ~ componentDidMount ~ startDate:', startDate) + console.log('🚀 ~ componentDidMount ~ endDate:', endDate) const dataItem = getDataItemFromColumns(columns) const periods = getPeriodsFromFilters(filters) - console.log( - '🚀 ~ ThematicDialog ~ componentDidMount ~ periods:', - periods - ) const { keyAnalysisRelativePeriod: defaultPeriod, hiddenPeriods } = systemSettings - console.log( - '🚀 ~ ThematicDialog ~ componentDidMount ~ systemSettings:', - systemSettings - ) - // Set value type if favorite is loaded if (!valueType) { + console.log('🚀 ~ componentDidMount ~ valueType:', valueType) + console.log('🚀 ~ componentDidMount ~ dataItem:', dataItem) + console.log('🚀 ~ componentDidMount ~ dimConf:', dimConf) if (dataItem && dataItem.dimensionItemType) { const dimension = Object.keys(dimConf).find( (dim) => @@ -157,26 +153,6 @@ class ThematicDialog extends Component { } } - console.log( - '🚀 ~ ThematicDialog ~ componentDidMount ~ periods?.length == 0:', - periods?.length == 0 - ) - console.log( - '🚀 ~ ThematicDialog ~ componentDidMount ~ !startDate:', - !startDate - ) - console.log( - '🚀 ~ ThematicDialog ~ componentDidMount ~ !endDate:', - !endDate - ) - console.log( - '🚀 ~ ThematicDialog ~ componentDidMount ~ defaultPeriod:', - defaultPeriod - ) - console.log( - '🚀 ~ ThematicDialog ~ componentDidMount ~ isPeriodAvailable(defaultPeriod, hiddenPeriods):', - isPeriodAvailable(defaultPeriod, hiddenPeriods) - ) // Set default period from system settings if ( periods?.length == 0 && @@ -192,7 +168,7 @@ class ThematicDialog extends Component { }, ] setPeriods(defaultPeriods) - //setPeriodType(RELATIVE_PERIODS) + setPeriodType({ value: PREDEFINED_PERIODS }) } // Set default org unit level @@ -221,14 +197,10 @@ class ThematicDialog extends Component { validateLayer, onLayerValidation, } = this.props - console.log( - '🚀 ~ ThematicDialog ~ componentDidUpdate ~ periodType:', - periodType - ) // Set rendering strategy to single if not relative period if ( - periodType !== RELATIVE_PERIODS && + periodType !== PREDEFINED_PERIODS && renderingStrategy !== RENDERING_STRATEGY_SINGLE ) { setRenderingStrategy(RENDERING_STRATEGY_SINGLE) @@ -260,22 +232,22 @@ class ThematicDialog extends Component { columns, dataElementGroup, filters, - // id, + id, indicatorGroup, noDataColor, operand, periodType, - // renderingStrategy, - // startDate, - // endDate, + renderingStrategy, + startDate, + endDate, program, valueType, thematicMapType, systemSettings, // periodsSettings, } = this.props - console.log('🚀 ~ ThematicDialog ~ render ~ filters:', filters) - console.log('🚀 ~ ThematicDialog ~ render ~ periodType:', periodType) + console.log('🚀 ~ render ~ endDate:', endDate) + console.log('🚀 ~ render ~ startDate:', startDate) const { // Handlers @@ -285,8 +257,8 @@ class ThematicDialog extends Component { setNoDataColor, setOperand, setPeriods, - // setPeriodType, - // setRenderingStrategy, + setPeriodType, + setRenderingStrategy, setProgram, setValueType, } = this.props @@ -303,20 +275,33 @@ class ThematicDialog extends Component { eventDataItemError, programIndicatorError, // periodTypeError, - //periodError, + periodError, orgUnitsError, legendSetError, } = this.state const periods = getPeriodsFromFilters(filters) - console.log('🚀 ~ ThematicDialog ~ render ~ periods:', periods) + console.log('🚀 ~ render ~ filters:', filters) + console.log('🚀 ~ render ~ periodType:', periodType) + const dataItem = getDataItemFromColumns(columns) const dimensions = getDimensionsFromFilters(filters) const setPeriodsFromFilter = (e) => { - console.log('🚀 ~ ThematicDialog ~ setPeriodsFromFilter ~ e:', e) setPeriods(e.items) - //setPeriodType(RELATIVE_PERIODS) + } + + const changePeriodType = (e) => { + console.log(e) + setPeriodType(e) + switch (e.value) { + case PREDEFINED_PERIODS: + // Remove Start-End dates + // Set Predefined periods (again) + case START_END_DATES: + // Remove Predefined periods + // Set value Start-End dates (again) + } } return ( @@ -477,15 +462,57 @@ class ThematicDialog extends Component { className={styles.flexRowFlow} data-test="thematicdialog-periodtab" > - +
+ +
+ {periodType === PREDEFINED_PERIODS && ( + + )} + {periodType === PREDEFINED_PERIODS && ( + + )} + {periodType === START_END_DATES && ( + + )} + {periodError && ( +
+ + {periodError} +
+ )}
)} {tab === 'orgunits' && ( @@ -559,10 +586,8 @@ class ThematicDialog extends Component { method, legendSet, } = this.props - console.log('🚀 ~ ThematicDialog ~ validate ~ filters:', filters) const dataItem = getDataItemFromColumns(columns) const periods = getPeriodsFromFilters(filters) - console.log('🚀 ~ ThematicDialog ~ validate ~ periods:', periods) // Indicators if (valueType === dimConf.indicator.objectName) { @@ -580,7 +605,6 @@ class ThematicDialog extends Component { ) } } - console.log('🚀 ~ ThematicDialog ~ validate ~ Indicators: OK') // Data elements if ( @@ -601,7 +625,6 @@ class ThematicDialog extends Component { ) } } - console.log('🚀 ~ ThematicDialog ~ validate ~ Data elements: OK') // Reporting rates if (valueType === dimConf.dataSet.objectName && !dataItem) { @@ -611,7 +634,6 @@ class ThematicDialog extends Component { 'data' ) } - console.log('🚀 ~ ThematicDialog ~ validate ~ Reporting rates: OK') // Event data items / Program indicators if ( @@ -638,9 +660,6 @@ class ThematicDialog extends Component { ) } } - console.log( - '🚀 ~ ThematicDialog ~ validate ~ Event data items / Program indicators: OK' - ) // Calculation if (valueType === dimConf.calculation.objectName && !dataItem) { @@ -650,28 +669,30 @@ class ThematicDialog extends Component { 'data' ) } - console.log('🚀 ~ ThematicDialog ~ validate ~ Calculation: OK') - console.log('🚀 ~ ThematicDialog ~ validate ~ periodType:', periodType) - console.log( - '🚀 ~ validate ~ periods?.length !== 0:', - periods?.length !== 0 - ) - if (periods?.length === 0) { - //&& periodType !== START_END_DATES) { + if (periods?.length === 0 && periodType !== START_END_DATES) { return this.setErrorState( 'periodError', i18n.t('Period is required'), 'period' ) } else if (periodType === START_END_DATES) { + console.log( + '🚀 ~ ThematicDialog ~ validate ~ periodType:', + periodType + ) const error = getStartEndDateError(startDate, endDate) + console.log( + '🚀 ~ ThematicDialog ~ validate ~ startDate:', + startDate + ) + console.log('🚀 ~ ThematicDialog ~ validate ~ endDate:', endDate) + console.log('🚀 ~ ThematicDialog ~ validate ~ error:', error) if (error) { return this.setErrorState('periodError', error, 'period') } } - console.log('🚀 ~ ThematicDialog ~ validate ~ periods: OK') if (!getOrgUnitsFromRows(rows).length) { return this.setErrorState( diff --git a/src/components/layerSources/ManageLayerSourcesModal.js b/src/components/layerSources/ManageLayerSourcesModal.js index 89f6a371d..d9e26337d 100644 --- a/src/components/layerSources/ManageLayerSourcesModal.js +++ b/src/components/layerSources/ManageLayerSourcesModal.js @@ -11,6 +11,7 @@ import PropTypes from 'prop-types' import React from 'react' import earthEngineLayers from '../../constants/earthEngineLayers/index.js' import useManagedLayerSourcesStore from '../../hooks/useManagedLayerSourcesStore.js' +import useKeyDown from '../../hooks/useKeyDown.js' import LayerSource from './LayerSource.js' import styles from './styles/ManageLayerSourcesModal.module.css' @@ -23,6 +24,8 @@ const ManageLayerSourcesModal = ({ onClose }) => { const { managedLayerSources, showLayerSource, hideLayerSource } = useManagedLayerSourcesStore() + useKeyDown('Escape', onClose) + return ( diff --git a/src/components/layers/overlays/AddLayerPopover.js b/src/components/layers/overlays/AddLayerPopover.js index 9aba2069f..13db783fd 100644 --- a/src/components/layers/overlays/AddLayerPopover.js +++ b/src/components/layers/overlays/AddLayerPopover.js @@ -7,6 +7,7 @@ import { addLayer, editLayer } from '../../../actions/layers.js' import earthEngineLayers from '../../../constants/earthEngineLayers/index.js' import { EXTERNAL_LAYER } from '../../../constants/layers.js' import useManagedLayerSourcesStore from '../../../hooks/useManagedLayerSourcesStore.js' +import useKeyDown from '../../../hooks/useKeyDown.js' import { isSplitViewMap } from '../../../util/helpers.js' import ManageLayerSourcesButton from '../../layerSources/ManageLayerSourcesButton.js' import LayerList from './LayerList.js' @@ -38,6 +39,8 @@ const AddLayerPopover = ({ anchorEl, onClose, onManaging }) => { managedLayerSources ) + useKeyDown('Escape', onClose) + const onLayerSelect = (layer) => { const config = { ...layer } const layerType = layer.layer diff --git a/src/components/layers/overlays/styles/AddLayerButton.module.css b/src/components/layers/overlays/styles/AddLayerButton.module.css index de920d003..d0c8b5dc6 100644 --- a/src/components/layers/overlays/styles/AddLayerButton.module.css +++ b/src/components/layers/overlays/styles/AddLayerButton.module.css @@ -42,7 +42,6 @@ } .button:focus { - outline: 3px solid var(--theme-focus); outline-offset: -3px; } diff --git a/src/components/map/Map.js b/src/components/map/Map.js index c999fd46e..b4cccb301 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -121,6 +121,7 @@ class Map extends Component { componentDidUpdate(prevProps) { const { resizeCount, isFullscreen, isPlugin } = this.props + console.log('🚀 ~ Map ~ componentDidUpdate ~ resizeCount:', resizeCount) if (resizeCount !== prevProps.resizeCount) { this.map.resize() diff --git a/src/components/periods/PeriodTypeSelect.js b/src/components/periods/PeriodTypeSelect.js index 2122545b6..815ee5c63 100644 --- a/src/components/periods/PeriodTypeSelect.js +++ b/src/components/periods/PeriodTypeSelect.js @@ -31,6 +31,11 @@ const PeriodTypeSelect = ({ ) if (!period || isRelativePeriod) { + console.log( + '🚀 ~ useEffect ~ isRelativePeriod:', + isRelativePeriod + ) + console.log('🚀 ~ useEffect ~ periodTypes[0]:', periodTypes[0]) // default to first period type onChange(periodTypes[0], isRelativePeriod) } diff --git a/src/components/periods/RenderingStrategy.js b/src/components/periods/RenderingStrategy.js index ea1c68e74..483ea6e85 100644 --- a/src/components/periods/RenderingStrategy.js +++ b/src/components/periods/RenderingStrategy.js @@ -64,10 +64,13 @@ const RenderingStrategy = ({ return ( { + const { startDate, endDate, setStartDate, setEndDate } = props + console.log('🚀 ~ useEffect ~ endDate:', endDate) + console.log('🚀 ~ useEffect ~ startDate:', startDate) + const hasDate = startDate !== undefined && endDate !== undefined + useEffect(() => { + if (!hasDate) { + setStartDate({ value: DEFAULT_START_DATE }) + setEndDate({ value: DEFAULT_END_DATE }) + } + }, [hasDate, setStartDate, setEndDate]) + + return hasDate ? ( + +
+ +
+ +
+ +
+
+ ) : null +} +StartEndDate.propTypes = { + setEndDate: PropTypes.func.isRequired, + setStartDate: PropTypes.func.isRequired, + startDate: PropTypes.string, + endDate: PropTypes.string, +} + +export default connect(null, { setStartDate, setEndDate })(StartEndDate) diff --git a/src/components/periods/StartEndDate.module.css b/src/components/periods/StartEndDate.module.css new file mode 100644 index 000000000..3f9a6bb4f --- /dev/null +++ b/src/components/periods/StartEndDate.module.css @@ -0,0 +1,15 @@ +.row { + display: flex; + gap: var(--spacers-dp4); + align-items: flex-end; +} +.icon { + flex-grow: 0; + height: 40px; + display: flex; + align-items: center; +} +.error { + margin-top: var(--spacers-dp8); + color: var(--theme-error); +} \ No newline at end of file diff --git a/src/components/periods/StartEndDates.js b/src/components/periods/StartEndDates.js index 12d3d8b1a..58030c249 100644 --- a/src/components/periods/StartEndDates.js +++ b/src/components/periods/StartEndDates.js @@ -20,8 +20,8 @@ const StartEndDates = (props) => { useEffect(() => { if (!hasDate) { - setStartDate(DEFAULT_START_DATE) - setEndDate(DEFAULT_END_DATE) + //setStartDate(DEFAULT_START_DATE) + //setEndDate(DEFAULT_END_DATE) } }, [hasDate, setStartDate, setEndDate]) @@ -29,13 +29,13 @@ const StartEndDates = (props) => { diff --git a/src/constants/periods.js b/src/constants/periods.js index 16deca6d0..70dfe3aa9 100644 --- a/src/constants/periods.js +++ b/src/constants/periods.js @@ -57,6 +57,7 @@ const LAST_YEAR = 'LAST_YEAR' const THIS_FINANCIAL_YEAR = 'THIS_FINANCIAL_YEAR' const LAST_FINANCIAL_YEAR = 'LAST_FINANCIAL_YEAR' +export const PREDEFINED_PERIODS = 'PREDEFINED_PERIODS' export const RELATIVE_PERIODS = 'RELATIVE_PERIODS' export const START_END_DATES = 'START_END_DATES' diff --git a/src/constants/settings.js b/src/constants/settings.js index 016a66ef1..c2334ffd0 100644 --- a/src/constants/settings.js +++ b/src/constants/settings.js @@ -6,14 +6,20 @@ export const DEFAULT_SYSTEM_SETTINGS = { keyDefaultBaseMap: FALLBACK_BASEMAP_ID, } +export const SYSTEM_SETTINGS_HIDE_DAILY_PERIODS = 'keyHideDailyPeriods' +export const SYSTEM_SETTINGS_HIDE_WEEKLY_PERIODS = 'keyHideWeeklyPeriods' +export const SYSTEM_SETTINGS_HIDE_BIWEEKLY_PERIODS = 'keyHideBiWeeklyPeriods' +export const SYSTEM_SETTINGS_HIDE_MONTHLY_PERIODS = 'keyHideMonthlyPeriods' +export const SYSTEM_SETTINGS_HIDE_BIMONTHLY_PERIODS = 'keyHideBiMonthlyPeriods' + export const SYSTEM_SETTINGS = [ 'keyAnalysisRelativePeriod', 'keyBingMapsApiKey', - 'keyHideDailyPeriods', - 'keyHideWeeklyPeriods', - 'keyHideBiWeeklyPeriods', - 'keyHideMonthlyPeriods', - 'keyHideBiMonthlyPeriods', + SYSTEM_SETTINGS_HIDE_DAILY_PERIODS, + SYSTEM_SETTINGS_HIDE_WEEKLY_PERIODS, + SYSTEM_SETTINGS_HIDE_BIWEEKLY_PERIODS, + SYSTEM_SETTINGS_HIDE_MONTHLY_PERIODS, + SYSTEM_SETTINGS_HIDE_BIMONTHLY_PERIODS, 'keyDefaultBaseMap', ] diff --git a/src/hooks/useKeyDown.js b/src/hooks/useKeyDown.js new file mode 100644 index 000000000..52bc9ae23 --- /dev/null +++ b/src/hooks/useKeyDown.js @@ -0,0 +1,18 @@ +import { useEffect } from 'react' + +const useKeyDown = (key, callback) => { + useEffect(() => { + const handleKeyDown = (event) => { + if (event.key === key) { + callback(event) + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => { + window.removeEventListener('keydown', handleKeyDown) + } + }, [key, callback]) +} + +export default useKeyDown diff --git a/src/loaders/thematicLoader.js b/src/loaders/thematicLoader.js index 1467ec14c..4e3dc81f7 100644 --- a/src/loaders/thematicLoader.js +++ b/src/loaders/thematicLoader.js @@ -355,6 +355,8 @@ const loadData = async (config, nameProperty) => { renderingStrategy = RENDERING_STRATEGY_SINGLE, eventStatus, } = config + console.log('🚀 ~ loadData ~ endDate:', endDate) + console.log('🚀 ~ loadData ~ startDate:', startDate) const orgUnits = getOrgUnitsFromRows(rows) const period = getPeriodFromFilters(filters) console.log('🚀 ~ loadData ~ period:', period) @@ -384,9 +386,10 @@ const loadData = async (config, nameProperty) => { // TODO Handle multiple maps analyticsRequest = analyticsRequest.addPeriodDimension(period.id) } else { - analyticsRequest = periodx - ? analyticsRequest.addPeriodFilter(periodx.map((pe) => pe.id)) - : analyticsRequest.withStartDate(startDate).withEndDate(endDate) + analyticsRequest = + periodx.length > 0 + ? analyticsRequest.addPeriodFilter(periodx.map((pe) => pe.id)) + : analyticsRequest.withStartDate(startDate).withEndDate(endDate) } if (dimensions) { diff --git a/src/reducers/layerEdit.js b/src/reducers/layerEdit.js index cbe8a3b37..dac685019 100644 --- a/src/reducers/layerEdit.js +++ b/src/reducers/layerEdit.js @@ -105,9 +105,13 @@ const layerEdit = (state = null, action) => { } case types.LAYER_EDIT_PERIOD_TYPE_SET: + console.log( + '🚀 ~ layerEdit ~ action.periodType:', + action.periodType + ) return { ...state, - periodType: action.periodType.id, + periodType: action.periodType.value, filters: action.keepPeriod ? state.filters : removePeriodFromFilters(state.filters), @@ -140,15 +144,16 @@ const layerEdit = (state = null, action) => { } case types.LAYER_EDIT_START_DATE_SET: + console.log('🚀 ~ layerEdit ~ action.startDate:', action.startDate) return { ...state, - startDate: action.startDate, + startDate: action.startDate.value, } case types.LAYER_EDIT_END_DATE_SET: return { ...state, - endDate: action.endDate, + endDate: action.endDate.value, } case types.LAYER_EDIT_AGGREGATION_TYPE_SET: From fcda751c6306f0b28441b048014a74a401d2e48d Mon Sep 17 00:00:00 2001 From: braimbault Date: Tue, 3 Dec 2024 15:36:43 +0100 Subject: [PATCH 06/28] feat: use PeriodDimension in ThematicDialog --- i18n/en.pot | 109 +---------------- .../edit/thematic/ThematicDialog.js | 113 +++++++++++++----- src/components/periods/StartEndDate.js | 32 ++--- src/reducers/layerEdit.js | 9 +- 4 files changed, 106 insertions(+), 157 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index a132fbb2b..9c411d406 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: 2024-12-02T09:29:29.011Z\n" -"PO-Revision-Date: 2024-12-02T09:29:29.011Z\n" +"POT-Creation-Date: 2024-12-03T09:20:13.790Z\n" +"PO-Revision-Date: 2024-12-03T09:20:13.790Z\n" msgid "2020" msgstr "2020" @@ -945,6 +945,9 @@ msgstr "Timeline" msgid "Split map views" msgstr "Split map views" +msgid "Start and end dates are inclusive and will be included in the outputs." +msgstr "Start and end dates are inclusive and will be included in the outputs." + msgid "Start date" msgstr "Start date" @@ -1419,108 +1422,6 @@ msgstr "Financial year (Start July)" msgid "Financial year (Start April)" msgstr "Financial year (Start April)" -msgid "Today" -msgstr "Today" - -msgid "Yesterday" -msgstr "Yesterday" - -msgid "Last 3 days" -msgstr "Last 3 days" - -msgid "Last 7 days" -msgstr "Last 7 days" - -msgid "Last 14 days" -msgstr "Last 14 days" - -msgid "This week" -msgstr "This week" - -msgid "Last week" -msgstr "Last week" - -msgid "Last 4 weeks" -msgstr "Last 4 weeks" - -msgid "Last 12 weeks" -msgstr "Last 12 weeks" - -msgid "Last 52 weeks" -msgstr "Last 52 weeks" - -msgid "Weeks this year" -msgstr "Weeks this year" - -msgid "This month" -msgstr "This month" - -msgid "Last month" -msgstr "Last month" - -msgid "Last 3 months" -msgstr "Last 3 months" - -msgid "Last 6 months" -msgstr "Last 6 months" - -msgid "Last 12 months" -msgstr "Last 12 months" - -msgid "Months this year" -msgstr "Months this year" - -msgid "This bimonth" -msgstr "This bimonth" - -msgid "Last bimonth" -msgstr "Last bimonth" - -msgid "Last 6 bimonths" -msgstr "Last 6 bimonths" - -msgid "Bimonths this year" -msgstr "Bimonths this year" - -msgid "This quarter" -msgstr "This quarter" - -msgid "Last quarter" -msgstr "Last quarter" - -msgid "Last 4 quarters" -msgstr "Last 4 quarters" - -msgid "Quarters this year" -msgstr "Quarters this year" - -msgid "This six-month" -msgstr "This six-month" - -msgid "Last six-month" -msgstr "Last six-month" - -msgid "Last 2 six-months" -msgstr "Last 2 six-months" - -msgid "This financial year" -msgstr "This financial year" - -msgid "Last financial year" -msgstr "Last financial year" - -msgid "Last 5 financial years" -msgstr "Last 5 financial years" - -msgid "This year" -msgstr "This year" - -msgid "Last year" -msgstr "Last year" - -msgid "Last 5 years" -msgstr "Last 5 years" - msgid "Cancelled" msgstr "Cancelled" diff --git a/src/components/edit/thematic/ThematicDialog.js b/src/components/edit/thematic/ThematicDialog.js index 7af8a6d01..942b50ae8 100644 --- a/src/components/edit/thematic/ThematicDialog.js +++ b/src/components/edit/thematic/ThematicDialog.js @@ -14,6 +14,8 @@ import { setOperand, setOrgUnits, setPeriods, + setStartDate, + setEndDate, setPeriodType, setRenderingStrategy, setProgram, @@ -86,7 +88,7 @@ class ThematicDialog extends Component { dataElementGroup: PropTypes.object, endDate: PropTypes.string, filters: PropTypes.array, - //id: PropTypes.string, + id: PropTypes.string, indicatorGroup: PropTypes.object, legendSet: PropTypes.object, method: PropTypes.number, @@ -94,7 +96,7 @@ class ThematicDialog extends Component { operand: PropTypes.bool, orgUnits: PropTypes.object, periodType: PropTypes.string, - //periodsSettings: PropTypes.object, + periodsSettings: PropTypes.object, program: PropTypes.object, radiusHigh: PropTypes.number, radiusLow: PropTypes.number, @@ -125,20 +127,27 @@ class ThematicDialog extends Component { setPeriodType, setOrgUnits, } = this.props + + console.log( + '🚀 ~ componentDidMount ~ periodType:', + this.props.periodType + ) + console.log('🚀 ~ componentDidMount ~ filters:', this.props.filters) console.log('🚀 ~ componentDidMount ~ startDate:', startDate) console.log('🚀 ~ componentDidMount ~ endDate:', endDate) const dataItem = getDataItemFromColumns(columns) const periods = getPeriodsFromFilters(filters) + console.log( + '🚀 ~ ThematicDialog ~ componentDidMount ~ periods:', + periods + ) const { keyAnalysisRelativePeriod: defaultPeriod, hiddenPeriods } = systemSettings // Set value type if favorite is loaded if (!valueType) { - console.log('🚀 ~ componentDidMount ~ valueType:', valueType) - console.log('🚀 ~ componentDidMount ~ dataItem:', dataItem) - console.log('🚀 ~ componentDidMount ~ dimConf:', dimConf) if (dataItem && dataItem.dimensionItemType) { const dimension = Object.keys(dimConf).find( (dim) => @@ -153,14 +162,38 @@ class ThematicDialog extends Component { } } + const hasDate = + startDate !== undefined && + startDate !== '' && + endDate !== undefined && + endDate !== '' + console.log( + '🚀 ~ ThematicDialog ~ componentDidMount ~ hasDate:', + hasDate + ) + + if (hasDate) { + console.log( + '🚀 ~ componentDidMount ~ setPeriodType HERE (START_END_DATES)' + ) + const keepPeriod = false + setPeriodType({ value: START_END_DATES }, keepPeriod) + } else { + console.log( + '🚀 ~ componentDidMount ~ setPeriodType HERE (PREDEFINED_PERIODS)' + ) + const keepPeriod = true + setPeriodType({ value: PREDEFINED_PERIODS }, keepPeriod) + } + // Set default period from system settings if ( periods?.length == 0 && - !startDate && - !endDate && + !hasDate && defaultPeriod && isPeriodAvailable(defaultPeriod, hiddenPeriods) ) { + console.log('🚀 ~ componentDidMount ~ setPeriods HERE') const defaultPeriods = [ { id: defaultPeriod, @@ -168,7 +201,6 @@ class ThematicDialog extends Component { }, ] setPeriods(defaultPeriods) - setPeriodType({ value: PREDEFINED_PERIODS }) } // Set default org unit level @@ -191,6 +223,9 @@ class ThematicDialog extends Component { columns, periodType, renderingStrategy, + setPeriods, + setStartDate, + setEndDate, setClassification, setLegendSet, setRenderingStrategy, @@ -224,11 +259,35 @@ class ThematicDialog extends Component { if (validateLayer && validateLayer !== prev.validateLayer) { onLayerValidation(this.validate()) } + + if (periodType !== prev.periodType) { + switch (periodType) { + case PREDEFINED_PERIODS: + // !TODO Backup Start-End dates + // Remove Start-End dates + // !TODO Restore Predefined periods backup + console.log( + '🚀 ~ ThematicDialog ~ componentDidUpdate ~ case PREDEFINED_PERIODS' + ) + setStartDate('') + setEndDate('') + break + case START_END_DATES: + // !TODO Backup Predefined periods + // Remove Predefined periods + // !TODO Restore Start-End dates backup + console.log( + '🚀 ~ ThematicDialog ~ componentDidUpdate ~ case START_END_DATES' + ) + setPeriods([]) + break + } + } } render() { const { - // layer options + // Layer options columns, dataElementGroup, filters, @@ -246,8 +305,6 @@ class ThematicDialog extends Component { systemSettings, // periodsSettings, } = this.props - console.log('🚀 ~ render ~ endDate:', endDate) - console.log('🚀 ~ render ~ startDate:', startDate) const { // Handlers @@ -280,30 +337,16 @@ class ThematicDialog extends Component { legendSetError, } = this.state - const periods = getPeriodsFromFilters(filters) - console.log('🚀 ~ render ~ filters:', filters) console.log('🚀 ~ render ~ periodType:', periodType) + console.log('🚀 ~ render ~ filters:', filters) + console.log('🚀 ~ render ~ startDate:', startDate) + console.log('🚀 ~ render ~ endDate:', endDate) + + const periods = getPeriodsFromFilters(filters) const dataItem = getDataItemFromColumns(columns) const dimensions = getDimensionsFromFilters(filters) - const setPeriodsFromFilter = (e) => { - setPeriods(e.items) - } - - const changePeriodType = (e) => { - console.log(e) - setPeriodType(e) - switch (e.value) { - case PREDEFINED_PERIODS: - // Remove Start-End dates - // Set Predefined periods (again) - case START_END_DATES: - // Remove Predefined periods - // Set value Start-End dates (again) - } - } - return (
this.setState({ tab })}> @@ -480,13 +523,15 @@ class ThematicDialog extends Component { }, ]} selected={periodType} - onChange={changePeriodType} + onChange={setPeriodType} >
{periodType === PREDEFINED_PERIODS && ( { + setPeriods(e.items) + }} excludedPeriodTypes={ systemSettings.hiddenPeriods } @@ -505,6 +550,8 @@ class ThematicDialog extends Component { )} {periodError && ( @@ -731,6 +778,8 @@ export default connect( setOperand, setOrgUnits, setPeriods, + setStartDate, + setEndDate, setPeriodType, setRenderingStrategy, setProgram, diff --git a/src/components/periods/StartEndDate.js b/src/components/periods/StartEndDate.js index 2ab52e548..f7acc7135 100644 --- a/src/components/periods/StartEndDate.js +++ b/src/components/periods/StartEndDate.js @@ -9,16 +9,22 @@ import styles from './StartEndDate.module.css' const StartEndDate = (props) => { const { startDate, endDate, setStartDate, setEndDate } = props - console.log('🚀 ~ useEffect ~ endDate:', endDate) - console.log('🚀 ~ useEffect ~ startDate:', startDate) + + const [start, setStart] = useState(startDate) + const [end, setEnd] = useState(endDate) + + console.log('🚀 ~ StartEndDate ~ startDate:', typeof startDate) + console.log('🚀 ~ StartEndDate ~ endDate:', typeof endDate) const hasDate = startDate !== undefined && endDate !== undefined - useEffect(() => { - if (!hasDate) { - setStartDate({ value: DEFAULT_START_DATE }) - setEndDate({ value: DEFAULT_END_DATE }) - } - }, [hasDate, setStartDate, setEndDate]) + const onStartDateChange = ({ value }) => { + setStart(value) + setStartDate(value) + } + const onEndDateChange = ({ value }) => { + setEnd(value) + setEndDate(value) + } return hasDate ? ( { >
{
{ ) : null } StartEndDate.propTypes = { - setEndDate: PropTypes.func.isRequired, - setStartDate: PropTypes.func.isRequired, startDate: PropTypes.string, endDate: PropTypes.string, } diff --git a/src/reducers/layerEdit.js b/src/reducers/layerEdit.js index dac685019..8d006d3ce 100644 --- a/src/reducers/layerEdit.js +++ b/src/reducers/layerEdit.js @@ -105,10 +105,6 @@ const layerEdit = (state = null, action) => { } case types.LAYER_EDIT_PERIOD_TYPE_SET: - console.log( - '🚀 ~ layerEdit ~ action.periodType:', - action.periodType - ) return { ...state, periodType: action.periodType.value, @@ -144,16 +140,15 @@ const layerEdit = (state = null, action) => { } case types.LAYER_EDIT_START_DATE_SET: - console.log('🚀 ~ layerEdit ~ action.startDate:', action.startDate) return { ...state, - startDate: action.startDate.value, + startDate: action.startDate, } case types.LAYER_EDIT_END_DATE_SET: return { ...state, - endDate: action.endDate.value, + endDate: action.endDate, } case types.LAYER_EDIT_AGGREGATION_TYPE_SET: From c1c8a4ab79f652d143fee412913359f38c3926e2 Mon Sep 17 00:00:00 2001 From: braimbault Date: Wed, 4 Dec 2024 15:54:58 +0100 Subject: [PATCH 07/28] feat: use PeriodDimension in ThematicDialog --- i18n/en.pot | 7 +- package.json | 2 +- .../edit/thematic/ThematicDialog.js | 7 +- src/components/periods/RenderingStrategy.js | 76 +++++++++++++------ src/constants/periods.js | 22 +++--- src/loaders/thematicLoader.js | 7 +- yarn.lock | 4 +- 7 files changed, 79 insertions(+), 46 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 9c411d406..9e2061348 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: 2024-12-03T09:20:13.790Z\n" -"PO-Revision-Date: 2024-12-03T09:20:13.790Z\n" +"POT-Creation-Date: 2024-12-04T12:41:51.035Z\n" +"PO-Revision-Date: 2024-12-04T12:41:51.035Z\n" msgid "2020" msgstr "2020" @@ -933,6 +933,9 @@ msgstr "Only one timeline is allowed." msgid "Remove other layers to enable split map views." msgstr "Remove other layers to enable split map views." +msgid "Only up to 12 periods can be selected." +msgstr "Only up to 12 periods can be selected." + msgid "Period display mode" msgstr "Period display mode" diff --git a/package.json b/package.json index da1f49e13..0f8668cdc 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "start-server-and-test": "^2.0.3" }, "dependencies": { - "@dhis2/analytics": "git+https://github.com/d2-ci/analytics.git#223ec5f2dd57fc9190aae3882ed8bbb51dde46fa", + "@dhis2/analytics": "git+https://github.com/d2-ci/analytics.git#d5d4d9f63a3aa7392e44bb37bc598f8368ae3979", "@dhis2/app-runtime": "^3.11.2", "@dhis2/app-runtime-adapter-d2": "^1.1.0", "@dhis2/app-service-datastore": "^1.0.0-beta.3", diff --git a/src/components/edit/thematic/ThematicDialog.js b/src/components/edit/thematic/ThematicDialog.js index 942b50ae8..b580eb17b 100644 --- a/src/components/edit/thematic/ThematicDialog.js +++ b/src/components/edit/thematic/ThematicDialog.js @@ -1,4 +1,4 @@ -import { PeriodDimension, getRelativePeriodsMap } from '@dhis2/analytics' +import { PeriodDimension, getRelativePeriodsName } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' import { SegmentedControl, IconErrorFilled24 } from '@dhis2/ui' import PropTypes from 'prop-types' @@ -194,10 +194,11 @@ class ThematicDialog extends Component { isPeriodAvailable(defaultPeriod, hiddenPeriods) ) { console.log('🚀 ~ componentDidMount ~ setPeriods HERE') + console.log('getRelativePeriodsName()', getRelativePeriodsName()) const defaultPeriods = [ { id: defaultPeriod, - name: getRelativePeriodsMap()[defaultPeriod], + name: getRelativePeriodsName()[defaultPeriod], }, ] setPeriods(defaultPeriods) @@ -541,7 +542,7 @@ class ThematicDialog extends Component { {periodType === PREDEFINED_PERIODS && ( diff --git a/src/components/periods/RenderingStrategy.js b/src/components/periods/RenderingStrategy.js index 483ea6e85..d2d72a549 100644 --- a/src/components/periods/RenderingStrategy.js +++ b/src/components/periods/RenderingStrategy.js @@ -7,17 +7,29 @@ import { RENDERING_STRATEGY_TIMELINE, RENDERING_STRATEGY_SPLIT_BY_PERIOD, } from '../../constants/layers.js' -import { - singleMapPeriods, - invalidSplitViewPeriods, -} from '../../constants/periods.js' +import { singleMapPeriods, MAX_PERIODS } from '../../constants/periods.js' +import { getPeriodsFromFilters } from '../../util/analytics.js' import usePrevious from '../../hooks/usePrevious.js' import { Radio, RadioGroup } from '../core/index.js' +import { getRelativePeriodsItemsCount } from '@dhis2/analytics' + +const countPeriods = (periods) => { + const itemsCount = getRelativePeriodsItemsCount() + console.log('itemsCount', itemsCount) + const total = periods.reduce( + (sum, period) => + sum + + (itemsCount[period.id] !== undefined ? itemsCount[period.id] : 1), + 0 + ) + console.log('total', total) + return total +} const RenderingStrategy = ({ layerId, value = RENDERING_STRATEGY_SINGLE, - period = {}, + periods = [], onChange, }) => { const hasOtherLayers = useSelector( @@ -31,36 +43,55 @@ const RenderingStrategy = ({ layer.id !== layerId ) ) - const prevPeriod = usePrevious(period) + const hasTooManyPeriods = useSelector(({ layerEdit }) => { + console.log('layerEdit', layerEdit) + const periods = getPeriodsFromFilters(layerEdit.filters) + console.log('periods', periods) + return countPeriods(periods) > MAX_PERIODS + }) + + const prevPeriods = usePrevious(periods) useEffect(() => { - if (period !== prevPeriod) { + if (periods !== prevPeriods) { if ( - singleMapPeriods.includes(period.id) && + periods.length === 1 && + singleMapPeriods.includes(periods[0].id) && value !== RENDERING_STRATEGY_SINGLE ) { onChange(RENDERING_STRATEGY_SINGLE) } else if ( - invalidSplitViewPeriods.includes(period.id) && - value === RENDERING_STRATEGY_SPLIT_BY_PERIOD + countPeriods(periods) > MAX_PERIODS && + (value === RENDERING_STRATEGY_TIMELINE || + value === RENDERING_STRATEGY_SPLIT_BY_PERIOD) ) { - // TODO: Switch to 'timeline' when we support it onChange(RENDERING_STRATEGY_SINGLE) } } - }, [value, period, prevPeriod, onChange]) + }, [value, periods, prevPeriods, onChange]) - if (singleMapPeriods.includes(period.id)) { + if (periods.length === 1 && singleMapPeriods.includes(periods[0].id)) { return null } - let helpText + let helpText = [] if (hasOtherTimelineLayers) { - helpText = i18n.t('Only one timeline is allowed.') - } else if (hasOtherLayers) { - helpText = i18n.t('Remove other layers to enable split map views.') + helpText.push(i18n.t('Only one timeline is allowed.')) + } + if (hasOtherLayers) { + helpText.push(i18n.t('Remove other layers to enable split map views.')) + } + if (hasTooManyPeriods) { + helpText.push( + i18n.t('Only up to ') + + MAX_PERIODS + + i18n.t( + ' periods can be selected to enable timeline or split map views.' + ) + ) } + helpText = helpText.join(' ') return ( - + ) @@ -93,7 +121,7 @@ const RenderingStrategy = ({ RenderingStrategy.propTypes = { onChange: PropTypes.func.isRequired, layerId: PropTypes.string, - period: PropTypes.object, + periods: PropTypes.array, value: PropTypes.string, } diff --git a/src/constants/periods.js b/src/constants/periods.js index 70dfe3aa9..72fcaf97c 100644 --- a/src/constants/periods.js +++ b/src/constants/periods.js @@ -52,10 +52,12 @@ const THIS_BIMONTH = 'THIS_BIMONTH' const LAST_BIMONTH = 'LAST_BIMONTH' const THIS_QUARTER = 'THIS_QUARTER' const LAST_QUARTER = 'LAST_QUARTER' -const THIS_YEAR = 'THIS_YEAR' -const LAST_YEAR = 'LAST_YEAR' +const THIS_SIX_MONTH = 'THIS_SIX_MONTH' +const LAST_SIX_MONTH = 'LAST_SIX_MONTH' const THIS_FINANCIAL_YEAR = 'THIS_FINANCIAL_YEAR' const LAST_FINANCIAL_YEAR = 'LAST_FINANCIAL_YEAR' +const THIS_YEAR = 'THIS_YEAR' +const LAST_YEAR = 'LAST_YEAR' export const PREDEFINED_PERIODS = 'PREDEFINED_PERIODS' export const RELATIVE_PERIODS = 'RELATIVE_PERIODS' @@ -175,22 +177,16 @@ export const singleMapPeriods = [ LAST_BIMONTH, THIS_QUARTER, LAST_QUARTER, - THIS_YEAR, - LAST_YEAR, + THIS_SIX_MONTH, + LAST_SIX_MONTH, THIS_FINANCIAL_YEAR, LAST_FINANCIAL_YEAR, + THIS_YEAR, + LAST_YEAR, ] // Periods not supported for split view (maximum 12 maps) -export const invalidSplitViewPeriods = [ - LAST_14_DAYS, - LAST_30_DAYS, - LAST_60_DAYS, - LAST_90_DAYS, - LAST_180_DAYS, - LAST_52_WEEKS, - WEEKS_THIS_YEAR, -] +export const MAX_PERIODS = 12 // Period types used for Earth Engine layers export const BY_YEAR = 'BY_YEAR' diff --git a/src/loaders/thematicLoader.js b/src/loaders/thematicLoader.js index 4e3dc81f7..ccd737764 100644 --- a/src/loaders/thematicLoader.js +++ b/src/loaders/thematicLoader.js @@ -102,11 +102,14 @@ const thematicLoader = async ({ config, engine, nameProperty }) => { console.log('🚀 ~ thematicLoader ~ period:', period) const periodx = getPeriodsFromFilters(config.filters) console.log('🚀 ~ thematicLoader ~ periodx:', periodx) + console.log('🚀 ~ thematicLoader ~ data.metaData:', data.metaData) const periods = getPeriodsFromMetaData(data.metaData) + console.log('🚀 ~ thematicLoader ~ periods:', periods) const dimensions = getValidDimensionsFromFilters(config.filters) const names = getApiResponseNames(data) // TODO Handle multiple maps const valuesByPeriod = !isSingleMap ? getValuesByPeriod(data) : null + console.log('🚀 ~ thematicLoader ~ valuesByPeriod:', valuesByPeriod) const valueById = getValueById(data) const valueFeatures = noDataColor ? features @@ -384,7 +387,9 @@ const loadData = async (config, nameProperty) => { if (!isSingleMap) { // TODO Handle multiple maps - analyticsRequest = analyticsRequest.addPeriodDimension(period.id) + analyticsRequest = analyticsRequest.addPeriodDimension( + periodx.map((pe) => pe.id) + ) } else { analyticsRequest = periodx.length > 0 diff --git a/yarn.lock b/yarn.lock index 0a6c5668f..22111db1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2018,9 +2018,9 @@ classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2/analytics@git+https://github.com/d2-ci/analytics.git#223ec5f2dd57fc9190aae3882ed8bbb51dde46fa": +"@dhis2/analytics@git+https://github.com/d2-ci/analytics.git#d5d4d9f63a3aa7392e44bb37bc598f8368ae3979": version "26.9.3" - resolved "git+https://github.com/d2-ci/analytics.git#223ec5f2dd57fc9190aae3882ed8bbb51dde46fa" + resolved "git+https://github.com/d2-ci/analytics.git#d5d4d9f63a3aa7392e44bb37bc598f8368ae3979" dependencies: "@dhis2/multi-calendar-dates" "^1.2.2" "@dnd-kit/core" "^6.0.7" From 68ac83be859ebde57d7f1391308ef792c1f1a3a9 Mon Sep 17 00:00:00 2001 From: braimbault Date: Thu, 5 Dec 2024 17:01:08 +0100 Subject: [PATCH 08/28] fix: correct RenderingStrategy periods count --- i18n/en.pot | 21 +++-- package.json | 2 +- src/components/periods/RenderingStrategy.js | 97 +++++++++++++++++---- src/constants/periods.js | 3 +- yarn.lock | 4 +- 5 files changed, 98 insertions(+), 29 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 9e2061348..84bcab976 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: 2024-12-04T12:41:51.035Z\n" -"PO-Revision-Date: 2024-12-04T12:41:51.035Z\n" +"POT-Creation-Date: 2024-12-05T11:28:26.227Z\n" +"PO-Revision-Date: 2024-12-05T11:28:26.227Z\n" msgid "2020" msgstr "2020" @@ -927,20 +927,29 @@ msgstr "Period type" msgid "Start/end dates" msgstr "Start/end dates" +msgid "Select " +msgstr "Select " + +msgid " or more periods to enable timeline or split map views." +msgstr " or more periods to enable timeline or split map views." + msgid "Only one timeline is allowed." msgstr "Only one timeline is allowed." msgid "Remove other layers to enable split map views." msgstr "Remove other layers to enable split map views." -msgid "Only up to 12 periods can be selected." -msgstr "Only up to 12 periods can be selected." +msgid "Only up to " +msgstr "Only up to " + +msgid " periods can be selected to enable timeline or split map views." +msgstr " periods can be selected to enable timeline or split map views." msgid "Period display mode" msgstr "Period display mode" -msgid "Single (aggregate)" -msgstr "Single (aggregate)" +msgid "Single (combine periods)" +msgstr "Single (combine periods)" msgid "Timeline" msgstr "Timeline" diff --git a/package.json b/package.json index 0f8668cdc..1b9f1c9fb 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "start-server-and-test": "^2.0.3" }, "dependencies": { - "@dhis2/analytics": "git+https://github.com/d2-ci/analytics.git#d5d4d9f63a3aa7392e44bb37bc598f8368ae3979", + "@dhis2/analytics": "git+https://github.com/d2-ci/analytics.git#c19fab1c5abe7f15f3d7799ef1b5be8a720fa169", "@dhis2/app-runtime": "^3.11.2", "@dhis2/app-runtime-adapter-d2": "^1.1.0", "@dhis2/app-service-datastore": "^1.0.0-beta.3", diff --git a/src/components/periods/RenderingStrategy.js b/src/components/periods/RenderingStrategy.js index d2d72a549..511a17d6a 100644 --- a/src/components/periods/RenderingStrategy.js +++ b/src/components/periods/RenderingStrategy.js @@ -7,23 +7,70 @@ import { RENDERING_STRATEGY_TIMELINE, RENDERING_STRATEGY_SPLIT_BY_PERIOD, } from '../../constants/layers.js' -import { singleMapPeriods, MAX_PERIODS } from '../../constants/periods.js' +import { + MULTIMAP_MIN_PERIODS, + MULTIMAP_MAX_PERIODS, +} from '../../constants/periods.js' import { getPeriodsFromFilters } from '../../util/analytics.js' import usePrevious from '../../hooks/usePrevious.js' import { Radio, RadioGroup } from '../core/index.js' -import { getRelativePeriodsItemsCount } from '@dhis2/analytics' +import { getRelativePeriodsDetails } from '@dhis2/analytics' const countPeriods = (periods) => { - const itemsCount = getRelativePeriodsItemsCount() - console.log('itemsCount', itemsCount) - const total = periods.reduce( + const periodsDetails = getRelativePeriodsDetails() + console.log('periodsDetails', periodsDetails) + + const total_v1 = periods.reduce( (sum, period) => sum + - (itemsCount[period.id] !== undefined ? itemsCount[period.id] : 1), + (periodsDetails[period.id] !== undefined + ? periodsDetails[period.id].duration + : 1), 0 ) - console.log('total', total) - return total + + const durationByType = periods.reduce((acc, period) => { + console.log('🚀 ~ test ~ period:', period) + const periodDetails = periodsDetails[period.id] + if (acc['FIXED_PERIOD'] === undefined) { + acc['FIXED_PERIOD'] = { + any: 0, + } + } + if (periodDetails === undefined) { + acc['FIXED_PERIOD'].any += 1 + return acc + } + const type = periodDetails.type + if (acc[type] === undefined) { + acc[type] = { + first: 0, + last: 0, + } + } + acc[type].first = Math.max(acc[type].first, 1 + periodDetails.offset) + acc[type].last = Math.max( + acc[type].last, + periodDetails.duration - (1 + periodDetails.offset) + ) + return acc + }, {}) + + const sumObjectValues = (obj) => + Object.values(obj).reduce((sum, value) => { + if (typeof value === 'object') { + return sum + sumObjectValues(value) + } else if (typeof value === 'number') { + return sum + value + } + return sum + }, 0) + + const total_v2 = sumObjectValues(durationByType) + + console.log('total_v1', total_v1) + console.log('total_v2', total_v2) + return total_v2 } const RenderingStrategy = ({ @@ -47,7 +94,7 @@ const RenderingStrategy = ({ console.log('layerEdit', layerEdit) const periods = getPeriodsFromFilters(layerEdit.filters) console.log('periods', periods) - return countPeriods(periods) > MAX_PERIODS + return countPeriods(periods) > MULTIMAP_MAX_PERIODS }) const prevPeriods = usePrevious(periods) @@ -55,13 +102,12 @@ const RenderingStrategy = ({ useEffect(() => { if (periods !== prevPeriods) { if ( - periods.length === 1 && - singleMapPeriods.includes(periods[0].id) && + periods.length < MULTIMAP_MIN_PERIODS && value !== RENDERING_STRATEGY_SINGLE ) { onChange(RENDERING_STRATEGY_SINGLE) } else if ( - countPeriods(periods) > MAX_PERIODS && + countPeriods(periods) > MULTIMAP_MAX_PERIODS && (value === RENDERING_STRATEGY_TIMELINE || value === RENDERING_STRATEGY_SPLIT_BY_PERIOD) ) { @@ -70,12 +116,17 @@ const RenderingStrategy = ({ } }, [value, periods, prevPeriods, onChange]) - if (periods.length === 1 && singleMapPeriods.includes(periods[0].id)) { - return null - } - let helpText = [] + if (periods.length < MULTIMAP_MIN_PERIODS) { + helpText.push( + i18n.t('Select ') + + MULTIMAP_MIN_PERIODS + + i18n.t( + ' or more periods to enable timeline or split map views.' + ) + ) + } if (hasOtherTimelineLayers) { helpText.push(i18n.t('Only one timeline is allowed.')) } @@ -85,7 +136,7 @@ const RenderingStrategy = ({ if (hasTooManyPeriods) { helpText.push( i18n.t('Only up to ') + - MAX_PERIODS + + MULTIMAP_MAX_PERIODS + i18n.t( ' periods can be selected to enable timeline or split map views.' ) @@ -107,12 +158,20 @@ const RenderingStrategy = ({
) diff --git a/src/constants/periods.js b/src/constants/periods.js index 72fcaf97c..148b9d57f 100644 --- a/src/constants/periods.js +++ b/src/constants/periods.js @@ -186,7 +186,8 @@ export const singleMapPeriods = [ ] // Periods not supported for split view (maximum 12 maps) -export const MAX_PERIODS = 12 +export const MULTIMAP_MIN_PERIODS = 2 +export const MULTIMAP_MAX_PERIODS = 12 // Period types used for Earth Engine layers export const BY_YEAR = 'BY_YEAR' diff --git a/yarn.lock b/yarn.lock index 22111db1e..4acd802bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2018,9 +2018,9 @@ classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2/analytics@git+https://github.com/d2-ci/analytics.git#d5d4d9f63a3aa7392e44bb37bc598f8368ae3979": +"@dhis2/analytics@git+https://github.com/d2-ci/analytics.git#c19fab1c5abe7f15f3d7799ef1b5be8a720fa169": version "26.9.3" - resolved "git+https://github.com/d2-ci/analytics.git#d5d4d9f63a3aa7392e44bb37bc598f8368ae3979" + resolved "git+https://github.com/d2-ci/analytics.git#c19fab1c5abe7f15f3d7799ef1b5be8a720fa169" dependencies: "@dhis2/multi-calendar-dates" "^1.2.2" "@dnd-kit/core" "^6.0.7" From a4322fbfb59acd0b90d52849b308663d57dc94d3 Mon Sep 17 00:00:00 2001 From: braimbault Date: Fri, 6 Dec 2024 11:38:41 +0100 Subject: [PATCH 09/28] fix: use CalendarInput in StartEndDate --- package.json | 2 +- .../edit/thematic/ThematicDialog.js | 3 +- src/components/periods/StartEndDate.js | 42 ++++++++++++------- yarn.lock | 4 +- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 1b9f1c9fb..9de7891a9 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "start-server-and-test": "^2.0.3" }, "dependencies": { - "@dhis2/analytics": "git+https://github.com/d2-ci/analytics.git#c19fab1c5abe7f15f3d7799ef1b5be8a720fa169", + "@dhis2/analytics": "git+https://github.com/d2-ci/analytics.git#19447b89130d615dc55e92d345b53869dca548ae", "@dhis2/app-runtime": "^3.11.2", "@dhis2/app-runtime-adapter-d2": "^1.1.0", "@dhis2/app-service-datastore": "^1.0.0-beta.3", diff --git a/src/components/edit/thematic/ThematicDialog.js b/src/components/edit/thematic/ThematicDialog.js index b580eb17b..a40b79629 100644 --- a/src/components/edit/thematic/ThematicDialog.js +++ b/src/components/edit/thematic/ThematicDialog.js @@ -304,7 +304,7 @@ class ThematicDialog extends Component { valueType, thematicMapType, systemSettings, - // periodsSettings, + periodsSettings, } = this.props const { @@ -553,6 +553,7 @@ class ThematicDialog extends Component { endDate={endDate} setStartDate={setStartDate} setEndDate={setEndDate} + periodsSettings={periodsSettings} /> )} {periodError && ( diff --git a/src/components/periods/StartEndDate.js b/src/components/periods/StartEndDate.js index f7acc7135..5a324fd39 100644 --- a/src/components/periods/StartEndDate.js +++ b/src/components/periods/StartEndDate.js @@ -1,5 +1,11 @@ import i18n from '@dhis2/d2-i18n' -import { Field, IconArrowRight16, InputField, colors } from '@dhis2/ui' +import { + Field, + IconArrowRight16, + InputField, + CalendarInput, + colors, +} from '@dhis2/ui' import PropTypes from 'prop-types' import React, { useEffect, useState } from 'react' import { connect } from 'react-redux' @@ -8,7 +14,10 @@ import { DEFAULT_START_DATE, DEFAULT_END_DATE } from '../../constants/layers.js' import styles from './StartEndDate.module.css' const StartEndDate = (props) => { - const { startDate, endDate, setStartDate, setEndDate } = props + const { startDate, endDate, setStartDate, setEndDate, periodsSettings } = + props + + console.log('🚀 ~ StartEndDate ~ periodsSettings:', periodsSettings) const [start, setStart] = useState(startDate) const [end, setEnd] = useState(endDate) @@ -17,11 +26,11 @@ const StartEndDate = (props) => { console.log('🚀 ~ StartEndDate ~ endDate:', typeof endDate) const hasDate = startDate !== undefined && endDate !== undefined - const onStartDateChange = ({ value }) => { + const onStartDateChange = ({ calendarDateString: value }) => { setStart(value) setStartDate(value) } - const onEndDateChange = ({ value }) => { + const onEndDateChange = ({ calendarDateString: value }) => { setEnd(value) setEndDate(value) } @@ -32,25 +41,25 @@ const StartEndDate = (props) => { )} >
-
-
@@ -60,6 +69,7 @@ const StartEndDate = (props) => { StartEndDate.propTypes = { startDate: PropTypes.string, endDate: PropTypes.string, + periodsSettings: PropTypes.object, } export default connect(null, { setStartDate, setEndDate })(StartEndDate) diff --git a/yarn.lock b/yarn.lock index 4acd802bb..0d74b0a24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2018,9 +2018,9 @@ classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2/analytics@git+https://github.com/d2-ci/analytics.git#c19fab1c5abe7f15f3d7799ef1b5be8a720fa169": +"@dhis2/analytics@git+https://github.com/d2-ci/analytics.git#19447b89130d615dc55e92d345b53869dca548ae": version "26.9.3" - resolved "git+https://github.com/d2-ci/analytics.git#c19fab1c5abe7f15f3d7799ef1b5be8a720fa169" + resolved "git+https://github.com/d2-ci/analytics.git#19447b89130d615dc55e92d345b53869dca548ae" dependencies: "@dhis2/multi-calendar-dates" "^1.2.2" "@dnd-kit/core" "^6.0.7" From b80c33c8d4989712594159228c1a998ad5632f08 Mon Sep 17 00:00:00 2001 From: braimbault Date: Fri, 6 Dec 2024 18:01:01 +0100 Subject: [PATCH 10/28] feat: handle multiple periods in timeline --- package.json | 2 +- src/components/map/layers/ThematicLayer.js | 12 +- src/components/periods/RenderingStrategy.js | 16 +- src/components/periods/Timeline.js | 142 ++++++++++++++++-- .../periods/styles/Timeline.module.css | 14 +- yarn.lock | 4 +- 6 files changed, 157 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 9de7891a9..3cf18a45b 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "start-server-and-test": "^2.0.3" }, "dependencies": { - "@dhis2/analytics": "git+https://github.com/d2-ci/analytics.git#19447b89130d615dc55e92d345b53869dca548ae", + "@dhis2/analytics": "git+https://github.com/d2-ci/analytics.git#8c539c2cee08357ef6c7daaab2521e3df6ef7801", "@dhis2/app-runtime": "^3.11.2", "@dhis2/app-runtime-adapter-d2": "^1.1.0", "@dhis2/app-service-datastore": "^1.0.0-beta.3", diff --git a/src/components/map/layers/ThematicLayer.js b/src/components/map/layers/ThematicLayer.js index 5d61c2122..e5de1eda6 100644 --- a/src/components/map/layers/ThematicLayer.js +++ b/src/components/map/layers/ThematicLayer.js @@ -140,16 +140,26 @@ class ThematicLayer extends Layer { periods, renderingStrategy = RENDERING_STRATEGY_SINGLE, } = this.props + console.log('🚀 ~ ThematicLayer ~ setPeriod ~ periods:', periods) + console.log('🚀 ~ ThematicLayer ~ setPeriod ~ period:', period) if (!period && !periods) { return } + let sortedPeriods = [] + if (periods) { + sortedPeriods = periods.sort((a, b) => b.level - a.level) + sortedPeriods = periods.sort( + (a, b) => new Date(a.startDate) - new Date(b.startDate) + ) + } + const initialPeriod = { period: renderingStrategy === RENDERING_STRATEGY_SINGLE ? null - : period || periods[0], + : period || sortedPeriods[0], } // setPeriod without callback is called from the constructor (unmounted) diff --git a/src/components/periods/RenderingStrategy.js b/src/components/periods/RenderingStrategy.js index 511a17d6a..ba38ec722 100644 --- a/src/components/periods/RenderingStrategy.js +++ b/src/components/periods/RenderingStrategy.js @@ -102,14 +102,13 @@ const RenderingStrategy = ({ useEffect(() => { if (periods !== prevPeriods) { if ( - periods.length < MULTIMAP_MIN_PERIODS && + countPeriods(periods) < MULTIMAP_MIN_PERIODS && value !== RENDERING_STRATEGY_SINGLE ) { onChange(RENDERING_STRATEGY_SINGLE) } else if ( countPeriods(periods) > MULTIMAP_MAX_PERIODS && - (value === RENDERING_STRATEGY_TIMELINE || - value === RENDERING_STRATEGY_SPLIT_BY_PERIOD) + value === RENDERING_STRATEGY_SPLIT_BY_PERIOD ) { onChange(RENDERING_STRATEGY_SINGLE) } @@ -118,7 +117,7 @@ const RenderingStrategy = ({ let helpText = [] - if (periods.length < MULTIMAP_MIN_PERIODS) { + if (countPeriods(periods) < MULTIMAP_MIN_PERIODS) { helpText.push( i18n.t('Select ') + MULTIMAP_MIN_PERIODS + @@ -137,9 +136,7 @@ const RenderingStrategy = ({ helpText.push( i18n.t('Only up to ') + MULTIMAP_MAX_PERIODS + - i18n.t( - ' periods can be selected to enable timeline or split map views.' - ) + i18n.t(' periods can be selected to enable split map views.') ) } helpText = helpText.join(' ') @@ -159,8 +156,7 @@ const RenderingStrategy = ({ value="TIMELINE" label={i18n.t('Timeline')} disabled={ - periods.length < MULTIMAP_MIN_PERIODS || - hasTooManyPeriods || + countPeriods(periods) < MULTIMAP_MIN_PERIODS || hasOtherTimelineLayers } /> @@ -168,7 +164,7 @@ const RenderingStrategy = ({ value="SPLIT_BY_PERIOD" label={i18n.t('Split map views')} disabled={ - periods.length < MULTIMAP_MIN_PERIODS || + countPeriods(periods) < MULTIMAP_MIN_PERIODS || hasTooManyPeriods || hasOtherLayers } diff --git a/src/components/periods/Timeline.js b/src/components/periods/Timeline.js index 4d4b8de3d..22421d9d8 100644 --- a/src/components/periods/Timeline.js +++ b/src/components/periods/Timeline.js @@ -6,6 +6,7 @@ import PropTypes from 'prop-types' import React, { Component } from 'react' import timeTicks from '../../util/timeTicks.js' import styles from './styles/Timeline.module.css' +import { PERIOD_TYPE_REGEX } from '@dhis2/analytics' const paddingLeft = 40 const paddingRight = 20 @@ -15,6 +16,88 @@ const playBtn = const pauseBtn = const doubleTicksPeriods = ['LAST_6_BIMONTHS', 'BIMONTHS_THIS_YEAR'] +const addPeriodType = (item) => { + const periodTypes = Object.keys(PERIOD_TYPE_REGEX) + let i = 0 + let type = undefined + let match = undefined + + while (i < periodTypes.length && !match) { + type = periodTypes[i] + match = item.id.match(PERIOD_TYPE_REGEX[type]) + i++ + } + + let level + switch (type) { + case 'DAILY': + level = 0 + break + case 'WEEKLY': + case 'WEEKLYWED': + case 'WEEKLYTHU': + case 'WEEKLYSAT': + case 'WEEKLYSUN': + level = 1 + break + case 'BIWEEKLY': + level = 2 + break + case 'MONTHLY': + level = 3 + break + case 'BIMONTHLY': + level = 4 + break + case 'QUARTERLY': + level = 5 + break + case 'SIXMONTHLY': + case 'SIXMONTHLYAPR': + level = 6 + break + case 'YEARLY': + case 'FYNOV': + case 'FYOCT': + case 'FYJUL': + case 'FYAPR': + level = 7 + break + default: + level = 8 + } + + item.type = type + item.level = level + + return item +} + +const countUniqueRanks = (periods) => { + let periodsWithType = periods.map((item) => addPeriodType(item)) + const levels = [...new Set(periodsWithType.map((item) => item.level))] + return levels.length +} + +const sortPeriods = (periods) => { + let periodsWithType = periods.map((item) => addPeriodType(item)) + + const levels = [...new Set(periodsWithType.map((item) => item.level))].sort( + (a, b) => b - a + ) + + periodsWithType = periodsWithType.map((item) => ({ + ...item, + levelRank: levels.indexOf(item.level), + })) + + const sortedPeriods = periodsWithType.sort( + (a, b) => a.levelRank - b.levelRank + ) + + return sortedPeriods +} + class Timeline extends Component { static contextTypes = { map: PropTypes.object, @@ -47,14 +130,19 @@ class Timeline extends Component { render() { const { mode } = this.state + const uniqueRanks = countUniqueRanks(this.props.periods) this.setTimeScale() return ( - + @@ -64,7 +152,9 @@ class Timeline extends Component { {this.getPeriodRects()} (this.node = node)} /> @@ -75,7 +165,9 @@ class Timeline extends Component { getPeriodRects = () => { const { period, periods } = this.props - return periods.map((item) => { + const sortedPeriods = sortPeriods(periods) + + return sortedPeriods.map((item) => { const isCurrent = period.id === item.id const { id, startDate, endDate } = item const x = this.timeScale(startDate) @@ -88,9 +180,9 @@ class Timeline extends Component { [styles.selected]: isCurrent, })} x={x} - y="0" + y={item.levelRank * 4} width={width} - height="16" + height={10} //{verticalScale(item).height} onClick={() => this.onPeriodClick(item)} /> ) @@ -100,14 +192,28 @@ class Timeline extends Component { // Set time scale setTimeScale = () => { const { periods } = this.props + console.log('🚀 ~ Timeline ~ this.props:', this.props) const { width } = this.state if (!periods.length) { return } - const { startDate } = periods[0] - const { endDate } = periods[periods.length - 1] + const { minStartDate: startDate, maxEndDate: endDate } = periods.reduce( + (acc, item) => { + const start = new Date(item.startDate) + const end = new Date(item.endDate) + return { + minStartDate: + start < acc.minStartDate ? start : acc.minStartDate, + maxEndDate: end > acc.maxEndDate ? end : acc.maxEndDate, + } + }, + { + minStartDate: new Date(periods[0].startDate), + maxEndDate: new Date(periods[0].endDate), + } + ) // Link time domain to timeline width this.timeScale = scaleTime() @@ -118,6 +224,7 @@ class Timeline extends Component { // Set timeline axis setTimeAxis = () => { const { periodId, periods } = this.props + console.log('🚀 ~ Timeline ~ this.props:', this.props) const numPeriods = periods.length * (doubleTicksPeriods.includes(periodId) ? 2 : 1) const { width } = this.state @@ -147,8 +254,13 @@ class Timeline extends Component { onPeriodClick(period) { // Switch to period if different if (period.id !== this.props.period.id) { + console.log( + '🚀 ~ Timeline ~ onPeriodClick ~ this.props:', + this.props + ) this.props.onChange(period) } + console.log('🚀 ~ Timeline ~ onPeriodClick ~ this.props:', this.props) // Stop animation if running this.stop() @@ -166,14 +278,20 @@ class Timeline extends Component { // Play animation play = () => { const { period, periods, onChange } = this.props - const index = periods.findIndex((p) => p.id === period.id) - const isLastPeriod = index === periods.length - 1 + console.log('🚀 ~ Timeline ~ this.props:', this.props) + let sortedPeriods + sortedPeriods = periods.sort((a, b) => b.level - a.level) + sortedPeriods = periods.sort( + (a, b) => new Date(a.startDate) - new Date(b.startDate) + ) + const index = sortedPeriods.findIndex((p) => p.id === period.id) + const isLastPeriod = index === sortedPeriods.length - 1 // If new animation if (!this.timeout) { // Switch to first period if last if (isLastPeriod) { - onChange(periods[0]) + onChange(sortedPeriods[0]) } this.setState({ mode: 'play' }) @@ -185,7 +303,7 @@ class Timeline extends Component { } // Switch to next period - onChange(periods[index + 1]) + onChange(sortedPeriods[index + 1]) } // Call itself after delay diff --git a/src/components/periods/styles/Timeline.module.css b/src/components/periods/styles/Timeline.module.css index 5f87f8ca4..0eef902ee 100644 --- a/src/components/periods/styles/Timeline.module.css +++ b/src/components/periods/styles/Timeline.module.css @@ -3,8 +3,7 @@ width: calc(100% - 20px); position: absolute; left: 10px; - bottom: 28px; - height: 48px; + bottom: 32px; z-index: 1000px; user-select: none; } @@ -19,18 +18,19 @@ } .period { - fill: #333; + fill: #ffffff; stroke: #555; stroke-width: 1px; - fill-opacity: 0.1; + fill-opacity: .8; cursor: pointer; } .period.selected { - fill: #2b675c; - fill-opacity: 0.9; + fill: #147cd7; + fill-opacity: 1; } .period:hover:not(.selected) { - fill-opacity: 0.2; + fill: #F3F5F7; + fill-opacity: .9; } diff --git a/yarn.lock b/yarn.lock index 0d74b0a24..f454373b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2018,9 +2018,9 @@ classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2/analytics@git+https://github.com/d2-ci/analytics.git#19447b89130d615dc55e92d345b53869dca548ae": +"@dhis2/analytics@git+https://github.com/d2-ci/analytics.git#8c539c2cee08357ef6c7daaab2521e3df6ef7801": version "26.9.3" - resolved "git+https://github.com/d2-ci/analytics.git#19447b89130d615dc55e92d345b53869dca548ae" + resolved "git+https://github.com/d2-ci/analytics.git#8c539c2cee08357ef6c7daaab2521e3df6ef7801" dependencies: "@dhis2/multi-calendar-dates" "^1.2.2" "@dnd-kit/core" "^6.0.7" From dbd09ba43bebc64cb07926ed3db8ffec7201a390 Mon Sep 17 00:00:00 2001 From: braimbault Date: Mon, 9 Dec 2024 18:51:07 +0100 Subject: [PATCH 11/28] feat: use PeriodDimension in ThematicDialog --- i18n/en.pot | 18 ++---- src/components/core/RadioGroup.js | 4 +- src/components/download/DownloadSettings.js | 2 +- src/components/edit/LayerEdit.js | 4 +- src/components/edit/event/EventDialog.js | 11 +++- .../edit/thematic/ThematicDialog.js | 51 +--------------- .../edit/trackedEntity/PeriodTypeSelect.js | 2 +- .../edit/trackedEntity/TrackedEntityDialog.js | 9 ++- .../layerSources/ManageLayerSourcesModal.js | 2 +- .../layers/overlays/AddLayerPopover.js | 2 +- src/components/map/Map.js | 1 - src/components/map/layers/EventPopup.js | 5 +- src/components/map/layers/ThematicLayer.js | 2 - .../map/layers/TrackedEntityPopup.js | 4 +- src/components/periods/PeriodTypeSelect.js | 5 -- src/components/periods/RenderingStrategy.js | 30 +++++----- src/components/periods/StartEndDate.js | 33 ++++------ src/components/periods/StartEndDates.js | 60 ------------------- src/components/periods/Timeline.js | 20 +++---- .../periods/__tests__/Timeline.spec.js | 1 + .../{ => styles}/StartEndDate.module.css | 0 src/constants/periods.js | 7 --- src/loaders/thematicLoader.js | 45 ++++++-------- .../__tests__/teiRelationshipsParser.spec.js | 6 -- 24 files changed, 87 insertions(+), 237 deletions(-) delete mode 100644 src/components/periods/StartEndDates.js rename src/components/periods/{ => styles}/StartEndDate.module.css (100%) diff --git a/i18n/en.pot b/i18n/en.pot index 84bcab976..3a5e2610e 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: 2024-12-05T11:28:26.227Z\n" -"PO-Revision-Date: 2024-12-05T11:28:26.227Z\n" +"POT-Creation-Date: 2024-12-09T14:15:43.944Z\n" +"PO-Revision-Date: 2024-12-09T14:15:43.944Z\n" msgid "2020" msgstr "2020" @@ -927,11 +927,8 @@ msgstr "Period type" msgid "Start/end dates" msgstr "Start/end dates" -msgid "Select " -msgstr "Select " - -msgid " or more periods to enable timeline or split map views." -msgstr " or more periods to enable timeline or split map views." +msgid "Select {{number}} or more periods to enable timeline or split map views." +msgstr "Select {{number}} or more periods to enable timeline or split map views." msgid "Only one timeline is allowed." msgstr "Only one timeline is allowed." @@ -939,11 +936,8 @@ msgstr "Only one timeline is allowed." msgid "Remove other layers to enable split map views." msgstr "Remove other layers to enable split map views." -msgid "Only up to " -msgstr "Only up to " - -msgid " periods can be selected to enable timeline or split map views." -msgstr " periods can be selected to enable timeline or split map views." +msgid "Only up to {{number}} periods can be selected to enable split map views." +msgstr "Only up to {{number}} periods can be selected to enable split map views." msgid "Period display mode" msgstr "Period display mode" diff --git a/src/components/core/RadioGroup.js b/src/components/core/RadioGroup.js index 2784c4052..e16600e6f 100644 --- a/src/components/core/RadioGroup.js +++ b/src/components/core/RadioGroup.js @@ -56,11 +56,11 @@ const RadioGroup = ({ RadioGroup.propTypes = { onChange: PropTypes.func.isRequired, + boldLabel: PropTypes.bool, children: PropTypes.arrayOf(PropTypes.node), + compact: PropTypes.bool, dataTest: PropTypes.string, display: PropTypes.string, - boldLabel: PropTypes.bool, - compact: PropTypes.bool, helpText: PropTypes.string, label: PropTypes.string, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), diff --git a/src/components/download/DownloadSettings.js b/src/components/download/DownloadSettings.js index 9a137cabc..de6567b61 100644 --- a/src/components/download/DownloadSettings.js +++ b/src/components/download/DownloadSettings.js @@ -2,8 +2,8 @@ import i18n from '@dhis2/d2-i18n' import { Button, ButtonStrip } from '@dhis2/ui' import React, { useState, useMemo, useCallback, useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' -import useKeyDown from '../../hooks/useKeyDown.js' import { setDownloadConfig } from '../../actions/download.js' +import useKeyDown from '../../hooks/useKeyDown.js' import { standardizeFilename } from '../../util/dataDownload.js' import { downloadMapImage, downloadSupport } from '../../util/export-image.js' import { getSplitViewLayer } from '../../util/helpers.js' diff --git a/src/components/edit/LayerEdit.js b/src/components/edit/LayerEdit.js index e260d855a..6002491d5 100644 --- a/src/components/edit/LayerEdit.js +++ b/src/components/edit/LayerEdit.js @@ -9,12 +9,12 @@ import { ButtonStrip, } from '@dhis2/ui' import PropTypes from 'prop-types' -import React, { useState, useEffect } from 'react' +import React, { useState } from 'react' import { connect } from 'react-redux' import { addLayer, updateLayer, cancelLayer } from '../../actions/layers.js' import { EARTH_ENGINE_LAYER } from '../../constants/layers.js' -import { useOrgUnits } from '../OrgUnitsProvider.js' import useKeyDown from '../../hooks/useKeyDown.js' +import { useOrgUnits } from '../OrgUnitsProvider.js' import EarthEngineDialog from './earthEngine/EarthEngineDialog.js' import EventDialog from './event/EventDialog.js' import FacilityDialog from './FacilityDialog.js' diff --git a/src/components/edit/event/EventDialog.js b/src/components/edit/event/EventDialog.js index 647dcee3a..41288a306 100644 --- a/src/components/edit/event/EventDialog.js +++ b/src/components/edit/event/EventDialog.js @@ -47,7 +47,7 @@ import FilterGroup from '../../dataItem/filter/FilterGroup.js' import StyleByDataItem from '../../dataItem/StyleByDataItem.js' import OrgUnitSelect from '../../orgunits/OrgUnitSelect.js' import RelativePeriodSelect from '../../periods/RelativePeriodSelect.js' -import StartEndDates from '../../periods/StartEndDates.js' +import StartEndDate from '../../periods/StartEndDate.js' import ProgramSelect from '../../program/ProgramSelect.js' import ProgramStageSelect from '../../program/ProgramStageSelect.js' import BufferRadius from '../shared/BufferRadius.js' @@ -82,6 +82,7 @@ class EventDialog extends Component { legendSet: PropTypes.object, method: PropTypes.number, orgUnits: PropTypes.object, + periodsSettings: PropTypes.object, program: PropTypes.shape({ id: PropTypes.string.isRequired, trackedEntityType: PropTypes.object, @@ -174,6 +175,7 @@ class EventDialog extends Component { programStage, startDate, legendSet, + periodsSettings, } = this.props const { @@ -272,10 +274,13 @@ class EventDialog extends Component { className={styles.select} /> {period && period.id === START_END_DATES && ( - )}
diff --git a/src/components/edit/thematic/ThematicDialog.js b/src/components/edit/thematic/ThematicDialog.js index a40b79629..53e7ac768 100644 --- a/src/components/edit/thematic/ThematicDialog.js +++ b/src/components/edit/thematic/ThematicDialog.js @@ -29,7 +29,6 @@ import { RENDERING_STRATEGY_SINGLE, } from '../../../constants/layers.js' import { - RELATIVE_PERIODS, PREDEFINED_PERIODS, START_END_DATES, } from '../../../constants/periods.js' @@ -55,6 +54,7 @@ import IndicatorGroupSelect from '../../indicator/IndicatorGroupSelect.js' import IndicatorSelect from '../../indicator/IndicatorSelect.js' import OrgUnitSelect from '../../orgunits/OrgUnitSelect.js' import RenderingStrategy from '../../periods/RenderingStrategy.js' +import StartEndDate from '../../periods/StartEndDate.js' import ProgramIndicatorSelect from '../../program/ProgramIndicatorSelect.js' import ProgramSelect from '../../program/ProgramSelect.js' import Labels from '../shared/Labels.js' @@ -65,13 +65,13 @@ import NoDataColor from './NoDataColor.js' import RadiusSelect, { isValidRadius } from './RadiusSelect.js' import ThematicMapTypeSelect from './ThematicMapTypeSelect.js' import ValueTypeSelect from './ValueTypeSelect.js' -import StartEndDate from '../../periods/StartEndDate.js' class ThematicDialog extends Component { static propTypes = { setClassification: PropTypes.func.isRequired, setDataElementGroup: PropTypes.func.isRequired, setDataItem: PropTypes.func.isRequired, + setEndDate: PropTypes.func.isRequired, setIndicatorGroup: PropTypes.func.isRequired, setLegendSet: PropTypes.func.isRequired, setNoDataColor: PropTypes.func.isRequired, @@ -81,6 +81,7 @@ class ThematicDialog extends Component { setPeriods: PropTypes.func.isRequired, setProgram: PropTypes.func.isRequired, setRenderingStrategy: PropTypes.func.isRequired, + setStartDate: PropTypes.func.isRequired, setValueType: PropTypes.func.isRequired, validateLayer: PropTypes.bool.isRequired, onLayerValidation: PropTypes.func.isRequired, @@ -128,20 +129,8 @@ class ThematicDialog extends Component { setOrgUnits, } = this.props - console.log( - '🚀 ~ componentDidMount ~ periodType:', - this.props.periodType - ) - console.log('🚀 ~ componentDidMount ~ filters:', this.props.filters) - console.log('🚀 ~ componentDidMount ~ startDate:', startDate) - console.log('🚀 ~ componentDidMount ~ endDate:', endDate) - const dataItem = getDataItemFromColumns(columns) const periods = getPeriodsFromFilters(filters) - console.log( - '🚀 ~ ThematicDialog ~ componentDidMount ~ periods:', - periods - ) const { keyAnalysisRelativePeriod: defaultPeriod, hiddenPeriods } = systemSettings @@ -167,21 +156,11 @@ class ThematicDialog extends Component { startDate !== '' && endDate !== undefined && endDate !== '' - console.log( - '🚀 ~ ThematicDialog ~ componentDidMount ~ hasDate:', - hasDate - ) if (hasDate) { - console.log( - '🚀 ~ componentDidMount ~ setPeriodType HERE (START_END_DATES)' - ) const keepPeriod = false setPeriodType({ value: START_END_DATES }, keepPeriod) } else { - console.log( - '🚀 ~ componentDidMount ~ setPeriodType HERE (PREDEFINED_PERIODS)' - ) const keepPeriod = true setPeriodType({ value: PREDEFINED_PERIODS }, keepPeriod) } @@ -193,8 +172,6 @@ class ThematicDialog extends Component { defaultPeriod && isPeriodAvailable(defaultPeriod, hiddenPeriods) ) { - console.log('🚀 ~ componentDidMount ~ setPeriods HERE') - console.log('getRelativePeriodsName()', getRelativePeriodsName()) const defaultPeriods = [ { id: defaultPeriod, @@ -267,9 +244,6 @@ class ThematicDialog extends Component { // !TODO Backup Start-End dates // Remove Start-End dates // !TODO Restore Predefined periods backup - console.log( - '🚀 ~ ThematicDialog ~ componentDidUpdate ~ case PREDEFINED_PERIODS' - ) setStartDate('') setEndDate('') break @@ -277,9 +251,6 @@ class ThematicDialog extends Component { // !TODO Backup Predefined periods // Remove Predefined periods // !TODO Restore Start-End dates backup - console.log( - '🚀 ~ ThematicDialog ~ componentDidUpdate ~ case START_END_DATES' - ) setPeriods([]) break } @@ -338,11 +309,6 @@ class ThematicDialog extends Component { legendSetError, } = this.state - console.log('🚀 ~ render ~ periodType:', periodType) - console.log('🚀 ~ render ~ filters:', filters) - console.log('🚀 ~ render ~ startDate:', startDate) - console.log('🚀 ~ render ~ endDate:', endDate) - const periods = getPeriodsFromFilters(filters) const dataItem = getDataItemFromColumns(columns) @@ -726,18 +692,7 @@ class ThematicDialog extends Component { 'period' ) } else if (periodType === START_END_DATES) { - console.log( - '🚀 ~ ThematicDialog ~ validate ~ periodType:', - periodType - ) const error = getStartEndDateError(startDate, endDate) - console.log( - '🚀 ~ ThematicDialog ~ validate ~ startDate:', - startDate - ) - console.log('🚀 ~ ThematicDialog ~ validate ~ endDate:', endDate) - console.log('🚀 ~ ThematicDialog ~ validate ~ error:', error) - if (error) { return this.setErrorState('periodError', error, 'period') } diff --git a/src/components/edit/trackedEntity/PeriodTypeSelect.js b/src/components/edit/trackedEntity/PeriodTypeSelect.js index 937c48427..b7751d420 100644 --- a/src/components/edit/trackedEntity/PeriodTypeSelect.js +++ b/src/components/edit/trackedEntity/PeriodTypeSelect.js @@ -19,7 +19,7 @@ const PeriodTypeSelect = ({ setPeriodType({ id: type })} + onChange={(type) => setPeriodType({ value: type })} > -
)} diff --git a/src/components/layerSources/ManageLayerSourcesModal.js b/src/components/layerSources/ManageLayerSourcesModal.js index d9e26337d..c382a1f29 100644 --- a/src/components/layerSources/ManageLayerSourcesModal.js +++ b/src/components/layerSources/ManageLayerSourcesModal.js @@ -10,8 +10,8 @@ import { import PropTypes from 'prop-types' import React from 'react' import earthEngineLayers from '../../constants/earthEngineLayers/index.js' -import useManagedLayerSourcesStore from '../../hooks/useManagedLayerSourcesStore.js' import useKeyDown from '../../hooks/useKeyDown.js' +import useManagedLayerSourcesStore from '../../hooks/useManagedLayerSourcesStore.js' import LayerSource from './LayerSource.js' import styles from './styles/ManageLayerSourcesModal.module.css' diff --git a/src/components/layers/overlays/AddLayerPopover.js b/src/components/layers/overlays/AddLayerPopover.js index 13db783fd..41cbeaf23 100644 --- a/src/components/layers/overlays/AddLayerPopover.js +++ b/src/components/layers/overlays/AddLayerPopover.js @@ -6,8 +6,8 @@ import { useSelector, useDispatch } from 'react-redux' import { addLayer, editLayer } from '../../../actions/layers.js' import earthEngineLayers from '../../../constants/earthEngineLayers/index.js' import { EXTERNAL_LAYER } from '../../../constants/layers.js' -import useManagedLayerSourcesStore from '../../../hooks/useManagedLayerSourcesStore.js' import useKeyDown from '../../../hooks/useKeyDown.js' +import useManagedLayerSourcesStore from '../../../hooks/useManagedLayerSourcesStore.js' import { isSplitViewMap } from '../../../util/helpers.js' import ManageLayerSourcesButton from '../../layerSources/ManageLayerSourcesButton.js' import LayerList from './LayerList.js' diff --git a/src/components/map/Map.js b/src/components/map/Map.js index b4cccb301..c999fd46e 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -121,7 +121,6 @@ class Map extends Component { componentDidUpdate(prevProps) { const { resizeCount, isFullscreen, isPlugin } = this.props - console.log('🚀 ~ Map ~ componentDidUpdate ~ resizeCount:', resizeCount) if (resizeCount !== prevProps.resizeCount) { this.map.resize() diff --git a/src/components/map/layers/EventPopup.js b/src/components/map/layers/EventPopup.js index 0bf677511..58ab1cb57 100644 --- a/src/components/map/layers/EventPopup.js +++ b/src/components/map/layers/EventPopup.js @@ -9,7 +9,6 @@ import Popup from '../Popup.js' // Returns true if value is not undefined or null; const hasValue = (value) => value !== undefined && value !== null - const EVENTS_QUERY = { events: { resource: 'tracker/events', @@ -40,10 +39,10 @@ const getDataRows = ({ displayElements, dataValues, styleDataItem, value }) => { if (valueType === 'COORDINATE' && value) { formattedValue = formatCoordinate(value) - } else if (options) { - formattedValue = options[value] } else if (!hasValue(value)) { formattedValue = i18n.t('Not set') + } else if (options) { + formattedValue = options[value] } dataRows.push( diff --git a/src/components/map/layers/ThematicLayer.js b/src/components/map/layers/ThematicLayer.js index e5de1eda6..d25d2d678 100644 --- a/src/components/map/layers/ThematicLayer.js +++ b/src/components/map/layers/ThematicLayer.js @@ -140,8 +140,6 @@ class ThematicLayer extends Layer { periods, renderingStrategy = RENDERING_STRATEGY_SINGLE, } = this.props - console.log('🚀 ~ ThematicLayer ~ setPeriod ~ periods:', periods) - console.log('🚀 ~ ThematicLayer ~ setPeriod ~ period:', period) if (!period && !periods) { return diff --git a/src/components/map/layers/TrackedEntityPopup.js b/src/components/map/layers/TrackedEntityPopup.js index f427b8948..ff34195b7 100644 --- a/src/components/map/layers/TrackedEntityPopup.js +++ b/src/components/map/layers/TrackedEntityPopup.js @@ -30,10 +30,10 @@ const getDataRows = ({ displayAttributes, attributes }) => { if (valueType === 'COORDINATE' && value) { formattedValue = formatCoordinate(value) - } else if (options) { - formattedValue = options[value] } else if (!hasValue(value)) { formattedValue = i18n.t('Not set') + } else if (options) { + formattedValue = options[value] } dataRows.push( diff --git a/src/components/periods/PeriodTypeSelect.js b/src/components/periods/PeriodTypeSelect.js index 815ee5c63..2122545b6 100644 --- a/src/components/periods/PeriodTypeSelect.js +++ b/src/components/periods/PeriodTypeSelect.js @@ -31,11 +31,6 @@ const PeriodTypeSelect = ({ ) if (!period || isRelativePeriod) { - console.log( - '🚀 ~ useEffect ~ isRelativePeriod:', - isRelativePeriod - ) - console.log('🚀 ~ useEffect ~ periodTypes[0]:', periodTypes[0]) // default to first period type onChange(periodTypes[0], isRelativePeriod) } diff --git a/src/components/periods/RenderingStrategy.js b/src/components/periods/RenderingStrategy.js index ba38ec722..f8aa0b2ab 100644 --- a/src/components/periods/RenderingStrategy.js +++ b/src/components/periods/RenderingStrategy.js @@ -1,3 +1,4 @@ +import { getRelativePeriodsDetails } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' import PropTypes from 'prop-types' import React, { useEffect } from 'react' @@ -11,14 +12,12 @@ import { MULTIMAP_MIN_PERIODS, MULTIMAP_MAX_PERIODS, } from '../../constants/periods.js' -import { getPeriodsFromFilters } from '../../util/analytics.js' import usePrevious from '../../hooks/usePrevious.js' +import { getPeriodsFromFilters } from '../../util/analytics.js' import { Radio, RadioGroup } from '../core/index.js' -import { getRelativePeriodsDetails } from '@dhis2/analytics' const countPeriods = (periods) => { const periodsDetails = getRelativePeriodsDetails() - console.log('periodsDetails', periodsDetails) const total_v1 = periods.reduce( (sum, period) => @@ -30,7 +29,6 @@ const countPeriods = (periods) => { ) const durationByType = periods.reduce((acc, period) => { - console.log('🚀 ~ test ~ period:', period) const periodDetails = periodsDetails[period.id] if (acc['FIXED_PERIOD'] === undefined) { acc['FIXED_PERIOD'] = { @@ -68,8 +66,6 @@ const countPeriods = (periods) => { const total_v2 = sumObjectValues(durationByType) - console.log('total_v1', total_v1) - console.log('total_v2', total_v2) return total_v2 } @@ -91,9 +87,7 @@ const RenderingStrategy = ({ ) ) const hasTooManyPeriods = useSelector(({ layerEdit }) => { - console.log('layerEdit', layerEdit) const periods = getPeriodsFromFilters(layerEdit.filters) - console.log('periods', periods) return countPeriods(periods) > MULTIMAP_MAX_PERIODS }) @@ -119,11 +113,12 @@ const RenderingStrategy = ({ if (countPeriods(periods) < MULTIMAP_MIN_PERIODS) { helpText.push( - i18n.t('Select ') + - MULTIMAP_MIN_PERIODS + - i18n.t( - ' or more periods to enable timeline or split map views.' - ) + i18n.t( + 'Select {{number}} or more periods to enable timeline or split map views.', + { + number: MULTIMAP_MIN_PERIODS, + } + ) ) } if (hasOtherTimelineLayers) { @@ -134,9 +129,12 @@ const RenderingStrategy = ({ } if (hasTooManyPeriods) { helpText.push( - i18n.t('Only up to ') + - MULTIMAP_MAX_PERIODS + - i18n.t(' periods can be selected to enable split map views.') + i18n.t( + 'Only up to {{number}} periods can be selected to enable split map views.', + { + number: MULTIMAP_MAX_PERIODS, + } + ) ) } helpText = helpText.join(' ') diff --git a/src/components/periods/StartEndDate.js b/src/components/periods/StartEndDate.js index 5a324fd39..0c5a6fbf2 100644 --- a/src/components/periods/StartEndDate.js +++ b/src/components/periods/StartEndDate.js @@ -1,38 +1,27 @@ import i18n from '@dhis2/d2-i18n' -import { - Field, - IconArrowRight16, - InputField, - CalendarInput, - colors, -} from '@dhis2/ui' +import { Field, IconArrowRight16, CalendarInput, colors } from '@dhis2/ui' import PropTypes from 'prop-types' -import React, { useEffect, useState } from 'react' +import React, { useState } from 'react' import { connect } from 'react-redux' import { setStartDate, setEndDate } from '../../actions/layerEdit.js' -import { DEFAULT_START_DATE, DEFAULT_END_DATE } from '../../constants/layers.js' -import styles from './StartEndDate.module.css' +import styles from './styles/StartEndDate.module.css' const StartEndDate = (props) => { const { startDate, endDate, setStartDate, setEndDate, periodsSettings } = props - console.log('🚀 ~ StartEndDate ~ periodsSettings:', periodsSettings) + const [start, setStart] = useState(startDate.slice(0, 10)) + const [end, setEnd] = useState(endDate.slice(0, 10)) - const [start, setStart] = useState(startDate) - const [end, setEnd] = useState(endDate) - - console.log('🚀 ~ StartEndDate ~ startDate:', typeof startDate) - console.log('🚀 ~ StartEndDate ~ endDate:', typeof endDate) const hasDate = startDate !== undefined && endDate !== undefined const onStartDateChange = ({ calendarDateString: value }) => { - setStart(value) - setStartDate(value) + setStart(value.slice(0, 10)) + setStartDate(value.slice(0, 10)) } const onEndDateChange = ({ calendarDateString: value }) => { - setEnd(value) - setEndDate(value) + setEnd(value.slice(0, 10)) + setEndDate(value.slice(0, 10)) } return hasDate ? ( { ) : null } StartEndDate.propTypes = { - startDate: PropTypes.string, + setEndDate: PropTypes.func.isRequired, + setStartDate: PropTypes.func.isRequired, endDate: PropTypes.string, periodsSettings: PropTypes.object, + startDate: PropTypes.string, } export default connect(null, { setStartDate, setEndDate })(StartEndDate) diff --git a/src/components/periods/StartEndDates.js b/src/components/periods/StartEndDates.js deleted file mode 100644 index 58030c249..000000000 --- a/src/components/periods/StartEndDates.js +++ /dev/null @@ -1,60 +0,0 @@ -import i18n from '@dhis2/d2-i18n' -import PropTypes from 'prop-types' -import React, { Fragment, useEffect } from 'react' -import { connect } from 'react-redux' -import { setStartDate, setEndDate } from '../../actions/layerEdit.js' -import { DEFAULT_START_DATE, DEFAULT_END_DATE } from '../../constants/layers.js' -import { DatePicker } from '../core/index.js' -import styles from '../edit/styles/LayerDialog.module.css' - -const StartEndDates = (props) => { - const { - startDate, - endDate, - setStartDate, - setEndDate, - errorText, - className, - } = props - const hasDate = startDate !== undefined && endDate !== undefined - - useEffect(() => { - if (!hasDate) { - //setStartDate(DEFAULT_START_DATE) - //setEndDate(DEFAULT_END_DATE) - } - }, [hasDate, setStartDate, setEndDate]) - - return hasDate ? ( - - - - {errorText && ( -
- {errorText} -
- )} -
- ) : null -} - -StartEndDates.propTypes = { - setEndDate: PropTypes.func.isRequired, - setStartDate: PropTypes.func.isRequired, - className: PropTypes.string, - endDate: PropTypes.string, - errorText: PropTypes.string, - startDate: PropTypes.string, -} - -export default connect(null, { setStartDate, setEndDate })(StartEndDates) diff --git a/src/components/periods/Timeline.js b/src/components/periods/Timeline.js index 22421d9d8..12ec73577 100644 --- a/src/components/periods/Timeline.js +++ b/src/components/periods/Timeline.js @@ -1,3 +1,4 @@ +import { PERIOD_TYPE_REGEX } from '@dhis2/analytics' import cx from 'classnames' import { axisBottom } from 'd3-axis' import { scaleTime } from 'd3-scale' @@ -6,7 +7,6 @@ import PropTypes from 'prop-types' import React, { Component } from 'react' import timeTicks from '../../util/timeTicks.js' import styles from './styles/Timeline.module.css' -import { PERIOD_TYPE_REGEX } from '@dhis2/analytics' const paddingLeft = 40 const paddingRight = 20 @@ -74,7 +74,7 @@ const addPeriodType = (item) => { } const countUniqueRanks = (periods) => { - let periodsWithType = periods.map((item) => addPeriodType(item)) + const periodsWithType = periods.map((item) => addPeriodType(item)) const levels = [...new Set(periodsWithType.map((item) => item.level))] return levels.length } @@ -137,8 +137,10 @@ class Timeline extends Component { return ( this.onPeriodClick(item)} /> ) @@ -192,7 +194,6 @@ class Timeline extends Component { // Set time scale setTimeScale = () => { const { periods } = this.props - console.log('🚀 ~ Timeline ~ this.props:', this.props) const { width } = this.state if (!periods.length) { @@ -224,7 +225,6 @@ class Timeline extends Component { // Set timeline axis setTimeAxis = () => { const { periodId, periods } = this.props - console.log('🚀 ~ Timeline ~ this.props:', this.props) const numPeriods = periods.length * (doubleTicksPeriods.includes(periodId) ? 2 : 1) const { width } = this.state @@ -254,13 +254,8 @@ class Timeline extends Component { onPeriodClick(period) { // Switch to period if different if (period.id !== this.props.period.id) { - console.log( - '🚀 ~ Timeline ~ onPeriodClick ~ this.props:', - this.props - ) this.props.onChange(period) } - console.log('🚀 ~ Timeline ~ onPeriodClick ~ this.props:', this.props) // Stop animation if running this.stop() @@ -278,7 +273,6 @@ class Timeline extends Component { // Play animation play = () => { const { period, periods, onChange } = this.props - console.log('🚀 ~ Timeline ~ this.props:', this.props) let sortedPeriods sortedPeriods = periods.sort((a, b) => b.level - a.level) sortedPeriods = periods.sort( diff --git a/src/components/periods/__tests__/Timeline.spec.js b/src/components/periods/__tests__/Timeline.spec.js index ecba61f4b..9b74aed5f 100644 --- a/src/components/periods/__tests__/Timeline.spec.js +++ b/src/components/periods/__tests__/Timeline.spec.js @@ -69,6 +69,7 @@ describe('Timeline', () => { it('should call onChange with the period clicked', () => { const wrapper = renderWithProps(props) wrapper.find('rect').first().simulate('click') + // Update periods[0] to account for the updated sorting logic expect(onChangeSpy).toHaveBeenCalledWith(periods[0]) }) diff --git a/src/components/periods/StartEndDate.module.css b/src/components/periods/styles/StartEndDate.module.css similarity index 100% rename from src/components/periods/StartEndDate.module.css rename to src/components/periods/styles/StartEndDate.module.css diff --git a/src/constants/periods.js b/src/constants/periods.js index 148b9d57f..1e136db48 100644 --- a/src/constants/periods.js +++ b/src/constants/periods.js @@ -35,15 +35,8 @@ export const periodGroups = [ // TODO: import from @dhis2/analytics (needs to be defined) const TODAY = 'TODAY' const YESTERDAY = 'YESTERDAY' -const LAST_14_DAYS = 'LAST_14_DAYS' -const LAST_30_DAYS = 'LAST_30_DAYS' -const LAST_60_DAYS = 'LAST_60_DAYS' -const LAST_90_DAYS = 'LAST_90_DAYS' -const LAST_180_DAYS = 'LAST_180_DAYS' const THIS_WEEK = 'THIS_WEEK' const LAST_WEEK = 'LAST_WEEK' -const LAST_52_WEEKS = 'LAST_52_WEEKS' -const WEEKS_THIS_YEAR = 'WEEKS_THIS_YEAR' const THIS_BIWEEK = 'THIS_BIWEEK' const LAST_BIWEEK = 'LAST_BIWEEK' const THIS_MONTH = 'THIS_MONTH' diff --git a/src/loaders/thematicLoader.js b/src/loaders/thematicLoader.js index ccd737764..29ca6751e 100644 --- a/src/loaders/thematicLoader.js +++ b/src/loaders/thematicLoader.js @@ -24,7 +24,6 @@ import { import { getOrgUnitsFromRows, getPeriodsFromFilters, - getPeriodFromFilters, getValidDimensionsFromFilters, getDataItemFromColumns, getApiResponseNames, @@ -98,18 +97,11 @@ const thematicLoader = async ({ config, engine, nameProperty }) => { const isSingleMap = renderingStrategy === RENDERING_STRATEGY_SINGLE const isBubbleMap = thematicMapType === THEMATIC_BUBBLE const isSingleColor = config.method === CLASSIFICATION_SINGLE_COLOR - const period = getPeriodFromFilters(config.filters) - console.log('🚀 ~ thematicLoader ~ period:', period) - const periodx = getPeriodsFromFilters(config.filters) - console.log('🚀 ~ thematicLoader ~ periodx:', periodx) - console.log('🚀 ~ thematicLoader ~ data.metaData:', data.metaData) + const presetPeriods = getPeriodsFromFilters(config.filters) const periods = getPeriodsFromMetaData(data.metaData) - console.log('🚀 ~ thematicLoader ~ periods:', periods) const dimensions = getValidDimensionsFromFilters(config.filters) const names = getApiResponseNames(data) - // TODO Handle multiple maps const valuesByPeriod = !isSingleMap ? getValuesByPeriod(data) : null - console.log('🚀 ~ thematicLoader ~ valuesByPeriod:', valuesByPeriod) const valueById = getValueById(data) const valueFeatures = noDataColor ? features @@ -157,15 +149,15 @@ const thematicLoader = async ({ config, engine, nameProperty }) => { const legend = { title: name, - period: period - ? periodx.map((pe) => names[pe.id] || pe.id).join(', ') - : formatStartEndDate( - getDateArray(config.startDate), - getDateArray(config.endDate) - ), + period: + presetPeriods.length > 0 + ? presetPeriods.map((pe) => names[pe.id] || pe.id).join(', ') + : formatStartEndDate( + getDateArray(config.startDate), + getDateArray(config.endDate) + ), items: legendItems, } - console.log('🚀 ~ thematicLoader ~ names:', names) if (dimensions && dimensions.length) { legend.filters = dimensions.map( @@ -271,7 +263,6 @@ const thematicLoader = async ({ config, engine, nameProperty }) => { if (noDataColor && Array.isArray(legend.items) && !isBubbleMap) { legend.items.push({ color: noDataColor, name: i18n.t('No data') }) } - console.log('🚀 ~ thematicLoader ~ legend:', legend) return { ...config, @@ -358,13 +349,8 @@ const loadData = async (config, nameProperty) => { renderingStrategy = RENDERING_STRATEGY_SINGLE, eventStatus, } = config - console.log('🚀 ~ loadData ~ endDate:', endDate) - console.log('🚀 ~ loadData ~ startDate:', startDate) const orgUnits = getOrgUnitsFromRows(rows) - const period = getPeriodFromFilters(filters) - console.log('🚀 ~ loadData ~ period:', period) - const periodx = getPeriodsFromFilters(filters) - console.log('🚀 ~ loadData ~ periodx:', periodx) + const presetPeriods = getPeriodsFromFilters(filters) const dimensions = getValidDimensionsFromFilters(config.filters) const dataItem = getDataItemFromColumns(columns) || {} const coordinateField = getCoordinateField(config) @@ -386,15 +372,18 @@ const loadData = async (config, nameProperty) => { .withDisplayProperty(nameProperty) if (!isSingleMap) { - // TODO Handle multiple maps analyticsRequest = analyticsRequest.addPeriodDimension( - periodx.map((pe) => pe.id) + presetPeriods.map((pe) => pe.id) ) } else { analyticsRequest = - periodx.length > 0 - ? analyticsRequest.addPeriodFilter(periodx.map((pe) => pe.id)) - : analyticsRequest.withStartDate(startDate).withEndDate(endDate) + presetPeriods.length > 0 + ? analyticsRequest.addPeriodFilter( + presetPeriods.map((pe) => pe.id) + ) + : analyticsRequest + .withStartDate(startDate.slice(0, 10)) + .withEndDate(endDate.slice(0, 10)) } if (dimensions) { diff --git a/src/util/__tests__/teiRelationshipsParser.spec.js b/src/util/__tests__/teiRelationshipsParser.spec.js index 2f80aa199..ae28b69e3 100644 --- a/src/util/__tests__/teiRelationshipsParser.spec.js +++ b/src/util/__tests__/teiRelationshipsParser.spec.js @@ -570,11 +570,8 @@ describe('getDataWithRelationships', () => { expect(result).toEqual(expected) } else { expect(result).toHaveProperty('primary') - console.log('primary', result.primary) expect(result).toHaveProperty('relationships') - console.log('relationships', result.relationships) expect(result).toHaveProperty('secondary') - console.log('secondary', result.secondary) const resultPrimaryIds = result.primary.map((item) => item.id) expect(resultPrimaryIds.sort()).toEqual(expected.primary.sort()) @@ -691,11 +688,8 @@ describe('getDataWithRelationships', () => { expect(result).toEqual(expected) } else { expect(result).toHaveProperty('primary') - //console.log('primary', result.primary) expect(result).toHaveProperty('relationships') - //console.log('relationships', result.relationships) expect(result).toHaveProperty('secondary') - //console.log('secondary', result.secondary) const resultPrimaryIds = result.primary.map((item) => item.id) expect(resultPrimaryIds.sort()).toEqual(expected.primary.sort()) From 245848aa119092abed079f63cd8617b2513e3551 Mon Sep 17 00:00:00 2001 From: braimbault Date: Tue, 10 Dec 2024 10:40:10 +0100 Subject: [PATCH 12/28] fix: add sumObjectValues helper and test --- src/util/__tests__/helpers.spec.js | 45 ++++++++++++++++++++++++++++++ src/util/helpers.js | 13 +++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/util/__tests__/helpers.spec.js diff --git a/src/util/__tests__/helpers.spec.js b/src/util/__tests__/helpers.spec.js new file mode 100644 index 000000000..be1a55e22 --- /dev/null +++ b/src/util/__tests__/helpers.spec.js @@ -0,0 +1,45 @@ +import { sumObjectValues } from '../helpers.js' + +describe('sumObjectValues', () => { + it('should return 0 for an empty object', () => { + expect(sumObjectValues({})).toBe(0) + }) + + it('should sum flat numeric values in an object', () => { + expect(sumObjectValues({ a: 1, b: 2, c: 3 })).toBe(6) + }) + + it('should handle nested objects correctly', () => { + const obj = { + a: 1, + b: { c: 2, d: { e: 3 } }, + } + expect(sumObjectValues(obj)).toBe(6) + }) + + it('should ignore non-numeric values', () => { + const obj = { + a: 1, + b: 'string', + c: { d: 2, e: null, f: true }, + } + expect(sumObjectValues(obj)).toBe(3) + }) + + it('should handle an object with only non-numeric values', () => { + const obj = { + a: 'string', + b: null, + c: undefined, + } + expect(sumObjectValues(obj)).toBe(0) + }) + + it('should handle arrays as object values', () => { + const obj = { + a: [1, 2, 3], + b: { c: [4, 5], d: 6 }, + } + expect(sumObjectValues(obj)).toBe(21) // Note: Array elements aren't handled differently, treated as objects. + }) +}) diff --git a/src/util/helpers.js b/src/util/helpers.js index cbb9e7251..fb118ddef 100644 --- a/src/util/helpers.js +++ b/src/util/helpers.js @@ -141,3 +141,16 @@ export const getLongestTextLength = (array, key) => : text, '' ).length + +// Sum all numbers in an object recursively +export const sumObjectValues = (obj) => + Object.values(obj).reduce((sum, value) => { + if (value === null || value === undefined) { + return sum + } else if (typeof value === 'object') { + return sum + sumObjectValues(value) + } else if (typeof value === 'number') { + return sum + value + } + return sum + }, 0) From e7447485430e1ba21a1cd7311a0eefe872d13c7f Mon Sep 17 00:00:00 2001 From: braimbault Date: Tue, 10 Dec 2024 14:43:02 +0100 Subject: [PATCH 13/28] fix: add getPeriodsDurationByType util and test --- src/constants/periods.js | 1 + src/util/__tests__/periods.spec.js | 34 +++++++++++++++++- src/util/periods.js | 55 +++++++++++++++++++++++++++++- 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/constants/periods.js b/src/constants/periods.js index 1e136db48..7d8914f33 100644 --- a/src/constants/periods.js +++ b/src/constants/periods.js @@ -54,6 +54,7 @@ const LAST_YEAR = 'LAST_YEAR' export const PREDEFINED_PERIODS = 'PREDEFINED_PERIODS' export const RELATIVE_PERIODS = 'RELATIVE_PERIODS' +export const FIXED_PERIODS = 'FIXED_PERIODS' export const START_END_DATES = 'START_END_DATES' export const periodTypes = (includeRelativePeriods) => [ diff --git a/src/util/__tests__/periods.spec.js b/src/util/__tests__/periods.spec.js index 2751bdb8d..3dbec5d72 100644 --- a/src/util/__tests__/periods.spec.js +++ b/src/util/__tests__/periods.spec.js @@ -1,4 +1,12 @@ -import { getFixedPeriodsByType } from '../periods.js' +import { getFixedPeriodsByType, getPeriodsDurationByType } from '../periods.js' + +const periods = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }] + +const periodsDetails = { + 1: { type: 'TYPE_A', offset: -1, duration: 3 }, + 2: { type: 'TYPE_B', offset: -1, duration: 2 }, + 3: { type: 'TYPE_A', offset: 0, duration: 4 }, +} describe('util/periods', () => { test('getFixedPeriodsByType - RELATIVE', () => { @@ -316,6 +324,7 @@ describe('util/periods', () => { }, ]) }) + test('getFixedPeriodsByType - MONTHLY 2020', () => { expect( getFixedPeriodsByType({ @@ -437,4 +446,27 @@ describe('util/periods', () => { }, ]) }) + + it('getPeriodsDurationByType - should correctly calculate the duration by type with deduplication', () => { + const result = getPeriodsDurationByType(periods, periodsDetails) + expect(result).toEqual({ + FIXED_PERIODS: { + any: 1, + first: 0, + last: 0, + }, + TYPE_A: { first: 1, last: 3 }, + TYPE_B: { first: 0, last: 2 }, + }) + }) + + it('getPeriodsDurationByType - should correctly calculate the duration by type without deduplication', () => { + const result = getPeriodsDurationByType(periods, periodsDetails, false) + expect(result).toEqual({ + 1: { any: 3 }, + 2: { any: 2 }, + 3: { any: 4 }, + 4: { any: 1 }, + }) + }) }) diff --git a/src/util/periods.js b/src/util/periods.js index 5c4b0e942..e7912a976 100644 --- a/src/util/periods.js +++ b/src/util/periods.js @@ -1,8 +1,14 @@ import { getFixedPeriodsOptionsById, getRelativePeriodsOptionsById, + getRelativePeriodsDetails, } from '@dhis2/analytics' -import { periodTypes, periodGroups } from '../constants/periods.js' +import { + periodTypes, + periodGroups, + FIXED_PERIODS, +} from '../constants/periods.js' +import { sumObjectValues } from '../util/helpers.js' const getYearOffsetFromNow = (year) => year - new Date(Date.now()).getFullYear() @@ -80,3 +86,50 @@ export const getHiddenPeriods = (systemSettings) => { ) .map((setting) => setting.match(periodSetting)[1].toUpperCase()) } + +// Count maximum number of periods returned by analytics api + +export const getPeriodsDurationByType = ( + periods, + periodsDetails, + deduplication = true +) => { + if (!deduplication) { + return periods.reduce((acc, { id }) => { + acc[id] = acc[id] || {} + acc[id].any = periodsDetails[id]?.duration || 1 + return acc + }, {}) + } else { + return periods.reduce((acc, { id }) => { + const { + type = FIXED_PERIODS, + offset = 0, + duration = 1, + } = periodsDetails[id] || {} + + acc[type] = acc[type] || { first: 0, last: 0 } + + if (type === FIXED_PERIODS && !periodsDetails[id]) { + acc[type].any = (acc[type].any || 0) + 1 + } else { + acc[type].first = Math.max(acc[type].first, 1 + offset) + acc[type].last = Math.max( + acc[type].last, + duration - (1 + offset) + ) + } + return acc + }, {}) + } +} + +export const countPeriods = (periods, deduplication) => { + const periodsDetails = getRelativePeriodsDetails() + const periodsDurationByType = getPeriodsDurationByType( + periods, + periodsDetails, + deduplication + ) + return sumObjectValues(periodsDurationByType) +} From 25d9700a9a4cb5e30b5794a6a5269de88e1d0079 Mon Sep 17 00:00:00 2001 From: braimbault Date: Tue, 10 Dec 2024 14:43:54 +0100 Subject: [PATCH 14/28] fix: refactor RenderingStrategy and add test --- src/components/periods/RenderingStrategy.js | 198 +++++++----------- .../__tests__/RenderingStrategy.spec.js | 88 ++++++++ 2 files changed, 169 insertions(+), 117 deletions(-) create mode 100644 src/components/periods/__tests__/RenderingStrategy.spec.js diff --git a/src/components/periods/RenderingStrategy.js b/src/components/periods/RenderingStrategy.js index f8aa0b2ab..5a8403123 100644 --- a/src/components/periods/RenderingStrategy.js +++ b/src/components/periods/RenderingStrategy.js @@ -1,7 +1,6 @@ -import { getRelativePeriodsDetails } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' import PropTypes from 'prop-types' -import React, { useEffect } from 'react' +import React, { useEffect, useMemo } from 'react' import { useSelector } from 'react-redux' import { RENDERING_STRATEGY_SINGLE, @@ -15,59 +14,7 @@ import { import usePrevious from '../../hooks/usePrevious.js' import { getPeriodsFromFilters } from '../../util/analytics.js' import { Radio, RadioGroup } from '../core/index.js' - -const countPeriods = (periods) => { - const periodsDetails = getRelativePeriodsDetails() - - const total_v1 = periods.reduce( - (sum, period) => - sum + - (periodsDetails[period.id] !== undefined - ? periodsDetails[period.id].duration - : 1), - 0 - ) - - const durationByType = periods.reduce((acc, period) => { - const periodDetails = periodsDetails[period.id] - if (acc['FIXED_PERIOD'] === undefined) { - acc['FIXED_PERIOD'] = { - any: 0, - } - } - if (periodDetails === undefined) { - acc['FIXED_PERIOD'].any += 1 - return acc - } - const type = periodDetails.type - if (acc[type] === undefined) { - acc[type] = { - first: 0, - last: 0, - } - } - acc[type].first = Math.max(acc[type].first, 1 + periodDetails.offset) - acc[type].last = Math.max( - acc[type].last, - periodDetails.duration - (1 + periodDetails.offset) - ) - return acc - }, {}) - - const sumObjectValues = (obj) => - Object.values(obj).reduce((sum, value) => { - if (typeof value === 'object') { - return sum + sumObjectValues(value) - } else if (typeof value === 'number') { - return sum + value - } - return sum - }, 0) - - const total_v2 = sumObjectValues(durationByType) - - return total_v2 -} +import { countPeriods } from '../../util/periods.js' const RenderingStrategy = ({ layerId, @@ -75,69 +22,90 @@ const RenderingStrategy = ({ periods = [], onChange, }) => { - const hasOtherLayers = useSelector( - ({ map }) => !!map.mapViews.filter(({ id }) => id !== layerId).length + const prevPeriods = usePrevious(periods) + const totalPeriods = useMemo(() => countPeriods(periods), [periods]) + + const hasOtherLayers = useSelector(({ map }) => + map.mapViews.some(({ id }) => id !== layerId) ) - const hasOtherTimelineLayers = useSelector( - ({ map }) => - !!map.mapViews.find( - (layer) => - layer.renderingStrategy === RENDERING_STRATEGY_TIMELINE && - layer.id !== layerId - ) + const hasOtherTimelineLayers = useSelector(({ map }) => + map.mapViews.some( + (layer) => + layer.renderingStrategy === RENDERING_STRATEGY_TIMELINE && + layer.id !== layerId + ) ) const hasTooManyPeriods = useSelector(({ layerEdit }) => { const periods = getPeriodsFromFilters(layerEdit.filters) return countPeriods(periods) > MULTIMAP_MAX_PERIODS }) - const prevPeriods = usePrevious(periods) - useEffect(() => { - if (periods !== prevPeriods) { - if ( - countPeriods(periods) < MULTIMAP_MIN_PERIODS && - value !== RENDERING_STRATEGY_SINGLE - ) { - onChange(RENDERING_STRATEGY_SINGLE) - } else if ( - countPeriods(periods) > MULTIMAP_MAX_PERIODS && - value === RENDERING_STRATEGY_SPLIT_BY_PERIOD - ) { - onChange(RENDERING_STRATEGY_SINGLE) - } + if (periods === prevPeriods) return + + if ( + totalPeriods < MULTIMAP_MIN_PERIODS && + value !== RENDERING_STRATEGY_SINGLE + ) { + onChange(RENDERING_STRATEGY_SINGLE) + } else if ( + totalPeriods > MULTIMAP_MAX_PERIODS && + value === RENDERING_STRATEGY_SPLIT_BY_PERIOD + ) { + onChange(RENDERING_STRATEGY_SINGLE) } }, [value, periods, prevPeriods, onChange]) - let helpText = [] - - if (countPeriods(periods) < MULTIMAP_MIN_PERIODS) { - helpText.push( - i18n.t( - 'Select {{number}} or more periods to enable timeline or split map views.', - { - number: MULTIMAP_MIN_PERIODS, - } + const helpText = useMemo(() => { + const messages = [] + if (totalPeriods < MULTIMAP_MIN_PERIODS) { + messages.push( + i18n.t( + 'Select {{number}} or more periods to enable timeline or split map views.', + { + number: MULTIMAP_MIN_PERIODS, + } + ) ) - ) - } - if (hasOtherTimelineLayers) { - helpText.push(i18n.t('Only one timeline is allowed.')) - } - if (hasOtherLayers) { - helpText.push(i18n.t('Remove other layers to enable split map views.')) - } - if (hasTooManyPeriods) { - helpText.push( - i18n.t( - 'Only up to {{number}} periods can be selected to enable split map views.', - { - number: MULTIMAP_MAX_PERIODS, - } + } + if (hasOtherTimelineLayers) { + messages.push(i18n.t('Only one timeline is allowed.')) + } + if (hasOtherLayers) { + messages.push( + i18n.t('Remove other layers to enable split map views.') ) - ) - } - helpText = helpText.join(' ') + } + if (hasTooManyPeriods) { + messages.push( + i18n.t( + 'Only up to {{number}} periods can be selected to enable split map views.', + { + number: MULTIMAP_MAX_PERIODS, + } + ) + ) + } + return messages.join(' ') + }, [ + totalPeriods, + hasOtherTimelineLayers, + hasOtherLayers, + hasTooManyPeriods, + ]) + + const isTimelineDisabled = useMemo( + () => totalPeriods < MULTIMAP_MIN_PERIODS || hasOtherTimelineLayers, + [totalPeriods, hasOtherTimelineLayers] + ) + + const isSplitViewDisabled = useMemo( + () => + totalPeriods < MULTIMAP_MIN_PERIODS || + hasTooManyPeriods || + hasOtherLayers, + [totalPeriods, hasTooManyPeriods, hasOtherLayers] + ) return ( - + ) diff --git a/src/components/periods/__tests__/RenderingStrategy.spec.js b/src/components/periods/__tests__/RenderingStrategy.spec.js new file mode 100644 index 000000000..01295e767 --- /dev/null +++ b/src/components/periods/__tests__/RenderingStrategy.spec.js @@ -0,0 +1,88 @@ +import { mount } from 'enzyme' +import React from 'react' +import { Provider } from 'react-redux' +import configureMockStore from 'redux-mock-store' +import RenderingStrategy from '../RenderingStrategy' +import { + RENDERING_STRATEGY_SINGLE, + RENDERING_STRATEGY_TIMELINE, + RENDERING_STRATEGY_SPLIT_BY_PERIOD, +} from '../../../constants/layers' +import { countPeriods } from '../../../util/periods' + +const mockStore = configureMockStore() + +const store = mockStore({ + map: { + mapViews: [{ id: 'layer1', renderingStrategy: 'SINGLE' }], + }, + layerEdit: { + filters: [], + }, +}) + +jest.mock('../../../util/periods', () => ({ + countPeriods: jest.fn(), +})) + +describe('RenderingStrategy', () => { + const renderWithProps = (props) => + mount( + + + + ) + + const layerId = 'layer1' + const value = RENDERING_STRATEGY_SPLIT_BY_PERIOD + const periods = [] + const mockOnChange = jest.fn() + let props + + beforeEach(() => { + jest.clearAllMocks() + props = { + layerId, + value, + periods, + onChange: mockOnChange, + } + }) + + it('renders all radio buttons with correct labels', () => { + countPeriods.mockReturnValue(5) + const wrapper = renderWithProps(props) + expect(wrapper.find('input').length).toBe(3) // Three radio buttons + expect(wrapper.find('label').at(0).text()).toBe('Period display mode') + expect(wrapper.find('label').at(1).text()).toBe( + 'Single (combine periods)' + ) + expect(wrapper.find('label').at(2).text()).toBe('Timeline') + expect(wrapper.find('label').at(3).text()).toBe('Split map views') + }) + + it('disables timeline and split map views when total periods are below the minimum', () => { + countPeriods.mockReturnValue(1) + const wrapper = renderWithProps(props) + expect(wrapper.find('input').at(1).prop('disabled')).toBeDefined() // Timeline should be disabled + expect(wrapper.find('input').at(2).prop('disabled')).toBeDefined() + }) + + it('calls onChange with correct value when a radio button is clicked', () => { + countPeriods.mockReturnValue(5) + const wrapper = renderWithProps(props) + wrapper + .find('input') + .at(1) + .simulate('change', { + target: { value: RENDERING_STRATEGY_TIMELINE }, + }) + expect(mockOnChange).toHaveBeenCalledWith(RENDERING_STRATEGY_TIMELINE) + }) + + it('automatically switches to SINGLE when conditions are not met', () => { + countPeriods.mockReturnValue(1) + const wrapper = renderWithProps(props) + expect(mockOnChange).toHaveBeenCalledWith(RENDERING_STRATEGY_SINGLE) + }) +}) From 86e6f18da4522029e21bc0ddaac0404edb73d08a Mon Sep 17 00:00:00 2001 From: braimbault Date: Tue, 10 Dec 2024 15:31:43 +0100 Subject: [PATCH 15/28] fix: refactor Timeline --- src/components/periods/Timeline.js | 120 ++++++------------ .../periods/styles/Timeline.module.css | 1 - src/util/periods.js | 61 ++++++++- 3 files changed, 101 insertions(+), 81 deletions(-) diff --git a/src/components/periods/Timeline.js b/src/components/periods/Timeline.js index 12ec73577..62c2a22d1 100644 --- a/src/components/periods/Timeline.js +++ b/src/components/periods/Timeline.js @@ -1,10 +1,13 @@ -import { PERIOD_TYPE_REGEX } from '@dhis2/analytics' import cx from 'classnames' import { axisBottom } from 'd3-axis' import { scaleTime } from 'd3-scale' import { select } from 'd3-selection' import PropTypes from 'prop-types' import React, { Component } from 'react' +import { + getPeriodTypeFromId, + getPeriodLevelFromPeriodType, +} from '../../util/periods.js' import timeTicks from '../../util/timeTicks.js' import styles from './styles/Timeline.module.css' @@ -15,86 +18,48 @@ const delay = 1500 const playBtn = const pauseBtn = const doubleTicksPeriods = ['LAST_6_BIMONTHS', 'BIMONTHS_THIS_YEAR'] +const rectHeight = 7 +const rectOffset = 10 -const addPeriodType = (item) => { - const periodTypes = Object.keys(PERIOD_TYPE_REGEX) - let i = 0 - let type = undefined - let match = undefined - - while (i < periodTypes.length && !match) { - type = periodTypes[i] - match = item.id.match(PERIOD_TYPE_REGEX[type]) - i++ - } - - let level - switch (type) { - case 'DAILY': - level = 0 - break - case 'WEEKLY': - case 'WEEKLYWED': - case 'WEEKLYTHU': - case 'WEEKLYSAT': - case 'WEEKLYSUN': - level = 1 - break - case 'BIWEEKLY': - level = 2 - break - case 'MONTHLY': - level = 3 - break - case 'BIMONTHLY': - level = 4 - break - case 'QUARTERLY': - level = 5 - break - case 'SIXMONTHLY': - case 'SIXMONTHLYAPR': - level = 6 - break - case 'YEARLY': - case 'FYNOV': - case 'FYOCT': - case 'FYJUL': - case 'FYAPR': - level = 7 - break - default: - level = 8 - } - - item.type = type - item.level = level - +const addPeriodTypeAndLevel = (item) => { + item.type = getPeriodTypeFromId(item.id) + item.level = getPeriodLevelFromPeriodType(item.type) return item } +const listLevelsFromPeriods = (periodsWithType) => [ + ...new Set(periodsWithType.map((item) => item.level)), +] + const countUniqueRanks = (periods) => { - const periodsWithType = periods.map((item) => addPeriodType(item)) - const levels = [...new Set(periodsWithType.map((item) => item.level))] - return levels.length + const periodsWithType = periods.map((item) => addPeriodTypeAndLevel(item)) + return listLevelsFromPeriods(periodsWithType).length } -const sortPeriods = (periods) => { - let periodsWithType = periods.map((item) => addPeriodType(item)) +const sortPeriodsByLevelRank = (periods) => { + let periodsWithDetails + periodsWithDetails = periods.map((item) => addPeriodTypeAndLevel(item)) - const levels = [...new Set(periodsWithType.map((item) => item.level))].sort( + const sortedLevels = listLevelsFromPeriods(periodsWithDetails).sort( (a, b) => b - a ) - - periodsWithType = periodsWithType.map((item) => ({ + periodsWithDetails = periodsWithDetails.map((item) => ({ ...item, - levelRank: levels.indexOf(item.level), + levelRank: sortedLevels.indexOf(item.level), })) - const sortedPeriods = periodsWithType.sort( + const sortedPeriods = periodsWithDetails.sort( (a, b) => a.levelRank - b.levelRank ) + return sortedPeriods +} +const sortPeriodsByLevelAndStartDate = (periods) => { + let sortedPeriods + sortedPeriods = periods.sort((a, b) => b.level - a.level) + sortedPeriods = periods.sort( + (a, b) => new Date(a.startDate) - new Date(b.startDate) + ) return sortedPeriods } @@ -133,29 +98,32 @@ class Timeline extends Component { const uniqueRanks = countUniqueRanks(this.props.periods) this.setTimeScale() - + const rectTotalHeight = rectHeight + (uniqueRanks - 1) * rectOffset return ( + // play/pause button {mode === 'play' ? pauseBtn : playBtn} + // rectangles {this.getPeriodRects()} + // x axis (this.node = node)} /> @@ -167,7 +135,7 @@ class Timeline extends Component { getPeriodRects = () => { const { period, periods } = this.props - const sortedPeriods = sortPeriods(periods) + const sortedPeriods = sortPeriodsByLevelRank(periods) return sortedPeriods.map((item) => { const isCurrent = period.id === item.id @@ -182,9 +150,9 @@ class Timeline extends Component { [styles.selected]: isCurrent, })} x={x} - y={item.levelRank * 4} + y={item.levelRank * rectOffset} width={width} - height={10} + height={rectHeight} onClick={() => this.onPeriodClick(item)} /> ) @@ -273,11 +241,7 @@ class Timeline extends Component { // Play animation play = () => { const { period, periods, onChange } = this.props - let sortedPeriods - sortedPeriods = periods.sort((a, b) => b.level - a.level) - sortedPeriods = periods.sort( - (a, b) => new Date(a.startDate) - new Date(b.startDate) - ) + const sortedPeriods = sortPeriodsByLevelAndStartDate(periods) const index = sortedPeriods.findIndex((p) => p.id === period.id) const isLastPeriod = index === sortedPeriods.length - 1 diff --git a/src/components/periods/styles/Timeline.module.css b/src/components/periods/styles/Timeline.module.css index 0eef902ee..11c431358 100644 --- a/src/components/periods/styles/Timeline.module.css +++ b/src/components/periods/styles/Timeline.module.css @@ -3,7 +3,6 @@ width: calc(100% - 20px); position: absolute; left: 10px; - bottom: 32px; z-index: 1000px; user-select: none; } diff --git a/src/util/periods.js b/src/util/periods.js index e7912a976..5ce5eb96b 100644 --- a/src/util/periods.js +++ b/src/util/periods.js @@ -1,7 +1,26 @@ import { + DAILY, + WEEKLY, + WEEKLYWED, + WEEKLYTHU, + WEEKLYSAT, + WEEKLYSUN, + BIWEEKLY, + MONTHLY, + BIMONTHLY, + QUARTERLY, + SIXMONTHLY, + SIXMONTHLYAPR, + YEARLY, + FINANCIAL, + FYNOV, + FYOCT, + FYJUL, + FYAPR, getFixedPeriodsOptionsById, getRelativePeriodsOptionsById, getRelativePeriodsDetails, + PERIOD_TYPE_REGEX, } from '@dhis2/analytics' import { periodTypes, @@ -88,7 +107,7 @@ export const getHiddenPeriods = (systemSettings) => { } // Count maximum number of periods returned by analytics api - +// Preliminary step export const getPeriodsDurationByType = ( periods, periodsDetails, @@ -123,7 +142,7 @@ export const getPeriodsDurationByType = ( }, {}) } } - +// Total count export const countPeriods = (periods, deduplication) => { const periodsDetails = getRelativePeriodsDetails() const periodsDurationByType = getPeriodsDurationByType( @@ -133,3 +152,41 @@ export const countPeriods = (periods, deduplication) => { ) return sumObjectValues(periodsDurationByType) } + +export const getPeriodTypeFromId = (periodId) => { + return Object.keys(PERIOD_TYPE_REGEX).find((type) => + periodId.match(PERIOD_TYPE_REGEX[type]) + ) +} + +export const getPeriodLevelFromPeriodType = (periodType) => { + switch (periodType) { + case DAILY: + return 0 + case WEEKLY: + case WEEKLYWED: + case WEEKLYTHU: + case WEEKLYSAT: + case WEEKLYSUN: + return 1 + case BIWEEKLY: + return 2 + case MONTHLY: + return 3 + case BIMONTHLY: + return 4 + case QUARTERLY: + return 5 + case SIXMONTHLY: + case SIXMONTHLYAPR: + return 6 + case YEARLY: + case FYNOV: + case FYOCT: + case FYJUL: + case FYAPR: + return 7 + default: + return 8 + } +} From c6c6fcf8957b6b02866095c31583262b9f490bfe Mon Sep 17 00:00:00 2001 From: braimbault Date: Wed, 11 Dec 2024 11:43:26 +0100 Subject: [PATCH 16/28] fix: refactor Timeline --- src/components/periods/Timeline.js | 101 ++++++++++++++--------------- src/constants/periods.js | 2 + src/util/periods.js | 50 ++++++-------- 3 files changed, 73 insertions(+), 80 deletions(-) diff --git a/src/components/periods/Timeline.js b/src/components/periods/Timeline.js index 62c2a22d1..65138bad4 100644 --- a/src/components/periods/Timeline.js +++ b/src/components/periods/Timeline.js @@ -8,61 +8,56 @@ import { getPeriodTypeFromId, getPeriodLevelFromPeriodType, } from '../../util/periods.js' +import { doubleTicksPeriods } from '../../constants/periods.js' import timeTicks from '../../util/timeTicks.js' import styles from './styles/Timeline.module.css' -const paddingLeft = 40 -const paddingRight = 20 -const labelWidth = 80 -const delay = 1500 -const playBtn = -const pauseBtn = -const doubleTicksPeriods = ['LAST_6_BIMONTHS', 'BIMONTHS_THIS_YEAR'] -const rectHeight = 7 -const rectOffset = 10 - -const addPeriodTypeAndLevel = (item) => { - item.type = getPeriodTypeFromId(item.id) - item.level = getPeriodLevelFromPeriodType(item.type) - return item +// Constants +const PADDING_LEFT = 40 +const PADDING_RIGHT = 20 +const LABEL_WIDTH = 80 +const RECT_HEIGHT = 8 +const RECT_OFFSET = 8 +const DELAY = 1500 +const PLAY_ICON = +const PAUSE_ICON = + +// Utility Functions +const addPeriodDetails = (period) => { + const type = getPeriodTypeFromId(period.id) + const level = getPeriodLevelFromPeriodType(type) + return { ...period, type, level } } -const listLevelsFromPeriods = (periodsWithType) => [ - ...new Set(periodsWithType.map((item) => item.level)), +const getUniqueLevels = (periodsWithLevel) => [ + ...new Set(periodsWithLevel.map((item) => item.level)), ] const countUniqueRanks = (periods) => { - const periodsWithType = periods.map((item) => addPeriodTypeAndLevel(item)) - return listLevelsFromPeriods(periodsWithType).length + const periodsWithDetails = periods.map(addPeriodDetails) + return getUniqueLevels(periodsWithDetails).length } const sortPeriodsByLevelRank = (periods) => { - let periodsWithDetails - periodsWithDetails = periods.map((item) => addPeriodTypeAndLevel(item)) - - const sortedLevels = listLevelsFromPeriods(periodsWithDetails).sort( + const periodsWithDetails = periods.map(addPeriodDetails) + const sortedLevels = getUniqueLevels(periodsWithDetails).sort( (a, b) => b - a ) - periodsWithDetails = periodsWithDetails.map((item) => ({ - ...item, - levelRank: sortedLevels.indexOf(item.level), - })) - - const sortedPeriods = periodsWithDetails.sort( - (a, b) => a.levelRank - b.levelRank - ) - return sortedPeriods + return periodsWithDetails + .map((item) => ({ + ...item, + levelRank: sortedLevels.indexOf(item.level), + })) + .sort((a, b) => a.levelRank - b.levelRank) } -const sortPeriodsByLevelAndStartDate = (periods) => { - let sortedPeriods - sortedPeriods = periods.sort((a, b) => b.level - a.level) - sortedPeriods = periods.sort( - (a, b) => new Date(a.startDate) - new Date(b.startDate) - ) - return sortedPeriods -} +const sortPeriodsByLevelAndStartDate = (periods) => + periods + .map(addPeriodDetails) + .sort((a, b) => b.level - a.level) + .sort((a, b) => new Date(a.startDate) - new Date(b.startDate)) +// Timeline Component class Timeline extends Component { static contextTypes = { map: PropTypes.object, @@ -98,7 +93,7 @@ class Timeline extends Component { const uniqueRanks = countUniqueRanks(this.props.periods) this.setTimeScale() - const rectTotalHeight = rectHeight + (uniqueRanks - 1) * rectOffset + const rectTotalHeight = RECT_HEIGHT + (uniqueRanks - 1) * RECT_OFFSET return ( - {mode === 'play' ? pauseBtn : playBtn} + {mode === 'play' ? PAUSE_ICON : PLAY_ICON} // rectangles - + {this.getPeriodRects()} // x axis (this.node = node)} @@ -150,9 +145,9 @@ class Timeline extends Component { [styles.selected]: isCurrent, })} x={x} - y={item.levelRank * rectOffset} + y={item.levelRank * RECT_OFFSET} width={width} - height={rectHeight} + height={RECT_HEIGHT} onClick={() => this.onPeriodClick(item)} /> ) @@ -192,11 +187,15 @@ class Timeline extends Component { // Set timeline axis setTimeAxis = () => { - const { periodId, periods } = this.props + const { periods } = this.props + const periodsType = periods.map(({ id }) => getPeriodTypeFromId(id)) const numPeriods = - periods.length * (doubleTicksPeriods.includes(periodId) ? 2 : 1) + periods.length * + (doubleTicksPeriods.some((element) => periodsType.includes(element)) + ? 2 + : 1) const { width } = this.state - const maxTicks = Math.round(width / labelWidth) + const maxTicks = Math.round(width / LABEL_WIDTH) const numTicks = maxTicks < numPeriods ? maxTicks : numPeriods const timeAxis = axisBottom(this.timeScale) const [startDate, endDate] = this.timeScale.domain() @@ -212,7 +211,7 @@ class Timeline extends Component { if (this.node) { // clientWith returns 0 for SVG elements in Firefox const box = this.node.parentNode.getBoundingClientRect() - const width = box.right - box.left - paddingLeft - paddingRight + const width = box.right - box.left - PADDING_LEFT - PADDING_RIGHT this.setState({ width }) } @@ -264,8 +263,8 @@ class Timeline extends Component { onChange(sortedPeriods[index + 1]) } - // Call itself after delay - this.timeout = setTimeout(this.play, delay) + // Call itself after DELAY + this.timeout = setTimeout(this.play, DELAY) } // Stop animation diff --git a/src/constants/periods.js b/src/constants/periods.js index 7d8914f33..837eb42f1 100644 --- a/src/constants/periods.js +++ b/src/constants/periods.js @@ -52,6 +52,8 @@ const LAST_FINANCIAL_YEAR = 'LAST_FINANCIAL_YEAR' const THIS_YEAR = 'THIS_YEAR' const LAST_YEAR = 'LAST_YEAR' +export const doubleTicksPeriods = [BIMONTHLY, SIXMONTHLY, SIXMONTHLYAPR] + export const PREDEFINED_PERIODS = 'PREDEFINED_PERIODS' export const RELATIVE_PERIODS = 'RELATIVE_PERIODS' export const FIXED_PERIODS = 'FIXED_PERIODS' diff --git a/src/util/periods.js b/src/util/periods.js index 5ce5eb96b..5237dd883 100644 --- a/src/util/periods.js +++ b/src/util/periods.js @@ -160,33 +160,25 @@ export const getPeriodTypeFromId = (periodId) => { } export const getPeriodLevelFromPeriodType = (periodType) => { - switch (periodType) { - case DAILY: - return 0 - case WEEKLY: - case WEEKLYWED: - case WEEKLYTHU: - case WEEKLYSAT: - case WEEKLYSUN: - return 1 - case BIWEEKLY: - return 2 - case MONTHLY: - return 3 - case BIMONTHLY: - return 4 - case QUARTERLY: - return 5 - case SIXMONTHLY: - case SIXMONTHLYAPR: - return 6 - case YEARLY: - case FYNOV: - case FYOCT: - case FYJUL: - case FYAPR: - return 7 - default: - return 8 - } + const periodTypesByLevel = [ + DAILY, + WEEKLY, + WEEKLYWED, + WEEKLYTHU, + WEEKLYSAT, + WEEKLYSUN, + BIWEEKLY, + MONTHLY, + BIMONTHLY, + QUARTERLY, + SIXMONTHLY, + SIXMONTHLYAPR, + YEARLY, + FINANCIAL, + FYNOV, + FYOCT, + FYJUL, + FYAPR, + ] + return periodTypesByLevel.indexOf(periodType) } From 170ac8534f37b43f1f8440f010c031315c9f77de Mon Sep 17 00:00:00 2001 From: braimbault Date: Wed, 11 Dec 2024 12:33:00 +0100 Subject: [PATCH 17/28] fix: refactor Timeline --- src/components/periods/Timeline.js | 229 ++++++++++++++--------------- 1 file changed, 111 insertions(+), 118 deletions(-) diff --git a/src/components/periods/Timeline.js b/src/components/periods/Timeline.js index 65138bad4..8bf4f7855 100644 --- a/src/components/periods/Timeline.js +++ b/src/components/periods/Timeline.js @@ -62,98 +62,16 @@ class Timeline extends Component { static contextTypes = { map: PropTypes.object, } - static propTypes = { period: PropTypes.object.isRequired, - periodId: PropTypes.string.isRequired, periods: PropTypes.array.isRequired, onChange: PropTypes.func.isRequired, } - state = { width: null, mode: 'start', } - componentDidMount() { - this.setWidth() - this.context.map.on('resize', this.setWidth) - } - - componentDidUpdate() { - this.setTimeAxis() - } - - componentWillUnmount() { - this.context.map.off('resize', this.setWidth) - } - - render() { - const { mode } = this.state - const uniqueRanks = countUniqueRanks(this.props.periods) - - this.setTimeScale() - const rectTotalHeight = RECT_HEIGHT + (uniqueRanks - 1) * RECT_OFFSET - return ( - - // play/pause button - - - {mode === 'play' ? PAUSE_ICON : PLAY_ICON} - - // rectangles - - {this.getPeriodRects()} - - // x axis - (this.node = node)} - /> - - ) - } - - // Returns array of period rectangles - getPeriodRects = () => { - const { period, periods } = this.props - - const sortedPeriods = sortPeriodsByLevelRank(periods) - - return sortedPeriods.map((item) => { - const isCurrent = period.id === item.id - const { id, startDate, endDate } = item - const x = this.timeScale(startDate) - const width = this.timeScale(endDate) - x - - return ( - this.onPeriodClick(item)} - /> - ) - }) - } - // Set time scale setTimeScale = () => { const { periods } = this.props @@ -163,10 +81,10 @@ class Timeline extends Component { return } - const { minStartDate: startDate, maxEndDate: endDate } = periods.reduce( - (acc, item) => { - const start = new Date(item.startDate) - const end = new Date(item.endDate) + const { minStartDate, maxEndDate } = periods.reduce( + (acc, { startDate, endDate }) => { + const start = new Date(startDate) + const end = new Date(endDate) return { minStartDate: start < acc.minStartDate ? start : acc.minStartDate, @@ -181,7 +99,7 @@ class Timeline extends Component { // Link time domain to timeline width this.timeScale = scaleTime() - .domain([startDate, endDate]) + .domain([minStartDate, maxEndDate]) .range([0, width]) } @@ -196,53 +114,29 @@ class Timeline extends Component { : 1) const { width } = this.state const maxTicks = Math.round(width / LABEL_WIDTH) - const numTicks = maxTicks < numPeriods ? maxTicks : numPeriods - const timeAxis = axisBottom(this.timeScale) - const [startDate, endDate] = this.timeScale.domain() - const ticks = timeTicks(startDate, endDate, numTicks) - - timeAxis.tickValues(ticks) + const numTicks = Math.min(maxTicks, numPeriods) + const ticks = timeTicks(...this.timeScale.domain(), numTicks) + const timeAxis = axisBottom(this.timeScale).tickValues(ticks) select(this.node).call(timeAxis) } - // Set timeline width from DOM el + // Set timeline width from DOM element setWidth = () => { if (this.node) { // clientWith returns 0 for SVG elements in Firefox const box = this.node.parentNode.getBoundingClientRect() const width = box.right - box.left - PADDING_LEFT - PADDING_RIGHT - this.setState({ width }) } } - // Handler for period click - onPeriodClick(period) { - // Switch to period if different - if (period.id !== this.props.period.id) { - this.props.onChange(period) - } - - // Stop animation if running - this.stop() - } - - // Handler for play/pause button - onPlayPause = () => { - if (this.state.mode === 'play') { - this.stop() - } else { - this.play() - } - } - // Play animation play = () => { const { period, periods, onChange } = this.props const sortedPeriods = sortPeriodsByLevelAndStartDate(periods) - const index = sortedPeriods.findIndex((p) => p.id === period.id) - const isLastPeriod = index === sortedPeriods.length - 1 + const currentIndex = sortedPeriods.findIndex((p) => p.id === period.id) + const isLastPeriod = currentIndex === sortedPeriods.length - 1 // If new animation if (!this.timeout) { @@ -260,7 +154,7 @@ class Timeline extends Component { } // Switch to next period - onChange(sortedPeriods[index + 1]) + onChange(sortedPeriods[currentIndex + 1]) } // Call itself after DELAY @@ -273,6 +167,105 @@ class Timeline extends Component { clearTimeout(this.timeout) delete this.timeout } + + // Handler for play/pause button + onPlayPause = () => { + if (this.state.mode === 'play') { + this.stop() + } else { + this.play() + } + } + + // Handler for period click + onPeriodClick(period) { + // Switch to period if different + if (period.id !== this.props.period.id) { + this.props.onChange(period) + } + + // Stop animation if running + this.stop() + } + + componentDidMount() { + this.setWidth() + this.context.map.on('resize', this.setWidth) + } + + componentDidUpdate() { + this.setTimeAxis() + } + + componentWillUnmount() { + this.context.map.off('resize', this.setWidth) + } + + render() { + const { mode } = this.state + const uniqueRanks = countUniqueRanks(this.props.periods) + const rectTotalHeight = RECT_HEIGHT + (uniqueRanks - 1) * RECT_OFFSET + + this.setTimeScale() + return ( + + {/* Play/Pause Button */} + + + {mode === 'play' ? PAUSE_ICON : PLAY_ICON} + + {/* Period Rectangles */} + + {this.getPeriodRects()} + + {/* X-Axis */} + (this.node = node)} + /> + + ) + } + + // Returns array of period rectangles + getPeriodRects = () => { + const { period, periods } = this.props + + const sortedPeriods = sortPeriodsByLevelRank(periods) + + return sortedPeriods.map((item) => { + const isCurrent = period.id === item.id + const { id, startDate, endDate } = item + const x = this.timeScale(startDate) + const width = this.timeScale(endDate) - x + + return ( + this.onPeriodClick(item)} + /> + ) + }) + } } export default Timeline From 709a8ba8ab4534074449ac05acf5ebeb1c5a9254 Mon Sep 17 00:00:00 2001 From: braimbault Date: Thu, 12 Dec 2024 12:59:52 +0100 Subject: [PATCH 18/28] chore: clean-up --- src/components/edit/LayerEdit.js | 18 ++++++++++++++++-- src/components/edit/event/EventDialog.js | 2 +- src/components/edit/thematic/ThematicDialog.js | 1 - src/components/layers/LayerCard.js | 5 +---- src/components/periods/RenderingStrategy.js | 8 +++++--- src/components/periods/Timeline.js | 2 +- .../__tests__/RenderingStrategy.spec.js | 8 ++++---- .../periods/__tests__/Timeline.spec.js | 14 +++++++++++--- src/loaders/thematicLoader.js | 9 ++++++--- 9 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/components/edit/LayerEdit.js b/src/components/edit/LayerEdit.js index 6002491d5..b0b4c5c94 100644 --- a/src/components/edit/LayerEdit.js +++ b/src/components/edit/LayerEdit.js @@ -9,7 +9,7 @@ import { ButtonStrip, } from '@dhis2/ui' import PropTypes from 'prop-types' -import React, { useState } from 'react' +import React, { useState, useRef, useEffect } from 'react' import { connect } from 'react-redux' import { addLayer, updateLayer, cancelLayer } from '../../actions/layers.js' import { EARTH_ENGINE_LAYER } from '../../constants/layers.js' @@ -46,6 +46,7 @@ const getLayerNames = () => ({ const LayerEdit = ({ layer, addLayer, updateLayer, cancelLayer }) => { const [isValidLayer, setIsValidLayer] = useState(false) + const modalRef = useRef(null) const { systemSettings, periodsSettings } = useCachedDataQuery() const orgUnits = useOrgUnits() @@ -74,6 +75,13 @@ const LayerEdit = ({ layer, addLayer, updateLayer, cancelLayer }) => { } useKeyDown('Escape', cancelLayer) + useKeyDown('Enter', onValidateLayer) + + useEffect(() => { + if (layer && modalRef.current) { + modalRef.current.focus() + } + }, [layer]) if (!layer) { return null @@ -97,7 +105,13 @@ const LayerEdit = ({ layer, addLayer, updateLayer, cancelLayer }) => { : i18n.t('Add new {{name}} layer', { name }) return ( - + {title}
diff --git a/src/components/edit/event/EventDialog.js b/src/components/edit/event/EventDialog.js index 41288a306..b4dcb8cbd 100644 --- a/src/components/edit/event/EventDialog.js +++ b/src/components/edit/event/EventDialog.js @@ -277,7 +277,7 @@ class EventDialog extends Component {

{title}

- { - // TODO Handle long list of periods - subtitle &&

{subtitle}

- } + {subtitle &&

{subtitle}

}
{isOverlay && } diff --git a/src/components/periods/RenderingStrategy.js b/src/components/periods/RenderingStrategy.js index 5a8403123..6868acdb4 100644 --- a/src/components/periods/RenderingStrategy.js +++ b/src/components/periods/RenderingStrategy.js @@ -13,8 +13,8 @@ import { } from '../../constants/periods.js' import usePrevious from '../../hooks/usePrevious.js' import { getPeriodsFromFilters } from '../../util/analytics.js' -import { Radio, RadioGroup } from '../core/index.js' import { countPeriods } from '../../util/periods.js' +import { Radio, RadioGroup } from '../core/index.js' const RenderingStrategy = ({ layerId, @@ -41,7 +41,9 @@ const RenderingStrategy = ({ }) useEffect(() => { - if (periods === prevPeriods) return + if (periods === prevPeriods) { + return + } if ( totalPeriods < MULTIMAP_MIN_PERIODS && @@ -54,7 +56,7 @@ const RenderingStrategy = ({ ) { onChange(RENDERING_STRATEGY_SINGLE) } - }, [value, periods, prevPeriods, onChange]) + }, [value, periods, prevPeriods, onChange, totalPeriods]) const helpText = useMemo(() => { const messages = [] diff --git a/src/components/periods/Timeline.js b/src/components/periods/Timeline.js index 8bf4f7855..470f9d432 100644 --- a/src/components/periods/Timeline.js +++ b/src/components/periods/Timeline.js @@ -4,11 +4,11 @@ import { scaleTime } from 'd3-scale' import { select } from 'd3-selection' import PropTypes from 'prop-types' import React, { Component } from 'react' +import { doubleTicksPeriods } from '../../constants/periods.js' import { getPeriodTypeFromId, getPeriodLevelFromPeriodType, } from '../../util/periods.js' -import { doubleTicksPeriods } from '../../constants/periods.js' import timeTicks from '../../util/timeTicks.js' import styles from './styles/Timeline.module.css' diff --git a/src/components/periods/__tests__/RenderingStrategy.spec.js b/src/components/periods/__tests__/RenderingStrategy.spec.js index 01295e767..b8fc1413c 100644 --- a/src/components/periods/__tests__/RenderingStrategy.spec.js +++ b/src/components/periods/__tests__/RenderingStrategy.spec.js @@ -2,13 +2,13 @@ import { mount } from 'enzyme' import React from 'react' import { Provider } from 'react-redux' import configureMockStore from 'redux-mock-store' -import RenderingStrategy from '../RenderingStrategy' import { RENDERING_STRATEGY_SINGLE, RENDERING_STRATEGY_TIMELINE, RENDERING_STRATEGY_SPLIT_BY_PERIOD, -} from '../../../constants/layers' -import { countPeriods } from '../../../util/periods' +} from '../../../constants/layers.js' +import { countPeriods } from '../../../util/periods.js' +import RenderingStrategy from '../RenderingStrategy.js' const mockStore = configureMockStore() @@ -82,7 +82,7 @@ describe('RenderingStrategy', () => { it('automatically switches to SINGLE when conditions are not met', () => { countPeriods.mockReturnValue(1) - const wrapper = renderWithProps(props) + renderWithProps(props) expect(mockOnChange).toHaveBeenCalledWith(RENDERING_STRATEGY_SINGLE) }) }) diff --git a/src/components/periods/__tests__/Timeline.spec.js b/src/components/periods/__tests__/Timeline.spec.js index 9b74aed5f..34385a72d 100644 --- a/src/components/periods/__tests__/Timeline.spec.js +++ b/src/components/periods/__tests__/Timeline.spec.js @@ -35,6 +35,15 @@ describe('Timeline', () => { endDate: new Date('2019-06-30T24:00:00.000'), }, ] + const periodOnChange = { + id: '201904', + name: 'April 2019', + startDate: new Date('2019-04-01T00:00:00.000'), + endDate: new Date('2019-04-30T24:00:00.000'), + level: 7, + levelRank: 0, + type: 'MONTHLY', + } const onChangeSpy = jest.fn() let props @@ -69,8 +78,7 @@ describe('Timeline', () => { it('should call onChange with the period clicked', () => { const wrapper = renderWithProps(props) wrapper.find('rect').first().simulate('click') - // Update periods[0] to account for the updated sorting logic - expect(onChangeSpy).toHaveBeenCalledWith(periods[0]) + expect(onChangeSpy).toHaveBeenCalledWith(periodOnChange) }) it('Should toggle play mode when play/pause button is clicked', () => { @@ -81,7 +89,7 @@ describe('Timeline', () => { playPauseBtn.simulate('click') expect(wrapper.state('mode')).toBe('play') // Called because current period is the last - expect(onChangeSpy).toHaveBeenCalledWith(periods[0]) + expect(onChangeSpy).toHaveBeenCalledWith(periodOnChange) playPauseBtn.simulate('click') expect(wrapper.state('mode')).toBe('stop') }) diff --git a/src/loaders/thematicLoader.js b/src/loaders/thematicLoader.js index 29ca6751e..f27b09c54 100644 --- a/src/loaders/thematicLoader.js +++ b/src/loaders/thematicLoader.js @@ -97,10 +97,13 @@ const thematicLoader = async ({ config, engine, nameProperty }) => { const isSingleMap = renderingStrategy === RENDERING_STRATEGY_SINGLE const isBubbleMap = thematicMapType === THEMATIC_BUBBLE const isSingleColor = config.method === CLASSIFICATION_SINGLE_COLOR - const presetPeriods = getPeriodsFromFilters(config.filters) + const names = getApiResponseNames(data) + const presetPeriods = getPeriodsFromFilters(config.filters).map((pe) => { + pe.name = names[pe.id] + return pe + }) const periods = getPeriodsFromMetaData(data.metaData) const dimensions = getValidDimensionsFromFilters(config.filters) - const names = getApiResponseNames(data) const valuesByPeriod = !isSingleMap ? getValuesByPeriod(data) : null const valueById = getValueById(data) const valueFeatures = noDataColor @@ -151,7 +154,7 @@ const thematicLoader = async ({ config, engine, nameProperty }) => { title: name, period: presetPeriods.length > 0 - ? presetPeriods.map((pe) => names[pe.id] || pe.id).join(', ') + ? presetPeriods.map((pe) => pe.name || pe.id).join(', ') : formatStartEndDate( getDateArray(config.startDate), getDateArray(config.endDate) From 2d74cb0cc8af1ac48836b822a7f56268517a2396 Mon Sep 17 00:00:00 2001 From: braimbault Date: Thu, 12 Dec 2024 13:36:13 +0100 Subject: [PATCH 19/28] chore: clean-up --- src/components/edit/LayerEdit.js | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/components/edit/LayerEdit.js b/src/components/edit/LayerEdit.js index b0b4c5c94..2696a49ec 100644 --- a/src/components/edit/LayerEdit.js +++ b/src/components/edit/LayerEdit.js @@ -9,7 +9,7 @@ import { ButtonStrip, } from '@dhis2/ui' import PropTypes from 'prop-types' -import React, { useState, useRef, useEffect } from 'react' +import React, { useState } from 'react' import { connect } from 'react-redux' import { addLayer, updateLayer, cancelLayer } from '../../actions/layers.js' import { EARTH_ENGINE_LAYER } from '../../constants/layers.js' @@ -46,7 +46,6 @@ const getLayerNames = () => ({ const LayerEdit = ({ layer, addLayer, updateLayer, cancelLayer }) => { const [isValidLayer, setIsValidLayer] = useState(false) - const modalRef = useRef(null) const { systemSettings, periodsSettings } = useCachedDataQuery() const orgUnits = useOrgUnits() @@ -77,12 +76,6 @@ const LayerEdit = ({ layer, addLayer, updateLayer, cancelLayer }) => { useKeyDown('Escape', cancelLayer) useKeyDown('Enter', onValidateLayer) - useEffect(() => { - if (layer && modalRef.current) { - modalRef.current.focus() - } - }, [layer]) - if (!layer) { return null } @@ -105,13 +98,7 @@ const LayerEdit = ({ layer, addLayer, updateLayer, cancelLayer }) => { : i18n.t('Add new {{name}} layer', { name }) return ( - + {title}
From 051a9bdecc25e566e624d6659c04be56efb88722 Mon Sep 17 00:00:00 2001 From: braimbault Date: Thu, 12 Dec 2024 16:35:14 +0100 Subject: [PATCH 20/28] fix: update periodError handling --- src/components/edit/event/EventDialog.js | 46 +++++++++++++------ .../edit/thematic/ThematicDialog.js | 30 ++++++------ .../edit/trackedEntity/TrackedEntityDialog.js | 24 ++++++++-- src/components/periods/StartEndDate.js | 16 ++++++- src/util/time.js | 2 +- 5 files changed, 84 insertions(+), 34 deletions(-) diff --git a/src/components/edit/event/EventDialog.js b/src/components/edit/event/EventDialog.js index b4dcb8cbd..2a18f06bc 100644 --- a/src/components/edit/event/EventDialog.js +++ b/src/components/edit/event/EventDialog.js @@ -1,5 +1,5 @@ import i18n from '@dhis2/d2-i18n' -import { NoticeBox } from '@dhis2/ui' +import { NoticeBox, IconErrorFilled24 } from '@dhis2/ui' import PropTypes from 'prop-types' import React, { Component } from 'react' import { connect } from 'react-redux' @@ -18,8 +18,6 @@ import { setOrgUnits, } from '../../../actions/layerEdit.js' import { - DEFAULT_START_DATE, - DEFAULT_END_DATE, EVENT_COLOR, EVENT_RADIUS, EVENT_BUFFER, @@ -117,8 +115,6 @@ class EventDialog extends Component { endDate, orgUnits, setPeriod, - setStartDate, - setEndDate, setOrgUnits, } = this.props @@ -126,20 +122,18 @@ class EventDialog extends Component { const { keyAnalysisRelativePeriod: defaultPeriod, hiddenPeriods } = systemSettings + const hasDate = startDate !== undefined && endDate !== undefined + // Set default period from system settings if ( !period && - !startDate && - !endDate && + !hasDate && defaultPeriod && isPeriodAvailable(defaultPeriod, hiddenPeriods) ) { setPeriod({ id: defaultPeriod, }) - } else if (!startDate && !endDate) { - setStartDate(DEFAULT_START_DATE) - setEndDate(DEFAULT_END_DATE) } // Set org unit tree roots as default @@ -152,18 +146,38 @@ class EventDialog extends Component { } componentDidUpdate(prev) { - const { validateLayer, onLayerValidation } = this.props + const { + validateLayer, + onLayerValidation, + filters, + startDate, + endDate, + setStartDate, + setEndDate, + } = this.props + const { periodError } = this.state if (validateLayer && validateLayer !== prev.validateLayer) { onLayerValidation(this.validate()) } + + const hasDate = startDate !== undefined || endDate !== undefined + if (hasDate && getPeriodFromFilters(filters) !== undefined) { + setStartDate() + setEndDate() + this.setErrorState('periodError', null, 'period') + } else if ( + periodError && + (startDate !== prev.startDate || endDate !== prev.endDate) + ) { + this.setErrorState('periodError', null, 'period') + } } render() { const { // layer options columns = [], - endDate, eventClustering, eventStatus, eventCoordinateField, @@ -174,6 +188,7 @@ class EventDialog extends Component { program, programStage, startDate, + endDate, legendSet, periodsSettings, } = this.props @@ -277,12 +292,17 @@ class EventDialog extends Component { )} + {periodError && ( +
+ + {periodError} +
+ )}
)} {tab === 'orgunits' && ( diff --git a/src/components/edit/thematic/ThematicDialog.js b/src/components/edit/thematic/ThematicDialog.js index a40cca5c4..46721b411 100644 --- a/src/components/edit/thematic/ThematicDialog.js +++ b/src/components/edit/thematic/ThematicDialog.js @@ -131,7 +131,6 @@ class ThematicDialog extends Component { const dataItem = getDataItemFromColumns(columns) const periods = getPeriodsFromFilters(filters) - const { keyAnalysisRelativePeriod: defaultPeriod, hiddenPeriods } = systemSettings @@ -151,11 +150,7 @@ class ThematicDialog extends Component { } } - const hasDate = - startDate !== undefined && - startDate !== '' && - endDate !== undefined && - endDate !== '' + const hasDate = startDate !== undefined && endDate !== undefined if (hasDate) { const keepPeriod = false @@ -209,7 +204,11 @@ class ThematicDialog extends Component { setRenderingStrategy, validateLayer, onLayerValidation, + startDate, + endDate, + filters, } = this.props + const { periodError } = this.state // Set rendering strategy to single if not relative period if ( @@ -241,19 +240,22 @@ class ThematicDialog extends Component { if (periodType !== prev.periodType) { switch (periodType) { case PREDEFINED_PERIODS: - // !TODO Backup Start-End dates - // Remove Start-End dates - // !TODO Restore Predefined periods backup - setStartDate('') - setEndDate('') + setStartDate() + setEndDate() break case START_END_DATES: - // !TODO Backup Predefined periods - // Remove Predefined periods - // !TODO Restore Start-End dates backup setPeriods([]) break } + this.setErrorState('periodError', null, 'period') + } else if ( + periodError && + (startDate !== prev.startDate || + endDate !== prev.endDate || + getPeriodsFromFilters(filters) !== + getPeriodsFromFilters(prev.filters)) + ) { + this.setErrorState('periodError', null, 'period') } } diff --git a/src/components/edit/trackedEntity/TrackedEntityDialog.js b/src/components/edit/trackedEntity/TrackedEntityDialog.js index 4142cf64d..06da3af2c 100644 --- a/src/components/edit/trackedEntity/TrackedEntityDialog.js +++ b/src/components/edit/trackedEntity/TrackedEntityDialog.js @@ -1,5 +1,5 @@ import i18n from '@dhis2/d2-i18n' -import { NoticeBox } from '@dhis2/ui' +import { NoticeBox, IconErrorFilled24 } from '@dhis2/ui' import cx from 'classnames' import PropTypes from 'prop-types' import React, { Component, Fragment } from 'react' @@ -106,8 +106,10 @@ class TrackedEntityDialog extends Component { setOrgUnits, } = this.props + const hasDate = startDate !== undefined && endDate !== undefined + // Set default period (last year) - if (!startDate && !endDate) { + if (!hasDate) { setStartDate(DEFAULT_START_DATE) setEndDate(DEFAULT_END_DATE) } @@ -128,11 +130,20 @@ class TrackedEntityDialog extends Component { } componentDidUpdate(prev) { - const { validateLayer, onLayerValidation } = this.props + const { validateLayer, onLayerValidation, startDate, endDate } = + this.props + const { periodError } = this.state if (validateLayer && validateLayer !== prev.validateLayer) { onLayerValidation(this.validate()) } + + if ( + periodError && + (startDate !== prev.startDate || endDate !== prev.endDate) + ) { + this.setErrorState('periodError', null, 'period') + } } render() { @@ -279,11 +290,16 @@ class TrackedEntityDialog extends Component { + {periodError && ( +
+ + {periodError} +
+ )}
)} {tab === 'orgunits' && ( diff --git a/src/components/periods/StartEndDate.js b/src/components/periods/StartEndDate.js index 0c5a6fbf2..52c6ade11 100644 --- a/src/components/periods/StartEndDate.js +++ b/src/components/periods/StartEndDate.js @@ -7,8 +7,14 @@ import { setStartDate, setEndDate } from '../../actions/layerEdit.js' import styles from './styles/StartEndDate.module.css' const StartEndDate = (props) => { - const { startDate, endDate, setStartDate, setEndDate, periodsSettings } = - props + const { + startDate = '', + endDate = '', + setStartDate, + setEndDate, + errorText, + periodsSettings, + } = props const [start, setStart] = useState(startDate.slice(0, 10)) const [end, setEnd] = useState(endDate.slice(0, 10)) @@ -52,6 +58,11 @@ const StartEndDate = (props) => { dataTest="end-date-input" /> + {errorText && ( +
+ {errorText} +
+ )}
) : null } @@ -59,6 +70,7 @@ StartEndDate.propTypes = { setEndDate: PropTypes.func.isRequired, setStartDate: PropTypes.func.isRequired, endDate: PropTypes.string, + errorText: PropTypes.string, periodsSettings: PropTypes.object, startDate: PropTypes.string, } diff --git a/src/util/time.js b/src/util/time.js index e4d305b55..02ed1900c 100644 --- a/src/util/time.js +++ b/src/util/time.js @@ -27,7 +27,7 @@ const shortDateRegexp = /^\d{4}-\d{2}-\d{2}$/ * @returns {String} */ const isValidDateFormat = (dateString) => - shortDateRegexp.test(dateString.substr(0, 10)) + dateString && shortDateRegexp.test(dateString.substr(0, 10)) /** * Formats a date string, timestamp or date array into format used by DHIS2 and date From 5d7c55d8cb803fd176b9c547ed0c5cd2253c0ec8 Mon Sep 17 00:00:00 2001 From: braimbault Date: Fri, 13 Dec 2024 14:34:51 +0100 Subject: [PATCH 21/28] fix: handle multiple periods in interpretations --- src/components/interpretations/InterpretationMap.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/interpretations/InterpretationMap.js b/src/components/interpretations/InterpretationMap.js index cfa7140ea..5d28022d0 100644 --- a/src/components/interpretations/InterpretationMap.js +++ b/src/components/interpretations/InterpretationMap.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types' import React, { useState, useEffect } from 'react' import useBasemapConfig from '../../hooks/useBasemapConfig.js' -import { getPeriodFromFilters } from '../../util/analytics.js' +import { getPeriodsFromFilters } from '../../util/analytics.js' import { getRelativePeriods } from '../../util/periods.js' import Map from '../plugin/Map.js' import styles from './styles/InterpretationMap.module.css' @@ -14,10 +14,12 @@ const InterpretationMap = ({ visualization, filters, onResponsesReceived }) => { // Find layers with relative periods const relativePeriodLayers = visualization.mapViews .filter((config) => { - const period = getPeriodFromFilters(config.filters) + const periods = getPeriodsFromFilters(config.filters) return ( - period && - getRelativePeriods().find((p) => p.id === period.id) + periods && + periods.some((period) => + getRelativePeriods().some((p) => p.id === period.id) + ) ) }) .map((layer) => ({ From 9f4ad6739b763b59c4b7b007310ff9440ce95680 Mon Sep 17 00:00:00 2001 From: braimbault Date: Fri, 13 Dec 2024 14:35:17 +0100 Subject: [PATCH 22/28] fix: avoid defaulting to period tab --- src/components/edit/thematic/ThematicDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/edit/thematic/ThematicDialog.js b/src/components/edit/thematic/ThematicDialog.js index 46721b411..978e6022a 100644 --- a/src/components/edit/thematic/ThematicDialog.js +++ b/src/components/edit/thematic/ThematicDialog.js @@ -247,10 +247,10 @@ class ThematicDialog extends Component { setPeriods([]) break } - this.setErrorState('periodError', null, 'period') } else if ( periodError && - (startDate !== prev.startDate || + (periodType !== prev.periodType || + startDate !== prev.startDate || endDate !== prev.endDate || getPeriodsFromFilters(filters) !== getPeriodsFromFilters(prev.filters)) From 07dd32f994b5889411bacdeace8f812c48bd9c50 Mon Sep 17 00:00:00 2001 From: braimbault Date: Fri, 13 Dec 2024 14:36:16 +0100 Subject: [PATCH 23/28] feat: add closing buttons "x" --- src/components/edit/LayerEdit.js | 7 ++++++- src/components/layerSources/ManageLayerSourcesModal.js | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/edit/LayerEdit.js b/src/components/edit/LayerEdit.js index 2696a49ec..fce24438f 100644 --- a/src/components/edit/LayerEdit.js +++ b/src/components/edit/LayerEdit.js @@ -98,7 +98,12 @@ const LayerEdit = ({ layer, addLayer, updateLayer, cancelLayer }) => { : i18n.t('Add new {{name}} layer', { name }) return ( - + {title}
diff --git a/src/components/layerSources/ManageLayerSourcesModal.js b/src/components/layerSources/ManageLayerSourcesModal.js index c382a1f29..7d55ba72e 100644 --- a/src/components/layerSources/ManageLayerSourcesModal.js +++ b/src/components/layerSources/ManageLayerSourcesModal.js @@ -27,7 +27,12 @@ const ManageLayerSourcesModal = ({ onClose }) => { useKeyDown('Escape', onClose) return ( - + {i18n.t('Configure available layer sources')} @@ -54,7 +59,7 @@ const ManageLayerSourcesModal = ({ onClose }) => { From 1487d0b2eb27289cfd71b2847061184439e32820 Mon Sep 17 00:00:00 2001 From: braimbault Date: Fri, 13 Dec 2024 16:21:43 +0100 Subject: [PATCH 24/28] feat: additional keyboard controls --- src/components/datatable/BottomPanel.js | 3 ++ src/components/datatable/DataTable.js | 2 +- .../edit/thematic/ThematicDialog.js | 1 - .../interpretations/InterpretationsToggle.js | 9 ++++++ src/components/map/Popup.js | 3 ++ src/components/orgunits/OrgUnitProfile.js | 3 ++ src/hooks/useKeyDown.js | 32 ++++++++++++++++--- 7 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/components/datatable/BottomPanel.js b/src/components/datatable/BottomPanel.js index 82a15c6e5..97a5018a5 100644 --- a/src/components/datatable/BottomPanel.js +++ b/src/components/datatable/BottomPanel.js @@ -8,6 +8,7 @@ import { LAYERS_PANEL_WIDTH, RIGHT_PANEL_WIDTH, } from '../../constants/layout.js' +import useKeyDown from '../../hooks/useKeyDown.js' import { useWindowDimensions } from '../WindowDimensionsProvider.js' import DataTable from './DataTable.js' import ErrorBoundary from './ErrorBoundary.js' @@ -37,6 +38,8 @@ const BottomPanel = () => { const tableWidth = width - layersWidth - rightPanelWidth const dataTableControlsHeight = 20 + useKeyDown('Escape', () => dispatch(closeDataTable()), true) + return (
{ }) useEffect(() => { - /* The combination of automtic table layout and virtual scrolling + /* The combination of automatic table layout and virtual scrolling * causes a content shift when scrolling and filtering because the * cells in the DOM have a different content length which causes the * columns to have a different width. To avoid that we measure the diff --git a/src/components/edit/thematic/ThematicDialog.js b/src/components/edit/thematic/ThematicDialog.js index 978e6022a..88bb09a78 100644 --- a/src/components/edit/thematic/ThematicDialog.js +++ b/src/components/edit/thematic/ThematicDialog.js @@ -305,7 +305,6 @@ class ThematicDialog extends Component { calculationError, eventDataItemError, programIndicatorError, - // periodTypeError, periodError, orgUnitsError, legendSetError, diff --git a/src/components/interpretations/InterpretationsToggle.js b/src/components/interpretations/InterpretationsToggle.js index a47cce286..d5b43ff48 100644 --- a/src/components/interpretations/InterpretationsToggle.js +++ b/src/components/interpretations/InterpretationsToggle.js @@ -5,6 +5,7 @@ import { openInterpretationsPanel, closeInterpretationsPanel, } from '../../actions/ui.js' +import useKeyDown from '../../hooks/useKeyDown.js' const InterpretationsToggle = () => { const interpretationsEnabled = useSelector((state) => Boolean(state.map.id)) @@ -21,6 +22,14 @@ const InterpretationsToggle = () => { } }, [dispatch, interpretationsOpen]) + const onClose = useCallback(() => { + if (interpretationsOpen) { + dispatch(closeInterpretationsPanel()) + } + }, [dispatch, interpretationsOpen]) + + useKeyDown('Escape', onClose, true) + return ( { const { map, isPlugin } = context const container = useMemo(() => document.createElement('div'), []) + useKeyDown('Escape', () => map.closePopup()) + // Create and open popup on map useEffect(() => { container.className = className diff --git a/src/components/orgunits/OrgUnitProfile.js b/src/components/orgunits/OrgUnitProfile.js index 099908e8d..669e53191 100644 --- a/src/components/orgunits/OrgUnitProfile.js +++ b/src/components/orgunits/OrgUnitProfile.js @@ -4,6 +4,7 @@ import { CenteredContent, CircularLoader, IconCross24 } from '@dhis2/ui' import React, { useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' import { closeOrgUnitProfile } from '../../actions/orgUnits.js' +import useKeyDown from '../../hooks/useKeyDown.js' import Drawer from '../core/Drawer.js' import OrgUnitData from './OrgUnitData.js' import OrgUnitInfo from './OrgUnitInfo.js' @@ -34,6 +35,8 @@ const OrgUnitProfile = () => { } }, [id, refetch]) + useKeyDown('Escape', () => dispatch(closeOrgUnitProfile())) + if (!id) { return null } diff --git a/src/hooks/useKeyDown.js b/src/hooks/useKeyDown.js index 52bc9ae23..a17499b75 100644 --- a/src/hooks/useKeyDown.js +++ b/src/hooks/useKeyDown.js @@ -1,18 +1,42 @@ -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' + +const useKeyDown = (key, callback, longPress = false) => { + const timerRef = useRef(null) -const useKeyDown = (key, callback) => { useEffect(() => { const handleKeyDown = (event) => { if (event.key === key) { - callback(event) + if (!longPress) { + callback(event) + } else { + // Start a timer for detecting long press + timerRef.current = setTimeout(() => { + callback(event) + }, 250) // Adjust delay for long press detection + } + } + } + + const handleKeyUp = (event) => { + if (event.key === key && longPress) { + // Clear the timer if the key is released before the delay + clearTimeout(timerRef.current) } } window.addEventListener('keydown', handleKeyDown) + window.addEventListener('keyup', handleKeyUp) + return () => { window.removeEventListener('keydown', handleKeyDown) + window.removeEventListener('keyup', handleKeyUp) } - }, [key, callback]) + }, [key, callback, longPress]) + + useEffect(() => { + // Cleanup on unmount + return () => clearTimeout(timerRef.current) + }, []) } export default useKeyDown From aff504456f7a182aee677b88f9fc4755d467c0a8 Mon Sep 17 00:00:00 2001 From: braimbault Date: Wed, 18 Dec 2024 09:54:24 +0100 Subject: [PATCH 25/28] fix: timeline not visible in Firefox --- src/components/periods/Timeline.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/periods/Timeline.js b/src/components/periods/Timeline.js index 470f9d432..5ba87b5ad 100644 --- a/src/components/periods/Timeline.js +++ b/src/components/periods/Timeline.js @@ -211,8 +211,8 @@ class Timeline extends Component { {/* Play/Pause Button */} From c0d702832f8cb2f87d85e424104ca840b38d194e Mon Sep 17 00:00:00 2001 From: braimbault Date: Fri, 20 Dec 2024 16:12:25 +0100 Subject: [PATCH 26/28] chore: fix cypress tests --- cypress/elements/layer.js | 15 +- cypress/elements/thematic_layer.js | 45 +++++- .../integration/layers/thematiclayer.cy.js | 14 +- cypress/integration/systemsettings.cy.js | 16 +- src/components/periods/StartEndDate.js | 141 +++++++++++++++--- src/hooks/useKeyDown.js | 13 +- 6 files changed, 191 insertions(+), 53 deletions(-) diff --git a/cypress/elements/layer.js b/cypress/elements/layer.js index f067f60bd..99a61b189 100644 --- a/cypress/elements/layer.js +++ b/cypress/elements/layer.js @@ -46,7 +46,7 @@ export class Layer { .contains(level) .find('input') .check() - cy.get('body').click() // Close the modal menu + cy.get('body').click() return this } @@ -59,27 +59,26 @@ export class Layer { .find('input') .uncheck() - cy.get('body').click() // Close the modal menu + cy.get('body').click() return this } typeStartDate(dateString) { - cy.get('label') - .contains('Start date') - .next() + cy.getByDataTest('start-date-input-content') .find('input') .type(dateString) + cy.get('body').click(0, 0) return this } typeEndDate(dateString) { - cy.get('label') - .contains('End date') - .next() + cy.getByDataTest('end-date-input-content') .find('input') .type(dateString) + cy.get('body').click(0, 0) + return this } diff --git a/cypress/elements/thematic_layer.js b/cypress/elements/thematic_layer.js index 4aed7bb38..77bbc9c32 100644 --- a/cypress/elements/thematic_layer.js +++ b/cypress/elements/thematic_layer.js @@ -55,9 +55,48 @@ export class ThematicLayer extends Layer { return this } - selectPeriodType(periodType) { - cy.get('[data-test="periodtypeselect"]').click() - cy.contains(periodType).click() + selectPeriodType(periodType, periodDimension = 'fixed', n = 'last') { + cy.getByDataTest( + `period-dimension-${periodDimension}-periods-button` + ).click() + cy.getByDataTest( + `period-dimension-${periodDimension}-period-filter${ + periodDimension === 'fixed' ? '-period-type' : '' + }-content` + ).click() + cy.get(`[data-value="${periodType}"]`).then(($el) => { + if ($el.hasClass('active')) { + cy.get('body').click('topLeft') + } else { + cy.wrap($el).click() + } + }) + if (n === 'last') { + cy.getByDataTest( + 'period-dimension-transfer-actions-removeall' + ).click() + cy.getByDataTest('period-dimension-transfer-option-content') + .last() + .dblclick() + } else { + cy.getByDataTest( + 'period-dimension-transfer-actions-removeall' + ).click() + cy.getByDataTest('period-dimension-transfer-option-content') + .eq(n) + .dblclick() + } + + return this + } + + selectPresets() { + cy.contains('Choose from presets').click() + + return this + } + selectStartEndDates() { + cy.contains('Define start - end dates').click() return this } diff --git a/cypress/integration/layers/thematiclayer.cy.js b/cypress/integration/layers/thematiclayer.cy.js index adeffb1d3..48e3655f4 100644 --- a/cypress/integration/layers/thematiclayer.cy.js +++ b/cypress/integration/layers/thematiclayer.cy.js @@ -41,7 +41,7 @@ context('Thematic Layers', () => { .selectIndicatorGroup('HIV') .selectIndicator(INDICATOR_NAME) .selectTab('Period') - .selectPeriodType('Yearly') + .selectPeriodType('YEARLY') .selectTab('Org Units') .selectOu('Sierra Leone') .addToMap() @@ -66,7 +66,7 @@ context('Thematic Layers', () => { .selectIndicatorGroup('HIV') .selectIndicator(INDICATOR_NAME) .selectTab('Period') - .selectPeriodType('Yearly') + .selectPeriodType('YEARLY') .selectTab('Org Units') .selectOu('Bombali') .selectOu('Bo') @@ -83,7 +83,7 @@ context('Thematic Layers', () => { .selectIndicatorGroup('HIV') .selectIndicator(INDICATOR_NAME) .selectTab('Period') - .selectPeriodType('Start/end dates') + .selectStartEndDates() .typeStartDate(`${CURRENT_YEAR}-02-01`) .typeEndDate(`${CURRENT_YEAR}-11-30`) .selectTab('Org Units') @@ -104,7 +104,7 @@ context('Thematic Layers', () => { .selectIndicatorGroup('Stock') .selectIndicator('BCG Stock PHU') .selectTab('Period') - .selectPeriodType('Start/end dates') + .selectStartEndDates() .typeStartDate(`${CURRENT_YEAR}-11-01`) .typeEndDate(`${CURRENT_YEAR}-11-30`) .selectTab('Style') @@ -175,9 +175,7 @@ context('Thematic Layers', () => { .selectTab('Org Units') .selectOu('Sierra Leone') .selectTab('Period') - - cy.getByDataTest('relative-period-select-content').click() - cy.contains('Last 3 months').click() + .selectPeriodType('MONTHLY', 'relative', 2) cy.get('[type="radio"]').should('have.length', 3) cy.get('[type="radio"]').check('SPLIT_BY_PERIOD') @@ -307,7 +305,7 @@ context('Thematic Layers', () => { .selectIndicatorGroup('HIV') .selectIndicator(INDICATOR_NAME) .selectTab('Period') - .selectPeriodType('Yearly') + .selectPeriodType('YEARLY') .selectTab('Org Units') .selectOu('Bo') .unselectOuLevel('District') diff --git a/cypress/integration/systemsettings.cy.js b/cypress/integration/systemsettings.cy.js index 52fd45885..0bb98d53c 100644 --- a/cypress/integration/systemsettings.cy.js +++ b/cypress/integration/systemsettings.cy.js @@ -34,14 +34,16 @@ describe('systemSettings', () => { Layer.openDialog('Thematic').selectTab('Period') - cy.getByDataTest('periodtypeselect-content').click() + cy.getByDataTest( + 'period-dimension-relative-period-filter-content' + ).click() cy.getByDataTest('dhis2-uicore-select-menu-menuwrapper') - .contains('Bi-weekly') + .contains('Bi-weeks') .should('be.visible') cy.getByDataTest('dhis2-uicore-select-menu-menuwrapper') - .contains('Weekly') + .contains('Weeks') .should('not.exist') }) @@ -52,14 +54,16 @@ describe('systemSettings', () => { Layer.openDialog('Thematic').selectTab('Period') - cy.getByDataTest('periodtypeselect-content').click() + cy.getByDataTest( + 'period-dimension-relative-period-filter-content' + ).click() cy.getByDataTest('dhis2-uicore-select-menu-menuwrapper') - .contains('Bi-weekly') + .contains('Bi-weeks') .should('be.visible') cy.getByDataTest('dhis2-uicore-select-menu-menuwrapper') - .contains('Weekly') + .contains('Weeks') .should('be.visible') }) diff --git a/src/components/periods/StartEndDate.js b/src/components/periods/StartEndDate.js index 52c6ade11..493d29d04 100644 --- a/src/components/periods/StartEndDate.js +++ b/src/components/periods/StartEndDate.js @@ -4,8 +4,81 @@ import PropTypes from 'prop-types' import React, { useState } from 'react' import { connect } from 'react-redux' import { setStartDate, setEndDate } from '../../actions/layerEdit.js' +import useKeyDown from '../../hooks/useKeyDown.js' import styles from './styles/StartEndDate.module.css' +const formatDate = (date, calendar = 'iso8601') => { + if (calendar === 'iso8601') { + return formatDateIso8601(date) + } + return formatDateDefault(date) +} +const formatDateDefault = (date) => { + if (!date) { + return '' + } + return date +} +const formatDateIso8601 = (date) => { + if (!date) { + return '' + } + + const numericDate = date.replace(/\D/g, '') + + if (numericDate.length < 5) { + return numericDate + } + + const year = numericDate.slice(0, 4) + const month = numericDate.slice(4, 6) + const day = numericDate.slice(6, 8) + + if (numericDate.length < 7) { + return `${year}-${month}` + } + if (numericDate.length < 8) { + return `${year}-${month}-${day}` + } + + const formattedYear = year === '0000' ? '2000' : year + let formattedMonth = month === '00' ? '01' : month + formattedMonth = formattedMonth > 12 ? '12' : formattedMonth + + let formattedDay = day === '00' ? '01' : day + + const maxDaysInMonth = getMaxDaysInMonth(formattedYear, formattedMonth) + formattedDay = formattedDay > maxDaysInMonth ? maxDaysInMonth : formattedDay + + return `${formattedYear}-${formattedMonth}-${formattedDay}` +} + +const getMaxDaysInMonth = (year, month) => { + const monthDays = { + '02': 28, + '04': 30, + '06': 30, + '09': 30, + 11: 30, + } + + if (month === '02') { + return isLeapYear(year) ? 29 : 28 + } + + return monthDays[month] || 31 +} + +const isLeapYear = (year) => { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 +} + +const createBoundHandler = (localSetter, reduxSetter, calendar) => (value) => { + const formattedDate = formatDate(value, calendar) + localSetter(formattedDate) + reduxSetter(formattedDate) +} + const StartEndDate = (props) => { const { startDate = '', @@ -15,21 +88,38 @@ const StartEndDate = (props) => { errorText, periodsSettings, } = props + const [startDateInput, setStartDateInput] = useState( + formatDate(startDate, periodsSettings?.calendar) + ) + const [endDateInput, setEndDateInput] = useState( + formatDate(endDate, periodsSettings?.calendar) + ) + + const onStartDateChange = createBoundHandler( + setStartDateInput, + setStartDate, + periodsSettings?.calendar + ) + const onEndDateChange = createBoundHandler( + setEndDateInput, + setEndDate, + periodsSettings?.calendar + ) - const [start, setStart] = useState(startDate.slice(0, 10)) - const [end, setEnd] = useState(endDate.slice(0, 10)) + // Forces calendar to close when using Tab/Enter navigation + useKeyDown(['Tab', 'Enter'], () => { + const backdropElement = document.querySelectorAll('.backdrop') + if (backdropElement?.length === 3) { + backdropElement[2].click() + } + }) const hasDate = startDate !== undefined && endDate !== undefined - - const onStartDateChange = ({ calendarDateString: value }) => { - setStart(value.slice(0, 10)) - setStartDate(value.slice(0, 10)) + if (!hasDate) { + return null } - const onEndDateChange = ({ calendarDateString: value }) => { - setEnd(value.slice(0, 10)) - setEndDate(value.slice(0, 10)) - } - return hasDate ? ( + + return ( {
+ onStartDateChange(e?.calendarDateString) + } + onChange={(e) => onStartDateChange(e?.value)} placeholder="YYYY-MM-DD" dataTest="start-date-input" + strictValidation={true} />
onEndDateChange(e?.calendarDateString)} + onChange={(e) => onEndDateChange(e?.value)} placeholder="YYYY-MM-DD" dataTest="end-date-input" + strictValidation={true} />
{errorText && ( @@ -64,14 +160,17 @@ const StartEndDate = (props) => {
)} - ) : null + ) } StartEndDate.propTypes = { setEndDate: PropTypes.func.isRequired, setStartDate: PropTypes.func.isRequired, endDate: PropTypes.string, errorText: PropTypes.string, - periodsSettings: PropTypes.object, + periodsSettings: PropTypes.shape({ + calendar: PropTypes.string, + locale: PropTypes.string, + }), startDate: PropTypes.string, } diff --git a/src/hooks/useKeyDown.js b/src/hooks/useKeyDown.js index a17499b75..e83da40fa 100644 --- a/src/hooks/useKeyDown.js +++ b/src/hooks/useKeyDown.js @@ -1,15 +1,16 @@ -import { useEffect, useRef } from 'react' +import { useEffect, useRef, useMemo } from 'react' const useKeyDown = (key, callback, longPress = false) => { const timerRef = useRef(null) + const keys = useMemo(() => (Array.isArray(key) ? key : [key]), [key]) + useEffect(() => { const handleKeyDown = (event) => { - if (event.key === key) { + if (keys.includes(event.key)) { if (!longPress) { callback(event) } else { - // Start a timer for detecting long press timerRef.current = setTimeout(() => { callback(event) }, 250) // Adjust delay for long press detection @@ -18,8 +19,7 @@ const useKeyDown = (key, callback, longPress = false) => { } const handleKeyUp = (event) => { - if (event.key === key && longPress) { - // Clear the timer if the key is released before the delay + if (keys.includes(event.key) && longPress) { clearTimeout(timerRef.current) } } @@ -31,10 +31,9 @@ const useKeyDown = (key, callback, longPress = false) => { window.removeEventListener('keydown', handleKeyDown) window.removeEventListener('keyup', handleKeyUp) } - }, [key, callback, longPress]) + }, [keys, callback, longPress]) useEffect(() => { - // Cleanup on unmount return () => clearTimeout(timerRef.current) }, []) } From 91a69bb4390f50ed66a58b5adcff75ee44f1e620 Mon Sep 17 00:00:00 2001 From: braimbault Date: Fri, 20 Dec 2024 16:49:35 +0100 Subject: [PATCH 27/28] chore: fix cypress tests --- cypress/elements/layer.js | 2 + cypress/integration/layers/eventlayer.cy.js | 46 ++++++++++---------- cypress/integration/layers/multilayers.cy.js | 2 +- cypress/integration/layers/telayer.cy.js | 42 +++++++++--------- src/components/periods/StartEndDate.js | 2 + 5 files changed, 49 insertions(+), 45 deletions(-) diff --git a/cypress/elements/layer.js b/cypress/elements/layer.js index 99a61b189..d9c16a0d9 100644 --- a/cypress/elements/layer.js +++ b/cypress/elements/layer.js @@ -65,6 +65,7 @@ export class Layer { } typeStartDate(dateString) { + cy.getByDataTest('calendar-clear-button').eq(0).click() cy.getByDataTest('start-date-input-content') .find('input') .type(dateString) @@ -74,6 +75,7 @@ export class Layer { } typeEndDate(dateString) { + cy.getByDataTest('calendar-clear-button').eq(1).click() cy.getByDataTest('end-date-input-content') .find('input') .type(dateString) diff --git a/cypress/integration/layers/eventlayer.cy.js b/cypress/integration/layers/eventlayer.cy.js index 432493e57..f31e5a5d6 100644 --- a/cypress/integration/layers/eventlayer.cy.js +++ b/cypress/integration/layers/eventlayer.cy.js @@ -3,11 +3,33 @@ import { EXTENDED_TIMEOUT } from '../../support/util.js' context('Event Layers', () => { beforeEach(() => { - cy.visit('/', EXTENDED_TIMEOUT) + cy.visit('/') }) const Layer = new EventLayer() + it('adds an event layer and applies style for boolean data element', () => { + Layer.openDialog('Events') + .selectProgram('E2E program') + .validateStage('Stage 1 - Repeatable') + .selectTab('Style') + + cy.getByDataTest('style-by-data-element-select').click() + + cy.getByDataTest('dhis2-uicore-singleselectoption') + .contains('E2E - Yes/no') + .click() + + cy.getByDataTest('dhis2-uicore-modalactions') + .contains('Add layer') + .click() + + Layer.validateDialogClosed(true) + + Layer.validateCardTitle('Stage 1 - Repeatable') + Layer.validateCardItems(['Yes', 'No', 'Not set']) + }) + it('shows error if no program selected', () => { Layer.openDialog('Events').addToMap() @@ -31,28 +53,6 @@ context('Event Layers', () => { Layer.validateCardItems(['Event']) }) - it('adds an event layer and applies style for boolean data element', () => { - Layer.openDialog('Events') - .selectProgram('E2E program') - .validateStage('Stage 1 - Repeatable') - .selectTab('Style') - - cy.getByDataTest('style-by-data-element-select').click() - - cy.getByDataTest('dhis2-uicore-singleselectoption') - .contains('E2E - Yes/no') - .click() - - cy.getByDataTest('dhis2-uicore-modalactions') - .contains('Add layer') - .click() - - Layer.validateDialogClosed(true) - - Layer.validateCardTitle('Stage 1 - Repeatable') - Layer.validateCardItems(['Yes', 'No', 'Not set']) - }) - it('opens an event popup', () => { Layer.openDialog('Events') .selectProgram('Inpatient morbidity and mortality') diff --git a/cypress/integration/layers/multilayers.cy.js b/cypress/integration/layers/multilayers.cy.js index 94ddf64fd..d3c4bafee 100644 --- a/cypress/integration/layers/multilayers.cy.js +++ b/cypress/integration/layers/multilayers.cy.js @@ -17,7 +17,7 @@ describe('Multiple Layers', () => { .selectIndicatorGroup('ANC') .selectIndicator(INDICATOR_NAME) .selectTab('Period') - .selectPeriodType('Yearly') + .selectPeriodType('YEARLY') .selectTab('Org Units') .selectOu('Sierra Leone') .selectOuLevel('District') diff --git a/cypress/integration/layers/telayer.cy.js b/cypress/integration/layers/telayer.cy.js index f1a41e993..f9b1025df 100644 --- a/cypress/integration/layers/telayer.cy.js +++ b/cypress/integration/layers/telayer.cy.js @@ -3,31 +3,11 @@ import { EXTENDED_TIMEOUT } from '../../support/util.js' describe('Tracked Entity Layers', () => { beforeEach(() => { - cy.visit('/', EXTENDED_TIMEOUT) + cy.visit('/') }) const Layer = new TeLayer() - it('adds a tracked entity layer', () => { - Layer.openDialog('Tracked entities') - .selectTab('Data') - .selectTeType('Malaria Entity') - .selectTeProgram( - 'Malaria case diagnosis, treatment and investigation' - ) - .selectTab('Org Units') - .selectOu('Bombali') - .selectOu('Bo') - .addToMap() - - Layer.validateDialogClosed(true) - - Layer.validateCardTitle( - 'Malaria case diagnosis, treatment and investigation' - ) - Layer.validateCardItems(['Malaria Entity']) - }) - it('opens a tracked entity layer popup', () => { Layer.openDialog('Tracked entities') .selectTab('Data') @@ -80,4 +60,24 @@ describe('Tracked Entity Layers', () => { Layer.validateCardTitle('Malaria focus investigation') Layer.validateCardItems(['Focus area']) }) + + it('adds a tracked entity layer', () => { + Layer.openDialog('Tracked entities') + .selectTab('Data') + .selectTeType('Malaria Entity') + .selectTeProgram( + 'Malaria case diagnosis, treatment and investigation' + ) + .selectTab('Org Units') + .selectOu('Bombali') + .selectOu('Bo') + .addToMap() + + Layer.validateDialogClosed(true) + + Layer.validateCardTitle( + 'Malaria case diagnosis, treatment and investigation' + ) + Layer.validateCardItems(['Malaria Entity']) + }) }) diff --git a/src/components/periods/StartEndDate.js b/src/components/periods/StartEndDate.js index 493d29d04..acbeb7924 100644 --- a/src/components/periods/StartEndDate.js +++ b/src/components/periods/StartEndDate.js @@ -138,6 +138,7 @@ const StartEndDate = (props) => { placeholder="YYYY-MM-DD" dataTest="start-date-input" strictValidation={true} + clearable={true} />
@@ -152,6 +153,7 @@ const StartEndDate = (props) => { placeholder="YYYY-MM-DD" dataTest="end-date-input" strictValidation={true} + clearable={true} />
{errorText && ( From 5b09c6e74b9f6624e1b747916a4ab3da1597f600 Mon Sep 17 00:00:00 2001 From: braimbault Date: Fri, 20 Dec 2024 21:42:27 +0100 Subject: [PATCH 28/28] chore: cypress update --- package.json | 2 +- yarn.lock | 143 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 95 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index 30984d948..ada9baab8 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@semantic-release/exec": "^6", "@semantic-release/git": "^10", "@testing-library/react": "^12.1.5", - "cypress": "^12.16.0", + "cypress": "^13.17.0", "cypress-tags": "^1.1.2", "cypress-wait-until": "^1.7.2", "enzyme": "^3.11.0", diff --git a/yarn.lock b/yarn.lock index f454373b6..6c488bd10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1404,10 +1404,10 @@ through2 "^2.0.0" watchify "^4.0.0" -"@cypress/request@^2.88.10": - version "2.88.11" - resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.11.tgz#5a4c7399bc2d7e7ed56e92ce5acb620c8b187047" - integrity sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w== +"@cypress/request@^3.0.6": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.7.tgz#6a74a4da98d9e5ae9121d6e2d9c14780c9b5cf1a" + integrity sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -1415,16 +1415,16 @@ combined-stream "~1.0.6" extend "~3.0.2" forever-agent "~0.6.1" - form-data "~2.3.2" - http-signature "~1.3.6" + form-data "~4.0.0" + http-signature "~1.4.0" is-typedarray "~1.0.0" isstream "~0.1.2" json-stringify-safe "~5.0.1" mime-types "~2.1.19" performance-now "^2.1.0" - qs "~6.10.3" + qs "6.13.1" safe-buffer "^5.1.2" - tough-cookie "~2.5.0" + tough-cookie "^5.0.0" tunnel-agent "^0.6.0" uuid "^8.3.2" @@ -3969,11 +3969,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.2.tgz#c076ed1d7b6095078ad3cf21dfeea951842778b1" integrity sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA== -"@types/node@^14.14.31": - version "14.18.36" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.36.tgz#c414052cb9d43fab67d679d5f3c641be911f5835" - integrity sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ== - "@types/normalize-package-data@^2.4.0", "@types/normalize-package-data@^2.4.1": version "2.4.4" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" @@ -5661,7 +5656,7 @@ buffer-xor@^1.0.3: resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== -buffer@^5.1.0, buffer@^5.5.0, buffer@^5.6.0: +buffer@^5.1.0, buffer@^5.5.0, buffer@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -5972,6 +5967,11 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== +ci-info@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.1.0.tgz#92319d2fa29d2620180ea5afed31f589bc98cf83" + integrity sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A== + cidr-regex@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-3.1.1.tgz#ba1972c57c66f61875f18fd7dd487469770b571d" @@ -6932,23 +6932,23 @@ cypress-wait-until@^1.7.2: resolved "https://registry.yarnpkg.com/cypress-wait-until/-/cypress-wait-until-1.7.2.tgz#7f534dd5a11c89b65359e7a0210f20d3dfc22107" integrity sha512-uZ+M8/MqRcpf+FII/UZrU7g1qYZ4aVlHcgyVopnladyoBrpoaMJ4PKZDrdOJ05H5RHbr7s9Tid635X3E+ZLU/Q== -cypress@^12.16.0: - version "12.16.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.16.0.tgz#d0dcd0725a96497f4c60cf54742242259847924c" - integrity sha512-mwv1YNe48hm0LVaPgofEhGCtLwNIQEjmj2dJXnAkY1b4n/NE9OtgPph4TyS+tOtYp5CKtRmDvBzWseUXQTjbTg== +cypress@^13.17.0: + version "13.17.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.17.0.tgz#34c3d68080c4497eace0f353bd1629587a5f600d" + integrity sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA== dependencies: - "@cypress/request" "^2.88.10" + "@cypress/request" "^3.0.6" "@cypress/xvfb" "^1.2.4" - "@types/node" "^14.14.31" "@types/sinonjs__fake-timers" "8.1.1" "@types/sizzle" "^2.3.2" arch "^2.2.0" blob-util "^2.0.2" bluebird "^3.7.2" - buffer "^5.6.0" + buffer "^5.7.1" cachedir "^2.3.0" chalk "^4.1.0" check-more-types "^2.24.0" + ci-info "^4.0.0" cli-cursor "^3.1.0" cli-table3 "~0.6.1" commander "^6.2.1" @@ -6963,7 +6963,6 @@ cypress@^12.16.0: figures "^3.2.0" fs-extra "^9.1.0" getos "^3.2.1" - is-ci "^3.0.0" is-installed-globally "~0.4.0" lazy-ass "^1.6.0" listr2 "^3.8.3" @@ -6972,11 +6971,13 @@ cypress@^12.16.0: minimist "^1.2.8" ospath "^1.2.2" pretty-bytes "^5.6.0" + process "^0.11.10" proxy-from-env "1.0.0" request-progress "^3.0.0" - semver "^7.3.2" + semver "^7.5.3" supports-color "^8.1.1" - tmp "~0.2.1" + tmp "~0.2.3" + tree-kill "1.2.2" untildify "^4.0.0" yauzl "^2.10.0" @@ -8978,6 +8979,15 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +form-data@~4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" + integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -9873,14 +9883,14 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -http-signature@~1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" - integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw== +http-signature@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.4.0.tgz#dee5a9ba2bf49416abc544abd6d967f6a94c8c3f" + integrity sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg== dependencies: assert-plus "^1.0.0" jsprim "^2.0.2" - sshpk "^1.14.1" + sshpk "^1.18.0" https-browserify@^1.0.0: version "1.0.0" @@ -10322,13 +10332,6 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-ci@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" - integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== - dependencies: - ci-info "^3.2.0" - is-cidr@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-4.0.2.tgz#94c7585e4c6c77ceabf920f8cde51b8c0fda8814" @@ -14524,7 +14527,7 @@ process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -process@~0.11.0: +process@^0.11.10, process@~0.11.0: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== @@ -14711,12 +14714,12 @@ qs@6.13.0: dependencies: side-channel "^1.0.6" -qs@~6.10.3: - version "6.10.5" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.5.tgz#974715920a80ff6a262264acd2c7e6c2a53282b4" - integrity sha512-O5RlPh0VFtR78y79rgcgKK4wbAI0C5zGVLztOIdpWX6ep368q5Hv6XRxDvXuZ9q3C6v+e3n8UfZZJw7IIG27eQ== +qs@6.13.1: + version "6.13.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.1.tgz#3ce5fc72bd3a8171b85c99b93c65dd20b7d1b16e" + integrity sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg== dependencies: - side-channel "^1.0.4" + side-channel "^1.0.6" qs@~6.5.2: version "6.5.3" @@ -15905,6 +15908,11 @@ semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.2.1, semver@^7.3.2, semve resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== +semver@^7.5.3: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + send@0.19.0: version "0.19.0" resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" @@ -16387,7 +16395,22 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -sshpk@^1.14.1, sshpk@^1.7.0: +sshpk@^1.18.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" + integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +sshpk@^1.7.0: version "1.17.0" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== @@ -17109,6 +17132,18 @@ tinyqueue@^2.0.3: resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA== +tldts-core@^6.1.69: + version "6.1.69" + resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.69.tgz#079ffcac8a4407bc74567e292aecf30b943674e1" + integrity sha512-nygxy9n2PBUFQUtAXAc122gGo+04/j5qr5TGQFZTHafTKYvmARVXt2cA5rgero2/dnXUfkdPtiJoKmrd3T+wdA== + +tldts@^6.1.32: + version "6.1.69" + resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.69.tgz#0fe1fcb1ad09510459693e72f96062cee2411f1f" + integrity sha512-Oh/CqRQ1NXNY7cy9NkTPUauOWiTro0jEYZTioGbOmcQh6EC45oribyIMJp0OJO3677r13tO6SKdWoGZUx2BDFw== + dependencies: + tldts-core "^6.1.69" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -17116,12 +17151,10 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" -tmp@~0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" - integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== - dependencies: - rimraf "^3.0.0" +tmp@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== tmpl@1.0.5: version "1.0.5" @@ -17200,6 +17233,13 @@ tough-cookie@^4.0.0: universalify "^0.2.0" url-parse "^1.5.3" +tough-cookie@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.0.0.tgz#6b6518e2b5c070cf742d872ee0f4f92d69eac1af" + integrity sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q== + dependencies: + tldts "^6.1.32" + tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -17232,6 +17272,11 @@ traverse@0.6.8: resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.8.tgz#5e5e0c41878b57e4b73ad2f3d1e36a715ea4ab15" integrity sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA== +tree-kill@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + treeverse@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/treeverse/-/treeverse-2.0.0.tgz#036dcef04bc3fd79a9b79a68d4da03e882d8a9ca"