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,