Skip to content

Commit

Permalink
feat: [DHIS2-15463] Use dhis2 ui calendarInput component in working l…
Browse files Browse the repository at this point in the history
…ist (#3712)

* feat: add calendarInput

* fix: calendarInput zindex

* fix: display date in wrong format when passing dd-mm-yyyy

* fix: remove editable prop

* fix: label type definition

* fix: add label

* fix: allow empty strings to run onBlur

* chore: remove unnecessary props

* fix: replace onKeyPress with onKeyDown

* feat: display errors

* fix: flow errors

* chore: update calendarInput ui version
  • Loading branch information
alaa-yahia authored Nov 25, 2024
1 parent 66642b8 commit 5f27455
Show file tree
Hide file tree
Showing 19 changed files with 2,410 additions and 2,469 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,9 @@ Then('the list should display data ordered descendingly by report date', () => {
.click();

cy.get('input[placeholder="From"]')
.type(`${lastYear}-01-01`);
.type(`${lastYear}-01-01`).blur();

cy.get('input[placeholder="To"]').click();
cy.get('input[placeholder="To"]').click().blur();

cy.contains('Update')
.click({ force: true });
Expand Down Expand Up @@ -399,10 +399,10 @@ When('you set the date of admission filter', () => {
cy.get('input[type="text"]')
.then(($elements) => {
cy.wrap($elements[0])
.type('2018-01-01');
.type('2018-01-01').blur();

cy.wrap($elements[1])
.type('2018-12-31');
.type('2018-12-31').blur();
});

cy.contains('Update')
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@dhis2/d2-ui-rich-text": "^7.4.0",
"@dhis2/d2-ui-sharing-dialog": "^7.3.3",
"@dhis2/ui": "^9.10.1",
"@dhis2-ui/calendar": "10.0.3",
"@joakim_sm/react-infinite-calendar": "^2.4.2",
"@material-ui/core": "3.9.4",
"@material-ui/icons": "3",
Expand Down Expand Up @@ -136,6 +137,7 @@
"@dhis2/app-runtime": "^3.10.2",
"@babel/preset-react": "7.16.7",
"@dhis2/ui": "^9.10.1",
"@dhis2-ui/calendar": "10.0.3",
"@js-temporal/polyfill": "0.4.3",
"core-js": "2.5.7",
"i18next": "^20.5.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,16 @@ import { isValidZeroOrPositiveInteger } from 'capture-core-utils/validators/form
import { SelectBoxes, orientations } from '../../FormFields/Options/SelectBoxes';
import { OptionSet } from '../../../metaData/OptionSet/OptionSet';
import { Option } from '../../../metaData/OptionSet/Option';

import type { UpdatableFilterContent } from '../types';
import { type DateValue } from './types';
import { FromDateFilter } from './From.component';
import { ToDateFilter } from './To.component';
import { isValidDate } from '../../../utils/validators/form';
import { parseDate } from '../../../utils/converters/date';
import { dataElementTypes } from '../../../metaData';
import type { UpdatableFilterContent } from '../types';
import './calendarFilterStyles.css';
import { mainOptionKeys, mainOptionTranslatedTexts } from './options';
import { getDateFilterData } from './dateFilterDataGetter';
import { RangeFilter } from './RangeFilter.component';
import { parseDate } from '../../../utils/converters/date';

const getStyles = (theme: Theme) => ({
fromToContainer: {
Expand All @@ -29,7 +28,7 @@ const getStyles = (theme: Theme) => ({
width: 30,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
alignItems: 'start',
paddingTop: theme.typography.pxToRem(6),
fontSize: theme.typography.body1.fontSize,
},
Expand All @@ -43,18 +42,16 @@ const getStyles = (theme: Theme) => ({
});

export type Value = ?{
from?: ?string,
to?: ?string,
from?: ?DateValue,
to?: ?DateValue,
main?: ?string,
start?: ?string,
end?: ?string,
};

type Props = {
onCommitValue: (value: ?{ from?: ?string, to?: ?string }) => void,
onUpdate: (commitValue?: any) => void,
value: Value,
type: $Keys<typeof dataElementTypes>,
classes: {
fromToContainer: string,
inputContainer: string,
Expand All @@ -69,33 +66,33 @@ type State = {
submitAttempted: boolean,
};

const getAbsoluteRangeErrors = (fromValue, toValue, type, submitAttempted) => {
let errors = {
minValueError: null,
maxValueError: null,
dateLogicError: null,
};
// eslint-disable-next-line complexity
const getAbsoluteRangeErrors = (fromValue, toValue, submitAttempted) => {
const fromValueString = fromValue?.value;
const toValueString = toValue?.value;
const isFromValueValid = fromValue?.isValid;
const isToValueValid = toValue?.isValid;

if (!fromValue && !toValue) {
errors = {
...errors,
dateLogicError: submitAttempted ? i18n.t(DateFilter.errorMessages.ABSOLUTE_RANGE_WITHOUT_VALUES) : null,
};
} else {
const { isValid: isMinValueValid, error: minValueError } = DateFilter.validateField(fromValue, type);
const { isValid: isMaxValueValid, error: maxValueError } = DateFilter.validateField(toValue, type);
const hasDateLogicError = () =>
isMinValueValid && isMaxValueValid && fromValue && toValue && DateFilter.isFromAfterTo(fromValue, toValue);

errors = {
...errors,
minValueError,
maxValueError,
dateLogicError: hasDateLogicError() ? i18n.t(DateFilter.errorMessages.FROM_GREATER_THAN_TO) : null,
if (!fromValueString && !toValueString) {
return {
dateLogicError: submitAttempted
? i18n.t(DateFilter.errorMessages.ABSOLUTE_RANGE_WITHOUT_VALUES)
: null,
};
}

return errors;
const hasDateLogicError =
fromValueString &&
toValueString &&
isFromValueValid &&
isToValueValid &&
DateFilter.isFromAfterTo(fromValueString, toValueString);

return {
dateLogicError: hasDateLogicError
? i18n.t(DateFilter.errorMessages.FROM_GREATER_THAN_TO)
: null,
};
};

const getRelativeRangeErrors = (startValue, endValue, submitAttempted) => {
Expand All @@ -110,8 +107,8 @@ const getRelativeRangeErrors = (startValue, endValue, submitAttempted) => {
bufferLogicError: submitAttempted ? i18n.t(DateFilter.errorMessages.RELATIVE_RANGE_WITHOUT_VALUES) : null,
};
}
const { error: startValueError } = DateFilter.validateField(startValue, dataElementTypes.INTEGER_ZERO_OR_POSITIVE);
const { error: endValueError } = DateFilter.validateField(endValue, dataElementTypes.INTEGER_ZERO_OR_POSITIVE);
const { error: startValueError } = DateFilter.validateRelativeRangeValue(startValue);
const { error: endValueError } = DateFilter.validateRelativeRangeValue(endValue);
errors = {
...errors,
startValueError,
Expand All @@ -125,27 +122,20 @@ const isAbsoluteRangeFilterValid = (fromValue, toValue) => {
return false;
}

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 && !fromValue.isValid) || (toValue && !toValue.isValid)) {
return false;
}
const isValidMomentDate = () =>
parseResultFrom.momentDate &&
parseResultTo.momentDate &&
parseResultFrom.momentDate.isAfter(parseResultTo.momentDate);

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

const isRelativeRangeFilterValid = (startValue, endValue) => {
if (!startValue && !endValue) {
return false;
}
if (
!DateFilter.validateField(startValue, dataElementTypes.INTEGER_ZERO_OR_POSITIVE).isValid ||
!DateFilter.validateField(endValue, dataElementTypes.INTEGER_ZERO_OR_POSITIVE).isValid
!DateFilter.validateRelativeRangeValue(startValue).isValid ||
!DateFilter.validateRelativeRangeValue(endValue).isValid
) {
return false;
}
Expand All @@ -154,29 +144,26 @@ const isRelativeRangeFilterValid = (startValue, endValue) => {

// $FlowFixMe[incompatible-variance] automated comment
class DateFilterPlain extends Component<Props, State> implements UpdatableFilterContent<Value> {
static validateField(value: ?string, type: $Keys<typeof dataElementTypes>) {
static validateRelativeRangeValue(value: ?string) {
if (!value) {
return {
isValid: true,
error: null,
};
}

// $FlowFixMe dataElementTypes flow error
const typeValidator = DateFilter.validatorForTypes[type];
const isValid = typeValidator(value);
const isValid = isValidZeroOrPositiveInteger(value);

return {
isValid,
// $FlowFixMe dataElementTypes flow error
error: isValid ? null : i18n.t(DateFilter.errorMessages[type]),
error: isValid ? null : i18n.t(DateFilter.errorMessages[dataElementTypes.INTEGER_ZERO_OR_POSITIVE]),
};
}

static isFilterValid(
mainValue?: ?string,
fromValue?: ?string,
toValue?: ?string,
fromValue?: ?DateValue,
toValue?: ?DateValue,
startValue?: ?string,
endValue?: ?string,
) {
Expand All @@ -199,24 +186,19 @@ class DateFilterPlain extends Component<Props, State> implements UpdatableFilter
}

toD2DateTextFieldInstance: any;

constructor(props: Props) {
super(props);
this.state = { submitAttempted: false };
}

static errorMessages = {
ABSOLUTE_RANGE_WITHOUT_VALUES: 'Please specify a range',
RELATIVE_RANGE_WITHOUT_VALUES: 'Please specify the number of days',
FROM_GREATER_THAN_TO: "The From date can't be after the To date",
MIN_GREATER_THAN_MAX: 'Days in the past cannot be greater than days in the future',
[dataElementTypes.DATE]: 'Please provide a valid date',
[dataElementTypes.INTEGER_ZERO_OR_POSITIVE]: 'Please provide zero or a positive integer',
};

static validatorForTypes = {
[dataElementTypes.DATE]: isValidDate,
[dataElementTypes.INTEGER_ZERO_OR_POSITIVE]: isValidZeroOrPositiveInteger,
};

static mainOptionSet = new OptionSet('mainOptions', [
new Option((_this) => {
_this.text = mainOptionTranslatedTexts[mainOptionKeys.TODAY];
Expand Down Expand Up @@ -251,12 +233,14 @@ class DateFilterPlain extends Component<Props, State> implements UpdatableFilter
_this.value = mainOptionKeys.ABSOLUTE_RANGE;
}),
]);

static optionSet = new OptionSet('mainOptions', [
new Option((_this) => {
_this.text = mainOptionTranslatedTexts[mainOptionKeys.RELATIVE_RANGE];
_this.value = mainOptionKeys.RELATIVE_RANGE;
}),
]);

onGetUpdateData(updatedValues?: Value) {
const value = typeof updatedValues !== 'undefined' ? updatedValues : this.props.value;

Expand Down Expand Up @@ -302,23 +286,11 @@ class DateFilterPlain extends Component<Props, State> implements UpdatableFilter
}

handleEnterKeyInFrom = () => {
this.toD2DateTextFieldInstance.focus();
this.toD2DateTextFieldInstance && this.toD2DateTextFieldInstance.focus();
};

handleDateSelectedFromCalendarInFrom = () => {
this.toD2DateTextFieldInstance.focus();
};

handleEnterKeyInTo = (value: { [key: string]: string }) => {
// validate with updated values
const values = this.getUpdatedValue(value);
this.setState({ submitAttempted: true });

if (values && !DateFilter.isFilterValid(values.main, values.from, values.to, values.start, values.end)) {
this.props.onCommitValue(values);
} else {
this.props.onUpdate(values || null);
}
this.toD2DateTextFieldInstance && this.toD2DateTextFieldInstance.focus();
};

handleFieldBlur = (value: { [key: string]: string }) => {
Expand All @@ -342,7 +314,6 @@ class DateFilterPlain extends Component<Props, State> implements UpdatableFilter
const toValue = values && values.to;
const startValue = values && values.start;
const endValue = values && values.end;
const type = this.props.type;
const errors = {
minValueError: null,
maxValueError: null,
Expand All @@ -353,7 +324,7 @@ class DateFilterPlain extends Component<Props, State> implements UpdatableFilter
};

if (mainValue === mainOptionKeys.ABSOLUTE_RANGE) {
return { ...errors, ...getAbsoluteRangeErrors(fromValue, toValue, type, submitAttempted) };
return { ...errors, ...getAbsoluteRangeErrors(fromValue, toValue, submitAttempted) };
}

if (mainValue === mainOptionKeys.RELATIVE_RANGE) {
Expand All @@ -364,8 +335,11 @@ class DateFilterPlain extends Component<Props, State> implements UpdatableFilter

render() {
const { value, classes, onFocusUpdateButton } = this.props;
const { minValueError, maxValueError, startValueError, endValueError, dateLogicError, bufferLogicError } =
const fromValue = value?.from;
const toValue = value?.to;
const { startValueError, endValueError, dateLogicError, bufferLogicError } =
this.getErrors();

return (
<div id="dateFilter">
<div>
Expand All @@ -384,26 +358,25 @@ class DateFilterPlain extends Component<Props, State> implements UpdatableFilter
{/* $FlowSuppress: Flow not working 100% with HOCs */}
{/* $FlowFixMe[prop-missing] automated comment */}
<FromDateFilter
value={value && value.from}
error={minValueError}
errorClass={classes.error}
value={fromValue?.value}
onBlur={this.handleFieldBlur}
onEnterKey={this.handleEnterKeyInFrom}
onDateSelectedFromCalendar={this.handleDateSelectedFromCalendarInFrom}
error={fromValue?.error}
errorClass={classes.error}
/>
</div>
<div className={classes.toLabelContainer}>{i18n.t('to')}</div>
<div className={classes.inputContainer}>
{/* $FlowSuppress: Flow not working 100% with HOCs */}
{/* $FlowFixMe[prop-missing] automated comment */}
<ToDateFilter
value={value && value.to}
error={maxValueError}
errorClass={classes.error}
value={toValue?.value}
onBlur={this.handleFieldBlur}
onEnterKey={this.handleEnterKeyInTo}
textFieldRef={this.setToD2DateTextFieldInstance}
onFocusUpdateButton={onFocusUpdateButton}
error={toValue?.error}
errorClass={classes.error}
/>
</div>
</div>
Expand All @@ -424,7 +397,6 @@ class DateFilterPlain extends Component<Props, State> implements UpdatableFilter
startValueError={startValueError}
endValueError={endValueError}
handleFieldBlur={this.handleFieldBlur}
handleEnterKeyInTo={this.handleEnterKeyInTo}
/>
</div>
<div className={classNames(classes.error, classes.logicErrorContainer)}>{dateLogicError}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ export class DateFilterManager extends React.Component<Props, State> {
static calculateAbsoluteRangeValueState(filter: DateFilterData) {
return {
main: mainOptionKeys.ABSOLUTE_RANGE,
from: filter.ge && DateFilterManager.convertDateForEdit(filter.ge),
to: filter.le && DateFilterManager.convertDateForEdit(filter.le),
from: filter.ge ? {
value: filter.ge && DateFilterManager.convertDateForEdit(filter.ge),
isValid: true,
} : undefined,
to: filter.le ? {
value: filter.le && DateFilterManager.convertDateForEdit(filter.le),
isValid: true,
} : undefined,
};
}
static calculateRelativeRangeValueState(filter: DateFilterData) {
Expand Down Expand Up @@ -74,7 +80,7 @@ export class DateFilterManager extends React.Component<Props, State> {
};
}

handleCommitValue = (value: ?Object) => {
handleCommitValue = (value: ?Value) => {
this.setState({ value });
this.props.handleCommitValue && this.props.handleCommitValue();
};
Expand Down
Loading

0 comments on commit 5f27455

Please sign in to comment.