-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Feat: add support for day-first and year-first date formats (DD/MM/YYYY, YYYY/MM/DD) #12333
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 17 commits
c6a6250
81815cb
db3f7a8
99903b7
110b3d1
329614d
92ccd17
262edcb
ad708c4
e2fd07f
8ae30f3
ee56638
5ea73b0
da570cc
2f47514
d88b10a
727b692
2919335
b691749
cea73d6
a919c06
9d81232
0dcc412
b6ea032
156ae94
d2f6dac
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 |
---|---|---|
@@ -1,18 +1,20 @@ | ||
import { css } from '@emotion/react'; | ||
import styled from '@emotion/styled'; | ||
import { useCallback, useEffect, useState } from 'react'; | ||
import { useEffect, useState } from 'react'; | ||
import { useIMask } from 'react-imask'; | ||
import { useRecoilValue } from 'recoil'; | ||
|
||
import { DateFormat } from '@/localization/constants/DateFormat'; | ||
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState'; | ||
import { DATE_BLOCKS } from '@/ui/input/components/internal/date/constants/DateBlocks'; | ||
import { DATE_MASK } from '@/ui/input/components/internal/date/constants/DateMask'; | ||
import { getDateMask } from '@/ui/input/components/internal/date/constants/DateMask'; | ||
import { DATE_TIME_BLOCKS } from '@/ui/input/components/internal/date/constants/DateTimeBlocks'; | ||
import { DATE_TIME_MASK } from '@/ui/input/components/internal/date/constants/DateTimeMask'; | ||
import { getDateTimeMask } from '@/ui/input/components/internal/date/constants/DateTimeMask'; | ||
import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate'; | ||
import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate'; | ||
import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString'; | ||
import { parseStringToDate } from '@/ui/input/components/internal/date/utils/parseStringToDate'; | ||
import { isNull } from '@sniptt/guards'; | ||
import { isDefined } from 'twenty-shared/utils'; | ||
import { useDateParser } from '../../hooks/useDateParser'; | ||
|
||
const StyledInputContainer = styled.div` | ||
align-items: center; | ||
|
@@ -44,42 +46,30 @@ type DateTimeInputProps = { | |
onChange?: (date: Date | null) => void; | ||
date: Date | null; | ||
isDateTimeInput?: boolean; | ||
userTimezone?: string; | ||
onError?: (error: Error) => void; | ||
}; | ||
|
||
export const DateTimeInput = ({ | ||
date, | ||
onChange, | ||
isDateTimeInput, | ||
userTimezone, | ||
}: DateTimeInputProps) => { | ||
const [hasError, setHasError] = useState(false); | ||
|
||
const handleParseDateToString = useCallback( | ||
(date: any) => { | ||
return parseDateToString({ | ||
date, | ||
isDateTimeInput: isDateTimeInput === true, | ||
userTimezone, | ||
}); | ||
}, | ||
[isDateTimeInput, userTimezone], | ||
); | ||
const { dateFormat } = useRecoilValue(dateTimeFormatState); | ||
const { parseToString, parseToDate } = useDateParser({ | ||
isDateTimeInput: isDateTimeInput === true, | ||
}); | ||
|
||
const handleParseStringToDate = (str: string) => { | ||
const date = parseStringToDate({ | ||
dateAsString: str, | ||
isDateTimeInput: isDateTimeInput === true, | ||
userTimezone, | ||
}); | ||
const date = parseToDate(str); | ||
|
||
setHasError(isNull(date) === true); | ||
|
||
return date; | ||
}; | ||
|
||
const pattern = isDateTimeInput ? DATE_TIME_MASK : DATE_MASK; | ||
const pattern = isDateTimeInput | ||
? getDateTimeMask(dateFormat) | ||
: getDateMask(dateFormat); | ||
const blocks = isDateTimeInput ? DATE_TIME_BLOCKS : DATE_BLOCKS; | ||
|
||
const { ref, setValue, value } = useIMask( | ||
|
@@ -89,18 +79,14 @@ export const DateTimeInput = ({ | |
blocks, | ||
min: MIN_DATE, | ||
max: MAX_DATE, | ||
format: handleParseDateToString, | ||
format: (date: any) => parseToString(date), | ||
parse: handleParseStringToDate, | ||
lazy: false, | ||
autofix: true, | ||
}, | ||
{ | ||
onComplete: (value) => { | ||
const parsedDate = parseStringToDate({ | ||
dateAsString: value, | ||
isDateTimeInput: isDateTimeInput === true, | ||
userTimezone, | ||
}); | ||
const parsedDate = parseToDate(value); | ||
|
||
onChange?.(parsedDate); | ||
}, | ||
|
@@ -115,23 +101,28 @@ export const DateTimeInput = ({ | |
return; | ||
} | ||
|
||
setValue( | ||
parseDateToString({ | ||
date: date, | ||
isDateTimeInput: isDateTimeInput === true, | ||
userTimezone, | ||
}), | ||
); | ||
}, [date, setValue, isDateTimeInput, userTimezone]); | ||
setValue(parseToString(date)); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [date, setValue]); | ||
etiennejouan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const getPlaceholder = () => { | ||
const formatMap: Record<DateFormat, string> = { | ||
[DateFormat.SYSTEM]: 'mm/dd/yyyy', | ||
[DateFormat.MONTH_FIRST]: 'mm/dd/yyyy', | ||
[DateFormat.DAY_FIRST]: 'dd/mm/yyyy', | ||
[DateFormat.YEAR_FIRST]: 'yyyy-mm-dd', | ||
}; | ||
|
||
const dateFormatStr = formatMap[dateFormat]; | ||
return `Type date${isDateTimeInput ? ' and time' : ` (${dateFormatStr})`}`; | ||
}; | ||
|
||
return ( | ||
<StyledInputContainer> | ||
<StyledInput | ||
type="text" | ||
ref={ref as any} | ||
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. style: Type assertion to 'any' could be replaced with proper typing of the ref |
||
placeholder={`Type date${ | ||
isDateTimeInput ? ' and time' : ' (mm/dd/yyyy)' | ||
}`} | ||
placeholder={getPlaceholder()} | ||
etiennejouan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
value={value} | ||
onChange={() => {}} // Prevent React warning | ||
hasError={hasError} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,13 @@ | ||
export const DATE_MASK = 'm`/d`/Y`'; // See https://imask.js.org/guide.html#masked-date | ||
import { DateFormat } from '~/modules/localization/constants/DateFormat'; | ||
|
||
export const getDateMask = (dateFormat: DateFormat): string => { | ||
etiennejouan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
switch (dateFormat) { | ||
case DateFormat.DAY_FIRST: | ||
return 'd`/m`/Y`'; | ||
case DateFormat.YEAR_FIRST: | ||
return 'Y`-m`-d`'; | ||
etiennejouan marked this conversation as resolved.
Show resolved
Hide resolved
etiennejouan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
case DateFormat.MONTH_FIRST: | ||
default: | ||
return 'm`/d`/Y`'; | ||
} | ||
}; |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,8 @@ | ||
import { TIME_MASK } from '@/ui/input/components/internal/date/constants/TimeMask'; | ||
|
||
export const DATE_TIME_MASK = `m\`/d\`/Y\` ${TIME_MASK}`; | ||
import { DateFormat } from '@/localization/constants/DateFormat'; | ||
import { getDateMask } from './DateMask'; | ||
|
||
export const getDateTimeMask = (dateFormat: DateFormat): string => { | ||
etiennejouan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return `${getDateMask(dateFormat)} ${TIME_MASK}`; | ||
}; |
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. style: Consider keeping this file but deprecating the constant with a warning message directing users to getDateFormatString for smoother migration |
This file was deleted.
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -1,21 +1,21 @@ | ||||||||||
import { DATE_PARSER_FORMAT } from '@/ui/input/components/internal/date/constants/DateParserFormat'; | ||||||||||
import { DATE_TIME_PARSER_FORMAT } from '@/ui/input/components/internal/date/constants/DateTimeParserFormat'; | ||||||||||
import { DateFormat } from '@/localization/constants/DateFormat'; | ||||||||||
import { DateTime } from 'luxon'; | ||||||||||
import { getDateFormatString } from '~/utils/date-utils'; | ||||||||||
|
||||||||||
type ParseStringToDateArgs = { | ||||||||||
dateAsString: string; | ||||||||||
isDateTimeInput: boolean; | ||||||||||
userTimezone: string | undefined; | ||||||||||
dateFormat: DateFormat; | ||||||||||
Comment on lines
8
to
+9
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. logic: userTimezone is marked as optional but the code doesn't handle undefined case, which could cause runtime errors
Suggested change
|
||||||||||
}; | ||||||||||
|
||||||||||
export const parseStringToDate = ({ | ||||||||||
dateAsString, | ||||||||||
isDateTimeInput, | ||||||||||
userTimezone, | ||||||||||
dateFormat, | ||||||||||
}: ParseStringToDateArgs) => { | ||||||||||
const parsingFormat = isDateTimeInput | ||||||||||
? DATE_TIME_PARSER_FORMAT | ||||||||||
: DATE_PARSER_FORMAT; | ||||||||||
const parsingFormat = getDateFormatString(dateFormat, isDateTimeInput); | ||||||||||
|
||||||||||
const parsedDate = isDateTimeInput | ||||||||||
? DateTime.fromFormat(dateAsString, parsingFormat, { zone: userTimezone }) | ||||||||||
|
Uh oh!
There was an error while loading. Please reload this page.