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:
committed by
GitHub
parent
5dfcc413cf
commit
5bd73e0df1
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export const DATE_PARSER_FORMAT = 'MM/dd/yyyy';
|
||||
@ -0,0 +1 @@
|
||||
export const DATE_TIME_PARSER_FORMAT = 'MM/dd/yyyy HH:mm';
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user