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 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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}
/>
<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