Date formatting per workspace member settings (#6408)

Implement date formatting per workspace member settings

We'll need another round to maybe initialize all workspaces on the
default settings.

For now the default behavior is to take system settings if nothing is
found in DB.

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Lucas Bordeau
2024-07-30 14:52:10 +02:00
committed by GitHub
parent 45ebb0b824
commit ccf4d1eeec
64 changed files with 1176 additions and 165 deletions

View File

@ -1,8 +1,8 @@
import { useCallback, useEffect, useState } from 'react';
import { useIMask } from 'react-imask';
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';
import { DATE_BLOCKS } from '@/ui/input/components/internal/date/constants/DateBlocks';
import { DATE_MASK } from '@/ui/input/components/internal/date/constants/DateMask';
@ -41,6 +41,7 @@ type DateTimeInputProps = {
onChange?: (date: Date | null) => void;
date: Date | null;
isDateTimeInput?: boolean;
userTimezone?: string;
onError?: (error: Error) => void;
};
@ -48,6 +49,7 @@ export const DateTimeInput = ({
date,
onChange,
isDateTimeInput,
userTimezone,
}: DateTimeInputProps) => {
const parsingFormat = isDateTimeInput ? 'MM/dd/yyyy HH:mm' : 'MM/dd/yyyy';
@ -55,7 +57,7 @@ export const DateTimeInput = ({
const parseDateToString = useCallback(
(date: any) => {
const dateParsed = DateTime.fromJSDate(date);
const dateParsed = DateTime.fromJSDate(date, { zone: userTimezone });
const dateWithoutTime = DateTime.fromJSDate(date)
.toLocal()
@ -70,19 +72,19 @@ export const DateTimeInput = ({
});
const formattedDate = isDateTimeInput
? dateParsed.toFormat(parsingFormat)
? dateParsed.setZone(userTimezone).toFormat(parsingFormat)
: dateWithoutTime.toFormat(parsingFormat);
return formattedDate;
},
[parsingFormat, isDateTimeInput],
[parsingFormat, isDateTimeInput, userTimezone],
);
const parseStringToDate = (str: string) => {
setHasError(false);
const parsedDate = isDateTimeInput
? DateTime.fromFormat(str, parsingFormat)
? DateTime.fromFormat(str, parsingFormat, { zone: userTimezone })
: DateTime.fromFormat(str, parsingFormat, { zone: 'utc' });
const isValid = parsedDate.isValid;

View File

@ -17,6 +17,8 @@ import { MenuItemLeftContent } from '@/ui/navigation/menu-item/internals/compone
import { StyledHoverableMenuItemBase } from '@/ui/navigation/menu-item/internals/components/StyledMenuItemBase';
import { isDefined } from '~/utils/isDefined';
import { UserContext } from '@/users/contexts/UserContext';
import { useContext } from 'react';
import 'react-datepicker/dist/react-datepicker.css';
export const months = [
@ -322,6 +324,8 @@ export const InternalDatePicker = ({
}: InternalDatePickerProps) => {
const internalDate = date ?? new Date();
const { timeZone } = useContext(UserContext);
const { closeDropdown } = useDropdown(MONTH_AND_YEAR_DROPDOWN_ID);
const { closeDropdown: closeDropdownMonthSelect } = useDropdown(
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
@ -377,10 +381,56 @@ export const InternalDatePicker = ({
onChange?.(newDate);
};
const handleAddMonth = () => {
const dateParsed = DateTime.fromJSDate(internalDate, { zone: timeZone })
.plus({ months: 1 })
.toJSDate();
onChange?.(dateParsed);
};
const handleSubtractMonth = () => {
const dateParsed = DateTime.fromJSDate(internalDate, { zone: timeZone })
.minus({ months: 1 })
.toJSDate();
onChange?.(dateParsed);
};
const handleChangeYear = (year: number) => {
const newDate = new Date(internalDate);
newDate.setFullYear(year);
onChange?.(newDate);
const dateParsed = DateTime.fromJSDate(internalDate, { zone: timeZone })
.set({ year: year })
.toJSDate();
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();
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();
handleMouseSelect?.(dateParsed);
};
const dateWithoutTime = DateTime.fromJSDate(internalDate)
@ -395,7 +445,29 @@ export const InternalDatePicker = ({
millisecond: 0,
})
.toJSDate();
const dateToUse = isDateTimeInput ? date : dateWithoutTime;
const dateParsed = DateTime.fromJSDate(internalDate, {
zone: isDateTimeInput ? timeZone : 'local',
});
// 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 dateToUse = isDateTimeInput ? endOfDayInLocalTimezone : dateWithoutTime;
return (
<StyledContainer onKeyDown={handleKeyDown}>
@ -405,27 +477,16 @@ export const InternalDatePicker = ({
selected={dateToUse}
openToDate={isDefined(dateToUse) ? dateToUse : undefined}
disabledKeyboardNavigation
onChange={(newDate) => {
newDate?.setHours(internalDate.getUTCHours());
newDate?.setUTCMinutes(internalDate.getUTCMinutes());
onChange?.(newDate);
}}
onChange={handleDateChange}
customInput={
<DateTimeInput
date={dateToUse}
date={internalDate}
isDateTimeInput={isDateTimeInput}
onChange={onChange}
userTimezone={timeZone}
/>
}
onMonthChange={(newDate) => {
onChange?.(newDate);
}}
onYearChange={(newDate) => {
onChange?.(newDate);
}}
renderCustomHeader={({
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
@ -434,6 +495,7 @@ export const InternalDatePicker = ({
date={internalDate}
isDateTimeInput={isDateTimeInput}
onChange={onChange}
userTimezone={timeZone}
/>
<StyledCustomDatePickerHeader>
<Select
@ -441,51 +503,33 @@ export const InternalDatePicker = ({
options={months}
disableBlur
onChange={handleChangeMonth}
value={internalDate.getUTCMonth()}
value={endOfDayInLocalTimezone.getMonth()}
fullWidth
/>
<Select
dropdownId={MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID}
onChange={handleChangeYear}
value={internalDate.getUTCFullYear()}
value={endOfDayInLocalTimezone.getFullYear()}
options={years}
disableBlur
fullWidth
/>
<LightIconButton
Icon={IconChevronLeft}
onClick={() => decreaseMonth()}
onClick={handleSubtractMonth}
size="medium"
disabled={prevMonthButtonDisabled}
/>
<LightIconButton
Icon={IconChevronRight}
onClick={() => increaseMonth()}
onClick={handleAddMonth}
size="medium"
disabled={nextMonthButtonDisabled}
/>
</StyledCustomDatePickerHeader>
</>
)}
onSelect={(date: Date) => {
const dateParsedWithoutTime = DateTime.fromObject(
{
day: date.getDate(),
month: date.getMonth() + 1,
year: date.getFullYear(),
hour: 0,
minute: 0,
second: 0,
},
{ zone: 'utc' },
).toJSDate();
const dateForUpdate = isDateTimeInput
? date
: dateParsedWithoutTime;
handleMouseSelect?.(dateForUpdate);
}}
onSelect={handleDateSelect}
/>
</div>
{clearable && (