-
Notifications
You must be signed in to change notification settings - Fork 0
ERA-11347: [P1] Desktop time field malfunctioning (Offset by 1 hour) #1283
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -59,4 +59,4 @@ export const shouldCompleteFirstDayDigitWithZero = (day) => /^[1-3]$/.test(day); | |
|
||
export const isDayInputComplete = (day) => /^(0[1-9]|[12][0-9]|3[01])$/.test(day); | ||
|
||
export const isValidDate = (time) => /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/.test(time); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small word fix. |
||
export const isValidDate = (date) => /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/.test(date); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -70,21 +70,21 @@ const DateRangeSelector = ({ | |
onFilterSettingsToggle && onFilterSettingsToggle(!filterSettingsOpen); | ||
}, [filterSettingsOpen, onFilterSettingsToggle]); | ||
|
||
const onStartDateTimePickerChange = (dateTime) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No changes here, I just updated the variable name to match other places where we implemented the date time picker. |
||
setStartDateTime(dateTime); | ||
const onStartDateTimePickerChange = (newStartDateTime) => { | ||
setStartDateTime(newStartDateTime); | ||
|
||
const parsedDateTime = parseISO(dateTime); | ||
if (isValid(parsedDateTime)) { | ||
onStartDateChange(parsedDateTime); | ||
const parsedNewStartDateTime = parseISO(newStartDateTime); | ||
if (isValid(parsedNewStartDateTime)) { | ||
onStartDateChange(parsedNewStartDateTime); | ||
} | ||
}; | ||
|
||
const onEndDateTimePickerChange = (dateTime) => { | ||
setEndDateTime(dateTime); | ||
const onEndDateTimePickerChange = (newEndDateTime) => { | ||
setEndDateTime(newEndDateTime); | ||
|
||
const parsedDateTime = parseISO(dateTime); | ||
if (isValid(parsedDateTime)) { | ||
onEndDateChange(parsedDateTime); | ||
const parsedNewEndDateTime = parseISO(newEndDateTime); | ||
if (isValid(parsedNewEndDateTime)) { | ||
onEndDateChange(parsedNewEndDateTime); | ||
} | ||
}; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,12 @@ | ||
import React, { memo, useImperativeHandle, useRef } from 'react'; | ||
|
||
import { getMaxDateAndTime, getMinDateAndTime } from './utils'; | ||
import { EMPTY_DATE_TIME_VALUE, getMaxDateAndTime, getMinDateAndTime } from './utils'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved the |
||
|
||
import DatePicker, { EMPTY_DATE_VALUE } from '../DatePicker'; | ||
import DatePicker from '../DatePicker'; | ||
import TimePicker, { EMPTY_TIME_VALUE } from '../TimePicker'; | ||
|
||
import * as styles from './styles.module.scss'; | ||
|
||
export const EMPTY_DATE_TIME_VALUE = `${EMPTY_DATE_VALUE}T${EMPTY_TIME_VALUE}`; | ||
|
||
const DateTimePicker = ({ | ||
className = '', | ||
datePickerProps = {}, | ||
|
@@ -77,4 +75,6 @@ const DateTimePicker = ({ | |
</div>; | ||
}; | ||
|
||
export { EMPTY_DATE_TIME_VALUE }; | ||
|
||
export default memo(DateTimePicker); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,7 +13,7 @@ import { | |
displayStartTimeForPatrol | ||
} from '../../utils/patrols'; | ||
import { fetchTrackedBySchema } from '../../ducks/trackedby'; | ||
import { getHoursAndMinutesString, getTimezoneOffsetString } from '../../utils/datetime'; | ||
import { getHoursAndMinutesString } from '../../utils/datetime'; | ||
import { updateUserPreferences } from '../../ducks/user-preferences'; | ||
import { setMapLocationSelectionPatrol } from '../../ducks/map-ui'; | ||
import { useMatchMedia } from '../../hooks'; | ||
|
@@ -58,55 +58,49 @@ const PlanSection = ({ | |
const [startDate, setStartDate] = useState(format(displayStartDate ?? new Date(), 'yyyy-MM-dd')); | ||
const [startTime, setStartTime] = useState(getHoursAndMinutesString(displayStartDate)); | ||
|
||
const handleEndDateChange = useCallback((date) => { | ||
setEndDate(date); | ||
const handleEndDateChange = useCallback((newEndDate) => { | ||
setEndDate(newEndDate); | ||
|
||
let dateISO = `${date}T`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file changes weren't really necessary, these pickers were working fine, but using the new approach is clearer, more concise, and I would even think stable since now we delegate the responsibility of calculating the offsets to |
||
dateISO += isValidTime(endTime) ? endTime : '00:00'; | ||
dateISO += `:00${getTimezoneOffsetString()}`; | ||
|
||
const parsedDate = parseISO(dateISO); | ||
if (isValid(parsedDate)) { | ||
onPatrolEndDateChange(parsedDate, shouldScheduleDate(parsedDate, isAutoEnd)); | ||
const parsedNewEndDate = parseISO(`${newEndDate}T${isValidTime(endTime) ? endTime : '00:00'}`); | ||
if (isValid(parsedNewEndDate)) { | ||
onPatrolEndDateChange(parsedNewEndDate, shouldScheduleDate(parsedNewEndDate, isAutoEnd)); | ||
} else { | ||
onPatrolEndDateChange(undefined); | ||
} | ||
}, [endTime, isAutoEnd, onPatrolEndDateChange]); | ||
|
||
const handleStartDateChange = useCallback((date) => { | ||
setStartDate(date); | ||
|
||
let dateISO = `${date}T`; | ||
dateISO += isValidTime(startTime) ? startTime : '00:00'; | ||
dateISO += `:00${getTimezoneOffsetString()}`; | ||
const handleStartDateChange = useCallback((newStartDate) => { | ||
setStartDate(newStartDate); | ||
|
||
const parsedDate = parseISO(dateISO); | ||
if (isValid(parsedDate)) { | ||
onPatrolStartDateChange(parsedDate, shouldScheduleDate(parsedDate, isAutoStart)); | ||
const parsedNewStartDate = parseISO(`${newStartDate}T${isValidTime(startTime) ? startTime : '00:00'}`); | ||
if (isValid(parsedNewStartDate)) { | ||
onPatrolStartDateChange(parsedNewStartDate, shouldScheduleDate(parsedNewStartDate, isAutoStart)); | ||
} else { | ||
onPatrolStartDateChange(undefined); | ||
} | ||
}, [isAutoStart, onPatrolStartDateChange, startTime]); | ||
|
||
const handleEndTimeChange = useCallback((endTime) => { | ||
setEndTime(endTime); | ||
|
||
const newEndTimeParts = endTime.split(':'); | ||
const updatedEndDateTime = displayEndDate ? new Date(displayEndDate) : new Date(); | ||
updatedEndDateTime.setHours(newEndTimeParts[0], newEndTimeParts[1], '00'); | ||
const handleEndTimeChange = useCallback((newEndTime) => { | ||
setEndTime(newEndTime); | ||
|
||
onPatrolEndDateChange(updatedEndDateTime, shouldScheduleDate(updatedEndDateTime, isAutoEnd)); | ||
}, [displayEndDate, isAutoEnd, onPatrolEndDateChange]); | ||
|
||
const handleStartTimeChange = useCallback((startTime) => { | ||
setStartTime(startTime); | ||
const parsedNewEndDate = parseISO(`${endDate}T${newEndTime}`); | ||
if (isValid(parsedNewEndDate)) { | ||
onPatrolEndDateChange(parsedNewEndDate, shouldScheduleDate(parsedNewEndDate, isAutoEnd)); | ||
} else { | ||
onPatrolEndDateChange(undefined); | ||
} | ||
}, [endDate, isAutoEnd, onPatrolEndDateChange]); | ||
|
||
const newStartTimeParts = startTime.split(':'); | ||
const updatedStartDateTime = displayStartDate ? new Date(displayStartDate) : new Date(); | ||
updatedStartDateTime.setHours(newStartTimeParts[0], newStartTimeParts[1], '00'); | ||
const handleStartTimeChange = useCallback((newStartTime) => { | ||
setStartTime(newStartTime); | ||
|
||
onPatrolStartDateChange(updatedStartDateTime, shouldScheduleDate(updatedStartDateTime, isAutoStart)); | ||
}, [displayStartDate, isAutoStart, onPatrolStartDateChange]); | ||
const parsedNewStartDate = parseISO(`${startDate}T${newStartTime}`); | ||
if (isValid(parsedNewStartDate)) { | ||
onPatrolStartDateChange(parsedNewStartDate, shouldScheduleDate(parsedNewStartDate, isAutoStart)); | ||
} else { | ||
onPatrolStartDateChange(undefined); | ||
} | ||
}, [isAutoStart, onPatrolStartDateChange, startDate]); | ||
|
||
const handleAutoEndChange = useCallback(() => { | ||
const newIsAutoEnd = !isAutoEnd; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,9 +2,8 @@ import React, { memo, useEffect, useState } from 'react'; | |
import { format, isValid, parseISO } from 'date-fns'; | ||
|
||
import { DATE_TIME_ELEMENT_INPUT_TYPES } from '../../constants'; | ||
import { getTimezoneOffsetString } from '../../../../../utils/datetime'; | ||
|
||
import DatePicker, { EMPTY_DATE_VALUE } from '../../../../../DatePicker'; | ||
import DatePicker, { isValidDate, EMPTY_DATE_VALUE } from '../../../../../DatePicker'; | ||
import DateTimePicker, { EMPTY_DATE_TIME_VALUE } from '../../../../../DateTimePicker'; | ||
import TimePicker, { EMPTY_TIME_VALUE } from '../../../../../TimePicker'; | ||
|
||
|
@@ -26,8 +25,11 @@ const DateTimeInput = ({ onChange, value, ...otherProps }) => { | |
if (newDateTime === EMPTY_DATE_TIME_VALUE) { | ||
onChange(undefined); | ||
} else { | ||
// JSON schema time format expects the time with the timezone offset. | ||
const dateTimeWithSecondsAndOffset = `${newDateTime}:00${getTimezoneOffsetString()}`; | ||
// JSON schema date time format expects the date time with seconds and timezone offset. If the new date is valid, | ||
// consider the offset of that date (to match the daylight saving), otherwise consider the current date offset. | ||
const [newDateValue] = newDateTime.split('T'); | ||
const newOffset = format(isValidDate(newDateValue) ? newDateValue : new Date(), 'xxx'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the actual fix for the new schemas component (EFB), this was not the reported issue since this is behind a feature flag, but the bug was here too. Instead of setting the user's current offset, we now calculate the offset corresponding to the selected date (considering daylight saving and avoiding that unwanted incorrect fixing). |
||
const dateTimeWithSecondsAndOffset = `${newDateTime}:00${newOffset}`; | ||
onChange(dateTimeWithSecondsAndOffset); | ||
} | ||
}; | ||
|
@@ -49,8 +51,8 @@ const TimeInput = ({ onChange, value, ...otherProps }) => { | |
if (newTime === EMPTY_TIME_VALUE) { | ||
onChange(undefined); | ||
} else { | ||
// JSON schema time format expects the time with the timezone offset. | ||
const timeWithSecondsAndOffset = `${newTime}:00${getTimezoneOffsetString()}`; | ||
// JSON schema time format expects the time with seconds and timezone offset. | ||
const timeWithSecondsAndOffset = `${newTime}:00${format(new Date(), 'xxx')}`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JSON schemas time formats need offsets too so the fix is also applied to time fields. |
||
onChange(timeWithSecondsAndOffset); | ||
} | ||
}; | ||
|
@@ -76,7 +78,7 @@ const DateTime = ({ autofillDefaultInput: _autofillDefaultInput, details, error, | |
// Date-time and time input types have a timezone offset, so we correct the input value to the current user timezone | ||
// before rendering it. | ||
useEffect(() => { | ||
if (!hasTimezoneBeenCorrected && value) { | ||
if (value && !hasTimezoneBeenCorrected) { | ||
if (details.inputType === DATE_TIME_ELEMENT_INPUT_TYPES.DATE_TIME) { | ||
const parsedDateTimeValue = parseISO(value); | ||
if (isValid(parsedDateTimeValue)) { | ||
|
@@ -85,8 +87,7 @@ const DateTime = ({ autofillDefaultInput: _autofillDefaultInput, details, error, | |
} | ||
|
||
if (details.inputType === DATE_TIME_ELEMENT_INPUT_TYPES.TIME) { | ||
// We add a dummy date just to make it a valid ISO date | ||
const parsedTimeValue = parseISO(`2000-01-01T${value}`); | ||
const parsedTimeValue = parseISO(`${format(new Date(), 'yyyy-MM-dd')}T${value}`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We used to fix the time value with a dummy date. I thought it wasn't really important, but it is, due to cities with daylight saving time. So now we use the current date to calculate the offset (in this case we do want the current date because this is the place where we fix the stored value into the current time zone offset). |
||
if (isValid(parsedTimeValue)) { | ||
onFieldChange(id, format(parsedTimeValue, 'HH:mm:ssXXX')); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import React, { memo, useCallback, useContext, useEffect, useState } from 'react'; | ||
import Dropdown from 'react-bootstrap/Dropdown'; | ||
import Form from '@rjsf/bootstrap-4'; | ||
import { format, isToday, isValid as isValidDate, parseISO } from 'date-fns'; | ||
import { format, isToday, isValid, parseISO } from 'date-fns'; | ||
import MoonLoader from 'react-spinners/MoonLoader'; | ||
import { useDispatch, useSelector } from 'react-redux'; | ||
import { useTranslation } from 'react-i18next'; | ||
|
@@ -106,13 +106,9 @@ const DetailsSection = ({ | |
const onDatePickerChange = (newDate) => { | ||
setDate(newDate); | ||
|
||
const parsedDate = parseISO(newDate); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another place were the changes weren't really needed but the new pattern improves the code and it makes it more resilient. |
||
if (isValidDate(parsedDate)) { | ||
if (isValidTime(time)) { | ||
const [hour, minute] = time.split(':'); | ||
parsedDate.setHours(hour, minute, '00'); | ||
} | ||
onReportDateChange(parsedDate); | ||
const parsedNewDate = parseISO(`${newDate}T${isValidTime(time) ? time : '00:00'}`); | ||
if (isValid(parsedNewDate)) { | ||
onReportDateChange(parsedNewDate); | ||
} else { | ||
onReportDateChange(undefined); | ||
} | ||
|
@@ -123,13 +119,11 @@ const DetailsSection = ({ | |
const onTimePickerChange = (newTime) => { | ||
setTime(newTime); | ||
|
||
const parsedDate = parseISO(date); | ||
if (isValidDate(parsedDate)) { | ||
if (isValidTime(newTime)) { | ||
const [newHour, newMinute] = newTime.split(':'); | ||
parsedDate.setHours(newHour, newMinute, '00'); | ||
} | ||
onReportDateChange(parsedDate); | ||
const parsedNewDate = parseISO(`${date}T${newTime}`); | ||
if (isValid(parsedNewDate)) { | ||
onReportDateChange(parsedNewDate); | ||
} else { | ||
onReportDateChange(undefined); | ||
} | ||
|
||
reportTracker.track('Change Report Time'); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not part of the bug, just an extra addition to the PR to improve our date picker. If the user decreased the day input with the down arrow or increase with the up arrow, we were setting always
31
as the upper limit of the month. But not all months have 31 days, some of them vary with the leap year. So this newlastValidDayOfMonth
calculation sets the last valid day instead of a fixed31
if we have the year and month available (otherwise, it's impossible to calculate the last valid day of the month).