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) }, []) }