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
Show file tree
Hide file tree
Changes from 16 commits
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
4 changes: 2 additions & 2 deletions src/components/AppLoader/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ async function initializeMetaDataAsync(dbLocale: string, onQueryApi: Function, m

async function initializeSystemSettingsAsync(
uiLocale: string,
systemSettings: { dateFormat: string, serverTimeZoneId: string },
systemSettings: { dateFormat: string, serverTimeZoneId: string, calendar: string, },
) {
const systemSettingsCacheData = await cacheSystemSettings(uiLocale, systemSettings);
await buildSystemSettingsAsync(systemSettingsCacheData);
Expand All @@ -158,7 +158,7 @@ export async function initializeAsync(
const systemSettings = await onQueryApi({
resource: 'system/info',
params: {
fields: 'dateFormat,serverTimeZoneId',
fields: 'dateFormat,serverTimeZoneId,calendar',
},
});

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

Expand All @@ -15,7 +16,7 @@ export const getDateFieldConfig = (metaData: DateDataElement, options: Object, q
maxWidth: options.formHorizontal ? 150 : 350,
calendarWidth: options.formHorizontal ? 250 : 350,
popupAnchorPosition: getCalendarAnchorPosition(options.formHorizontal),
calendarMaxMoment: !metaData.allowFutureDate ? moment() : undefined,
calendarMax: !metaData.allowFutureDate ? convertDateObjectToDateFormatString(moment()) : undefined,
}, options, metaData);

return createFieldConfig({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import moment from 'moment';
import { createFieldConfig, createProps } from '../base/configBaseCustomForm';
import { DateFieldForCustomForm } from '../../Components';
import { convertDateObjectToDateFormatString } from '../../../../../../capture-core/utils/converters/date';
import type { DateDataElement } from '../../../../../metaData';
import type { QuerySingleResource } from '../../../../../utils/api/api.types';

Expand All @@ -10,7 +11,7 @@ export const getDateFieldConfigForCustomForm = (metaData: DateDataElement, optio
width: 350,
maxWidth: 350,
calendarWidth: 350,
calendarMaxMoment: !metaData.allowFutureDate ? moment() : undefined,
calendarMax: !metaData.allowFutureDate ? convertDateObjectToDateFormatString(moment()) : undefined,
}, metaData);

return createFieldConfig({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
withAOCFieldBuilder,
withDataEntryFields,
} from '../../DataEntryDhis2Helpers';
import { convertDateObjectToDateFormatString } from '../../../../capture-core/utils/converters/date';

const overrideMessagePropNames = {
errorMessage: 'validationError',
Expand Down Expand Up @@ -111,7 +112,7 @@ const getEnrollmentDateSettings = () => {
required: true,
calendarWidth: props.formHorizontal ? 250 : 350,
popupAnchorPosition: getCalendarAnchorPosition(props.formHorizontal),
calendarMaxMoment: !props.enrollmentMetadata.allowFutureEnrollmentDate ? moment() : undefined,
calendarMax: !props.enrollmentMetadata.allowFutureEnrollmentDate ? convertDateObjectToDateFormatString(moment()) : undefined,
}),
getPropName: () => 'enrolledAt',
getValidatorContainers: getEnrollmentDateValidatorContainer,
Expand Down Expand Up @@ -159,7 +160,9 @@ const getIncidentDateSettings = () => {
required: true,
calendarWidth: props.formHorizontal ? 250 : 350,
popupAnchorPosition: getCalendarAnchorPosition(props.formHorizontal),
calendarMaxMoment: !props.enrollmentMetadata.allowFutureIncidentDate ? moment() : undefined,
calendarMax: !props.enrollmentMetadata.allowFutureIncidentDate ?
convertDateObjectToDateFormatString(moment()) :
undefined,
}),
getPropName: () => 'occurredAt',
getPassOnFieldData: () => true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { Component } from 'react';
import classNames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import i18n from '@dhis2/d2-i18n';
import { Temporal } from '@js-temporal/polyfill';
import { isValidZeroOrPositiveInteger } from 'capture-core-utils/validators/form';
import { SelectBoxes, orientations } from '../../FormFields/Options/SelectBoxes';
import { OptionSet } from '../../../metaData/OptionSet/OptionSet';
Expand All @@ -16,7 +17,7 @@ import './calendarFilterStyles.css';
import { mainOptionKeys, mainOptionTranslatedTexts } from './options';
import { getDateFilterData } from './dateFilterDataGetter';
import { RangeFilter } from './RangeFilter.component';
import { parseDate } from '../../../utils/converters/date';
import { convertStringToDateFormat } from '../../../utils/converters/date';

const getStyles = (theme: Theme) => ({
fromToContainer: {
Expand Down Expand Up @@ -117,24 +118,27 @@ const getRelativeRangeErrors = (startValue, endValue, submitAttempted) => {
return errors;
};

// eslint-disable-next-line complexity
const isAbsoluteRangeFilterValid = (from, to) => {
if (!from?.value && !to?.value) {
return false;
}
const fromValue = from?.value;
const toValue = to?.value;
const parseResultFrom = fromValue ? parseDate(fromValue) : { isValid: true, moment: null };
const parseResultTo = toValue ? parseDate(toValue) : { isValid: true, moment: null };

if (!(parseResultFrom.isValid && parseResultTo.isValid)) {
if (!fromValue && !toValue) {
return false;
}

const isFromValueValid = from ? from.isValid : true;
const isToValueValid = to ? to.isValid : true;

if (!isFromValueValid || !isToValueValid) {
return false;
}
const isValidMomentDate = () =>
parseResultFrom.momentDate &&
parseResultTo.momentDate &&
parseResultFrom.momentDate.isAfter(parseResultTo.momentDate);

return !isValidMomentDate();
if ((!fromValue && toValue) || (fromValue && !toValue)) {
return true;
}

return !DateFilter.isFromAfterTo(fromValue, toValue);
};

const isRelativeRangeFilterValid = (startValue, endValue) => {
Expand Down Expand Up @@ -186,11 +190,9 @@ class DateFilterPlain extends Component<Props, State> implements UpdatableFilter
}

static isFromAfterTo(valueFrom: string, valueTo: string) {
const momentFrom = parseDate(valueFrom).momentDate;
const momentTo = parseDate(valueTo).momentDate;
// $FlowFixMe[incompatible-use] automated comment
// $FlowFixMe[incompatible-call] automated comment
return momentFrom.isAfter(momentTo);
const formattedFrom = convertStringToDateFormat(valueFrom, 'YYYY-MM-DD');
const fromattedTo = convertStringToDateFormat(valueTo, 'YYYY-MM-DD');
return Temporal.PlainDate.compare(formattedFrom, fromattedTo) > 0;
}

toD2DateTextFieldInstance: any;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// @flow
import * as React from 'react';
import moment from 'moment';
import log from 'loglevel';
import { convertMomentToDateFormatString } from '../../../utils/converters/date';
import { convertIsoToLocalCalendar } from '../../../utils/converters/date';
import { DateFilter } from './DateFilter.component';
import { mainOptionKeys } from './options';
import { dateFilterTypes } from './constants';
Expand All @@ -22,8 +21,8 @@ type State = {

export class DateFilterManager extends React.Component<Props, State> {
static convertDateForEdit(rawValue: string) {
const momentInstance = moment(rawValue);
return convertMomentToDateFormatString(momentInstance);
const localDate = convertIsoToLocalCalendar(rawValue);
return localDate;
}
static calculateAbsoluteRangeValueState(filter: DateFilterData) {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { parseNumber } from 'capture-core-utils/parsers';
import { mainOptionKeys } from './options';
import { dateFilterTypes } from './constants';
import { parseDate } from '../../../utils/converters/date';
import { convertLocalToIsoCalendar } from '../../../utils/converters/date';
import { type AbsoluteDateFilterData, type RelativeDateFilterData, type DateValue } from './types';

type Value = {
Expand All @@ -20,13 +20,13 @@ function convertAbsoluteDate(fromValue: ?string, toValue: ?string) {

if (fromValue) {
// $FlowFixMe[incompatible-type] automated comment
const fromClientValue: string = parseDate(fromValue).momentDate;
const fromClientValue: string = convertLocalToIsoCalendar(fromValue);
rangeData.ge = fromClientValue;
}

if (toValue) {
// $FlowFixMe[incompatible-type] automated comment
const toClientValue: string = parseDate(toValue).momentDate;
const toClientValue: string = convertLocalToIsoCalendar(toValue);
rangeData.le = toClientValue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { type DateValue } from '../../../FiltersForTypes/Date/types/date.types';
type Props = {
label?: ?string,
value: ?string,
calendar?: string,
calendarWidth?: ?number,
inputWidth?: ?number,
onBlur: (value: DateValue) => void,
Expand Down Expand Up @@ -50,7 +49,6 @@ export class D2Date extends React.Component<Props, State> {

render() {
const {
calendar,
calendarWidth,
inputWidth,
classes,
Expand All @@ -62,7 +60,7 @@ export class D2Date extends React.Component<Props, State> {
...passOnProps
} = this.props;

const calendarType = calendar || 'gregory';
const calendarType = systemSettingsStore.get().calendar || 'gregory';
const format = systemSettingsStore.get().dateFormat;

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
import * as React from 'react';
import { withStyles, withTheme } from '@material-ui/core/styles';
import { AgeField as UIAgeField } from 'capture-ui';
import moment from 'moment';
import { parseDate, convertMomentToDateFormatString } from '../../../../../utils/converters/date';
import { systemSettingsStore } from '../../../../../metaDataMemoryStores';

const getStyles = (theme: Theme) => ({
Expand Down Expand Up @@ -50,9 +48,6 @@ const AgeFieldPlain = (props: Props) => {
return (
// $FlowFixMe[cannot-spread-inexact] automated comment
<UIAgeField
onParseDate={parseDate}
onGetFormattedDateStringFromMoment={convertMomentToDateFormatString}
moment={moment}
datePlaceholder={systemSettingsStore.get().dateFormat.toLowerCase()}
{...passOnProps}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// @flow
import i18n from '@dhis2/d2-i18n';
import { pipe } from 'capture-core-utils';
import moment from 'moment';
import { convertMomentToDateFormatString } from '../../../../../../utils/converters/date';
import { convertIsoToLocalCalendar } from '../../../../../../utils/converters/date';
import type { DateFilterData, AbsoluteDateFilterData } from '../../../../../FiltersForTypes';
import { areRelativeRangeValuesSupported }
from '../../../../../../utils/validation/validators/areRelativeRangeValuesSupported';
Expand Down Expand Up @@ -30,11 +29,6 @@ const translatedPeriods = {
[periods.RELATIVE_RANGE]: i18n.t('Relative range'),
};

const convertToViewValue = (filterValue: string) => pipe(
value => moment(value),
momentDate => convertMomentToDateFormatString(momentDate),
)(filterValue);

function translateAbsoluteDate(filter: AbsoluteDateFilterData) {
let appliedText = '';
const fromValue = filter.ge;
Expand All @@ -44,18 +38,18 @@ function translateAbsoluteDate(filter: AbsoluteDateFilterData) {
const momentFrom = moment(fromValue);
const momentTo = moment(toValue);
if (momentFrom.isSame(momentTo)) {
appliedText = convertMomentToDateFormatString(momentFrom);
appliedText = convertIsoToLocalCalendar(fromValue);
} else {
const appliedTextFrom = convertMomentToDateFormatString(momentFrom);
const appliedTextTo = convertMomentToDateFormatString(momentTo);
const appliedTextFrom = convertIsoToLocalCalendar(fromValue);
const appliedTextTo = convertIsoToLocalCalendar(toValue);
appliedText = i18n.t('{{fromDate}} to {{toDate}}', { fromDate: appliedTextFrom, toDate: appliedTextTo });
}
} else if (fromValue) {
const appliedTextFrom = convertToViewValue(fromValue);
const appliedTextFrom = convertIsoToLocalCalendar(fromValue);
appliedText = i18n.t('after or equal to {{date}}', { date: appliedTextFrom });
} else {
// $FlowFixMe[incompatible-call] automated comment
const appliedTextTo = convertToViewValue(toValue);
const appliedTextTo = convertIsoToLocalCalendar(toValue);
appliedText = i18n.t('before or equal to {{date}}', { date: appliedTextTo });
}
return appliedText;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// @flow
import React, { useState, useCallback } from 'react';
import moment from 'moment';
import { DateField } from 'capture-core/components/FormFields/New';
import {
Button,
CalendarInput,
IconCalendar16,
IconEdit16,
colors,
Expand All @@ -12,8 +11,11 @@ import {
import i18n from '@dhis2/d2-i18n';
import { withStyles } from '@material-ui/core';
import { convertValue as convertValueClientToView } from '../../../converters/clientToView';
import { convertValue as convertValueFormToClient } from '../../../converters/formToClient';
import { convertValue as convertValueClientToServer } from '../../../converters/clientToServer';
import { dataElementTypes } from '../../../metaData';


type Props = {
date: string,
dateLabel: string,
Expand All @@ -24,7 +26,7 @@ type Props = {
...CssClasses,
}

const styles = {
const styles = (theme: Theme) => ({
editButton: {
display: 'inline-flex',
alignItems: 'center',
Expand Down Expand Up @@ -62,7 +64,11 @@ const styles = {
fontSize: '12px',
color: colors.grey700,
},
};
error: {
...theme.typography.caption,
color: theme.palette.error.main,
},
});

const DateComponentPlain = ({
date,
Expand All @@ -75,21 +81,23 @@ const DateComponentPlain = ({
}: Props) => {
const [editMode, setEditMode] = useState(false);
const [selectedDate, setSelectedDate] = useState();
const dateChangeHandler = useCallback(({ calendarDateString }) => {
setSelectedDate(calendarDateString);
const [validation, setValidation] = useState();

const dateChangeHandler = useCallback((dateString, internalComponentError) => {
setSelectedDate(dateString);
setValidation(internalComponentError);
}, [setSelectedDate]);
const displayDate = String(convertValueClientToView(date, dataElementTypes.DATE));

const onOpenEdit = () => {
// CalendarInput component only supports the YYYY-MM-DD format
setSelectedDate(moment(date).format('YYYY-MM-DD'));
setSelectedDate(String(convertValueClientToView(date, dataElementTypes.DATE)));
setEditMode(true);
};
const saveHandler = () => {
// CalendarInput component only supports the YYYY-MM-DD format
if (selectedDate) {
const newDate = moment.utc(selectedDate, 'YYYY-MM-DD').format('YYYY-MM-DDTHH:mm:ss.SSS');
if (newDate !== date) {
const newClientDate = convertValueFormToClient(selectedDate, dataElementTypes.DATE);
const newDate = convertValueClientToServer(newClientDate, dataElementTypes.DATE);
if (typeof newDate === 'string' && newDate !== date) {
onSave(newDate);
}
}
Expand All @@ -99,21 +107,24 @@ const DateComponentPlain = ({
return editMode ? (
<div data-test="widget-enrollment-date">
<div className={classes.inputField}>
<CalendarInput
calendar="gregory"
dense
className={classes.calendar}
<DateField
width={200}
value={selectedDate}
onBlur={dateChangeHandler}
label={dateLabel}
date={selectedDate}
dense
locale={locale}
onDateSelect={dateChangeHandler}
/>
simonadomnisoru marked this conversation as resolved.
Show resolved Hide resolved
<div className={classes.error}>
{validation && validation.error ? i18n.t('Please provide a valid date') : ''}
</div>
</div>
<div className={classes.buttonStrip}>
<Button
primary
small
onClick={saveHandler}
disabled={!!validation?.error}
>
{i18n.t('Save')}
</Button>
Expand Down
Loading
Loading