From 3cb3c2682c124bbab1640dfd6b4df360adb5a5ef Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Fri, 11 Oct 2024 10:33:33 +0200 Subject: [PATCH] fix: convert to/from ISO dates --- i18n/en.pot | 10 +++- package.json | 2 +- src/data-workspace/inputs/datetime-input.js | 20 ++++++- .../inputs/datetime-input.test.js | 58 +++++++++++++++++-- src/shared/date/conversion-functions.js | 23 ++++++++ src/shared/date/index.js | 4 ++ yarn.lock | 39 ++++++++++--- 7 files changed, 139 insertions(+), 17 deletions(-) create mode 100644 src/shared/date/conversion-functions.js diff --git a/i18n/en.pot b/i18n/en.pot index ec19704ca..e348cad85 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-01-15T14:35:08.966Z\n" -"PO-Revision-Date: 2024-01-15T14:35:08.966Z\n" +"POT-Creation-Date: 2024-10-10T14:28:50.013Z\n" +"PO-Revision-Date: 2024-10-10T14:28:50.014Z\n" msgid "Not authorized" msgstr "Not authorized" @@ -525,6 +525,12 @@ msgstr "No" msgid "Clear" msgstr "Clear" +msgid "Pick a date" +msgstr "Pick a date" + +msgid "Pick a time" +msgstr "Pick a time" + msgid "Delete" msgstr "Delete" diff --git a/package.json b/package.json index 6e9bcfeec..de560a3ce 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "node": ">=14.0.0" }, "resolutions": { - "@dhis2/multi-calendar-dates": "^1.1.0", + "@dhis2/multi-calendar-dates": "^1.3.2", "@dhis2/ui": "^9.2.0", "@dhis2/app-runtime": "^3.10.2" } diff --git a/src/data-workspace/inputs/datetime-input.js b/src/data-workspace/inputs/datetime-input.js index 5f1ecd520..312fa2534 100644 --- a/src/data-workspace/inputs/datetime-input.js +++ b/src/data-workspace/inputs/datetime-input.js @@ -3,7 +3,12 @@ import i18n from '@dhis2/d2-i18n' import { Button, CalendarInput } from '@dhis2/ui' import React, { useState } from 'react' import { useField } from 'react-final-form' -import { useSetDataValueMutation, useUserInfo } from '../../shared/index.js' +import { + convertFromIso8601ToString, + convertToIso8601ToString, + useSetDataValueMutation, + useUserInfo, +} from '../../shared/index.js' import styles from './inputs.module.css' import { InputPropTypes } from './utils.js' @@ -94,10 +99,19 @@ export const DateTimeInput = ({ onKeyDown={onKeyDown} disabled={disabled} readOnly={locked} - date={date} + date={ + date === '' + ? '' + : convertFromIso8601ToString(date, calendar) + } calendar={calendar} onDateSelect={(date) => { - const selectedDate = date?.calendarDateString ?? '' + const selectedDate = date?.calendarDateString + ? convertToIso8601ToString( + date?.calendarDateString, + calendar + ) + : '' setDate(selectedDate) handleChange({ date: selectedDate, time }) }} diff --git a/src/data-workspace/inputs/datetime-input.test.js b/src/data-workspace/inputs/datetime-input.test.js index 6b21abc47..0ca7b3b19 100644 --- a/src/data-workspace/inputs/datetime-input.test.js +++ b/src/data-workspace/inputs/datetime-input.test.js @@ -232,7 +232,7 @@ describe('date input field', () => { expect(mutate.mock.calls[0][0]).toHaveProperty('value', '') }) - it('works with ethiopian calendar', async () => { + it('posts ISO date to backend with ethiopian calendar', async () => { // this is 2016-02-30 Ethopian jest.setSystemTime(new Date('2023-11-10T09:05:00.000Z')) @@ -281,13 +281,38 @@ describe('date input field', () => { fireEvent.change(timepicker, { target: { value: '12:34' } }) expect(mutate.mock.calls).toHaveLength(1) + + // date is converted back to ISO equivalent before being sent to backend expect(mutate.mock.calls[0][0]).toHaveProperty( 'value', - '2016-02-30T12:34' + '2023-11-10T12:34' ) }) - it('works with nepali calendar', async () => { + it('populates the ethiopian equivalent of the persisted ISO date', async () => { + jest.setSystemTime(new Date('2024-07-25T09:05:00.000Z')) + + useConfig.mockReturnValue({ + systemInfo: { calendar: 'ethiopian' }, + }) + + // 2021-04-22 ISO = 2013-08-14 ethiopian + const { getByRole, getByTestId } = render( + + + + ) + + const calendarInput = getByRole('textbox') + expect(calendarInput.value).toBe('2013-08-14') + + const timepicker = getByTestId('time-input') + expect(timepicker.value).toBe('13:17') + }) + + it('posts ISO date to backend with nepali calendar', async () => { // this is 2080-02-32 Nepali jest.setSystemTime(new Date('2023-06-15T09:05:00.000Z')) @@ -330,9 +355,34 @@ describe('date input field', () => { fireEvent.change(timepicker, { target: { value: '12:34' } }) expect(mutate.mock.calls).toHaveLength(1) + + // date is converted back to ISO equivalent before being sent to backend expect(mutate.mock.calls[0][0]).toHaveProperty( 'value', - '2080-02-32T12:34' + '2023-06-15T12:34' ) }) + + it('populates the nepali equivalent of the persisted ISO date', async () => { + jest.setSystemTime(new Date('2024-07-25T09:05:00.000Z')) + + useConfig.mockReturnValue({ + systemInfo: { calendar: 'nepali' }, + }) + + // 2021-04-22 ISO = 2078-01-09 ethiopian + const { getByRole, getByTestId } = render( + + + + ) + + const calendarInput = getByRole('textbox') + expect(calendarInput.value).toBe('2078-01-09') + + const timepicker = getByTestId('time-input') + expect(timepicker.value).toBe('13:17') + }) }) diff --git a/src/shared/date/conversion-functions.js b/src/shared/date/conversion-functions.js new file mode 100644 index 000000000..3c68c569d --- /dev/null +++ b/src/shared/date/conversion-functions.js @@ -0,0 +1,23 @@ +import { + convertFromIso8601, + convertToIso8601, +} from '@dhis2/multi-calendar-dates' + +const padWithZeros = (val, count) => String(val).padStart(count, '0') + +export const convertFromIso8601ToString = (date, calendar) => { + const { year, eraYear, month, day } = convertFromIso8601(date, calendar) + const ISOyear = calendar === 'ethiopian' ? eraYear : year + return `${padWithZeros(ISOyear, 4)}-${padWithZeros( + month, + 2 + )}-${padWithZeros(day, 2)}` +} + +export const convertToIso8601ToString = (date, calendar) => { + const { year, month, day } = convertToIso8601(date, calendar) + return `${padWithZeros(year, 4)}-${padWithZeros(month, 2)}-${padWithZeros( + day, + 2 + )}` +} diff --git a/src/shared/date/index.js b/src/shared/date/index.js index 9b099e8d7..ff074d508 100644 --- a/src/shared/date/index.js +++ b/src/shared/date/index.js @@ -2,3 +2,7 @@ export { default as formatJsDateToDateString } from './format-js-date-to-date-st export { default as useClientServerDate } from './use-client-server-date.js' export { default as useClientServerDateUtils } from './use-client-server-date-utils.js' export { default as useServerTimeOffset } from './use-server-time-offset.js' +export { + convertFromIso8601ToString, + convertToIso8601ToString, +} from './conversion-functions.js' diff --git a/yarn.lock b/yarn.lock index 9f71bafe6..f5c24ed69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2617,12 +2617,22 @@ i18next "^10.3" moment "^2.24.0" +"@dhis2/d2-i18n@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@dhis2/d2-i18n/-/d2-i18n-1.1.3.tgz#ad73030f7cfceeed1b5bcaad86a9b336130bdfb1" + integrity sha512-vOu6RDNumOJM396mHt35bETk9ai9b6XJyAwlUy1HstUZNvfET61F8rjCmMuXZU6zJ8ELux8kMFqlH8IG0vDJmA== + dependencies: + "@types/i18next" "^11.9.0" + i18next "^10.3" + moment "^2.24.0" + "@dhis2/multi-calendar-dates@1.0.2", "@dhis2/multi-calendar-dates@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@dhis2/multi-calendar-dates/-/multi-calendar-dates-1.1.0.tgz#897d27eefa70eb60d3e8e72348f09ee739755ebb" - integrity sha512-VAuq3dIcI42mt1pCL3Xp9pebAOpUEz2ngShTIPD1ll7oMeanRAXkweBT1gburc5W3SfD6jbS8GuwemJ4zg4d3Q== + version "1.3.2" + resolved "https://registry.yarnpkg.com/@dhis2/multi-calendar-dates/-/multi-calendar-dates-1.3.2.tgz#34e5896f7fdfb761a2ee6035d848cba00446cde5" + integrity sha512-H37EptumkqZeHUpbR4wl3y2NZjeipxUNUI2VaHX28z2fbVD7O9H+k1InSskeP5nNWzTLMdPAM4lt/zQP8oRbrg== dependencies: - "@js-temporal/polyfill" "^0.4.2" + "@dhis2/d2-i18n" "^1.1.3" + "@js-temporal/polyfill" "0.4.3" classnames "^2.3.2" "@dhis2/prop-types@^3.0.0-beta.1", "@dhis2/prop-types@^3.1.2": @@ -3257,7 +3267,7 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@js-temporal/polyfill@^0.4.2": +"@js-temporal/polyfill@0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@js-temporal/polyfill/-/polyfill-0.4.3.tgz#e8f8cf86745eb5050679c46a5ebedb9a9cc1f09b" integrity sha512-6Fmjo/HlkyVCmJzAPnvtEWlcbQUSRhi8qlN9EtJA/wP7FqXsevLLrlojR44kzNzrRkpf7eDJ+z7b4xQD/Ycypw== @@ -3850,6 +3860,11 @@ dependencies: "@types/node" "*" +"@types/i18next@^11.9.0": + version "11.9.3" + resolved "https://registry.yarnpkg.com/@types/i18next/-/i18next-11.9.3.tgz#04d84c6539908ad69665d26d8967f942d1638550" + integrity sha512-snM7bMKy6gt7UYdpjsxycqSCAy0fr2JVPY0B8tJ2vp9bN58cE7C880k20PWFM4KXxQ3KsstKM8DLCawGCIH0tg== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -5632,11 +5647,16 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@2.3.2, classnames@^2.2.6, classnames@^2.3.1, classnames@^2.3.2: +classnames@2.3.2, classnames@^2.2.6, classnames@^2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== +classnames@^2.3.2: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + clean-css@^5.2.2: version "5.3.2" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.2.tgz#70ecc7d4d4114921f5d298349ff86a31a9975224" @@ -14983,11 +15003,16 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1: +tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== +tslib@^2.3.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"