From 62352901ba10164a59896a3b9ef4ac051278a6f3 Mon Sep 17 00:00:00 2001 From: Guillim Date: Tue, 12 Aug 2025 14:11:18 +0200 Subject: [PATCH] [Morph] frontend basics init (#13822) Basic setup for frontend work on morph relations. FieldMetadata definition and related adjustments --- .../object-metadata/graphql/queries.ts | 21 ++++++++++ ...ormatFieldMetadataItemAsFieldDefinition.ts | 13 +++++- .../record-field/types/FieldMetadata.ts | 10 +++++ .../types/guards/assertFieldMetadata.ts | 41 ++++++++++--------- .../types/guards/isFieldMorphRelation.ts | 11 +++++ .../isFieldMorphRelationManyToOneObject.ts | 13 ++++++ .../isFieldMorphRelationOneToManyObjects.ts | 13 ++++++ .../record-field/utils/isFieldValueEmpty.ts | 7 ++++ .../useRecordIndexLazyFetchRecords.test.ts | 1 + .../SettingsDataModelFieldPreview.tsx | 20 +++++---- 10 files changed, 121 insertions(+), 29 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldMorphRelation.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldMorphRelationManyToOneObject.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldMorphRelationOneToManyObjects.ts 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 42fd9ca9cf..16b5be0b8a 100644 --- a/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts +++ b/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts @@ -80,6 +80,27 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql` name } } + morphRelations { + type + sourceObjectMetadata { + id + nameSingular + namePlural + } + targetObjectMetadata { + id + nameSingular + namePlural + } + sourceFieldMetadata { + id + name + } + targetFieldMetadata { + id + name + } + } } } } 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 a7668cd9dd..2c116749af 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts @@ -2,6 +2,7 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI import { type FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { type FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { FieldMetadataType } from 'twenty-shared/types'; import { type FieldMetadataItem } from '../types/FieldMetadataItem'; import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon'; @@ -22,10 +23,20 @@ export const formatFieldMetadataItemAsFieldDefinition = ({ const relationFieldMetadataId = field.relation?.targetFieldMetadata.id; + const isRelation = field.type === FieldMetadataType.RELATION; + const isMorphRelation = field.type === FieldMetadataType.MORPH_RELATION; + + const relationType = isRelation + ? field.relation?.type + : isMorphRelation + ? field.morphRelations?.[0]?.type + : undefined; + const fieldDefintionMetadata = { fieldName: field.name, placeHolder: field.label, - relationType: field.relation?.type, + relationType, + morphRelations: isMorphRelation ? field.morphRelations : [], relationFieldMetadataId, relationObjectMetadataNameSingular: relationObjectMetadataItem?.nameSingular ?? '', 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 fdab41ee35..7a8d01dfa3 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 @@ -1,3 +1,4 @@ +import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation'; import { type RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues'; import { type ZodHelperLiteral } from '@/object-record/record-field/types/ZodHelperLiteral'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; @@ -139,6 +140,7 @@ export type FieldPositionMetadata = BaseFieldMetadata & { settings?: null; }; +// for later: refactor this in order to directly use relation without mapping export type FieldRelationMetadata = BaseFieldMetadata & { relationFieldMetadataId: string; relationObjectMetadataNamePlural: string; @@ -150,6 +152,13 @@ export type FieldRelationMetadata = BaseFieldMetadata & { settings?: null; }; +export type FieldMorphRelationMetadata = BaseFieldMetadata & { + morphRelations: FieldMetadataItemRelation[]; + relationType: RelationType; + useEditButton?: boolean; + settings?: null; +}; + export type FieldSelectMetadata = BaseFieldMetadata & { options: { label: string; color: ThemeColor; value: string }[]; isNullable: boolean; @@ -190,6 +199,7 @@ export type FieldMetadata = | FieldPhoneMetadata | FieldRatingMetadata | FieldRelationMetadata + | FieldMorphRelationMetadata | FieldRichTextMetadata | FieldSelectMetadata | FieldMultiSelectMetadata diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts index 3c27e4dc6f..ba637ff314 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts @@ -15,6 +15,7 @@ import { type FieldLinkMetadata, type FieldLinksMetadata, type FieldMetadata, + type FieldMorphRelationMetadata, type FieldMultiSelectMetadata, type FieldNumberMetadata, type FieldPhoneMetadata, @@ -61,25 +62,27 @@ type AssertFieldMetadataFunction = < ? FieldPhoneMetadata : E extends 'RELATION' ? FieldRelationMetadata - : E extends 'TEXT' - ? FieldTextMetadata - : E extends 'UUID' - ? FieldUuidMetadata - : E extends 'ADDRESS' - ? FieldAddressMetadata - : E extends 'RAW_JSON' - ? FieldRawJsonMetadata - : E extends 'RICH_TEXT_V2' - ? FieldRichTextV2Metadata - : E extends 'RICH_TEXT' - ? FieldRichTextMetadata - : E extends 'ACTOR' - ? FieldActorMetadata - : E extends 'ARRAY' - ? FieldArrayMetadata - : E extends 'PHONES' - ? FieldPhonesMetadata - : never, + : E extends 'MORPH_RELATION' + ? FieldMorphRelationMetadata + : E extends 'TEXT' + ? FieldTextMetadata + : E extends 'UUID' + ? FieldUuidMetadata + : E extends 'ADDRESS' + ? FieldAddressMetadata + : E extends 'RAW_JSON' + ? FieldRawJsonMetadata + : E extends 'RICH_TEXT_V2' + ? FieldRichTextV2Metadata + : E extends 'RICH_TEXT' + ? FieldRichTextMetadata + : E extends 'ACTOR' + ? FieldActorMetadata + : E extends 'ARRAY' + ? FieldArrayMetadata + : E extends 'PHONES' + ? FieldPhonesMetadata + : never, >( fieldType: E, fieldTypeGuard: ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldMorphRelation.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldMorphRelation.ts new file mode 100644 index 0000000000..2c280a00ca --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldMorphRelation.ts @@ -0,0 +1,11 @@ +import { type FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; +import { + type FieldMetadata, + type FieldMorphRelationMetadata, +} from '@/object-record/record-field/types/FieldMetadata'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +export const isFieldMorphRelation = ( + field: Pick, 'type'>, +): field is FieldDefinition => + field.type === FieldMetadataType.MORPH_RELATION; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldMorphRelationManyToOneObject.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldMorphRelationManyToOneObject.ts new file mode 100644 index 0000000000..a45ea59afc --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldMorphRelationManyToOneObject.ts @@ -0,0 +1,13 @@ +import { isFieldMorphRelation } from '@/object-record/record-field/types/guards/isFieldMorphRelation'; +import { RelationType } from '~/generated-metadata/graphql'; +import { type FieldDefinition } from '../FieldDefinition'; +import { + type FieldMetadata, + type FieldMorphRelationMetadata, +} from '../FieldMetadata'; + +export const isFieldMorphRelationManyToOneObject = ( + field: Pick, 'type' | 'metadata'>, +): field is FieldDefinition => + isFieldMorphRelation(field) && + field.metadata.morphRelations?.[0].type === RelationType.MANY_TO_ONE; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldMorphRelationOneToManyObjects.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldMorphRelationOneToManyObjects.ts new file mode 100644 index 0000000000..c38389975b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldMorphRelationOneToManyObjects.ts @@ -0,0 +1,13 @@ +import { isFieldMorphRelation } from '@/object-record/record-field/types/guards/isFieldMorphRelation'; +import { RelationType } from '~/generated-metadata/graphql'; +import { type FieldDefinition } from '../FieldDefinition'; +import { + type FieldMetadata, + type FieldMorphRelationMetadata, +} from '../FieldMetadata'; + +export const isFieldMorphRelationOneToManyObjects = ( + field: Pick, 'type' | 'metadata'>, +): field is FieldDefinition => + isFieldMorphRelation(field) && + field.metadata.morphRelations?.[0].type === RelationType.ONE_TO_MANY; diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts index f1f31649a4..14252eb912 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts @@ -20,6 +20,7 @@ import { isFieldFullName } from '@/object-record/record-field/types/guards/isFie import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue'; import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; import { isFieldLinksValue } from '@/object-record/record-field/types/guards/isFieldLinksValue'; +import { isFieldMorphRelation } from '@/object-record/record-field/types/guards/isFieldMorphRelation'; import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect'; import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue'; import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber'; @@ -80,6 +81,12 @@ export const isFieldValueEmpty = ({ return isValueEmpty(fieldValue); } + if (isFieldMorphRelation(fieldDefinition)) { + return isArray(fieldValue) + ? !isNonEmptyArray(fieldValue) + : isValueEmpty(fieldValue); + } + if (isFieldMultiSelect(fieldDefinition) || isFieldArray(fieldDefinition)) { return ( !isFieldArrayValue(fieldValue) || diff --git a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useRecordIndexLazyFetchRecords.test.ts b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useRecordIndexLazyFetchRecords.test.ts index f67e1c69ca..236369f397 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useRecordIndexLazyFetchRecords.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useRecordIndexLazyFetchRecords.test.ts @@ -232,6 +232,7 @@ describe('useRecordData', () => { relationObjectMetadataNamePlural: '', relationObjectMetadataNameSingular: '', relationType: undefined, + morphRelations: [], targetFieldMetadataName: '', settings: { displayFormat: 'RELATIVE', 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 89c91bc10a..3c99bc5048 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 @@ -104,6 +104,16 @@ export const SettingsDataModelFieldPreview = ({ previewRecord?.id ?? `${objectMetadataItem.nameSingular}-${fieldName}-${fieldMetadataItem.relation?.type}-${relationObjectMetadataItem?.nameSingular}-preview`; + const metadata = { + fieldName, + objectMetadataNameSingular: objectMetadataItem.nameSingular, + relationObjectMetadataNameSingular: + relationObjectMetadataItem?.nameSingular || '', + options: fieldMetadataItem.options ?? [], + settings: fieldMetadataItem.settings, + relationType: fieldMetadataItem.relation?.type, + }; + return ( <>