Skip to content

Commit c33ddb8

Browse files
authored
Merge pull request #1283 from PADAS/ERA-11347
ERA-11347: [P1] Desktop time field malfunctioning (Offset by 1 hour)
2 parents b469658 + 3441eed commit c33ddb8

File tree

12 files changed

+126
-104
lines changed

12 files changed

+126
-104
lines changed

src/DatePicker/index.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { memo, useEffect, useImperativeHandle, useRef, useState } from 'react';
2-
import { parseISO } from 'date-fns';
2+
import { lastDayOfMonth, parseISO } from 'date-fns';
33
import { useTranslation } from 'react-i18next';
44

55
import { ReactComponent as CalendarIcon } from '../common/images/icons/calendar.svg';
@@ -13,6 +13,7 @@ import {
1313
isMonthInputComplete,
1414
isSecondDayDigitPossible,
1515
isSecondMonthDigitPossible,
16+
isValidDate,
1617
isValidDayInput,
1718
isValidMonthInput,
1819
isValidYearInput,
@@ -231,14 +232,20 @@ const DatePicker = ({
231232
};
232233

233234
const onDayInputKeyDown = (event) => {
235+
// If the year and month inputs are complete, we can calculate the last valid day of the current month to set when
236+
// the user decreases or increases the day with the arrows.
237+
const lastValidDayOfMonth = isYearInputComplete(year) && isMonthInputComplete(month)
238+
? lastDayOfMonth(new Date(year, month - 1)).getDate().toString()
239+
: '31';
240+
234241
switch (event.key) {
235242
case 'ArrowDown':
236243
if (!readOnly) {
237244
event.preventDefault();
238245

239246
// Decrease the day when the user presses the down arrow.
240247
if (day === '' || parseInt(day) === 1) {
241-
onDayChange('31');
248+
onDayChange(lastValidDayOfMonth);
242249
} else if (isValidDayInput(day)) {
243250
const dayMinusOne = (parseInt(day) - 1).toString().padStart(2, '0');
244251
onDayChange(dayMinusOne);
@@ -263,7 +270,7 @@ const DatePicker = ({
263270
event.preventDefault();
264271

265272
// Increase the day when the user presses the up arrow.
266-
if (day === '' || day === '31') {
273+
if (day === '' || day === lastValidDayOfMonth) {
267274
onDayChange('01');
268275
} else if (isValidDayInput(day)) {
269276
const dayPlusOne = (parseInt(day) + 1).toString().padStart(2, '0');
@@ -380,6 +387,6 @@ const DatePicker = ({
380387
</div>;
381388
};
382389

383-
export { EMPTY_DATE_VALUE };
390+
export { EMPTY_DATE_VALUE, isValidDate };
384391

385392
export default memo(DatePicker);

src/DatePicker/index.test.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,7 @@ describe('DatePicker', () => {
798798
expect(onChange).toHaveBeenCalledWith('--01');
799799
});
800800

801-
test('sets the day to 01 if the input has the value 31 and is focused and the user presses the up arrow', async () => {
801+
test('sets the day to 01 if the year and month are not valid and the day input has the value 31 and is focused and the user presses the up arrow', async () => {
802802
renderDatePicker({ value: '--31' });
803803

804804
await userEvent.click(screen.getByLabelText('Day'));
@@ -811,6 +811,19 @@ describe('DatePicker', () => {
811811
expect(onChange).toHaveBeenCalledWith('--01');
812812
});
813813

814+
test('sets the day to 01 if the year and month are valid and the day input has the last valid day of the month and is focused and the user presses the up arrow', async () => {
815+
renderDatePicker({ value: '2020-02-29' });
816+
817+
await userEvent.click(screen.getByLabelText('Day'));
818+
819+
expect(onChange).not.toHaveBeenCalled();
820+
821+
await userEvent.keyboard('[ArrowUp]');
822+
823+
expect(onChange).toHaveBeenCalledTimes(1);
824+
expect(onChange).toHaveBeenCalledWith('2020-02-01');
825+
});
826+
814827
test('increments the day if the input has a valid value and is focused and the user presses the up arrow', async () => {
815828
renderDatePicker({ value: '--18' });
816829

@@ -837,7 +850,7 @@ describe('DatePicker', () => {
837850
expect(onChange).toHaveBeenCalledWith('--31');
838851
});
839852

840-
test('sets the day to 31 if the input has the value 01 and is focused and the user presses the down arrow', async () => {
853+
test('sets the day to 31 if the year and month are not valid and the day input has the value 01 and is focused and the user presses the down arrow', async () => {
841854
renderDatePicker({ value: '--01' });
842855

843856
await userEvent.click(screen.getByLabelText('Day'));
@@ -850,6 +863,19 @@ describe('DatePicker', () => {
850863
expect(onChange).toHaveBeenCalledWith('--31');
851864
});
852865

866+
test('sets the day to the last valid day of the month if the year and month are valid and the day input has the value 01 and is focused and the user presses the down arrow', async () => {
867+
renderDatePicker({ value: '2020-02-01' });
868+
869+
await userEvent.click(screen.getByLabelText('Day'));
870+
871+
expect(onChange).not.toHaveBeenCalled();
872+
873+
await userEvent.keyboard('[ArrowDown]');
874+
875+
expect(onChange).toHaveBeenCalledTimes(1);
876+
expect(onChange).toHaveBeenCalledWith('2020-02-29');
877+
});
878+
853879
test('decrements the day if the input has a valid value and is focused and the user presses the down arrow', async () => {
854880
renderDatePicker({ value: '--18' });
855881

src/DatePicker/utils/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,4 @@ export const shouldCompleteFirstDayDigitWithZero = (day) => /^[1-3]$/.test(day);
5959

6060
export const isDayInputComplete = (day) => /^(0[1-9]|[12][0-9]|3[01])$/.test(day);
6161

62-
export const isValidDate = (time) => /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/.test(time);
62+
export const isValidDate = (date) => /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/.test(date);

src/DateRangeSelector/index.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,21 +70,21 @@ const DateRangeSelector = ({
7070
onFilterSettingsToggle && onFilterSettingsToggle(!filterSettingsOpen);
7171
}, [filterSettingsOpen, onFilterSettingsToggle]);
7272

73-
const onStartDateTimePickerChange = (dateTime) => {
74-
setStartDateTime(dateTime);
73+
const onStartDateTimePickerChange = (newStartDateTime) => {
74+
setStartDateTime(newStartDateTime);
7575

76-
const parsedDateTime = parseISO(dateTime);
77-
if (isValid(parsedDateTime)) {
78-
onStartDateChange(parsedDateTime);
76+
const parsedNewStartDateTime = parseISO(newStartDateTime);
77+
if (isValid(parsedNewStartDateTime)) {
78+
onStartDateChange(parsedNewStartDateTime);
7979
}
8080
};
8181

82-
const onEndDateTimePickerChange = (dateTime) => {
83-
setEndDateTime(dateTime);
82+
const onEndDateTimePickerChange = (newEndDateTime) => {
83+
setEndDateTime(newEndDateTime);
8484

85-
const parsedDateTime = parseISO(dateTime);
86-
if (isValid(parsedDateTime)) {
87-
onEndDateChange(parsedDateTime);
85+
const parsedNewEndDateTime = parseISO(newEndDateTime);
86+
if (isValid(parsedNewEndDateTime)) {
87+
onEndDateChange(parsedNewEndDateTime);
8888
}
8989
};
9090

src/DateTimePicker/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import React, { memo, useImperativeHandle, useRef } from 'react';
22

3-
import { getMaxDateAndTime, getMinDateAndTime } from './utils';
3+
import { EMPTY_DATE_TIME_VALUE, getMaxDateAndTime, getMinDateAndTime } from './utils';
44

5-
import DatePicker, { EMPTY_DATE_VALUE } from '../DatePicker';
5+
import DatePicker from '../DatePicker';
66
import TimePicker, { EMPTY_TIME_VALUE } from '../TimePicker';
77

88
import * as styles from './styles.module.scss';
99

10-
export const EMPTY_DATE_TIME_VALUE = `${EMPTY_DATE_VALUE}T${EMPTY_TIME_VALUE}`;
11-
1210
const DateTimePicker = ({
1311
className = '',
1412
datePickerProps = {},
@@ -77,4 +75,6 @@ const DateTimePicker = ({
7775
</div>;
7876
};
7977

78+
export { EMPTY_DATE_TIME_VALUE };
79+
8080
export default memo(DateTimePicker);

src/DateTimePicker/utils/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import { EMPTY_DATE_VALUE } from '../../DatePicker';
2+
import { EMPTY_TIME_VALUE } from '../../TimePicker';
3+
4+
export const EMPTY_DATE_TIME_VALUE = `${EMPTY_DATE_VALUE}T${EMPTY_TIME_VALUE}`;
5+
16
export const getMaxDateAndTime = (max, value) => {
27
const [dateValue] = value.split('T');
38
const [maxDate, maxTime] = max.split('T');

src/PatrolDetailView/PlanSection/index.js

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
displayStartTimeForPatrol
1414
} from '../../utils/patrols';
1515
import { fetchTrackedBySchema } from '../../ducks/trackedby';
16-
import { getHoursAndMinutesString, getTimezoneOffsetString } from '../../utils/datetime';
16+
import { getHoursAndMinutesString } from '../../utils/datetime';
1717
import { updateUserPreferences } from '../../ducks/user-preferences';
1818
import { setMapLocationSelectionPatrol } from '../../ducks/map-ui';
1919
import { useMatchMedia } from '../../hooks';
@@ -58,55 +58,49 @@ const PlanSection = ({
5858
const [startDate, setStartDate] = useState(format(displayStartDate ?? new Date(), 'yyyy-MM-dd'));
5959
const [startTime, setStartTime] = useState(getHoursAndMinutesString(displayStartDate));
6060

61-
const handleEndDateChange = useCallback((date) => {
62-
setEndDate(date);
61+
const handleEndDateChange = useCallback((newEndDate) => {
62+
setEndDate(newEndDate);
6363

64-
let dateISO = `${date}T`;
65-
dateISO += isValidTime(endTime) ? endTime : '00:00';
66-
dateISO += `:00${getTimezoneOffsetString()}`;
67-
68-
const parsedDate = parseISO(dateISO);
69-
if (isValid(parsedDate)) {
70-
onPatrolEndDateChange(parsedDate, shouldScheduleDate(parsedDate, isAutoEnd));
64+
const parsedNewEndDate = parseISO(`${newEndDate}T${isValidTime(endTime) ? endTime : '00:00'}`);
65+
if (isValid(parsedNewEndDate)) {
66+
onPatrolEndDateChange(parsedNewEndDate, shouldScheduleDate(parsedNewEndDate, isAutoEnd));
7167
} else {
7268
onPatrolEndDateChange(undefined);
7369
}
7470
}, [endTime, isAutoEnd, onPatrolEndDateChange]);
7571

76-
const handleStartDateChange = useCallback((date) => {
77-
setStartDate(date);
78-
79-
let dateISO = `${date}T`;
80-
dateISO += isValidTime(startTime) ? startTime : '00:00';
81-
dateISO += `:00${getTimezoneOffsetString()}`;
72+
const handleStartDateChange = useCallback((newStartDate) => {
73+
setStartDate(newStartDate);
8274

83-
const parsedDate = parseISO(dateISO);
84-
if (isValid(parsedDate)) {
85-
onPatrolStartDateChange(parsedDate, shouldScheduleDate(parsedDate, isAutoStart));
75+
const parsedNewStartDate = parseISO(`${newStartDate}T${isValidTime(startTime) ? startTime : '00:00'}`);
76+
if (isValid(parsedNewStartDate)) {
77+
onPatrolStartDateChange(parsedNewStartDate, shouldScheduleDate(parsedNewStartDate, isAutoStart));
8678
} else {
8779
onPatrolStartDateChange(undefined);
8880
}
8981
}, [isAutoStart, onPatrolStartDateChange, startTime]);
9082

91-
const handleEndTimeChange = useCallback((endTime) => {
92-
setEndTime(endTime);
93-
94-
const newEndTimeParts = endTime.split(':');
95-
const updatedEndDateTime = displayEndDate ? new Date(displayEndDate) : new Date();
96-
updatedEndDateTime.setHours(newEndTimeParts[0], newEndTimeParts[1], '00');
83+
const handleEndTimeChange = useCallback((newEndTime) => {
84+
setEndTime(newEndTime);
9785

98-
onPatrolEndDateChange(updatedEndDateTime, shouldScheduleDate(updatedEndDateTime, isAutoEnd));
99-
}, [displayEndDate, isAutoEnd, onPatrolEndDateChange]);
100-
101-
const handleStartTimeChange = useCallback((startTime) => {
102-
setStartTime(startTime);
86+
const parsedNewEndDate = parseISO(`${endDate}T${newEndTime}`);
87+
if (isValid(parsedNewEndDate)) {
88+
onPatrolEndDateChange(parsedNewEndDate, shouldScheduleDate(parsedNewEndDate, isAutoEnd));
89+
} else {
90+
onPatrolEndDateChange(undefined);
91+
}
92+
}, [endDate, isAutoEnd, onPatrolEndDateChange]);
10393

104-
const newStartTimeParts = startTime.split(':');
105-
const updatedStartDateTime = displayStartDate ? new Date(displayStartDate) : new Date();
106-
updatedStartDateTime.setHours(newStartTimeParts[0], newStartTimeParts[1], '00');
94+
const handleStartTimeChange = useCallback((newStartTime) => {
95+
setStartTime(newStartTime);
10796

108-
onPatrolStartDateChange(updatedStartDateTime, shouldScheduleDate(updatedStartDateTime, isAutoStart));
109-
}, [displayStartDate, isAutoStart, onPatrolStartDateChange]);
97+
const parsedNewStartDate = parseISO(`${startDate}T${newStartTime}`);
98+
if (isValid(parsedNewStartDate)) {
99+
onPatrolStartDateChange(parsedNewStartDate, shouldScheduleDate(parsedNewStartDate, isAutoStart));
100+
} else {
101+
onPatrolStartDateChange(undefined);
102+
}
103+
}, [isAutoStart, onPatrolStartDateChange, startDate]);
110104

111105
const handleAutoEndChange = useCallback(() => {
112106
const newIsAutoEnd = !isAutoEnd;

src/ReportManager/DetailsSection/SchemaForm/fields/DateTime/index.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ import React, { memo, useEffect, useState } from 'react';
22
import { format, isValid, parseISO } from 'date-fns';
33

44
import { DATE_TIME_ELEMENT_INPUT_TYPES } from '../../constants';
5-
import { getTimezoneOffsetString } from '../../../../../utils/datetime';
65

7-
import DatePicker, { EMPTY_DATE_VALUE } from '../../../../../DatePicker';
6+
import DatePicker, { isValidDate, EMPTY_DATE_VALUE } from '../../../../../DatePicker';
87
import DateTimePicker, { EMPTY_DATE_TIME_VALUE } from '../../../../../DateTimePicker';
98
import TimePicker, { EMPTY_TIME_VALUE } from '../../../../../TimePicker';
109

@@ -26,8 +25,11 @@ const DateTimeInput = ({ onChange, value, ...otherProps }) => {
2625
if (newDateTime === EMPTY_DATE_TIME_VALUE) {
2726
onChange(undefined);
2827
} else {
29-
// JSON schema time format expects the time with the timezone offset.
30-
const dateTimeWithSecondsAndOffset = `${newDateTime}:00${getTimezoneOffsetString()}`;
28+
// JSON schema date time format expects the date time with seconds and timezone offset. If the new date is valid,
29+
// consider the offset of that date (to match the daylight saving), otherwise consider the current date offset.
30+
const [newDateValue] = newDateTime.split('T');
31+
const newOffset = format(isValidDate(newDateValue) ? newDateValue : new Date(), 'xxx');
32+
const dateTimeWithSecondsAndOffset = `${newDateTime}:00${newOffset}`;
3133
onChange(dateTimeWithSecondsAndOffset);
3234
}
3335
};
@@ -49,8 +51,8 @@ const TimeInput = ({ onChange, value, ...otherProps }) => {
4951
if (newTime === EMPTY_TIME_VALUE) {
5052
onChange(undefined);
5153
} else {
52-
// JSON schema time format expects the time with the timezone offset.
53-
const timeWithSecondsAndOffset = `${newTime}:00${getTimezoneOffsetString()}`;
54+
// JSON schema time format expects the time with seconds and timezone offset.
55+
const timeWithSecondsAndOffset = `${newTime}:00${format(new Date(), 'xxx')}`;
5456
onChange(timeWithSecondsAndOffset);
5557
}
5658
};
@@ -76,7 +78,7 @@ const DateTime = ({ autofillDefaultInput: _autofillDefaultInput, details, error,
7678
// Date-time and time input types have a timezone offset, so we correct the input value to the current user timezone
7779
// before rendering it.
7880
useEffect(() => {
79-
if (!hasTimezoneBeenCorrected && value) {
81+
if (value && !hasTimezoneBeenCorrected) {
8082
if (details.inputType === DATE_TIME_ELEMENT_INPUT_TYPES.DATE_TIME) {
8183
const parsedDateTimeValue = parseISO(value);
8284
if (isValid(parsedDateTimeValue)) {
@@ -85,8 +87,7 @@ const DateTime = ({ autofillDefaultInput: _autofillDefaultInput, details, error,
8587
}
8688

8789
if (details.inputType === DATE_TIME_ELEMENT_INPUT_TYPES.TIME) {
88-
// We add a dummy date just to make it a valid ISO date
89-
const parsedTimeValue = parseISO(`2000-01-01T${value}`);
90+
const parsedTimeValue = parseISO(`${format(new Date(), 'yyyy-MM-dd')}T${value}`);
9091
if (isValid(parsedTimeValue)) {
9192
onFieldChange(id, format(parsedTimeValue, 'HH:mm:ssXXX'));
9293
}

src/ReportManager/DetailsSection/SchemaForm/fields/DateTime/index.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { render, screen } from '../../../../../test-utils';
66
import { DATE_TIME_ELEMENT_INPUT_TYPES } from '../../constants';
77

88
import DateTime from './';
9-
import { getTimezoneOffsetString } from '../../../../../utils/datetime';
109

1110
const transformISOToCurrentTimezone = (dateValue) => format(parseISO(dateValue), 'yyyy-MM-dd\'T\'HH:mm:ssXXX');
1211

@@ -138,12 +137,13 @@ describe('ReportManager - DetailsSection - SchemaForm - fields - DateTime', () =
138137
await userEvent.click(screen.getByLabelText('Choose Monday, January 13th, 2020'));
139138

140139
expect(onFieldChange).toHaveBeenCalledTimes(2);
141-
expect(onFieldChange).toHaveBeenCalledWith('date-time-1', `2020-01-13T06:30:00${getTimezoneOffsetString()}`);
140+
expect(onFieldChange).toHaveBeenCalledWith('date-time-1', transformISOToCurrentTimezone('2020-01-13T06:30'));
142141

143142
await userEvent.click(screen.getByLabelText('Open time options'));
144143
await userEvent.click(screen.getByText('08:00 AM'));
145144

146145
expect(onFieldChange).toHaveBeenCalledTimes(3);
147-
expect(onFieldChange).toHaveBeenCalledWith('date-time-1', `2020-01-01T08:00:00${getTimezoneOffsetString()}`);
146+
expect(onFieldChange.mock.calls[2][0]).toBe('date-time-1');
147+
expect(onFieldChange).toHaveBeenCalledWith('date-time-1', transformISOToCurrentTimezone('2020-01-01T08:00'));
148148
});
149149
});

src/ReportManager/DetailsSection/index.js

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { memo, useCallback, useContext, useEffect, useState } from 'react';
22
import Dropdown from 'react-bootstrap/Dropdown';
33
import Form from '@rjsf/bootstrap-4';
4-
import { format, isToday, isValid as isValidDate, parseISO } from 'date-fns';
4+
import { format, isToday, isValid, parseISO } from 'date-fns';
55
import MoonLoader from 'react-spinners/MoonLoader';
66
import { useDispatch, useSelector } from 'react-redux';
77
import { useTranslation } from 'react-i18next';
@@ -106,13 +106,9 @@ const DetailsSection = ({
106106
const onDatePickerChange = (newDate) => {
107107
setDate(newDate);
108108

109-
const parsedDate = parseISO(newDate);
110-
if (isValidDate(parsedDate)) {
111-
if (isValidTime(time)) {
112-
const [hour, minute] = time.split(':');
113-
parsedDate.setHours(hour, minute, '00');
114-
}
115-
onReportDateChange(parsedDate);
109+
const parsedNewDate = parseISO(`${newDate}T${isValidTime(time) ? time : '00:00'}`);
110+
if (isValid(parsedNewDate)) {
111+
onReportDateChange(parsedNewDate);
116112
} else {
117113
onReportDateChange(undefined);
118114
}
@@ -123,13 +119,11 @@ const DetailsSection = ({
123119
const onTimePickerChange = (newTime) => {
124120
setTime(newTime);
125121

126-
const parsedDate = parseISO(date);
127-
if (isValidDate(parsedDate)) {
128-
if (isValidTime(newTime)) {
129-
const [newHour, newMinute] = newTime.split(':');
130-
parsedDate.setHours(newHour, newMinute, '00');
131-
}
132-
onReportDateChange(parsedDate);
122+
const parsedNewDate = parseISO(`${date}T${newTime}`);
123+
if (isValid(parsedNewDate)) {
124+
onReportDateChange(parsedNewDate);
125+
} else {
126+
onReportDateChange(undefined);
133127
}
134128

135129
reportTracker.track('Change Report Time');

0 commit comments

Comments
 (0)