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

@ -6,7 +6,7 @@ import {
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { SettingsAppearance } from '../SettingsAppearance';
import { SettingsAppearance } from '../profile/appearance/components/SettingsAppearance';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/SettingsAppearance',

View File

@ -0,0 +1,145 @@
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { DateFormat } from '@/localization/constants/DateFormat';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
import { getWorkspaceDateFormatFromDateFormat } from '@/localization/utils/getWorkspaceDateFormatFromDateFormat';
import { getWorkspaceTimeFormatFromTimeFormat } from '@/localization/utils/getWorkspaceTimeFormatFromTimeFormat';
import {
WorkspaceMemberDateFormatEnum,
WorkspaceMemberTimeFormatEnum,
} from '~/generated/graphql';
import { DateTimeSettingsDateFormatSelect } from '~/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect';
import { DateTimeSettingsTimeFormatSelect } from '~/pages/settings/profile/appearance/components/DateTimeSettingsTimeFormatSelect';
import { DateTimeSettingsTimeZoneSelect } from '~/pages/settings/profile/appearance/components/DateTimeSettingsTimeZoneSelect';
import { isDefined } from '~/utils/isDefined';
import { isEmptyObject } from '~/utils/isEmptyObject';
import { logError } from '~/utils/logError';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
`;
export const DateTimeSettings = () => {
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
currentWorkspaceMemberState,
);
const [dateTimeFormat, setDateTimeFormat] =
useRecoilState(dateTimeFormatState);
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
});
const updateWorkspaceMember = async (changedFields: any) => {
if (!currentWorkspaceMember?.id) {
throw new Error('User is not logged in');
}
try {
await updateOneRecord({
idToUpdate: currentWorkspaceMember.id,
updateOneRecordInput: changedFields,
});
} catch (error) {
logError(error);
}
};
if (!isDefined(currentWorkspaceMember)) return;
const handleSettingsChange = (
settingName: 'timeZone' | 'dateFormat' | 'timeFormat',
value: string,
) => {
const workspaceMember: any = {};
const dateTime: any = {};
switch (settingName) {
case 'timeZone': {
workspaceMember[settingName] = value;
dateTime[settingName] = value === 'system' ? detectTimeZone() : value;
break;
}
case 'dateFormat': {
workspaceMember[settingName] = getWorkspaceDateFormatFromDateFormat(
value as DateFormat,
);
dateTime[settingName] =
(value as DateFormat) === DateFormat.SYSTEM
? detectDateFormat()
: (value as DateFormat);
break;
}
case 'timeFormat': {
workspaceMember[settingName] = getWorkspaceTimeFormatFromTimeFormat(
value as TimeFormat,
);
dateTime[settingName] =
(value as TimeFormat) === TimeFormat.SYSTEM
? detectTimeFormat()
: (value as TimeFormat);
break;
}
}
if (!isEmptyObject(dateTime)) {
setDateTimeFormat({
...dateTimeFormat,
...dateTime,
});
}
if (!isEmptyObject(workspaceMember)) {
setCurrentWorkspaceMember({
...currentWorkspaceMember,
...workspaceMember,
});
updateWorkspaceMember(workspaceMember);
}
};
const timeZone =
currentWorkspaceMember.timeZone === 'system'
? 'system'
: dateTimeFormat.timeZone;
const dateFormat =
currentWorkspaceMember.dateFormat === WorkspaceMemberDateFormatEnum.System
? DateFormat.SYSTEM
: dateTimeFormat.dateFormat;
const timeFormat =
currentWorkspaceMember.timeFormat === WorkspaceMemberTimeFormatEnum.System
? TimeFormat.SYSTEM
: dateTimeFormat.timeFormat;
return (
<StyledContainer>
<DateTimeSettingsTimeZoneSelect
value={timeZone}
onChange={(value) => handleSettingsChange('timeZone', value)}
/>
<DateTimeSettingsDateFormatSelect
value={dateFormat}
onChange={(value) => handleSettingsChange('dateFormat', value)}
timeZone={timeZone}
/>
<DateTimeSettingsTimeFormatSelect
value={timeFormat}
onChange={(value) => handleSettingsChange('timeFormat', value)}
timeZone={timeZone}
/>
</StyledContainer>
);
};

View File

@ -0,0 +1,59 @@
import { formatInTimeZone } from 'date-fns-tz';
import { DateFormat } from '@/localization/constants/DateFormat';
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
import { Select } from '@/ui/input/components/Select';
type DateTimeSettingsDateFormatSelectProps = {
value: DateFormat;
onChange: (nextValue: DateFormat) => void;
timeZone: string;
};
export const DateTimeSettingsDateFormatSelect = ({
onChange,
timeZone,
value,
}: DateTimeSettingsDateFormatSelectProps) => {
const setTimeZone = timeZone === 'system' ? detectTimeZone() : timeZone;
return (
<Select
dropdownId="datetime-settings-date-format"
dropdownWidth={218}
label="Date format"
fullWidth
value={value}
options={[
{
label: `System settings`,
value: DateFormat.SYSTEM,
},
{
label: `${formatInTimeZone(
Date.now(),
setTimeZone,
DateFormat.MONTH_FIRST,
)}`,
value: DateFormat.MONTH_FIRST,
},
{
label: `${formatInTimeZone(
Date.now(),
setTimeZone,
DateFormat.DAY_FIRST,
)}`,
value: DateFormat.DAY_FIRST,
},
{
label: `${formatInTimeZone(
Date.now(),
setTimeZone,
DateFormat.YEAR_FIRST,
)}`,
value: DateFormat.YEAR_FIRST,
},
]}
onChange={onChange}
/>
);
};

View File

@ -0,0 +1,51 @@
import { formatInTimeZone } from 'date-fns-tz';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
import { Select } from '@/ui/input/components/Select';
type DateTimeSettingsTimeFormatSelectProps = {
value: TimeFormat;
onChange: (nextValue: TimeFormat) => void;
timeZone: string;
};
export const DateTimeSettingsTimeFormatSelect = ({
onChange,
timeZone,
value,
}: DateTimeSettingsTimeFormatSelectProps) => {
const setTimeZone = timeZone === 'system' ? detectTimeZone() : timeZone;
return (
<Select
dropdownId="datetime-settings-time-format"
dropdownWidth={218}
label="Time format"
fullWidth
value={value}
options={[
{
label: 'System settings',
value: TimeFormat.SYSTEM,
},
{
label: `24h (${formatInTimeZone(
Date.now(),
setTimeZone,
TimeFormat.HOUR_24,
)})`,
value: TimeFormat.HOUR_24,
},
{
label: `12h (${formatInTimeZone(
Date.now(),
setTimeZone,
TimeFormat.HOUR_12,
)})`,
value: TimeFormat.HOUR_12,
},
]}
onChange={onChange}
/>
);
};

View File

@ -0,0 +1,34 @@
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
import { findAvailableTimeZoneOption } from '@/localization/utils/findAvailableTimeZoneOption';
import { AVAILABLE_TIMEZONE_OPTIONS } from '@/settings/accounts/constants/AvailableTimezoneOptions';
import { Select } from '@/ui/input/components/Select';
type DateTimeSettingsTimeZoneSelectProps = {
value?: string;
onChange: (nextValue: string) => void;
};
export const DateTimeSettingsTimeZoneSelect = ({
value = detectTimeZone(),
onChange,
}: DateTimeSettingsTimeZoneSelectProps) => {
return (
<Select
dropdownId="settings-accounts-calendar-time-zone"
dropdownWidth={416}
label="Time zone"
fullWidth
value={
value === 'system'
? 'System settings'
: findAvailableTimeZoneOption(value)?.value
}
options={[
{ label: 'System settings', value: 'system' },
...AVAILABLE_TIMEZONE_OPTIONS,
]}
onChange={onChange}
withSearchInput
/>
);
};

View File

@ -6,6 +6,7 @@ import { ColorSchemePicker } from '@/ui/input/color-scheme/components/ColorSchem
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { useColorScheme } from '@/ui/theme/hooks/useColorScheme';
import { DateTimeSettings } from '~/pages/settings/profile/appearance/components/DateTimeSettings';
const StyledH1Title = styled(H1Title)`
margin-bottom: 0;
@ -22,6 +23,13 @@ export const SettingsAppearance = () => {
<H2Title title="Theme" />
<ColorSchemePicker value={colorScheme} onChange={setColorScheme} />
</Section>
<Section>
<H2Title
title="Date and time"
description="Configure how dates are displayed across the app"
/>
<DateTimeSettings />
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);