Morph-front-display-table (#13979)

## Add Morph Relation Field Display Support for Tables (one to many &
many to one)

**Features:**
- Display morph relation fields (one-to-many, many-to-one) in table
cells
- Multi-field record retrieval for improved performance
- Enhanced table cell rendering with morph relation support

**Components Added:**
- `MorphRelationManyToOneFieldDisplay` - Shows many-to-one morph
relations
- `MorphRelationOneToManyFieldDisplay` - Shows one-to-many morph
relations


TODO : 
handle notes and tasks case if we choose these kind of objects as
targets/source for a morph relation

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Guillim
2025-08-25 16:12:49 +02:00
committed by GitHub
parent dff00e813c
commit 3d1842dcb3
75 changed files with 1656 additions and 1098 deletions
@@ -1,5 +1,4 @@
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { isUndefined } from '@sniptt/guards';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
@@ -7,7 +6,11 @@ import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObject
import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { isNonCompositeField } from '@/object-record/object-filter-dropdown/utils/isNonCompositeField';
import { type ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
computeMorphRelationFieldJoinColumnName,
computeMorphRelationFieldName,
isDefined,
} from 'twenty-shared/utils';
import { type FieldMetadataItem } from '../types/FieldMetadataItem';
type MapFieldMetadataToGraphQLQueryArgs = {
@@ -15,7 +18,7 @@ type MapFieldMetadataToGraphQLQueryArgs = {
gqlField: string;
fieldMetadata: Pick<
FieldMetadataItem,
'name' | 'type' | 'relation' | 'settings'
'name' | 'type' | 'relation' | 'morphRelations' | 'settings'
>;
relationRecordGqlFields?: RecordGqlFields;
computeReferences?: boolean;
@@ -41,6 +44,90 @@ export const mapFieldMetadataToGraphQLQuery = ({
return gqlField;
}
if (
fieldType === FieldMetadataType.MORPH_RELATION &&
(fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY ||
fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE)
) {
let gqlMorphField = '';
for (const morphRelation of fieldMetadata.morphRelations ?? []) {
const relationFieldName = computeMorphRelationFieldName({
fieldName: fieldMetadata.name,
relationDirection: fieldMetadata.settings?.relationType,
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
namePlural: morphRelation.targetObjectMetadata.namePlural,
});
const relationMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id === morphRelation.targetObjectMetadata.id,
);
if (!isDefined(relationMetadataItem)) {
continue;
}
if (
isDefined(objectPermissionsByObjectMetadataId) &&
isDefined(relationMetadataItem.id)
) {
if (!isDefined(morphRelation.targetObjectMetadata.id)) {
throw new Error(
`Target object metadata id not found with field metadata ${fieldMetadata.name}`,
);
}
const objectPermission = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
morphRelation.targetObjectMetadata.id,
);
if (!objectPermission.canReadObjectRecords) {
continue;
}
}
if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) {
gqlMorphField += `${relationFieldName}
{
edges {
node ${mapObjectMetadataToGraphQLQuery({
objectMetadataItems,
objectMetadataItem: relationMetadataItem,
recordGqlFields: relationRecordGqlFields,
computeReferences,
isRootLevel: false,
objectPermissionsByObjectMetadataId,
})}
}
}`;
}
if (fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE) {
const joinColumnName = computeMorphRelationFieldJoinColumnName({
name: fieldMetadata.name,
targetObjectMetadataNameSingular:
morphRelation.targetObjectMetadata.nameSingular,
});
if (gqlField === joinColumnName) {
gqlMorphField += `${gqlField}
`;
continue;
}
gqlMorphField += `${relationFieldName}
${mapObjectMetadataToGraphQLQuery({
objectMetadataItems,
objectMetadataItem: relationMetadataItem,
recordGqlFields: relationRecordGqlFields,
computeReferences,
isRootLevel: false,
objectPermissionsByObjectMetadataId,
})}`;
}
}
return `${gqlMorphField}`;
}
if (
fieldType === FieldMetadataType.RELATION &&
fieldMetadata.relation?.type === RelationType.MANY_TO_ONE
@@ -51,7 +138,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
fieldMetadata.relation?.targetObjectMetadata.id,
);
if (isUndefined(relationMetadataItem)) {
if (!isDefined(relationMetadataItem)) {
return '';
}
@@ -100,7 +187,7 @@ ${mapObjectMetadataToGraphQLQuery({
fieldMetadata.relation?.targetObjectMetadata.id,
);
if (isUndefined(relationMetadataItem)) {
if (!isDefined(relationMetadataItem)) {
return '';
}