diff --git a/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToRelativeDate.ts b/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToRelativeDate.ts
new file mode 100644
index 000000000..2bcc93a30
--- /dev/null
+++ b/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToRelativeDate.ts
@@ -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 });
+};
diff --git a/packages/twenty-front/src/modules/object-metadata/graphql/mutations.ts b/packages/twenty-front/src/modules/object-metadata/graphql/mutations.ts
index ab37d5249..80814a64b 100644
--- a/packages/twenty-front/src/modules/object-metadata/graphql/mutations.ts
+++ b/packages/twenty-front/src/modules/object-metadata/graphql/mutations.ts
@@ -35,6 +35,7 @@ export const CREATE_ONE_FIELD_METADATA_ITEM = gql`
isNullable
createdAt
updatedAt
+ settings
defaultValue
options
}
@@ -73,6 +74,7 @@ export const UPDATE_ONE_FIELD_METADATA_ITEM = gql`
isNullable
createdAt
updatedAt
+ settings
}
}
`;
@@ -136,6 +138,7 @@ export const DELETE_ONE_FIELD_METADATA_ITEM = gql`
isNullable
createdAt
updatedAt
+ settings
}
}
`;
diff --git a/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts b/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts
index 8dbaad032..a61811431 100644
--- a/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts
+++ b/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts
@@ -41,6 +41,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
updatedAt
defaultValue
options
+ settings
relationDefinition {
relationId
direction
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts
index 0a73b0d2a..f3c3e93f1 100644
--- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts
@@ -17,6 +17,7 @@ const baseFields = `
isNullable
createdAt
updatedAt
+ settings
`;
export const queries = {
@@ -73,6 +74,7 @@ export const variables = {
label: 'fieldLabel',
name: 'fieldLabel',
options: undefined,
+ settings: undefined,
objectMetadataId,
type: 'TEXT',
},
@@ -96,6 +98,7 @@ const defaultResponseData = {
isNullable: false,
createdAt: '1977-09-28T13:56:55.157Z',
updatedAt: '1996-10-10T08:27:57.117Z',
+ settings: undefined,
};
const fieldRelationResponseData = {
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItem.ts
index 75f9f5a2e..3f39f8927 100644
--- a/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItem.ts
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItem.ts
@@ -1,6 +1,6 @@
import { useDeleteOneRelationMetadataItem } from '@/object-metadata/hooks/useDeleteOneRelationMetadataItem';
-import { Field } from '~/generated/graphql';
import { FieldMetadataType } from '~/generated-metadata/graphql';
+import { Field } from '~/generated/graphql';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
import { formatFieldMetadataItemInput } from '../utils/formatFieldMetadataItemInput';
@@ -18,7 +18,13 @@ export const useFieldMetadataItem = () => {
const createMetadataField = (
input: Pick<
Field,
- 'label' | 'icon' | 'description' | 'defaultValue' | 'type' | 'options'
+ | 'label'
+ | 'icon'
+ | 'description'
+ | 'defaultValue'
+ | 'type'
+ | 'options'
+ | 'settings'
> & {
objectMetadataId: string;
},
diff --git a/packages/twenty-front/src/modules/object-metadata/types/FieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/types/FieldMetadataItem.ts
index ed4529ff1..61ce60263 100644
--- a/packages/twenty-front/src/modules/object-metadata/types/FieldMetadataItem.ts
+++ b/packages/twenty-front/src/modules/object-metadata/types/FieldMetadataItem.ts
@@ -36,4 +36,7 @@ export type FieldMetadataItem = Omit<
'id' | 'nameSingular' | 'namePlural'
>;
} | null;
+ settings?: {
+ displayAsRelativeDate?: boolean;
+ };
};
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts
index 7d26d1150..8ba9ebe23 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts
@@ -37,6 +37,7 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
targetFieldMetadataName:
field.relationDefinition?.targetFieldMetadata?.name ?? '',
options: field.options,
+ settings: field.settings,
isNullable: field.isNullable,
};
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemInput.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemInput.ts
index 15bc319a9..5900beed1 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemInput.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemInput.ts
@@ -5,7 +5,13 @@ export const formatFieldMetadataItemInput = (
input: Partial<
Pick<
FieldMetadataItem,
- 'type' | 'label' | 'defaultValue' | 'icon' | 'description' | 'options'
+ | 'type'
+ | 'label'
+ | 'defaultValue'
+ | 'icon'
+ | 'description'
+ | 'options'
+ | 'settings'
>
>,
) => {
@@ -18,5 +24,6 @@ export const formatFieldMetadataItemInput = (
label,
name: label ? computeMetadataNameFromLabelOrThrow(label) : undefined,
options: input.options,
+ settings: input.settings,
};
};
diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/fieldMetadataItemSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/fieldMetadataItemSchema.ts
index 42a976b5d..18fc5722a 100644
--- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/fieldMetadataItemSchema.ts
+++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/fieldMetadataItemSchema.ts
@@ -35,6 +35,7 @@ export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
)
.nullable()
.optional(),
+ settings: z.any().optional(),
relationDefinition: z
.object({
__typename: z.literal('RelationDefinition').optional(),
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/DateFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/DateFieldDisplay.tsx
index 05b88fd26..86039881b 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/DateFieldDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/DateFieldDisplay.tsx
@@ -2,7 +2,15 @@ import { useDateFieldDisplay } from '@/object-record/record-field/meta-types/hoo
import { DateDisplay } from '@/ui/field/display/components/DateDisplay';
export const DateFieldDisplay = () => {
- const { fieldValue } = useDateFieldDisplay();
+ const { fieldValue, fieldDefinition } = useDateFieldDisplay();
- return ;
+ const displayAsRelativeDate =
+ fieldDefinition.metadata?.settings?.displayAsRelativeDate;
+
+ return (
+
+ );
};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/DateTimeFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/DateTimeFieldDisplay.tsx
index 03ffc92d3..9d67dff92 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/DateTimeFieldDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/DateTimeFieldDisplay.tsx
@@ -2,7 +2,15 @@ import { useDateTimeFieldDisplay } from '@/object-record/record-field/meta-types
import { DateTimeDisplay } from '@/ui/field/display/components/DateTimeDisplay';
export const DateTimeFieldDisplay = () => {
- const { fieldValue } = useDateTimeFieldDisplay();
+ const { fieldValue, fieldDefinition } = useDateTimeFieldDisplay();
- return ;
+ const displayAsRelativeDate =
+ fieldDefinition.metadata?.settings?.displayAsRelativeDate;
+
+ return (
+
+ );
};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useDateFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useDateFieldDisplay.ts
index 3f7e5407f..2e400704d 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useDateFieldDisplay.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useDateFieldDisplay.ts
@@ -2,6 +2,8 @@ import { useContext } from 'react';
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';
export const useDateFieldDisplay = () => {
@@ -16,7 +18,10 @@ export const useDateFieldDisplay = () => {
);
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,
fieldValue,
hotkeyScope,
clearable,
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useDateTimeFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useDateTimeFieldDisplay.ts
index bc41e36a4..412271428 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useDateTimeFieldDisplay.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useDateTimeFieldDisplay.ts
@@ -2,6 +2,8 @@ import { useContext } from 'react';
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';
export const useDateTimeFieldDisplay = () => {
@@ -16,7 +18,7 @@ export const useDateTimeFieldDisplay = () => {
);
return {
- fieldDefinition,
+ fieldDefinition: fieldDefinition as FieldDefinition,
fieldValue,
hotkeyScope,
clearable,
diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts
index 0349367f2..a2a3339e1 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts
@@ -27,12 +27,18 @@ export type FieldDateTimeMetadata = {
objectMetadataNameSingular?: string;
placeHolder: string;
fieldName: string;
+ settings?: {
+ displayAsRelativeDate?: boolean;
+ };
};
export type FieldDateMetadata = {
objectMetadataNameSingular?: string;
placeHolder: string;
fieldName: string;
+ settings?: {
+ displayAsRelativeDate?: boolean;
+ };
};
export type FieldNumberMetadata = {
diff --git a/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelPreviewFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelPreviewFormCard.tsx
index 34e514abe..bf02470ca 100644
--- a/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelPreviewFormCard.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelPreviewFormCard.tsx
@@ -1,6 +1,7 @@
-import { ReactNode } from 'react';
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 { CardContent } from '@/ui/layout/card/components/CardContent';
@@ -14,14 +15,6 @@ const StyledPreviewContainer = styled(CardContent)`
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)`
padding: 0;
`;
@@ -33,7 +26,7 @@ export const SettingsDataModelPreviewFormCard = ({
}: SettingsDataModelPreviewFormCardProps) => (
- Preview
+ Preview
{preview}
{!!form && {form}}
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/components/StyledFormCardTitle.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/components/StyledFormCardTitle.tsx
new file mode 100644
index 000000000..03d4ebb91
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/components/StyledFormCardTitle.tsx
@@ -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)};
+`;
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx
index 114993114..d4771446e 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx
@@ -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,
});
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx
index 139a1036a..a71cc1654 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx
@@ -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 &
Partial>;
} & Pick;
@@ -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 (
+ );
+ }
+
+ if (
+ fieldMetadataItem.type === FieldMetadataType.Date ||
+ fieldMetadataItem.type === FieldMetadataType.DateTime
+ ) {
+ return (
+
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldToggle.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldToggle.tsx
new file mode 100644
index 000000000..e859d652d
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldToggle.tsx
@@ -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 (
+
+
+ {Icon && (
+
+ )}
+ {label}
+
+
+ {tooltip && (
+
+ )}
+ {tooltip &&
+ createPortal(
+ ,
+ document.body,
+ )}
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm.tsx
new file mode 100644
index 000000000..c7d029abe
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm.tsx
@@ -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;
+};
+
+export const SettingsDataModelFieldDateForm = ({
+ disabled,
+ fieldMetadataItem,
+}: SettingsDataModelFieldDateFormProps) => {
+ const { control } = useFormContext();
+
+ const { initialDisplayAsRelativeDateValue } =
+ useDateSettingsFormInitialValues({
+ fieldMetadataItem,
+ });
+
+ return (
+
+ (
+ <>
+ Options
+
+ >
+ )}
+ />
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateSettingsFormCard.tsx
new file mode 100644
index 000000000..418d9d93c
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateSettingsFormCard.tsx
@@ -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;
+
+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();
+
+ return (
+
+ }
+ form={
+
+ }
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/hooks/useDateSettingsFormInitialValues.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/hooks/useDateSettingsFormInitialValues.ts
new file mode 100644
index 000000000..4726db3ec
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/date/hooks/useDateSettingsFormInitialValues.ts
@@ -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;
+}) => {
+ const initialDisplayAsRelativeDateValue =
+ fieldMetadataItem?.settings?.displayAsRelativeDate;
+
+ const { resetField } = useFormContext();
+
+ const resetDefaultValueField = () =>
+ resetField('settings.displayAsRelativeDate', {
+ defaultValue: initialDisplayAsRelativeDateValue,
+ });
+
+ return {
+ initialDisplayAsRelativeDateValue,
+ resetDefaultValueField,
+ };
+};
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema.ts
index 085aa958e..4d7a171f4 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema.ts
@@ -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);
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx
index a7b69af6a..1fbefb2d3 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx
@@ -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,
},
diff --git a/packages/twenty-front/src/modules/ui/field/display/components/DateDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/DateDisplay.tsx
index 982e7a9af..767671d62 100644
--- a/packages/twenty-front/src/modules/ui/field/display/components/DateDisplay.tsx
+++ b/packages/twenty-front/src/modules/ui/field/display/components/DateDisplay.tsx
@@ -1,17 +1,24 @@
import { formatDateISOStringToDate } from '@/localization/utils/formatDateISOStringToDate';
+import { formatDateISOStringToRelativeDate } from '@/localization/utils/formatDateISOStringToRelativeDate';
import { UserContext } from '@/users/contexts/UserContext';
import { useContext } from 'react';
import { EllipsisDisplay } from './EllipsisDisplay';
type DateDisplayProps = {
value: string | null | undefined;
+ displayAsRelativeDate?: boolean;
};
-export const DateDisplay = ({ value }: DateDisplayProps) => {
+export const DateDisplay = ({
+ value,
+ displayAsRelativeDate,
+}: DateDisplayProps) => {
const { dateFormat, timeZone } = useContext(UserContext);
const formattedDate = value
- ? formatDateISOStringToDate(value, timeZone, dateFormat)
+ ? displayAsRelativeDate
+ ? formatDateISOStringToRelativeDate(value, true)
+ : formatDateISOStringToDate(value, timeZone, dateFormat)
: '';
return {formattedDate};
diff --git a/packages/twenty-front/src/modules/ui/field/display/components/DateTimeDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/DateTimeDisplay.tsx
index f90e4a0c5..7f2432639 100644
--- a/packages/twenty-front/src/modules/ui/field/display/components/DateTimeDisplay.tsx
+++ b/packages/twenty-front/src/modules/ui/field/display/components/DateTimeDisplay.tsx
@@ -1,17 +1,24 @@
import { formatDateISOStringToDateTime } from '@/localization/utils/formatDateISOStringToDateTime';
+import { formatDateISOStringToRelativeDate } from '@/localization/utils/formatDateISOStringToRelativeDate';
import { UserContext } from '@/users/contexts/UserContext';
import { useContext } from 'react';
import { EllipsisDisplay } from './EllipsisDisplay';
type DateTimeDisplayProps = {
value: string | null | undefined;
+ displayAsRelativeDate?: boolean;
};
-export const DateTimeDisplay = ({ value }: DateTimeDisplayProps) => {
+export const DateTimeDisplay = ({
+ value,
+ displayAsRelativeDate,
+}: DateTimeDisplayProps) => {
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
const formattedDate = value
- ? formatDateISOStringToDateTime(value, timeZone, dateFormat, timeFormat)
+ ? displayAsRelativeDate
+ ? formatDateISOStringToRelativeDate(value)
+ : formatDateISOStringToDateTime(value, timeZone, dateFormat, timeFormat)
: '';
return {formattedDate};
diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx
index 5e4769913..dd11d6dbb 100644
--- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx
+++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx
@@ -229,7 +229,6 @@ export const SettingsObjectFieldEdit = () => {
diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx
index b07901935..90a1e4d10 100644
--- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx
+++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx
@@ -248,6 +248,7 @@ export const SettingsObjectNewFieldStep2 = () => {
/>
=
diff --git a/packages/twenty-server/src/engine/twenty-orm/base.workspace-entity.ts b/packages/twenty-server/src/engine/twenty-orm/base.workspace-entity.ts
index f122f29c4..d9a19a689 100644
--- a/packages/twenty-server/src/engine/twenty-orm/base.workspace-entity.ts
+++ b/packages/twenty-server/src/engine/twenty-orm/base.workspace-entity.ts
@@ -25,6 +25,9 @@ export abstract class BaseWorkspaceEntity {
description: 'Creation date',
icon: 'IconCalendar',
defaultValue: 'now',
+ settings: {
+ displayAsRelativeDate: true,
+ },
})
createdAt: string;
@@ -35,6 +38,9 @@ export abstract class BaseWorkspaceEntity {
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
defaultValue: 'now',
+ settings: {
+ displayAsRelativeDate: true,
+ },
})
updatedAt: string;
@@ -44,6 +50,9 @@ export abstract class BaseWorkspaceEntity {
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
+ settings: {
+ displayAsRelativeDate: true,
+ },
})
@WorkspaceIsNullable()
deletedAt?: string | null;
diff --git a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts
index fd2b280fd..c6dd8bfd7 100644
--- a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts
+++ b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts
@@ -69,6 +69,7 @@ export function WorkspaceField(
icon: options.icon,
defaultValue,
options: options.options,
+ settings: options.settings,
isPrimary,
isNullable,
isSystem,
diff --git a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface.ts b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface.ts
index 862d72a25..09c339c93 100644
--- a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface.ts
+++ b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface.ts
@@ -1,5 +1,6 @@
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 { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@@ -54,6 +55,11 @@ export interface WorkspaceFieldMetadataArgs {
*/
readonly options?: FieldMetadataOptions;
+ /**
+ * Field settings.
+ */
+ readonly settings?: FieldMetadataSettings;
+
/**
* Is primary field.
*/
diff --git a/packages/twenty-server/src/engine/twenty-orm/storage/metadata-args.storage.ts b/packages/twenty-server/src/engine/twenty-orm/storage/metadata-args.storage.ts
index 7ea7f9d76..69807d28d 100644
--- a/packages/twenty-server/src/engine/twenty-orm/storage/metadata-args.storage.ts
+++ b/packages/twenty-server/src/engine/twenty-orm/storage/metadata-args.storage.ts
@@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/ban-types */
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 { 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 { 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 { 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 {
private readonly entities: WorkspaceEntityMetadataArgs[] = [];
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts
index 3c06dc9b5..9cbe04fd1 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts
@@ -160,6 +160,7 @@ export class StandardFieldFactory {
description: workspaceFieldMetadataArgs.description,
defaultValue: workspaceFieldMetadataArgs.defaultValue,
options: workspaceFieldMetadataArgs.options,
+ settings: workspaceFieldMetadataArgs.settings,
workspaceId: context.workspaceId,
isNullable: workspaceFieldMetadataArgs.isNullable,
isCustom: workspaceFieldMetadataArgs.isDeprecated ? true : false,
diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
index 8688d709a..5d4cc31d9 100644
--- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
+++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
@@ -48,6 +48,7 @@ export {
IconCircleX,
IconClick,
IconClockHour8,
+ IconClockShare,
IconCode,
IconCoins,
IconColorSwatch,