Date field format display (#11384)
## Introduction This PR enables functionality discussed in [Layout Date Formatting](https://github.com/twentyhq/core-team-issues/issues/97). ### TLDR; It enables greater control of date formatting at the object's field level by upgrading all DATE and DATE_TIME fields' settings from: ```ts { displayAsRelativeDate: boolean } ``` to: ```ts type FieldDateDisplayFormat = 'full_date' | 'relative_date' | 'date' | 'time' | 'year' | 'custom' { displayFormat: FieldDateDisplayFormat } ``` PR also includes an upgrade command that will update any existing DATE and DATE_TIME fields to the new settings value --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@ -1,20 +1,44 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { validateCustomDateFormat } from '@/localization/utils/validateCustomDateFormat';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { FieldDateDisplayFormat } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isDateFieldCustomDisplayFormat } from '@/object-record/record-field/types/guards/isDateFIeldCustomDisplayFormat';
|
||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
||||
import { ADVANCED_SETTINGS_ANIMATION_DURATION } from '@/settings/constants/AdvancedSettingsAnimationDurations';
|
||||
import { useDateSettingsFormInitialValues } from '@/settings/data-model/fields/forms/date/hooks/useDateSettingsFormInitialValues';
|
||||
import { getDisplayFormatLabel } from '@/settings/data-model/fields/forms/date/utils/getDisplayFormatLabel';
|
||||
import { getDisplayFormatSelectDescription } from '@/settings/data-model/fields/forms/date/utils/getDisplayFormatSelectDescription';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconSlash } from 'twenty-ui/display';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
|
||||
const fieldDateSettings = z.discriminatedUnion('displayFormat', [
|
||||
z.object({
|
||||
displayFormat: z.enum([
|
||||
FieldDateDisplayFormat.RELATIVE,
|
||||
FieldDateDisplayFormat.USER_SETTINGS,
|
||||
]),
|
||||
}),
|
||||
z.object({
|
||||
displayFormat: z.literal(FieldDateDisplayFormat.CUSTOM),
|
||||
customUnicodeDateFormat: z.string().refine(validateCustomDateFormat),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const settingsDataModelFieldDateFormSchema = z.object({
|
||||
settings: z
|
||||
.object({
|
||||
displayAsRelativeDate: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
settings: fieldDateSettings.optional(),
|
||||
});
|
||||
|
||||
const StyledTextInput = styled(TextInput)`
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
padding-top: 0;
|
||||
`;
|
||||
|
||||
export type SettingsDataModelFieldDateFormValues = z.infer<
|
||||
typeof settingsDataModelFieldDateFormSchema
|
||||
>;
|
||||
@ -30,27 +54,78 @@ export const SettingsDataModelFieldDateForm = ({
|
||||
}: SettingsDataModelFieldDateFormProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const { control } = useFormContext<SettingsDataModelFieldDateFormValues>();
|
||||
const { control, watch } =
|
||||
useFormContext<SettingsDataModelFieldDateFormValues>();
|
||||
|
||||
const { initialDisplayAsRelativeDateValue } =
|
||||
const { initialDisplayFormat, initialCustomUnicodeDateFormat } =
|
||||
useDateSettingsFormInitialValues({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
const displayFormatFromForm = watch('settings.displayFormat');
|
||||
|
||||
const activeDisplayFormat = displayFormatFromForm
|
||||
? displayFormatFromForm
|
||||
: initialDisplayFormat;
|
||||
|
||||
const showCustomFormatTextInput =
|
||||
isDateFieldCustomDisplayFormat(activeDisplayFormat);
|
||||
|
||||
const displayFormatSelectDescription =
|
||||
getDisplayFormatSelectDescription(activeDisplayFormat);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name="settings.displayAsRelativeDate"
|
||||
control={control}
|
||||
defaultValue={initialDisplayAsRelativeDateValue}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconSlash}
|
||||
title={t`Display as relative date`}
|
||||
checked={value ?? false}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
<>
|
||||
<Controller
|
||||
name="settings.displayFormat"
|
||||
control={control}
|
||||
defaultValue={initialDisplayFormat}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<SettingsOptionCardContentSelect
|
||||
Icon={IconSlash}
|
||||
title={t`Display Format`}
|
||||
disabled={disabled}
|
||||
description={displayFormatSelectDescription}
|
||||
>
|
||||
<Select<FieldDateDisplayFormat>
|
||||
disabled={disabled}
|
||||
selectSizeVariant="small"
|
||||
dropdownWidth={120}
|
||||
dropdownId="selectFieldDateDisplayFormat"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={Object.keys(FieldDateDisplayFormat).map((key) => {
|
||||
return {
|
||||
label: getDisplayFormatLabel(key as FieldDateDisplayFormat),
|
||||
value: key as FieldDateDisplayFormat,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</SettingsOptionCardContentSelect>
|
||||
)}
|
||||
/>
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={showCustomFormatTextInput}
|
||||
dimension={'height'}
|
||||
animationDurations={ADVANCED_SETTINGS_ANIMATION_DURATION}
|
||||
mode="scroll-height"
|
||||
containAnimation={false}
|
||||
>
|
||||
<Controller
|
||||
name="settings.customUnicodeDateFormat"
|
||||
control={control}
|
||||
defaultValue={initialCustomUnicodeDateFormat}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<StyledTextInput
|
||||
placeholder={t`Format e.g. d-MMM-y (qqq''yy)`}
|
||||
value={value}
|
||||
onChange={(value) => onChange(value)}
|
||||
disabled={false}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</AnimatedExpandableContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -31,7 +31,7 @@ export const SettingsDataModelFieldDateSettingsFormCard = ({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
}: SettingsDataModelFieldDateSettingsFormCardProps) => {
|
||||
const { initialDisplayAsRelativeDateValue } =
|
||||
const { initialDisplayFormat, initialCustomUnicodeDateFormat } =
|
||||
useDateSettingsFormInitialValues({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
@ -46,9 +46,13 @@ export const SettingsDataModelFieldDateSettingsFormCard = ({
|
||||
fieldMetadataItem={{
|
||||
...fieldMetadataItem,
|
||||
settings: {
|
||||
displayAsRelativeDate: watchFormValue(
|
||||
'settings.displayAsRelativeDate',
|
||||
initialDisplayAsRelativeDateValue,
|
||||
displayFormat: watchFormValue(
|
||||
'settings.displayFormat',
|
||||
initialDisplayFormat,
|
||||
),
|
||||
customUnicodeDateFormat: watchFormValue(
|
||||
'settings.customUnicodeDateFormat',
|
||||
initialCustomUnicodeDateFormat,
|
||||
),
|
||||
},
|
||||
}}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { FieldDateDisplayFormat } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { SettingsDataModelFieldDateFormValues } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm';
|
||||
|
||||
export const useDateSettingsFormInitialValues = ({
|
||||
@ -8,18 +9,24 @@ export const useDateSettingsFormInitialValues = ({
|
||||
}: {
|
||||
fieldMetadataItem?: Pick<FieldMetadataItem, 'settings'>;
|
||||
}) => {
|
||||
const initialDisplayAsRelativeDateValue =
|
||||
fieldMetadataItem?.settings?.displayAsRelativeDate;
|
||||
const initialDisplayFormat = fieldMetadataItem?.settings
|
||||
?.displayFormat as FieldDateDisplayFormat;
|
||||
const initialCustomUnicodeDateFormat = fieldMetadataItem?.settings
|
||||
?.customUnicodeDateFormat as string;
|
||||
|
||||
const { resetField } = useFormContext<SettingsDataModelFieldDateFormValues>();
|
||||
|
||||
const resetDefaultValueField = () =>
|
||||
resetField('settings.displayAsRelativeDate', {
|
||||
defaultValue: initialDisplayAsRelativeDateValue,
|
||||
resetField('settings', {
|
||||
defaultValue: {
|
||||
displayFormat: initialDisplayFormat,
|
||||
customUnicodeDateFormat: initialCustomUnicodeDateFormat,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
initialDisplayAsRelativeDateValue,
|
||||
initialDisplayFormat,
|
||||
initialCustomUnicodeDateFormat,
|
||||
resetDefaultValueField,
|
||||
};
|
||||
};
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
import { FieldDateDisplayFormat } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
export const getDisplayFormatLabel = (
|
||||
displayFormat: FieldDateDisplayFormat,
|
||||
) => {
|
||||
switch (displayFormat) {
|
||||
case FieldDateDisplayFormat.CUSTOM:
|
||||
return t`Custom`;
|
||||
case FieldDateDisplayFormat.RELATIVE:
|
||||
return t`Relative`;
|
||||
case FieldDateDisplayFormat.USER_SETTINGS:
|
||||
return t`Default`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,24 @@
|
||||
import { FieldDateDisplayFormat } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
export const getDisplayFormatSelectDescription = (
|
||||
selectedDisplayFormat: FieldDateDisplayFormat,
|
||||
) => {
|
||||
if (selectedDisplayFormat === FieldDateDisplayFormat.CUSTOM) {
|
||||
return (
|
||||
<Trans>
|
||||
Enter in{' '}
|
||||
<a
|
||||
href="https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ textDecoration: 'underline', color: 'inherit' }}
|
||||
>
|
||||
Unicode
|
||||
</a>{' '}
|
||||
format
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
return <Trans>Choose the format used to display date value</Trans>;
|
||||
};
|
||||
Reference in New Issue
Block a user