'Display as relative date' field formatting option for dateTime and date fields #5398 (#6945)

Implements #5398.

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
ad-elias
2024-09-25 11:42:16 +02:00
committed by GitHub
parent b3a0cba961
commit 092496f2db
35 changed files with 430 additions and 34 deletions

View File

@ -0,0 +1,9 @@
import styled from '@emotion/styled';
export const StyledFormCardTitle = styled.h3`
color: ${({ theme }) => theme.font.color.extraLight};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
margin: 0;
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;

View File

@ -9,9 +9,9 @@ import { IconPicker } from '@/ui/input/components/IconPicker';
import { TextInput } from '@/ui/input/components/TextInput';
export const settingsDataModelFieldIconLabelFormSchema = (
existingLabels?: string[],
existingOtherLabels: string[] = [],
) => {
return fieldMetadataItemSchema(existingLabels || []).pick({
return fieldMetadataItemSchema(existingOtherLabels).pick({
icon: true,
label: true,
});

View File

@ -9,6 +9,8 @@ import { settingsDataModelFieldBooleanFormSchema } from '@/settings/data-model/f
import { SettingsDataModelFieldBooleanSettingsFormCard } from '@/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanSettingsFormCard';
import { settingsDataModelFieldCurrencyFormSchema } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm';
import { SettingsDataModelFieldCurrencySettingsFormCard } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencySettingsFormCard';
import { settingsDataModelFieldDateFormSchema } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm';
import { SettingsDataModelFieldDateSettingsFormCard } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateSettingsFormCard';
import { settingsDataModelFieldRelationFormSchema } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm';
import { SettingsDataModelFieldRelationSettingsFormCard } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard';
import {
@ -30,6 +32,14 @@ const currencyFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.Currency) })
.merge(settingsDataModelFieldCurrencyFormSchema);
const dateFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.Date) })
.merge(settingsDataModelFieldDateFormSchema);
const dateTimeFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.DateTime) })
.merge(settingsDataModelFieldDateFormSchema);
const relationFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.Relation) })
.merge(settingsDataModelFieldRelationFormSchema);
@ -51,6 +61,8 @@ const otherFieldsFormSchema = z.object({
FieldMetadataType.Relation,
FieldMetadataType.Select,
FieldMetadataType.MultiSelect,
FieldMetadataType.Date,
FieldMetadataType.DateTime,
]),
) as [FieldMetadataType, ...FieldMetadataType[]],
),
@ -61,6 +73,8 @@ export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion(
[
booleanFieldFormSchema,
currencyFieldFormSchema,
dateFieldFormSchema,
dateTimeFieldFormSchema,
relationFieldFormSchema,
selectFieldFormSchema,
multiSelectFieldFormSchema,
@ -69,7 +83,7 @@ export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion(
);
type SettingsDataModelFieldSettingsFormCardProps = {
disableCurrencyForm?: boolean;
isCreatingField?: boolean;
fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> &
Partial<Omit<FieldMetadataItem, 'icon' | 'label' | 'type'>>;
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
@ -102,7 +116,7 @@ const previewableTypes = [
];
export const SettingsDataModelFieldSettingsFormCard = ({
disableCurrencyForm,
isCreatingField,
fieldMetadataItem,
objectMetadataItem,
}: SettingsDataModelFieldSettingsFormCardProps) => {
@ -120,7 +134,20 @@ export const SettingsDataModelFieldSettingsFormCard = ({
if (fieldMetadataItem.type === FieldMetadataType.Currency) {
return (
<SettingsDataModelFieldCurrencySettingsFormCard
disabled={disableCurrencyForm}
disabled={!isCreatingField}
fieldMetadataItem={fieldMetadataItem}
objectMetadataItem={objectMetadataItem}
/>
);
}
if (
fieldMetadataItem.type === FieldMetadataType.Date ||
fieldMetadataItem.type === FieldMetadataType.DateTime
) {
return (
<SettingsDataModelFieldDateSettingsFormCard
disabled={!isCreatingField}
fieldMetadataItem={fieldMetadataItem}
objectMetadataItem={objectMetadataItem}
/>

View File

@ -0,0 +1,91 @@
import { Toggle } from '@/ui/input/components/Toggle';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { createPortal } from 'react-dom';
import {
AppTooltip,
IconComponent,
IconInfoCircle,
TooltipDelay,
} from 'twenty-ui';
const StyledContainer = styled.div<{ disabled?: boolean }>`
align-items: center;
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
box-sizing: border-box;
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ disabled, theme }) =>
disabled ? theme.font.color.tertiary : theme.font.color.primary};
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ theme }) => theme.spacing(8)};
justify-content: space-between;
padding: 0 ${({ theme }) => theme.spacing(2)};
`;
const StyledGroup = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
interface SettingsDataModelFieldToggleProps {
disabled?: boolean;
Icon?: IconComponent;
label: string;
tooltip?: string;
value?: boolean;
onChange: (value: boolean) => void;
}
export const SettingsDataModelFieldToggle = ({
disabled,
Icon,
label,
tooltip,
value,
onChange,
}: SettingsDataModelFieldToggleProps) => {
const theme = useTheme();
const infoCircleElementId = `info-circle-id-${Math.random().toString(36).slice(2)}`;
return (
<StyledContainer>
<StyledGroup>
{Icon && (
<Icon color={theme.font.color.tertiary} size={theme.icon.size.md} />
)}
{label}
</StyledGroup>
<StyledGroup>
{tooltip && (
<IconInfoCircle
id={infoCircleElementId}
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
)}
{tooltip &&
createPortal(
<AppTooltip
anchorSelect={`#${infoCircleElementId}`}
content={tooltip}
offset={5}
noArrow
place="bottom"
positionStrategy="absolute"
delay={TooltipDelay.shortDelay}
/>,
document.body,
)}
<Toggle
disabled={disabled}
value={value}
onChange={onChange}
toggleSize="small"
/>
</StyledGroup>
</StyledContainer>
);
};

View File

@ -0,0 +1,63 @@
import { Controller, useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { StyledFormCardTitle } from '@/settings/data-model/fields/components/StyledFormCardTitle';
import { SettingsDataModelFieldToggle } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldToggle';
import { useDateSettingsFormInitialValues } from '@/settings/data-model/fields/forms/date/hooks/useDateSettingsFormInitialValues';
import { CardContent } from '@/ui/layout/card/components/CardContent';
import { IconClockShare } from 'twenty-ui';
export const settingsDataModelFieldDateFormSchema = z.object({
settings: z
.object({
displayAsRelativeDate: z.boolean().optional(),
})
.optional(),
});
export type SettingsDataModelFieldDateFormValues = z.infer<
typeof settingsDataModelFieldDateFormSchema
>;
type SettingsDataModelFieldDateFormProps = {
disabled?: boolean;
fieldMetadataItem: Pick<FieldMetadataItem, 'settings'>;
};
export const SettingsDataModelFieldDateForm = ({
disabled,
fieldMetadataItem,
}: SettingsDataModelFieldDateFormProps) => {
const { control } = useFormContext<SettingsDataModelFieldDateFormValues>();
const { initialDisplayAsRelativeDateValue } =
useDateSettingsFormInitialValues({
fieldMetadataItem,
});
return (
<CardContent>
<Controller
name="settings.displayAsRelativeDate"
control={control}
defaultValue={initialDisplayAsRelativeDateValue}
render={({ field: { onChange, value } }) => (
<>
<StyledFormCardTitle>Options</StyledFormCardTitle>
<SettingsDataModelFieldToggle
label="Display as relative date"
Icon={IconClockShare}
onChange={onChange}
value={value}
disabled={disabled}
tooltip={
'Show dates in a human-friendly format. Example: "13 mins ago" instead of "Jul 30, 2024 7:11pm"'
}
/>
</>
)}
/>
</CardContent>
);
};

View File

@ -0,0 +1,66 @@
import styled from '@emotion/styled';
import { useFormContext } from 'react-hook-form';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
import {
SettingsDataModelFieldDateForm,
SettingsDataModelFieldDateFormValues,
} from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm';
import { useDateSettingsFormInitialValues } from '@/settings/data-model/fields/forms/date/hooks/useDateSettingsFormInitialValues';
import {
SettingsDataModelFieldPreviewCard,
SettingsDataModelFieldPreviewCardProps,
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
type SettingsDataModelFieldDateSettingsFormCardProps = {
disabled?: boolean;
fieldMetadataItem: Pick<
FieldMetadataItem,
'icon' | 'label' | 'type' | 'settings'
>;
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
display: grid;
flex: 1 1 100%;
`;
export const SettingsDataModelFieldDateSettingsFormCard = ({
disabled,
fieldMetadataItem,
objectMetadataItem,
}: SettingsDataModelFieldDateSettingsFormCardProps) => {
const { initialDisplayAsRelativeDateValue } =
useDateSettingsFormInitialValues({
fieldMetadataItem,
});
const { watch: watchFormValue } =
useFormContext<SettingsDataModelFieldDateFormValues>();
return (
<SettingsDataModelPreviewFormCard
preview={
<StyledFieldPreviewCard
fieldMetadataItem={{
...fieldMetadataItem,
settings: {
displayAsRelativeDate: watchFormValue(
'settings.displayAsRelativeDate',
initialDisplayAsRelativeDateValue,
),
},
}}
objectMetadataItem={objectMetadataItem}
/>
}
form={
<SettingsDataModelFieldDateForm
disabled={disabled}
fieldMetadataItem={fieldMetadataItem}
/>
}
/>
);
};

View File

@ -0,0 +1,25 @@
import { useFormContext } from 'react-hook-form';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { SettingsDataModelFieldDateFormValues } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm';
export const useDateSettingsFormInitialValues = ({
fieldMetadataItem,
}: {
fieldMetadataItem?: Pick<FieldMetadataItem, 'settings'>;
}) => {
const initialDisplayAsRelativeDateValue =
fieldMetadataItem?.settings?.displayAsRelativeDate;
const { resetField } = useFormContext<SettingsDataModelFieldDateFormValues>();
const resetDefaultValueField = () =>
resetField('settings.displayAsRelativeDate', {
defaultValue: initialDisplayAsRelativeDateValue,
});
return {
initialDisplayAsRelativeDateValue,
resetDefaultValueField,
};
};

View File

@ -5,10 +5,10 @@ import { settingsDataModelFieldIconLabelFormSchema } from '@/settings/data-model
import { settingsDataModelFieldSettingsFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
import { settingsDataModelFieldTypeFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect';
export const settingsFieldFormSchema = (existingLabels?: string[]) => {
export const settingsFieldFormSchema = (existingOtherLabels?: string[]) => {
return z
.object({})
.merge(settingsDataModelFieldIconLabelFormSchema(existingLabels))
.merge(settingsDataModelFieldIconLabelFormSchema(existingOtherLabels))
.merge(settingsDataModelFieldDescriptionFormSchema())
.merge(settingsDataModelFieldTypeFormSchema)
.and(settingsDataModelFieldSettingsFormSchema);

View File

@ -18,7 +18,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
export type SettingsDataModelFieldPreviewProps = {
fieldMetadataItem: Pick<
FieldMetadataItem,
'icon' | 'label' | 'type' | 'defaultValue' | 'options'
'icon' | 'label' | 'type' | 'defaultValue' | 'options' | 'settings'
> & {
id?: string;
name?: string;
@ -132,6 +132,7 @@ export const SettingsDataModelFieldPreview = ({
relationObjectMetadataNameSingular:
relationObjectMetadataItem?.nameSingular,
options: fieldMetadataItem.options ?? [],
settings: fieldMetadataItem.settings,
},
defaultValue: fieldMetadataItem.defaultValue,
},