'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,25 @@
import {
differenceInDays,
formatDistance,
isToday,
startOfDay,
} from 'date-fns';
export const formatDateISOStringToRelativeDate = (
isoDate: string,
isDayMaximumPrecision = false,
) => {
const now = new Date();
const targetDate = new Date(isoDate);
if (isDayMaximumPrecision && isToday(targetDate)) return 'Today';
const isWithin24h = Math.abs(differenceInDays(targetDate, now)) < 1;
if (isDayMaximumPrecision || !isWithin24h)
return formatDistance(startOfDay(targetDate), startOfDay(now), {
addSuffix: true,
});
return formatDistance(targetDate, now, { addSuffix: true });
};

View File

@ -35,6 +35,7 @@ export const CREATE_ONE_FIELD_METADATA_ITEM = gql`
isNullable isNullable
createdAt createdAt
updatedAt updatedAt
settings
defaultValue defaultValue
options options
} }
@ -73,6 +74,7 @@ export const UPDATE_ONE_FIELD_METADATA_ITEM = gql`
isNullable isNullable
createdAt createdAt
updatedAt updatedAt
settings
} }
} }
`; `;
@ -136,6 +138,7 @@ export const DELETE_ONE_FIELD_METADATA_ITEM = gql`
isNullable isNullable
createdAt createdAt
updatedAt updatedAt
settings
} }
} }
`; `;

View File

@ -41,6 +41,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
updatedAt updatedAt
defaultValue defaultValue
options options
settings
relationDefinition { relationDefinition {
relationId relationId
direction direction

View File

@ -17,6 +17,7 @@ const baseFields = `
isNullable isNullable
createdAt createdAt
updatedAt updatedAt
settings
`; `;
export const queries = { export const queries = {
@ -73,6 +74,7 @@ export const variables = {
label: 'fieldLabel', label: 'fieldLabel',
name: 'fieldLabel', name: 'fieldLabel',
options: undefined, options: undefined,
settings: undefined,
objectMetadataId, objectMetadataId,
type: 'TEXT', type: 'TEXT',
}, },
@ -96,6 +98,7 @@ const defaultResponseData = {
isNullable: false, isNullable: false,
createdAt: '1977-09-28T13:56:55.157Z', createdAt: '1977-09-28T13:56:55.157Z',
updatedAt: '1996-10-10T08:27:57.117Z', updatedAt: '1996-10-10T08:27:57.117Z',
settings: undefined,
}; };
const fieldRelationResponseData = { const fieldRelationResponseData = {

View File

@ -1,6 +1,6 @@
import { useDeleteOneRelationMetadataItem } from '@/object-metadata/hooks/useDeleteOneRelationMetadataItem'; import { useDeleteOneRelationMetadataItem } from '@/object-metadata/hooks/useDeleteOneRelationMetadataItem';
import { Field } from '~/generated/graphql';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { Field } from '~/generated/graphql';
import { FieldMetadataItem } from '../types/FieldMetadataItem'; import { FieldMetadataItem } from '../types/FieldMetadataItem';
import { formatFieldMetadataItemInput } from '../utils/formatFieldMetadataItemInput'; import { formatFieldMetadataItemInput } from '../utils/formatFieldMetadataItemInput';
@ -18,7 +18,13 @@ export const useFieldMetadataItem = () => {
const createMetadataField = ( const createMetadataField = (
input: Pick< input: Pick<
Field, Field,
'label' | 'icon' | 'description' | 'defaultValue' | 'type' | 'options' | 'label'
| 'icon'
| 'description'
| 'defaultValue'
| 'type'
| 'options'
| 'settings'
> & { > & {
objectMetadataId: string; objectMetadataId: string;
}, },

View File

@ -36,4 +36,7 @@ export type FieldMetadataItem = Omit<
'id' | 'nameSingular' | 'namePlural' 'id' | 'nameSingular' | 'namePlural'
>; >;
} | null; } | null;
settings?: {
displayAsRelativeDate?: boolean;
};
}; };

View File

@ -37,6 +37,7 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
targetFieldMetadataName: targetFieldMetadataName:
field.relationDefinition?.targetFieldMetadata?.name ?? '', field.relationDefinition?.targetFieldMetadata?.name ?? '',
options: field.options, options: field.options,
settings: field.settings,
isNullable: field.isNullable, isNullable: field.isNullable,
}; };

View File

@ -5,7 +5,13 @@ export const formatFieldMetadataItemInput = (
input: Partial< input: Partial<
Pick< Pick<
FieldMetadataItem, FieldMetadataItem,
'type' | 'label' | 'defaultValue' | 'icon' | 'description' | 'options' | 'type'
| 'label'
| 'defaultValue'
| 'icon'
| 'description'
| 'options'
| 'settings'
> >
>, >,
) => { ) => {
@ -18,5 +24,6 @@ export const formatFieldMetadataItemInput = (
label, label,
name: label ? computeMetadataNameFromLabelOrThrow(label) : undefined, name: label ? computeMetadataNameFromLabelOrThrow(label) : undefined,
options: input.options, options: input.options,
settings: input.settings,
}; };
}; };

View File

@ -35,6 +35,7 @@ export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
) )
.nullable() .nullable()
.optional(), .optional(),
settings: z.any().optional(),
relationDefinition: z relationDefinition: z
.object({ .object({
__typename: z.literal('RelationDefinition').optional(), __typename: z.literal('RelationDefinition').optional(),

View File

@ -2,7 +2,15 @@ import { useDateFieldDisplay } from '@/object-record/record-field/meta-types/hoo
import { DateDisplay } from '@/ui/field/display/components/DateDisplay'; import { DateDisplay } from '@/ui/field/display/components/DateDisplay';
export const DateFieldDisplay = () => { export const DateFieldDisplay = () => {
const { fieldValue } = useDateFieldDisplay(); const { fieldValue, fieldDefinition } = useDateFieldDisplay();
return <DateDisplay value={fieldValue} />; const displayAsRelativeDate =
fieldDefinition.metadata?.settings?.displayAsRelativeDate;
return (
<DateDisplay
value={fieldValue}
displayAsRelativeDate={displayAsRelativeDate}
/>
);
}; };

View File

@ -2,7 +2,15 @@ import { useDateTimeFieldDisplay } from '@/object-record/record-field/meta-types
import { DateTimeDisplay } from '@/ui/field/display/components/DateTimeDisplay'; import { DateTimeDisplay } from '@/ui/field/display/components/DateTimeDisplay';
export const DateTimeFieldDisplay = () => { export const DateTimeFieldDisplay = () => {
const { fieldValue } = useDateTimeFieldDisplay(); const { fieldValue, fieldDefinition } = useDateTimeFieldDisplay();
return <DateTimeDisplay value={fieldValue} />; const displayAsRelativeDate =
fieldDefinition.metadata?.settings?.displayAsRelativeDate;
return (
<DateTimeDisplay
value={fieldValue}
displayAsRelativeDate={displayAsRelativeDate}
/>
);
}; };

View File

@ -2,6 +2,8 @@ import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldDateMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { FieldContext } from '../../contexts/FieldContext'; import { FieldContext } from '../../contexts/FieldContext';
export const useDateFieldDisplay = () => { export const useDateFieldDisplay = () => {
@ -16,7 +18,10 @@ export const useDateFieldDisplay = () => {
); );
return { return {
fieldDefinition, // TODO: we have to use this because we removed the assertion that would have otherwise narrowed the type because
// it impacts performance. We should find a way to assert the type in a way that doesn't impact performance.
// Maybe a level above ?
fieldDefinition: fieldDefinition as FieldDefinition<FieldDateMetadata>,
fieldValue, fieldValue,
hotkeyScope, hotkeyScope,
clearable, clearable,

View File

@ -2,6 +2,8 @@ import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldDateTimeMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { FieldContext } from '../../contexts/FieldContext'; import { FieldContext } from '../../contexts/FieldContext';
export const useDateTimeFieldDisplay = () => { export const useDateTimeFieldDisplay = () => {
@ -16,7 +18,7 @@ export const useDateTimeFieldDisplay = () => {
); );
return { return {
fieldDefinition, fieldDefinition: fieldDefinition as FieldDefinition<FieldDateTimeMetadata>,
fieldValue, fieldValue,
hotkeyScope, hotkeyScope,
clearable, clearable,

View File

@ -27,12 +27,18 @@ export type FieldDateTimeMetadata = {
objectMetadataNameSingular?: string; objectMetadataNameSingular?: string;
placeHolder: string; placeHolder: string;
fieldName: string; fieldName: string;
settings?: {
displayAsRelativeDate?: boolean;
};
}; };
export type FieldDateMetadata = { export type FieldDateMetadata = {
objectMetadataNameSingular?: string; objectMetadataNameSingular?: string;
placeHolder: string; placeHolder: string;
fieldName: string; fieldName: string;
settings?: {
displayAsRelativeDate?: boolean;
};
}; };
export type FieldNumberMetadata = { export type FieldNumberMetadata = {

View File

@ -1,6 +1,7 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { ReactNode } from 'react';
import { StyledFormCardTitle } from '@/settings/data-model/fields/components/StyledFormCardTitle';
import { Card } from '@/ui/layout/card/components/Card'; import { Card } from '@/ui/layout/card/components/Card';
import { CardContent } from '@/ui/layout/card/components/CardContent'; import { CardContent } from '@/ui/layout/card/components/CardContent';
@ -14,14 +15,6 @@ const StyledPreviewContainer = styled(CardContent)`
background-color: ${({ theme }) => theme.background.transparent.lighter}; background-color: ${({ theme }) => theme.background.transparent.lighter};
`; `;
const StyledTitle = 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)};
`;
const StyledFormContainer = styled(CardContent)` const StyledFormContainer = styled(CardContent)`
padding: 0; padding: 0;
`; `;
@ -33,7 +26,7 @@ export const SettingsDataModelPreviewFormCard = ({
}: SettingsDataModelPreviewFormCardProps) => ( }: SettingsDataModelPreviewFormCardProps) => (
<Card className={className} fullWidth> <Card className={className} fullWidth>
<StyledPreviewContainer divider={!!form}> <StyledPreviewContainer divider={!!form}>
<StyledTitle>Preview</StyledTitle> <StyledFormCardTitle>Preview</StyledFormCardTitle>
{preview} {preview}
</StyledPreviewContainer> </StyledPreviewContainer>
{!!form && <StyledFormContainer>{form}</StyledFormContainer>} {!!form && <StyledFormContainer>{form}</StyledFormContainer>}

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

View File

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

View File

@ -1,17 +1,24 @@
import { formatDateISOStringToDate } from '@/localization/utils/formatDateISOStringToDate'; import { formatDateISOStringToDate } from '@/localization/utils/formatDateISOStringToDate';
import { formatDateISOStringToRelativeDate } from '@/localization/utils/formatDateISOStringToRelativeDate';
import { UserContext } from '@/users/contexts/UserContext'; import { UserContext } from '@/users/contexts/UserContext';
import { useContext } from 'react'; import { useContext } from 'react';
import { EllipsisDisplay } from './EllipsisDisplay'; import { EllipsisDisplay } from './EllipsisDisplay';
type DateDisplayProps = { type DateDisplayProps = {
value: string | null | undefined; value: string | null | undefined;
displayAsRelativeDate?: boolean;
}; };
export const DateDisplay = ({ value }: DateDisplayProps) => { export const DateDisplay = ({
value,
displayAsRelativeDate,
}: DateDisplayProps) => {
const { dateFormat, timeZone } = useContext(UserContext); const { dateFormat, timeZone } = useContext(UserContext);
const formattedDate = value const formattedDate = value
? formatDateISOStringToDate(value, timeZone, dateFormat) ? displayAsRelativeDate
? formatDateISOStringToRelativeDate(value, true)
: formatDateISOStringToDate(value, timeZone, dateFormat)
: ''; : '';
return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>; return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>;

View File

@ -1,17 +1,24 @@
import { formatDateISOStringToDateTime } from '@/localization/utils/formatDateISOStringToDateTime'; import { formatDateISOStringToDateTime } from '@/localization/utils/formatDateISOStringToDateTime';
import { formatDateISOStringToRelativeDate } from '@/localization/utils/formatDateISOStringToRelativeDate';
import { UserContext } from '@/users/contexts/UserContext'; import { UserContext } from '@/users/contexts/UserContext';
import { useContext } from 'react'; import { useContext } from 'react';
import { EllipsisDisplay } from './EllipsisDisplay'; import { EllipsisDisplay } from './EllipsisDisplay';
type DateTimeDisplayProps = { type DateTimeDisplayProps = {
value: string | null | undefined; value: string | null | undefined;
displayAsRelativeDate?: boolean;
}; };
export const DateTimeDisplay = ({ value }: DateTimeDisplayProps) => { export const DateTimeDisplay = ({
value,
displayAsRelativeDate,
}: DateTimeDisplayProps) => {
const { dateFormat, timeFormat, timeZone } = useContext(UserContext); const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
const formattedDate = value const formattedDate = value
? formatDateISOStringToDateTime(value, timeZone, dateFormat, timeFormat) ? displayAsRelativeDate
? formatDateISOStringToRelativeDate(value)
: formatDateISOStringToDateTime(value, timeZone, dateFormat, timeFormat)
: ''; : '';
return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>; return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>;

View File

@ -229,7 +229,6 @@ export const SettingsObjectFieldEdit = () => {
<Section> <Section>
<H2Title title="Values" description="The values of this field" /> <H2Title title="Values" description="The values of this field" />
<SettingsDataModelFieldSettingsFormCard <SettingsDataModelFieldSettingsFormCard
disableCurrencyForm
fieldMetadataItem={fieldMetadataItem} fieldMetadataItem={fieldMetadataItem}
objectMetadataItem={objectMetadataItem} objectMetadataItem={objectMetadataItem}
/> />

View File

@ -248,6 +248,7 @@ export const SettingsObjectNewFieldStep2 = () => {
/> />
<SettingsDataModelFieldSettingsFormCard <SettingsDataModelFieldSettingsFormCard
isCreatingField
fieldMetadataItem={{ fieldMetadataItem={{
icon: formConfig.watch('icon'), icon: formConfig.watch('icon'),
label: formConfig.watch('label') || 'Employees', label: formConfig.watch('label') || 'Employees',

View File

@ -14,8 +14,18 @@ type FieldMetadataNumberSettings = {
dataType: NumberDataType; dataType: NumberDataType;
}; };
type FieldMetadataDateSettings = {
displayAsRelativeDate?: boolean;
};
type FieldMetadataDateTimeSettings = {
displayAsRelativeDate?: boolean;
};
type FieldMetadataSettingsMapping = { type FieldMetadataSettingsMapping = {
[FieldMetadataType.NUMBER]: FieldMetadataNumberSettings; [FieldMetadataType.NUMBER]: FieldMetadataNumberSettings;
[FieldMetadataType.DATE]: FieldMetadataDateSettings;
[FieldMetadataType.DATE_TIME]: FieldMetadataDateTimeSettings;
}; };
type SettingsByFieldMetadata<T extends FieldMetadataType | 'default'> = type SettingsByFieldMetadata<T extends FieldMetadataType | 'default'> =

View File

@ -25,6 +25,9 @@ export abstract class BaseWorkspaceEntity {
description: 'Creation date', description: 'Creation date',
icon: 'IconCalendar', icon: 'IconCalendar',
defaultValue: 'now', defaultValue: 'now',
settings: {
displayAsRelativeDate: true,
},
}) })
createdAt: string; createdAt: string;
@ -35,6 +38,9 @@ export abstract class BaseWorkspaceEntity {
description: 'Last time the record was changed', description: 'Last time the record was changed',
icon: 'IconCalendarClock', icon: 'IconCalendarClock',
defaultValue: 'now', defaultValue: 'now',
settings: {
displayAsRelativeDate: true,
},
}) })
updatedAt: string; updatedAt: string;
@ -44,6 +50,9 @@ export abstract class BaseWorkspaceEntity {
label: 'Deleted at', label: 'Deleted at',
description: 'Date when the record was deleted', description: 'Date when the record was deleted',
icon: 'IconCalendarMinus', icon: 'IconCalendarMinus',
settings: {
displayAsRelativeDate: true,
},
}) })
@WorkspaceIsNullable() @WorkspaceIsNullable()
deletedAt?: string | null; deletedAt?: string | null;

View File

@ -69,6 +69,7 @@ export function WorkspaceField<T extends FieldMetadataType>(
icon: options.icon, icon: options.icon,
defaultValue, defaultValue,
options: options.options, options: options.options,
settings: options.settings,
isPrimary, isPrimary,
isNullable, isNullable,
isSystem, isSystem,

View File

@ -1,5 +1,6 @@
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface'; import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -54,6 +55,11 @@ export interface WorkspaceFieldMetadataArgs {
*/ */
readonly options?: FieldMetadataOptions; readonly options?: FieldMetadataOptions;
/**
* Field settings.
*/
readonly settings?: FieldMetadataSettings;
/** /**
* Is primary field. * Is primary field.
*/ */

View File

@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable @typescript-eslint/ban-types */
import { WorkspaceDynamicRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface'; import { WorkspaceDynamicRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface';
import { WorkspaceFieldMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface';
import { WorkspaceEntityMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-entity-metadata-args.interface'; import { WorkspaceEntityMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-entity-metadata-args.interface';
import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface';
import { WorkspaceExtendedEntityMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-extended-entity-metadata-args.interface'; import { WorkspaceExtendedEntityMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-extended-entity-metadata-args.interface';
import { WorkspaceFieldMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface';
import { WorkspaceIndexMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-index-metadata-args.interface'; import { WorkspaceIndexMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-index-metadata-args.interface';
import { WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-metadata-args.interface'; import { WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-metadata-args.interface';
import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface';
export class MetadataArgsStorage { export class MetadataArgsStorage {
private readonly entities: WorkspaceEntityMetadataArgs[] = []; private readonly entities: WorkspaceEntityMetadataArgs[] = [];

View File

@ -160,6 +160,7 @@ export class StandardFieldFactory {
description: workspaceFieldMetadataArgs.description, description: workspaceFieldMetadataArgs.description,
defaultValue: workspaceFieldMetadataArgs.defaultValue, defaultValue: workspaceFieldMetadataArgs.defaultValue,
options: workspaceFieldMetadataArgs.options, options: workspaceFieldMetadataArgs.options,
settings: workspaceFieldMetadataArgs.settings,
workspaceId: context.workspaceId, workspaceId: context.workspaceId,
isNullable: workspaceFieldMetadataArgs.isNullable, isNullable: workspaceFieldMetadataArgs.isNullable,
isCustom: workspaceFieldMetadataArgs.isDeprecated ? true : false, isCustom: workspaceFieldMetadataArgs.isDeprecated ? true : false,

View File

@ -48,6 +48,7 @@ export {
IconCircleX, IconCircleX,
IconClick, IconClick,
IconClockHour8, IconClockHour8,
IconClockShare,
IconCode, IconCode,
IconCoins, IconCoins,
IconColorSwatch, IconColorSwatch,