[Morph] frontend basics init (#13822)

Basic setup for frontend work on morph relations.

FieldMetadata definition and related adjustments
This commit is contained in:
Guillim
2025-08-12 14:11:18 +02:00
committed by GitHub
parent 3c6a115ace
commit 62352901ba
10 changed files with 121 additions and 29 deletions
@@ -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
}
}
}
}
}
@@ -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 ?? '',
@@ -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
@@ -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: (
@@ -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<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldMorphRelationMetadata> =>
field.type === FieldMetadataType.MORPH_RELATION;
@@ -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<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
): field is FieldDefinition<FieldMorphRelationMetadata> =>
isFieldMorphRelation(field) &&
field.metadata.morphRelations?.[0].type === RelationType.MANY_TO_ONE;
@@ -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<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
): field is FieldDefinition<FieldMorphRelationMetadata> =>
isFieldMorphRelation(field) &&
field.metadata.morphRelations?.[0].type === RelationType.ONE_TO_MANY;
@@ -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) ||
@@ -232,6 +232,7 @@ describe('useRecordData', () => {
relationObjectMetadataNamePlural: '',
relationObjectMetadataNameSingular: '',
relationType: undefined,
morphRelations: [],
targetFieldMetadataName: '',
settings: {
displayFormat: 'RELATIVE',
@@ -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 (
<>
<RecordFieldComponentInstanceContext.Provider
@@ -139,15 +149,7 @@ export const SettingsDataModelFieldPreview = ({
iconName: 'FieldIcon',
fieldMetadataId: fieldMetadataItem.id || '',
label: fieldMetadataItem.label,
metadata: {
fieldName,
objectMetadataNameSingular: objectMetadataItem.nameSingular,
relationObjectMetadataNameSingular:
relationObjectMetadataItem?.nameSingular,
options: fieldMetadataItem.options ?? [],
settings: fieldMetadataItem.settings,
relationType: fieldMetadataItem.relation?.type,
},
metadata,
defaultValue: fieldMetadataItem.defaultValue,
},
isRecordFieldReadOnly: