Add date form field (#9021)

- Add an option to hide the input from the
`packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx`
component
- Create a form field for dates
  - Integrate the picker
- Create an input **without a mask** to let the user types a date and
validate it by pressing the Enter key
- Extract some utils to parse and format dates
This commit is contained in:
Baptiste Devessier
2024-12-17 11:11:19 +01:00
committed by GitHub
parent 5dfcc413cf
commit 5bd73e0df1
12 changed files with 886 additions and 55 deletions

View File

@ -1,5 +1,4 @@
import styled from '@emotion/styled';
import { useRef, useState } from 'react';
import { Nullable } from 'twenty-ui';
import {
@ -10,8 +9,9 @@ import {
} from '@/ui/input/components/internal/date/components/InternalDatePicker';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useRef, useState } from 'react';
const StyledCalendarContainer = styled.div`
export const StyledCalendarContainer = styled.div`
background: ${({ theme }) => theme.background.transparent.secondary};
backdrop-filter: ${({ theme }) => theme.blur.medium};
border: 1px solid ${({ theme }) => theme.border.color.light};
@ -32,6 +32,7 @@ export type DateInputProps = {
isDateTimeInput?: boolean;
onClear?: () => void;
onSubmit?: (newDate: Nullable<Date>) => void;
hideHeaderInput?: boolean;
};
export const DateInput = ({
@ -44,6 +45,7 @@ export const DateInput = ({
isDateTimeInput,
onClear,
onSubmit,
hideHeaderInput,
}: DateInputProps) => {
const [internalValue, setInternalValue] = useState(value);
@ -97,6 +99,7 @@ export const DateInput = ({
onEnter={onEnter}
onEscape={onEscape}
onClear={handleClear}
hideHeaderInput={hideHeaderInput}
/>
</StyledCalendarContainer>
</div>

View File

@ -38,6 +38,7 @@ type AbsoluteDatePickerHeaderProps = {
nextMonthButtonDisabled: boolean;
isDateTimeInput?: boolean;
timeZone: string;
hideInput?: boolean;
};
export const AbsoluteDatePickerHeader = ({
@ -51,6 +52,7 @@ export const AbsoluteDatePickerHeader = ({
nextMonthButtonDisabled,
isDateTimeInput,
timeZone,
hideInput = false,
}: AbsoluteDatePickerHeaderProps) => {
const endOfDayDateTimeInLocalTimezone = DateTime.now().set({
day: date.getDate(),
@ -66,12 +68,15 @@ export const AbsoluteDatePickerHeader = ({
return (
<>
<DateTimeInput
date={date}
isDateTimeInput={isDateTimeInput}
onChange={onChange}
userTimezone={timeZone}
/>
{!hideInput && (
<DateTimeInput
date={date}
isDateTimeInput={isDateTimeInput}
onChange={onChange}
userTimezone={timeZone}
/>
)}
<StyledCustomDatePickerHeader>
<Select
dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID}

View File

@ -1,6 +1,5 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { DateTime } from 'luxon';
import { useCallback, useEffect, useState } from 'react';
import { useIMask } from 'react-imask';
@ -10,6 +9,10 @@ import { DATE_TIME_BLOCKS } from '@/ui/input/components/internal/date/constants/
import { DATE_TIME_MASK } 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-ui';
const StyledInputContainer = styled.div`
align-items: center;
@ -53,53 +56,29 @@ export const DateTimeInput = ({
isDateTimeInput,
userTimezone,
}: DateTimeInputProps) => {
const parsingFormat = isDateTimeInput ? 'MM/dd/yyyy HH:mm' : 'MM/dd/yyyy';
const [hasError, setHasError] = useState(false);
const parseDateToString = useCallback(
const handleParseDateToString = useCallback(
(date: any) => {
const dateParsed = DateTime.fromJSDate(date, { zone: userTimezone });
const dateWithoutTime = DateTime.fromJSDate(date)
.toLocal()
.set({
day: date.getUTCDate(),
month: date.getUTCMonth() + 1,
year: date.getUTCFullYear(),
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
});
const formattedDate = isDateTimeInput
? dateParsed.setZone(userTimezone).toFormat(parsingFormat)
: dateWithoutTime.toFormat(parsingFormat);
return formattedDate;
return parseDateToString({
date,
isDateTimeInput: isDateTimeInput === true,
userTimezone,
});
},
[parsingFormat, isDateTimeInput, userTimezone],
[isDateTimeInput, userTimezone],
);
const parseStringToDate = (str: string) => {
setHasError(false);
const handleParseStringToDate = (str: string) => {
const date = parseStringToDate({
dateAsString: str,
isDateTimeInput: isDateTimeInput === true,
userTimezone,
});
const parsedDate = isDateTimeInput
? DateTime.fromFormat(str, parsingFormat, { zone: userTimezone })
: DateTime.fromFormat(str, parsingFormat, { zone: 'utc' });
setHasError(isNull(date) === true);
const isValid = parsedDate.isValid;
if (!isValid) {
setHasError(true);
return null;
}
const jsDate = parsedDate.toJSDate();
return jsDate;
return date;
};
const pattern = isDateTimeInput ? DATE_TIME_MASK : DATE_MASK;
@ -112,14 +91,18 @@ export const DateTimeInput = ({
blocks,
min: MIN_DATE,
max: MAX_DATE,
format: parseDateToString,
parse: parseStringToDate,
format: handleParseDateToString,
parse: handleParseStringToDate,
lazy: false,
autofix: true,
},
{
onComplete: (value) => {
const parsedDate = parseStringToDate(value);
const parsedDate = parseStringToDate({
dateAsString: value,
isDateTimeInput: isDateTimeInput === true,
userTimezone,
});
onChange?.(parsedDate);
},
@ -130,8 +113,18 @@ export const DateTimeInput = ({
);
useEffect(() => {
setValue(parseDateToString(date));
}, [date, setValue, parseDateToString]);
if (!isDefined(date)) {
return;
}
setValue(
parseDateToString({
date: date,
isDateTimeInput: isDateTimeInput === true,
userTimezone,
}),
);
}, [date, setValue, isDateTimeInput, userTimezone]);
return (
<StyledInputContainer>

View File

@ -9,11 +9,11 @@ import {
StyledHoverableMenuItemBase,
} from 'twenty-ui';
import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { isDefined } from '~/utils/isDefined';
import { AbsoluteDatePickerHeader } from '@/ui/input/components/internal/date/components/AbsoluteDatePickerHeader';
import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput';
import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader';
import { getHighlightedDates } from '@/ui/input/components/internal/date/utils/getHighlightedDates';
import { UserContext } from '@/users/contexts/UserContext';
@ -276,6 +276,7 @@ const StyledButton = styled(MenuItemLeftContent)`
type InternalDatePickerProps = {
isRelative?: boolean;
hideHeaderInput?: boolean;
date: Date | null;
relativeDate?: {
direction: VariableDateViewFilterValueDirection;
@ -317,6 +318,7 @@ export const InternalDatePicker = ({
relativeDate,
onRelativeDateChange,
highlightedDateRange,
hideHeaderInput,
}: InternalDatePickerProps) => {
const internalDate = date ?? new Date();
@ -510,6 +512,7 @@ export const InternalDatePicker = ({
nextMonthButtonDisabled={nextMonthButtonDisabled}
isDateTimeInput={isDateTimeInput}
timeZone={timeZone}
hideInput={hideHeaderInput}
/>
)
}

View File

@ -0,0 +1 @@
export const DATE_PARSER_FORMAT = 'MM/dd/yyyy';

View File

@ -0,0 +1 @@
export const DATE_TIME_PARSER_FORMAT = 'MM/dd/yyyy HH:mm';

View File

@ -0,0 +1,39 @@
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 { DateTime } from 'luxon';
type ParseDateToStringArgs = {
date: Date;
isDateTimeInput: boolean;
userTimezone: string | undefined;
};
export const parseDateToString = ({
date,
isDateTimeInput,
userTimezone,
}: ParseDateToStringArgs) => {
const parsingFormat = isDateTimeInput
? DATE_TIME_PARSER_FORMAT
: DATE_PARSER_FORMAT;
const dateParsed = DateTime.fromJSDate(date, { zone: userTimezone });
const dateWithoutTime = DateTime.fromJSDate(date)
.toLocal()
.set({
day: date.getUTCDate(),
month: date.getUTCMonth() + 1,
year: date.getUTCFullYear(),
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
});
const formattedDate = isDateTimeInput
? dateParsed.setZone(userTimezone).toFormat(parsingFormat)
: dateWithoutTime.toFormat(parsingFormat);
return formattedDate;
};

View File

@ -0,0 +1,33 @@
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 { DateTime } from 'luxon';
type ParseStringToDateArgs = {
dateAsString: string;
isDateTimeInput: boolean;
userTimezone: string | undefined;
};
export const parseStringToDate = ({
dateAsString,
isDateTimeInput,
userTimezone,
}: ParseStringToDateArgs) => {
const parsingFormat = isDateTimeInput
? DATE_TIME_PARSER_FORMAT
: DATE_PARSER_FORMAT;
const parsedDate = isDateTimeInput
? DateTime.fromFormat(dateAsString, parsingFormat, { zone: userTimezone })
: DateTime.fromFormat(dateAsString, parsingFormat, { zone: 'utc' });
const isValid = parsedDate.isValid;
if (!isValid) {
return null;
}
const jsDate = parsedDate.toJSDate();
return jsDate;
};