Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [DHIS2-18310] enable non-Gregorian calendars in views & lists & forms #3900

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
136430d
feat: enable non gregorian calendars in views and lists
alaa-yahia Dec 4, 2024
b8fd941
fix: working list filters to use gregorian
alaa-yahia Dec 4, 2024
e378551
fix: working list filter runtime error when dd-mm-yyyy format is passed
alaa-yahia Dec 10, 2024
8e44c3c
fix: missing rules engine in dependencies
alaa-yahia Dec 11, 2024
1f2518d
fix: working list filter not displaying local date
alaa-yahia Dec 11, 2024
9a3d609
fix: reduce the complexity of getUpdatedValue
alaa-yahia Dec 11, 2024
3a3d0b2
chore: refactor working lists filter component
alaa-yahia Dec 11, 2024
53ef00e
fix: convert age values to local date
alaa-yahia Dec 11, 2024
1d3faa6
feat: [DHIS2 15466] typing the date when editing enrollment and incid…
alaa-yahia Dec 16, 2024
7968ea7
fix: age values not filled correctly
alaa-yahia Dec 16, 2024
e8d226f
fix: keep local calendar date in state in working list filters
alaa-yahia Dec 22, 2024
8ce68dd
fix: rename calendarMaxMoment to calendarMax
alaa-yahia Dec 22, 2024
b0dc55c
fix: remove formating lines
alaa-yahia Dec 22, 2024
2cc70be
fix: day value not giving correct result in age field
alaa-yahia Dec 23, 2024
383c66f
fix: flow errors
alaa-yahia Dec 23, 2024
013db2c
fix: display local time in tooltips
alaa-yahia Jan 6, 2025
8dd1110
fix: date is not valid error not displayed
alaa-yahia Jan 6, 2025
209860c
fix: remove app specific objects
alaa-yahia Jan 9, 2025
c8d1fbd
refactor: simplify converters logic
alaa-yahia Jan 13, 2025
9805435
fix: logic of dateRange & dateTimeRange validators
alaa-yahia Jan 13, 2025
d67810b
chore: pass format and calendar as props
alaa-yahia Jan 13, 2025
068c3ef
fix: status label in the StagesAndEvents widget display ISO date
alaa-yahia Jan 15, 2025
cda3dab
fix: currentSearchTerms are not converted correctly
alaa-yahia Jan 16, 2025
217dc25
fix: enrollment and incident date display in iso
alaa-yahia Jan 16, 2025
d05011a
fix: runtime error when selecting a date in age field in ethiopian ca…
alaa-yahia Jan 16, 2025
78a2af8
Merge remote-tracking branch 'origin/master' into DHIS2-18310-enable-…
alaa-yahia Jan 16, 2025
fbdfe20
fix: use localDate in chip component
alaa-yahia Jan 21, 2025
4ceacde
fix: revert to iso date calculations when calendar isn't supported
alaa-yahia Jan 23, 2025
79102e2
fix: age calculations
alaa-yahia Jan 24, 2025
3493da4
fix: age and dateTimeRange searchterms
alaa-yahia Jan 28, 2025
79f3368
fix: invalidDate in event notes
alaa-yahia Jan 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: remove app specific objects
alaa-yahia committed Jan 9, 2025
commit 209860c630fce98c981aff7413513b7bda13676d
3 changes: 3 additions & 0 deletions src/core_modules/capture-core-utils/date/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
// @flow
export { getFormattedStringFromMomentUsingEuropeanGlyphs } from './date.utils';
export { padWithZeros } from './padWithZeros';
export { temporalToString } from './temporalToString';
export { stringToTemporal } from './stringToTemporal';
45 changes: 45 additions & 0 deletions src/core_modules/capture-core-utils/date/stringToTemporal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// @flow
import { Temporal } from '@js-temporal/polyfill';

/**
* Converts a date string into a Temporal.PlainDate object using the specified calendar
* @export
* @param {?string} dateString - The date string to convert
* @param {?string} calendarType - The calendar type to use
* @param {?string} dateFormat - The current system date format ('YYYY-MM-DD' or 'DD-MM-YYYY')
* @returns {(Temporal.PlainDate | null)}
*/

type PlainDate = {
year: number,
month: number,
day: number
};

export function stringToTemporal(dateString: ?string,
calendarType: ?string,
dateFormat: ?string): PlainDate | null {
if (!dateString) {
return null;
}
try {
const dateWithHyphen = dateString.replace(/[\/\.]/g, '-');

let year; let month; let day;

if (dateFormat === 'YYYY-MM-DD') {
[year, month, day] = dateWithHyphen.split('-').map(Number);
}
if (dateFormat === 'DD-MM-YYYY') {
[day, month, year] = dateWithHyphen.split('-').map(Number);
}
return Temporal.PlainDate.from({
year,
month,
day,
calendarType,
});
} catch (error) {
return null;
}
}
33 changes: 33 additions & 0 deletions src/core_modules/capture-core-utils/date/temporalToString.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// @flow
import { padWithZeros } from './padWithZeros';

/**
* Converts a Temporal.PlainDate to a formatted date string (YYYY-MM-DD or DD-MM-YYYY)
* @param {Temporal.PlainDate | null} temporalDate - The Temporal date to convert
* @param {?string} dateFormat - The current system date format ('YYYY-MM-DD' or 'DD-MM-YYYY')
* @returns {string} Formatted date string, or empty string if invalid
*/

type PlainDate = {
year: number,
month: number,
day: number
};

export function temporalToString(temporalDate: PlainDate | null, dateFormat: ?string): string {
if (!temporalDate) {
return '';
}

try {
const year = temporalDate.year;
const month = temporalDate.month;
const day = temporalDate.day;

return dateFormat === 'YYYY-MM-DD' ?
`${padWithZeros(year, 4)}-${padWithZeros(month, 2)}-${padWithZeros(day, 2)}` :
`${padWithZeros(day, 2)}-${padWithZeros(month, 2)}-${padWithZeros(year, 4)}`;
} catch (error) {
return '';
}
}
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
import { orientations } from '../../../../FormFields/New';
import { createFieldConfig, createProps } from '../base/configBaseDefaultForm';
import { AgeFieldForForm } from '../../Components';
import { systemSettingsStore } from '../../../../../metaDataMemoryStores';
import { type DataElement } from '../../../../../metaData';
import type { QuerySingleResource } from '../../../../../utils/api/api.types';

@@ -15,6 +16,8 @@ export const getAgeFieldConfig = (metaData: DataElement, options: Object, queryS
shrinkDisabled: options.formHorizontal,
dateCalendarWidth: options.formHorizontal ? 250 : 350,
datePopupAnchorPosition: getCalendarAnchorPosition(options.formHorizontal),
calendarType: systemSettingsStore.get().calendar,
dateFormat: systemSettingsStore.get().dateFormat,
}, options, metaData);

return createFieldConfig({
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import moment from 'moment';
import { createFieldConfig, createProps } from '../base/configBaseDefaultForm';
import { DateFieldForForm } from '../../Components';
import { convertDateObjectToDateFormatString } from '../../../../../../capture-core/utils/converters/date';
import { systemSettingsStore } from '../../../../../metaDataMemoryStores';
import type { DateDataElement } from '../../../../../metaData';
import type { QuerySingleResource } from '../../../../../utils/api/api.types';

@@ -17,6 +18,8 @@ export const getDateFieldConfig = (metaData: DateDataElement, options: Object, q
calendarWidth: options.formHorizontal ? 250 : 350,
popupAnchorPosition: getCalendarAnchorPosition(options.formHorizontal),
calendarMax: !metaData.allowFutureDate ? convertDateObjectToDateFormatString(moment()) : undefined,
calendarType: systemSettingsStore.get().calendar,
dateFormat: systemSettingsStore.get().dateFormat,
}, options, metaData);

return createFieldConfig({
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import {
convertFromIso8601,
} from '@dhis2/multi-calendar-dates';
import { systemSettingsStore } from '../../../../capture-core/metaDataMemoryStores';
import { padWithZeros } from './padWithZeros';
import { padWithZeros } from '../../../../capture-core-utils/date';

/**
* Converts a date from ISO calendar to local calendar
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import {
convertToIso8601,
} from '@dhis2/multi-calendar-dates';
import { systemSettingsStore } from '../../../../capture-core/metaDataMemoryStores';
import { padWithZeros } from './padWithZeros';
import { padWithZeros } from '../../../../capture-core-utils/date';

/**
* Converts a date from local calendar to ISO calendar
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow
import { Temporal } from '@js-temporal/polyfill';
import { systemSettingsStore } from '../../../metaDataMemoryStores';
import { stringToTemporal } from '../../../../capture-core-utils/date';

/**
* Converts a date string into a Temporal.PlainDate object using the system set calendar
@@ -19,27 +19,7 @@ export function convertStringToTemporal(dateString: ?string): PlainDate | null {
if (!dateString) {
return null;
}
try {
const dateWithHyphen = dateString.replace(/[\/\.]/g, '-');

const calendar = systemSettingsStore.get().calendar;
const dateFormat = systemSettingsStore.get().dateFormat;

let year; let month; let day;

if (dateFormat === 'YYYY-MM-DD') {
[year, month, day] = dateWithHyphen.split('-').map(Number);
}
if (dateFormat === 'DD-MM-YYYY') {
[day, month, year] = dateWithHyphen.split('-').map(Number);
}
return Temporal.PlainDate.from({
year,
month,
day,
calendar,
});
} catch (error) {
return null;
}
const calendar = systemSettingsStore.get().calendar;
const dateFormat = systemSettingsStore.get().dateFormat;
return stringToTemporal(dateString, calendar, dateFormat);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow
import { padWithZeros } from './padWithZeros';
import { systemSettingsStore } from '../../../../capture-core/metaDataMemoryStores';
import { temporalToString } from '../../../../capture-core-utils/date';

/**
* Converts a Temporal.PlainDate to a formatted date string (YYYY-MM-DD || DD-MM-YYYY)
@@ -19,16 +19,5 @@ export function convertTemporalToString(temporalDate: PlainDate | null): string
return '';
}
const dateFormat = systemSettingsStore.get().dateFormat;

try {
const year = temporalDate.year;
const month = temporalDate.month;
const day = temporalDate.day;

return dateFormat === 'YYYY-MM-DD' ?
`${padWithZeros(year, 4)}-${padWithZeros(month, 2)}-${padWithZeros(day, 2)}` :
`${padWithZeros(day, 2)}-${padWithZeros(month, 2)}-${padWithZeros(year, 4)}`;
} catch (error) {
return '';
}
return temporalToString(temporalDate, dateFormat);
}
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@ export { parseDate } from './parser';
export { convertDateObjectToDateFormatString } from './dateObjectToDateFormatString';
export { convertMomentToDateFormatString } from './momentToDateFormatString';
export { convertStringToDateFormat } from './stringToMomentDateFormat';
export { padWithZeros } from './padWithZeros';
export { convertIsoToLocalCalendar } from './convertIsoToLocalCalendar';
export { convertLocalToIsoCalendar } from './convertLocalToIsoCalendar';
export { convertStringToTemporal } from './convertStringToTemporal';
55 changes: 28 additions & 27 deletions src/core_modules/capture-ui/AgeField/AgeField.component.js
alaa-yahia marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -2,18 +2,16 @@
import React, { Component } from 'react';
import { Temporal } from '@js-temporal/polyfill';
import { isValidPositiveInteger } from 'capture-core-utils/validators/form';
import { systemSettingsStore } from 'capture-core/metaDataMemoryStores';
import i18n from '@dhis2/d2-i18n';
import classNames from 'classnames';
import { IconButton } from 'capture-ui';
import { IconCross24 } from '@dhis2/ui';
import { parseDate } from 'capture-core/utils/converters/date';
import { AgeNumberInput } from '../internal/AgeInput/AgeNumberInput.component';
import { AgeDateInput } from '../internal/AgeInput/AgeDateInput.component';
import defaultClasses from './ageField.module.css';
import { orientations } from '../constants/orientations.const';
import { withInternalChangeHandler } from '../HOC/withInternalChangeHandler';
import { convertStringToTemporal, convertTemporalToString } from '../../capture-core/utils/converters/date';
import { stringToTemporal, temporalToString } from '../../capture-core-utils/date';

type AgeValues = {
date?: ?string,
@@ -53,31 +51,21 @@ type Props = {
dateCalendarOnConvertValueOut: (value: string) => string,
datePlaceholder?: ?string,
disabled?: ?boolean,
dateFormat: ?string,
calendarType: ?string,
};

function getCalculatedValues(dateValue: ?string): AgeValues {
const parseData = dateValue && parseDate(dateValue);
if (!parseData || !parseData.isValid) {
return {
date: dateValue,
years: '',
months: '',
days: '',
};
}

const calendar = systemSettingsStore.get().calendar;

const now = Temporal.Now.plainDateISO().withCalendar(calendar);
function getCalculatedValues(dateValue: ?string, calendarType: ?string, dateFormat: ?string): AgeValues {
const now = Temporal.Now.plainDateISO().withCalendar(calendarType);
simonadomnisoru marked this conversation as resolved.
Show resolved Hide resolved

const age = convertStringToTemporal(dateValue);
const age = stringToTemporal(dateValue, calendarType, dateFormat);

const diff = now.since(age, {
largestUnit: 'years',
smallestUnit: 'days',
});

const date = convertTemporalToString(age);
const date = temporalToString(age, dateFormat);

return {
date,
@@ -117,7 +105,7 @@ class D2AgeFieldPlain extends Component<Props> {
}

handleNumberBlur = (values: AgeValues) => {
const { onRemoveFocus } = this.props;
const { onRemoveFocus, calendarType = 'gregory', dateFormat = 'YYYY-MM-DD' } = this.props;

onRemoveFocus && onRemoveFocus();
if (D2AgeFieldPlain.isEmptyNumbers(values)) {
@@ -130,24 +118,37 @@ class D2AgeFieldPlain extends Component<Props> {
return;
}

const calendar = systemSettingsStore.get().calendar;

const now = Temporal.Now.plainDateISO().withCalendar(calendar);
const now = Temporal.Now.plainDateISO().withCalendar(calendarType);

const calculatedDate = now.subtract({
years: D2AgeFieldPlain.getNumberOrZero(values.years),
months: D2AgeFieldPlain.getNumberOrZero(values.months),
days: D2AgeFieldPlain.getNumberOrZero(values.days),
});

const calculatedValues = getCalculatedValues(convertTemporalToString(calculatedDate));
const dateString = temporalToString(calculatedDate, dateFormat);
const calculatedValues = getCalculatedValues(dateString, calendarType, dateFormat);
this.props.onBlur(calculatedValues);
}

handleDateBlur = (date: ?string, options: ?ValidationOptions) => {
const { onRemoveFocus } = this.props;
const { onRemoveFocus, calendarType = 'gregory', dateFormat = 'YYYY-MM-DD' } = this.props;
onRemoveFocus && onRemoveFocus();
const calculatedValues = date ? getCalculatedValues(date) : null;
const isDateValid = options && !options.error;
if (!date) {
this.props.onBlur(null, options);
return;
}
if (!isDateValid) {
const calculatedValues = {
date,
years: '',
months: '',
days: '',
};
this.props.onBlur(calculatedValues, options);
return;
}
const calculatedValues = getCalculatedValues(date, calendarType, dateFormat);
this.props.onBlur(calculatedValues, options);
}

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// @flow
import React from 'react';
import { CalendarInput } from '@dhis2/ui';
import { systemSettingsStore } from '../../../capture-core/metaDataMemoryStores';

type ValidationOptions = {
error?: ?string,
@@ -21,7 +20,9 @@ type Props = {
placeholder?: string,
label?: string,
calendarMax?: any,
innerMessage?: any
innerMessage?: any,
dateFormat: ?string,
calendarType: ?string,
};

type Validation = {|
@@ -64,11 +65,13 @@ export class DateField extends React.Component<Props, State> {
calendarMax,
value,
innerMessage,
calendarType,
dateFormat,
} = this.props;
const calculatedInputWidth = inputWidth || width;
const calculatedCalendarWidth = calendarWidth || width;
const calendarType = systemSettingsStore.get().calendar || 'gregory';
const format = systemSettingsStore.get().dateFormat;
const calendar = calendarType || 'gregory';
const format = dateFormat || 'YYYY-MM-DD';
const errorProps = innerMessage && innerMessage.messageType === 'error'
? { error: !!innerMessage.message?.dateInnerErrorMessage,
validationText: innerMessage.message?.dateInnerErrorMessage }
@@ -86,7 +89,7 @@ export class DateField extends React.Component<Props, State> {
placeholder={this.props.placeholder}
format={format}
onDateSelect={this.handleDateSelected}
calendar={calendarType}
calendar={calendar}
date={value}
width={String(calculatedCalendarWidth)}
inputWidth={String(calculatedInputWidth)}