Remove Luxon from codebase (#14448)

Fixes #14444 

We shouldn't have 2 libraries to manage dates, one is enough
This commit is contained in:
Félix Malfait
2025-09-12 23:25:05 +02:00
committed by GitHub
parent 1e97ad48c0
commit a0cfff6000
21 changed files with 215 additions and 290 deletions
@@ -10,7 +10,6 @@ import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { id } from 'date-fns/locale';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div`
@@ -37,7 +36,9 @@ export const AdvancedFilterRootRecordFilterGroup = () => {
}
return (
<ScrollWrapper componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}>
<ScrollWrapper
componentInstanceId={`scroll-wrapper-dropdown-menu-${rootRecordFilterGroup.id}`}
>
<DropdownContent widthInPixels={ADVANCED_FILTER_DROPDOWN_CONTENT_WIDTH}>
<StyledContainer>
{childRecordFiltersAndRecordFilterGroups.map(
@@ -114,7 +114,9 @@ export const FormDateTimeFieldInput = ({
);
const draftValueAsDate =
isDefined(draftValue.value) && isNonEmptyString(draftValue.value)
isDefined(draftValue.value) &&
isNonEmptyString(draftValue.value) &&
draftValue.type === 'static'
? new Date(draftValue.value)
: null;
@@ -10,7 +10,6 @@ import {
waitForElementToBeRemoved,
within,
} from '@storybook/test';
import { DateTime } from 'luxon';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
@@ -208,23 +207,21 @@ export const DefaultsToMinValueWhenTypingReallyOldDate: Story = {
}),
);
const expectedDate = DateTime.fromJSDate(MIN_DATE)
.toLocal()
.set({
day: MIN_DATE.getUTCDate(),
month: MIN_DATE.getUTCMonth() + 1,
year: MIN_DATE.getUTCFullYear(),
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
});
const expectedDate = new Date(
MIN_DATE.getUTCFullYear(),
MIN_DATE.getUTCMonth(),
MIN_DATE.getUTCDate(),
0,
0,
0,
0,
);
const selectedDay = within(datePicker).getByRole('option', {
selected: true,
name: (accessibleName) => {
// The name looks like "Choose Sunday, December 31st, 1899"
return accessibleName.includes(expectedDate.toFormat('yyyy'));
return accessibleName.includes(expectedDate.getFullYear().toString());
},
});
expect(selectedDay).toBeVisible();
@@ -264,23 +261,23 @@ export const DefaultsToMaxValueWhenTypingReallyFarDate: Story = {
);
}),
waitFor(() => {
const expectedDate = DateTime.fromJSDate(MAX_DATE)
.toLocal()
.set({
day: MAX_DATE.getUTCDate(),
month: MAX_DATE.getUTCMonth() + 1,
year: MAX_DATE.getUTCFullYear(),
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
});
const expectedDate = new Date(
MAX_DATE.getUTCFullYear(),
MAX_DATE.getUTCMonth(),
MAX_DATE.getUTCDate(),
0,
0,
0,
0,
);
const selectedDay = within(datePicker).getByRole('option', {
selected: true,
name: (accessibleName) => {
// The name looks like "Choose Thursday, December 30th, 2100"
return accessibleName.includes(expectedDate.toFormat('yyyy'));
return accessibleName.includes(
expectedDate.getFullYear().toString(),
);
},
});
expect(selectedDay).toBeVisible();
@@ -315,13 +312,7 @@ export const SwitchesToStandaloneVariable: Story = {
const variableTag = await canvas.findByText('Creation date');
expect(variableTag).toBeVisible();
const removeVariableButton = canvasElement.querySelector(
'button .tabler-icon-x',
);
if (!removeVariableButton) {
throw new Error('Remove variable button not found');
}
const removeVariableButton = canvas.getByLabelText('Remove variable');
await Promise.all([
userEvent.click(removeVariableButton),
@@ -342,8 +333,12 @@ export const ClickingOutsideDoesNotResetInputState: Story = {
onChange: fn(),
},
play: async ({ canvasElement, args }) => {
if (!args.defaultValue) {
throw new Error('This test requires a defaultValue');
}
const defaultValueAsDisplayString = parseDateToString({
date: new Date(args.defaultValue!),
date: new Date(args.defaultValue),
isDateTimeInput: false,
userTimezone: undefined,
});
@@ -11,7 +11,6 @@ import {
waitForElementToBeRemoved,
within,
} from '@storybook/test';
import { DateTime } from 'luxon';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
@@ -257,23 +256,23 @@ export const DefaultsToMinValueWhenTypingReallyOldDate: Story = {
);
}),
waitFor(() => {
const expectedDate = DateTime.fromJSDate(MIN_DATE)
.toLocal()
.set({
day: MIN_DATE.getUTCDate(),
month: MIN_DATE.getUTCMonth() + 1,
year: MIN_DATE.getUTCFullYear(),
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
});
const expectedDate = new Date(
MIN_DATE.getUTCFullYear(),
MIN_DATE.getUTCMonth(),
MIN_DATE.getUTCDate(),
0,
0,
0,
0,
);
const selectedDay = within(datePicker).getByRole('option', {
selected: true,
name: (accessibleName) => {
// The name looks like "Choose Sunday, December 31st, 1899"
return accessibleName.includes(expectedDate.toFormat('yyyy'));
return accessibleName.includes(
expectedDate.getFullYear().toString(),
);
},
});
expect(selectedDay).toBeVisible();
@@ -315,23 +314,23 @@ export const DefaultsToMaxValueWhenTypingReallyFarDate: Story = {
);
}),
waitFor(() => {
const expectedDate = DateTime.fromJSDate(MAX_DATE)
.toLocal()
.set({
day: MAX_DATE.getUTCDate(),
month: MAX_DATE.getUTCMonth() + 1,
year: MAX_DATE.getUTCFullYear(),
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
});
const expectedDate = new Date(
MAX_DATE.getUTCFullYear(),
MAX_DATE.getUTCMonth(),
MAX_DATE.getUTCDate(),
0,
0,
0,
0,
);
const selectedDay = within(datePicker).getByRole('option', {
selected: true,
name: (accessibleName) => {
// The name looks like "Choose Thursday, December 30th, 2100"
return accessibleName.includes(expectedDate.toFormat('yyyy'));
return accessibleName.includes(
expectedDate.getFullYear().toString(),
);
},
});
expect(selectedDay).toBeVisible();
@@ -366,13 +365,7 @@ export const SwitchesToStandaloneVariable: Story = {
const variableTag = await canvas.findByText('Creation date');
expect(variableTag).toBeVisible();
const removeVariableButton = canvasElement.querySelector(
'button .tabler-icon-x',
);
if (!removeVariableButton) {
throw new Error('Remove variable button not found');
}
const removeVariableButton = canvas.getByLabelText('Remove variable');
await Promise.all([
userEvent.click(removeVariableButton),
@@ -393,8 +386,12 @@ export const ClickingOutsideDoesNotResetInputState: Story = {
onChange: fn(),
},
play: async ({ canvasElement, args }) => {
if (!args.defaultValue) {
throw new Error('This test requires a defaultValue');
}
const defaultValueAsDisplayString = parseDateToString({
date: new Date(args.defaultValue!),
date: new Date(args.defaultValue),
isDateTimeInput: true,
userTimezone: undefined,
});
@@ -1,4 +1,4 @@
import { DateTime } from 'luxon';
import { parseISO, isEqual, isAfter, isBefore } from 'date-fns';
import { type DateFilter } from '@/object-record//graphql/types/RecordGqlOperationFilter';
@@ -11,10 +11,10 @@ export const isMatchingDateFilter = ({
}) => {
switch (true) {
case dateFilter.eq !== undefined: {
return DateTime.fromISO(value).equals(DateTime.fromISO(dateFilter.eq));
return isEqual(parseISO(value), parseISO(dateFilter.eq));
}
case dateFilter.neq !== undefined: {
return !DateTime.fromISO(value).equals(DateTime.fromISO(dateFilter.neq));
return !isEqual(parseISO(value), parseISO(dateFilter.neq));
}
case dateFilter.in !== undefined: {
return dateFilter.in.includes(value);
@@ -27,16 +27,20 @@ export const isMatchingDateFilter = ({
}
}
case dateFilter.gt !== undefined: {
return DateTime.fromISO(value) > DateTime.fromISO(dateFilter.gt);
return isAfter(parseISO(value), parseISO(dateFilter.gt));
}
case dateFilter.gte !== undefined: {
return DateTime.fromISO(value) >= DateTime.fromISO(dateFilter.gte);
const valueDate = parseISO(value);
const filterDate = parseISO(dateFilter.gte);
return isAfter(valueDate, filterDate) || isEqual(valueDate, filterDate);
}
case dateFilter.lt !== undefined: {
return DateTime.fromISO(value) < DateTime.fromISO(dateFilter.lt);
return isBefore(parseISO(value), parseISO(dateFilter.lt));
}
case dateFilter.lte !== undefined: {
return DateTime.fromISO(value) <= DateTime.fromISO(dateFilter.lte);
const valueDate = parseISO(value);
const filterDate = parseISO(dateFilter.lte);
return isBefore(valueDate, filterDate) || isEqual(valueDate, filterDate);
}
default: {
throw new Error(
@@ -1,4 +1,4 @@
import { DateTime } from 'luxon';
import { formatISO, parseISO, subDays, subHours } from 'date-fns';
import { type BlocklistItem } from '@/accounts/types/BlocklistItem';
@@ -8,9 +8,7 @@ export const mockedBlocklist: BlocklistItem[] = [
handle: 'test1@twenty.com',
workspaceMemberId: '1',
createdAt:
DateTime.fromISO('2023-04-26T10:12:42.33625+00:00')
.minus({ hours: 2 })
.toISO() ?? '',
formatISO(subHours(parseISO('2023-04-26T10:12:42.33625+00:00'), 2)) ?? '',
__typename: 'BlocklistItem',
},
{
@@ -18,9 +16,7 @@ export const mockedBlocklist: BlocklistItem[] = [
handle: 'test2@twenty.com',
workspaceMemberId: '1',
createdAt:
DateTime.fromISO('2023-04-26T10:12:42.33625+00:00')
.minus({ days: 2 })
.toISO() ?? '',
formatISO(subDays(parseISO('2023-04-26T10:12:42.33625+00:00'), 2)) ?? '',
__typename: 'BlocklistItem',
},
{
@@ -28,9 +24,7 @@ export const mockedBlocklist: BlocklistItem[] = [
handle: 'test3@twenty.com',
workspaceMemberId: '1',
createdAt:
DateTime.fromISO('2023-04-26T10:12:42.33625+00:00')
.minus({ days: 3 })
.toISO() ?? '',
formatISO(subDays(parseISO('2023-04-26T10:12:42.33625+00:00'), 3)) ?? '',
__typename: 'BlocklistItem',
},
{
@@ -38,9 +32,7 @@ export const mockedBlocklist: BlocklistItem[] = [
handle: '@twenty.com',
workspaceMemberId: '1',
createdAt:
DateTime.fromISO('2023-04-26T10:12:42.33625+00:00')
.minus({ days: 4 })
.toISO() ?? '',
formatISO(subDays(parseISO('2023-04-26T10:12:42.33625+00:00'), 4)) ?? '',
__typename: 'BlocklistItem',
},
];
@@ -1,4 +1,4 @@
import { DateTime } from 'luxon';
import { differenceInDays, parseISO, addDays } from 'date-fns';
export const computeNewExpirationDate = (
expiresAt: string | null | undefined,
@@ -7,8 +7,8 @@ export const computeNewExpirationDate = (
if (!expiresAt) {
return null;
}
const days = DateTime.fromISO(expiresAt).diff(DateTime.fromISO(createdAt), [
'days',
]).days;
return DateTime.utc().plus({ days }).toISO();
const expirationDate = parseISO(expiresAt);
const creationDate = parseISO(createdAt);
const days = differenceInDays(expirationDate, creationDate);
return addDays(new Date(), days).toISOString();
};
@@ -1,16 +1,15 @@
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { DateTime } from 'luxon';
import { differenceInYears, parseISO } from 'date-fns';
import { NEVER_EXPIRE_DELTA_IN_YEARS } from '@/settings/developers/constants/NeverExpireDeltaInYears';
import { beautifyDateDiff } from '~/utils/date-utils';
export const doesNeverExpire = (expiresAt: string) => {
const dateDiff = DateTime.fromISO(expiresAt).diff(DateTime.now(), [
'years',
'days',
]);
return dateDiff.years > NEVER_EXPIRE_DELTA_IN_YEARS / 10;
const expirationDate = parseISO(expiresAt);
const now = new Date();
const yearsDiff = differenceInYears(expirationDate, now);
return yearsDiff > NEVER_EXPIRE_DELTA_IN_YEARS / 10;
};
export const isExpired = (expiresAt: string | null) => {
@@ -1,5 +1,4 @@
import styled from '@emotion/styled';
import { DateTime } from 'luxon';
import { useRecoilValue } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
@@ -60,17 +59,15 @@ export const AbsoluteDatePickerHeader = ({
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const userLocale = currentWorkspaceMember?.locale ?? SOURCE_LOCALE;
const endOfDayDateTimeInLocalTimezone = DateTime.now().set({
day: date.getDate(),
month: date.getMonth() + 1,
year: date.getFullYear(),
hour: 23,
minute: 59,
second: 59,
millisecond: 999,
});
const endOfDayInLocalTimezone = endOfDayDateTimeInLocalTimezone.toJSDate();
const endOfDayInLocalTimezone = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate(),
23,
59,
59,
999,
);
return (
<>
@@ -1,6 +1,6 @@
import styled from '@emotion/styled';
import { DateTime } from 'luxon';
import { lazy, Suspense, useContext, type ComponentType } from 'react';
import { addMonths, setDate, setMonth, setYear, subMonths } from 'date-fns';
import { lazy, Suspense, type ComponentType } from 'react';
import type { ReactDatePickerProps as ReactDatePickerLibProps } from 'react-datepicker';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
@@ -13,7 +13,6 @@ import { DateTimeInput } from '@/ui/input/components/internal/date/components/Da
import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader';
import { getHighlightedDates } from '@/ui/input/components/internal/date/utils/getHighlightedDates';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { UserContext } from '@/users/contexts/UserContext';
import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
import 'react-datepicker/dist/react-datepicker.css';
@@ -352,7 +351,6 @@ export const DateTimePicker = ({
}: DateTimePickerProps) => {
const internalDate = date ?? new Date();
const { timeZone } = useContext(UserContext);
const theme = useTheme();
const { closeDropdown: closeDropdownMonthSelect } = useCloseDropdown();
@@ -380,90 +378,60 @@ export const DateTimePicker = ({
};
const handleAddMonth = () => {
const dateParsed = DateTime.fromJSDate(internalDate, { zone: timeZone })
.plus({ months: 1 })
.toJSDate();
const dateParsed = addMonths(internalDate, 1);
onChange?.(dateParsed);
};
const handleSubtractMonth = () => {
const dateParsed = DateTime.fromJSDate(internalDate, { zone: timeZone })
.minus({ months: 1 })
.toJSDate();
const dateParsed = subMonths(internalDate, 1);
onChange?.(dateParsed);
};
const handleChangeYear = (year: number) => {
const dateParsed = DateTime.fromJSDate(internalDate, { zone: timeZone })
.set({ year: year })
.toJSDate();
const dateParsed = setYear(internalDate, year);
onChange?.(dateParsed);
};
const handleDateChange = (date: Date) => {
const dateParsed = DateTime.fromJSDate(internalDate, {
zone: isDateTimeInput ? timeZone : 'local',
})
.set({
day: date.getDate(),
month: date.getMonth() + 1,
year: date.getFullYear(),
})
.toJSDate();
let dateParsed = setYear(internalDate, date.getFullYear());
dateParsed = setMonth(dateParsed, date.getMonth());
dateParsed = setDate(dateParsed, date.getDate());
onChange?.(dateParsed);
};
const handleDateSelect = (date: Date) => {
const dateParsed = DateTime.fromJSDate(internalDate, {
zone: isDateTimeInput ? timeZone : 'local',
})
.set({
day: date.getDate(),
month: date.getMonth() + 1,
year: date.getFullYear(),
})
.toJSDate();
let dateParsed = setYear(internalDate, date.getFullYear());
dateParsed = setMonth(dateParsed, date.getMonth());
dateParsed = setDate(dateParsed, date.getDate());
handleClose?.(dateParsed);
};
const dateWithoutTime = DateTime.fromJSDate(internalDate)
.toLocal()
.set({
day: internalDate.getUTCDate(),
month: internalDate.getUTCMonth() + 1,
year: internalDate.getUTCFullYear(),
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
})
.toJSDate();
const dateParsed = DateTime.fromJSDate(internalDate, {
zone: isDateTimeInput ? timeZone : 'local',
});
const dateWithoutTime = new Date(
internalDate.getUTCFullYear(),
internalDate.getUTCMonth(),
internalDate.getUTCDate(),
0,
0,
0,
0,
);
// We have to force a end of day on the computer local timezone with the given date
// Because JS Date API cannot hold a timezone other than the local one
// And if we don't do that workaround we will have problems when changing the date
// Because the shown date will have 1 day more or less than the real date
// Leading to bugs where we select 1st of January and it shows 31st of December for example
const endOfDayDateTimeInLocalTimezone = DateTime.now().set({
day: dateParsed.get('day'),
month: dateParsed.get('month'),
year: dateParsed.get('year'),
hour: 23,
minute: 59,
second: 59,
millisecond: 999,
});
const endOfDayInLocalTimezone = endOfDayDateTimeInLocalTimezone.toJSDate();
const endOfDayInLocalTimezone = new Date(
internalDate.getFullYear(),
internalDate.getMonth(),
internalDate.getDate(),
23,
59,
59,
999,
);
const dateToUse = isDateTimeInput ? endOfDayInLocalTimezone : dateWithoutTime;
@@ -1,5 +1,7 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { DateTime } from 'luxon';
import { format } from 'date-fns';
import { formatInTimeZone } from 'date-fns-tz';
import { isDefined } from 'twenty-shared/utils';
import { getDateFormatString } from '~/utils/date-utils';
type ParseDateToStringArgs = {
@@ -17,23 +19,14 @@ export const parseDateToString = ({
}: ParseDateToStringArgs) => {
const parsingFormat = getDateFormatString(dateFormat, isDateTimeInput);
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;
if (isDateTimeInput && isDefined(userTimezone)) {
return formatInTimeZone(date, userTimezone, parsingFormat);
} else if (isDateTimeInput) {
return format(date, parsingFormat);
} else {
const dateWithoutTime = new Date(
Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()),
);
return format(dateWithoutTime, parsingFormat);
}
};
@@ -1,5 +1,7 @@
import { type DateFormat } from '@/localization/constants/DateFormat';
import { DateTime } from 'luxon';
import { isValid, parse } from 'date-fns';
import { zonedTimeToUtc } from 'date-fns-tz';
import { isDefined } from 'twenty-shared/utils';
import { getDateFormatString } from '~/utils/date-utils';
type ParseStringToDateArgs = {
@@ -16,18 +18,17 @@ export const parseStringToDate = ({
dateFormat,
}: ParseStringToDateArgs) => {
const parsingFormat = getDateFormatString(dateFormat, isDateTimeInput);
const referenceDate = new Date();
const parsedDate = isDateTimeInput
? DateTime.fromFormat(dateAsString, parsingFormat, { zone: userTimezone })
: DateTime.fromFormat(dateAsString, parsingFormat, { zone: 'utc' });
const parsedDate = parse(dateAsString, parsingFormat, referenceDate);
const isValid = parsedDate.isValid;
if (!isValid) {
if (!isValid(parsedDate)) {
return null;
}
const jsDate = parsedDate.toJSDate();
if (isDateTimeInput && isDefined(userTimezone)) {
return zonedTimeToUtc(parsedDate, userTimezone);
}
return jsDate;
return parsedDate;
};