Skip to content

Commit

Permalink
fix: date-picker bug for negative UTC timezones attempt 3 (#6261)
Browse files Browse the repository at this point in the history
* fix: remove timezone from datepicker

* fix: datepicker bugs due to timezones

- (FE) normalize dates to UTC before storing in DB
- (FE) load normalized dates to local timezone
- (BE) remove adjustments due to timezone

* docs: futureOnly and pastOnly validators

* refactor: allow null inputs for new date util fns

* fix: date normalization fns to use date as numbers

* refactor: remove unnecessary var assignment

* fix: unset date validation

* fix: compare to dates in earliest & latest tzs

* fix: date validation bug in release v6.41.0

* fix: restore original date validators

* fix: normalizeDateToUtc to return Date object

* test: frontend date utils

* build(deps): add timezone-mock to mock local time

* test: date utils in the most extreme timezones

* test: add check to ensure that timezone-mock works
  • Loading branch information
LinHuiqing authored May 24, 2023
1 parent 551a322 commit a21f0e1
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 66 deletions.
11 changes: 11 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"simplur": "^3.0.1",
"spark-md5": "^3.0.2",
"stripe": "^11.1.0",
"timezone-mock": "^1.3.6",
"type-fest": "^2.8.0",
"typescript": "^4.5.3",
"use-debounce": "^7.0.1",
Expand Down
31 changes: 9 additions & 22 deletions frontend/src/components/DatePicker/DatePickerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
useMultiStyleConfig,
} from '@chakra-ui/react'
import { format, isValid, parse } from 'date-fns'
import { zonedTimeToUtc } from 'date-fns-tz'

import { ThemeColorScheme } from '~theme/foundations/colours'
import { useIsMobile } from '~hooks/useIsMobile'
Expand Down Expand Up @@ -84,7 +83,6 @@ const useProvideDatePicker = ({
isReadOnly: isReadOnlyProp,
isRequired: isRequiredProp,
isInvalid: isInvalidProp,
timeZone = 'UTC',
locale,
isDateUnavailable,
allowManualInput = true,
Expand Down Expand Up @@ -120,9 +118,9 @@ const useProvideDatePicker = ({
const formatInputValue = useCallback(
(date: Date | null) => {
if (!date || !isValid(date)) return ''
return format(zonedTimeToUtc(date, timeZone), displayFormat, { locale })
return format(date, displayFormat, { locale })
},
[displayFormat, locale, timeZone],
[displayFormat, locale],
)

// What is rendered as a string in the input according to given display format.
Expand All @@ -142,11 +140,7 @@ const useProvideDatePicker = ({

const handleInputBlur: FocusEventHandler<HTMLInputElement> = useCallback(
(e) => {
const date = parse(
internalInputValue,
dateFormat,
zonedTimeToUtc(new Date(), timeZone),
)
const date = parse(internalInputValue, dateFormat, new Date())
// Clear if input is invalid on blur if invalid dates are not allowed.
if (!allowInvalidDates && !isValid(date)) {
setInternalValue(null)
Expand All @@ -161,7 +155,6 @@ const useProvideDatePicker = ({
onBlur,
setInternalInputValue,
setInternalValue,
timeZone,
],
)

Expand All @@ -179,12 +172,11 @@ const useProvideDatePicker = ({

const handleDateChange = useCallback(
(date: Date | null) => {
const zonedDate = date ? zonedTimeToUtc(date, timeZone) : null
if (allowInvalidDates || isValid(zonedDate) || !zonedDate) {
setInternalValue(zonedDate)
if (allowInvalidDates || isValid(date) || !date) {
setInternalValue(date)
}
if (zonedDate) {
setInternalInputValue(format(zonedDate, displayFormat, { locale }))
if (date) {
setInternalInputValue(format(date, displayFormat, { locale }))
} else {
setInternalInputValue('')
}
Expand All @@ -198,23 +190,18 @@ const useProvideDatePicker = ({
locale,
setInternalInputValue,
setInternalValue,
timeZone,
],
)

const handleInputChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const date = parse(
event.target.value,
dateFormat,
zonedTimeToUtc(new Date(), timeZone),
)
const date = parse(event.target.value, dateFormat, new Date())
setInternalInputValue(event.target.value)
if (isValid(date)) {
setInternalValue(date)
}
},
[dateFormat, setInternalInputValue, setInternalValue, timeZone],
[dateFormat, setInternalInputValue, setInternalValue],
)

const handleInputClick: MouseEventHandler<HTMLInputElement> = useCallback(
Expand Down
6 changes: 0 additions & 6 deletions frontend/src/components/DatePicker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,4 @@ export interface DatePickerBaseProps
refocusOnClose?: boolean
/** date-fns's Locale of the date to be applied if provided. */
locale?: Locale
/**
* Time zone of date created.
* Defaults to `'UTC'`.
* Accepts all possible `Intl.Locale.prototype.timeZones` values
*/
timeZone?: string
}
34 changes: 11 additions & 23 deletions frontend/src/components/DateRangePicker/DateRangePickerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
useMultiStyleConfig,
} from '@chakra-ui/react'
import { format, isValid, parse } from 'date-fns'
import { zonedTimeToUtc } from 'date-fns-tz'

import { ThemeColorScheme } from '~theme/foundations/colours'
import { useIsMobile } from '~hooks/useIsMobile'
Expand Down Expand Up @@ -92,7 +91,6 @@ const useProvideDateRangePicker = ({
isReadOnly: isReadOnlyProp,
isRequired: isRequiredProp,
isInvalid: isInvalidProp,
timeZone = 'UTC',
locale,
isDateUnavailable,
allowManualInput = true,
Expand Down Expand Up @@ -129,13 +127,13 @@ const useProvideDateRangePicker = ({
// What is rendered as a string in the start date range input according to given display format.
const [startInputDisplay, setStartInputDisplay] = useState(
startDate && isValid(startDate)
? format(zonedTimeToUtc(startDate, timeZone), displayFormat, { locale })
? format(startDate, displayFormat, { locale })
: '',
)
// What is rendered as a string in the end date range input according to given display format.
const [endInputDisplay, setEndInputDisplay] = useState(
endDate && isValid(endDate)
? format(zonedTimeToUtc(endDate, timeZone), displayFormat, { locale })
? format(endDate, displayFormat, { locale })
: '',
)

Expand All @@ -151,24 +149,18 @@ const useProvideDateRangePicker = ({
) as DateRangeValue

const [nextStart, nextEnd] = sortedRange
const zonedStartDate = nextStart
? zonedTimeToUtc(nextStart, timeZone)
: null
const zonedEndDate = nextEnd ? zonedTimeToUtc(nextEnd, timeZone) : null
if (zonedStartDate) {
if (isValid(zonedStartDate)) {
setStartInputDisplay(
format(zonedStartDate, displayFormat, { locale }),
)
if (nextStart) {
if (isValid(nextStart)) {
setStartInputDisplay(format(nextStart, displayFormat, { locale }))
} else if (!allowInvalidDates) {
setStartInputDisplay('')
}
} else {
setStartInputDisplay('')
}
if (zonedEndDate) {
if (isValid(zonedEndDate)) {
setEndInputDisplay(format(zonedEndDate, displayFormat, { locale }))
if (nextEnd) {
if (isValid(nextEnd)) {
setEndInputDisplay(format(nextEnd, displayFormat, { locale }))
} else if (!allowInvalidDates) {
setEndInputDisplay('')
}
Expand All @@ -177,7 +169,7 @@ const useProvideDateRangePicker = ({
}
setInternalValue(validRange)
},
[allowInvalidDates, displayFormat, locale, setInternalValue, timeZone],
[allowInvalidDates, displayFormat, locale, setInternalValue],
)

const fcProps = useFormControlProps({
Expand Down Expand Up @@ -274,11 +266,8 @@ const useProvideDateRangePicker = ({

const handleCalendarDateChange = useCallback(
(date: DateRangeValue) => {
const zonedDateRange = date.map((d) =>
d ? zonedTimeToUtc(d, timeZone) : null,
) as DateRangeValue
const [nextStartDate, nextEndDate] = zonedDateRange
setInternalValue(zonedDateRange)
const [nextStartDate, nextEndDate] = date
setInternalValue(date)
setStartInputDisplay(
nextStartDate ? format(nextStartDate, displayFormat, { locale }) : '',
)
Expand All @@ -296,7 +285,6 @@ const useProvideDateRangePicker = ({
displayFormat,
locale,
setInternalValue,
timeZone,
],
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo } from 'react'
import { useCallback, useMemo } from 'react'
import { Controller, RegisterOptions } from 'react-hook-form'
import { Box, FormControl, SimpleGrid } from '@chakra-ui/react'
import { isBefore, isEqual, isValid } from 'date-fns'
Expand All @@ -10,7 +10,11 @@ import {
DateValidationOptions,
} from '~shared/types/field'

import { fromUtcToLocalDate, isDateOutOfRange } from '~utils/date'
import {
isDateOutOfRange,
loadDateFromNormalizedDate,
normalizeDateToUtc,
} from '~utils/date'
import { createBaseValidationRules } from '~utils/fieldValidation'
import { DatePicker } from '~components/DatePicker'
import { SingleSelect } from '~components/Dropdown'
Expand Down Expand Up @@ -49,10 +53,10 @@ const transformDateFieldToEditForm = (field: DateFieldBase): EditDateInputs => {
selectedDateValidation:
field.dateValidation.selectedDateValidation ?? ('' as const),
customMaxDate: field.dateValidation.selectedDateValidation
? field.dateValidation.customMaxDate ?? null
? loadDateFromNormalizedDate(field.dateValidation.customMaxDate)
: null,
customMinDate: field.dateValidation.selectedDateValidation
? field.dateValidation.customMinDate ?? null
? loadDateFromNormalizedDate(field.dateValidation.customMinDate)
: null,
}
return {
Expand Down Expand Up @@ -97,6 +101,28 @@ const transformDateEditFormToField = (
}

export const EditDate = ({ field }: EditDateProps): JSX.Element => {
const preSubmitTransform = useCallback(
(inputs: EditDateInputs, output: DateFieldBase): DateFieldBase => {
// normalize time to UTC before saving
return {
...output,
dateValidation: {
selectedDateValidation:
inputs.dateValidation.selectedDateValidation === ''
? null
: inputs.dateValidation.selectedDateValidation,
customMinDate: normalizeDateToUtc(
inputs.dateValidation.customMinDate,
),
customMaxDate: normalizeDateToUtc(
inputs.dateValidation.customMaxDate,
),
},
} as DateFieldBase
},
[],
)

const {
register,
formState: { errors },
Expand All @@ -111,6 +137,7 @@ export const EditDate = ({ field }: EditDateProps): JSX.Element => {
transform: {
input: transformDateFieldToEditForm,
output: transformDateEditFormToField,
preSubmit: preSubmitTransform,
},
})

Expand Down Expand Up @@ -222,9 +249,7 @@ export const EditDate = ({ field }: EditDateProps): JSX.Element => {
isDateUnavailable={(d) =>
isDateOutOfRange(
d,
fromUtcToLocalDate(
getValues('dateValidation.customMinDate'),
),
getValues('dateValidation.customMinDate'),
)
}
{...field}
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/templates/Field/Date/DateField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { FormColorTheme } from '~shared/types'
import { DateSelectedValidation } from '~shared/types/field'

import {
fromUtcToLocalDate,
isDateAfterToday,
isDateBeforeToday,
isDateOutOfRange,
loadDateFromNormalizedDate,
} from '~utils/date'
import { createDateValidationRules } from '~utils/fieldValidation'
import { DatePicker } from '~components/DatePicker'
Expand Down Expand Up @@ -53,8 +53,8 @@ export const DateField = ({
// need to convert to local time but with the same date as UTC.
return isDateOutOfRange(
date,
fromUtcToLocalDate(customMinDate),
fromUtcToLocalDate(customMaxDate),
loadDateFromNormalizedDate(customMinDate),
loadDateFromNormalizedDate(customMaxDate),
)
}
default:
Expand Down
Loading

0 comments on commit a21f0e1

Please sign in to comment.