fix(front): resolve labelIdentifier per target for morph relation depth=1 (#20305)

## Summary

On the show page, morph relations were showing "Untitled" entries for
targets whose `labelIdentifier` is not `name` (for example
`Note.title`). The GraphQL response only contained `id` for those
records.

`generateDepthRecordGqlFieldsFromFields` was hardcoding the morph
depth=1 sub-selection to `{ id, name }` for every target instead of
resolving each target's `labelIdentifier` (and `imageIdentifier`) from
`objectMetadataItems`, the way the non-morph relation branch already
does. The morph branch was also ignoring
`shouldOnlyLoadRelationIdentifiers`.

<img width="1300" height="860" alt="image"
src="https://github.com/user-attachments/assets/ebdb5287-0b4c-4a96-95a2-33b19b31446e"
/>
This commit is contained in:
Charles Bochet
2026-05-06 11:03:22 +02:00
committed by GitHub
parent 88988e5a55
commit 0608bae9ae
2 changed files with 60 additions and 99 deletions
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`generateActivityTargetGqlFields snapshot tests should match snapshot for Note with loadRelations="activity" 1`] = `
{
@@ -24,13 +24,13 @@ exports[`generateActivityTargetGqlFields snapshot tests should match snapshot fo
"position": true,
"searchVector": true,
"targetCompany": {
"domainName": true,
"id": true,
"name": true,
},
"targetCompanyId": true,
"targetEmploymentHistory": {
"id": true,
"name": true,
},
"targetEmploymentHistoryId": true,
"targetOpportunity": {
@@ -39,6 +39,7 @@ exports[`generateActivityTargetGqlFields snapshot tests should match snapshot fo
},
"targetOpportunityId": true,
"targetPerson": {
"avatarFile": true,
"id": true,
"name": true,
},
@@ -49,7 +50,6 @@ exports[`generateActivityTargetGqlFields snapshot tests should match snapshot fo
},
"targetPetCareAgreement": {
"id": true,
"name": true,
},
"targetPetCareAgreementId": true,
"targetPetId": true,
@@ -76,45 +76,21 @@ exports[`generateActivityTargetGqlFields snapshot tests should match snapshot fo
"id": true,
"position": true,
"searchVector": true,
"targetCompany": {
"id": true,
"name": true,
},
"targetCompany": true,
"targetCompanyId": true,
"targetEmploymentHistory": {
"id": true,
"name": true,
},
"targetEmploymentHistory": true,
"targetEmploymentHistoryId": true,
"targetOpportunity": {
"id": true,
"name": true,
},
"targetOpportunity": true,
"targetOpportunityId": true,
"targetPerson": {
"id": true,
"name": true,
},
"targetPerson": true,
"targetPersonId": true,
"targetPet": {
"id": true,
"name": true,
},
"targetPetCareAgreement": {
"id": true,
"name": true,
},
"targetPet": true,
"targetPetCareAgreement": true,
"targetPetCareAgreementId": true,
"targetPetId": true,
"targetRocket": {
"id": true,
"name": true,
},
"targetRocket": true,
"targetRocketId": true,
"targetSurveyResult": {
"id": true,
"name": true,
},
"targetSurveyResult": true,
"targetSurveyResultId": true,
"updatedAt": true,
"updatedBy": true,
@@ -140,13 +116,13 @@ exports[`generateActivityTargetGqlFields snapshot tests should match snapshot fo
"position": true,
"searchVector": true,
"targetCompany": {
"domainName": true,
"id": true,
"name": true,
},
"targetCompanyId": true,
"targetEmploymentHistory": {
"id": true,
"name": true,
},
"targetEmploymentHistoryId": true,
"targetOpportunity": {
@@ -155,6 +131,7 @@ exports[`generateActivityTargetGqlFields snapshot tests should match snapshot fo
},
"targetOpportunityId": true,
"targetPerson": {
"avatarFile": true,
"id": true,
"name": true,
},
@@ -165,7 +142,6 @@ exports[`generateActivityTargetGqlFields snapshot tests should match snapshot fo
},
"targetPetCareAgreement": {
"id": true,
"name": true,
},
"targetPetCareAgreementId": true,
"targetPetId": true,
@@ -197,45 +173,21 @@ exports[`generateActivityTargetGqlFields snapshot tests should match snapshot fo
"id": true,
"position": true,
"searchVector": true,
"targetCompany": {
"id": true,
"name": true,
},
"targetCompany": true,
"targetCompanyId": true,
"targetEmploymentHistory": {
"id": true,
"name": true,
},
"targetEmploymentHistory": true,
"targetEmploymentHistoryId": true,
"targetOpportunity": {
"id": true,
"name": true,
},
"targetOpportunity": true,
"targetOpportunityId": true,
"targetPerson": {
"id": true,
"name": true,
},
"targetPerson": true,
"targetPersonId": true,
"targetPet": {
"id": true,
"name": true,
},
"targetPetCareAgreement": {
"id": true,
"name": true,
},
"targetPet": true,
"targetPetCareAgreement": true,
"targetPetCareAgreementId": true,
"targetPetId": true,
"targetRocket": {
"id": true,
"name": true,
},
"targetRocket": true,
"targetRocketId": true,
"targetSurveyResult": {
"id": true,
"name": true,
},
"targetSurveyResult": true,
"targetSurveyResultId": true,
"updatedAt": true,
"updatedBy": true,
@@ -6,9 +6,8 @@ import {
} from 'twenty-shared/types';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { getImageIdentifierFieldMetadataItem } from '@/object-metadata/utils/getImageIdentifierFieldMetadataItem';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { type RecordGqlFields } from '@/object-record/graphql/record-gql-fields/types/RecordGqlFields';
import { buildIdentifierGqlFields } from '@/object-record/graphql/record-gql-fields/utils/buildIdentifierGqlFields';
import { generateActivityTargetGqlFields } from '@/object-record/graphql/record-gql-fields/utils/generateActivityTargetGqlFields';
import { generateJunctionRelationGqlFields } from '@/object-record/graphql/record-gql-fields/utils/generateJunctionRelationGqlFields';
import { isJunctionRelationField } from '@/object-record/record-field/ui/utils/junction/isJunctionRelationField';
@@ -96,21 +95,9 @@ export const generateDepthRecordGqlFieldsFromFields = ({
}
}
const labelIdentifierFieldMetadataItem =
getLabelIdentifierFieldMetadataItem(targetObjectMetadataItem);
const imageIdentifierFieldMetadataItem =
getImageIdentifierFieldMetadataItem(targetObjectMetadataItem);
const relationIdentifierSubGqlFields = {
id: true,
...(isDefined(labelIdentifierFieldMetadataItem)
? { [labelIdentifierFieldMetadataItem.name]: true }
: {}),
...(isDefined(imageIdentifierFieldMetadataItem)
? { [imageIdentifierFieldMetadataItem.name]: true }
: {}),
};
const relationIdentifierSubGqlFields = buildIdentifierGqlFields(
targetObjectMetadataItem,
);
const manyToOneGqlFields = {
[`${fieldMetadata.name}Id`]: true,
@@ -138,17 +125,33 @@ export const generateDepthRecordGqlFieldsFromFields = ({
}
const morphGqlFields = fieldMetadata.morphRelations.map(
(morphRelation) => ({
gqlField: computeMorphRelationFieldName({
fieldName: fieldMetadata.name,
relationType: morphRelation.type,
targetObjectMetadataNameSingular:
morphRelation.targetObjectMetadata.nameSingular,
targetObjectMetadataNamePlural:
morphRelation.targetObjectMetadata.namePlural,
}),
fieldMetadata,
}),
(morphRelation) => {
const morphTargetObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id === morphRelation.targetObjectMetadata.id,
);
if (!morphTargetObjectMetadataItem) {
throw new Error(
`Target object metadata item not found for ${fieldMetadata.name} (morph target ${morphRelation.targetObjectMetadata.nameSingular})`,
);
}
return {
gqlField: computeMorphRelationFieldName({
fieldName: fieldMetadata.name,
relationType: morphRelation.type,
targetObjectMetadataNameSingular:
morphRelation.targetObjectMetadata.nameSingular,
targetObjectMetadataNamePlural:
morphRelation.targetObjectMetadata.namePlural,
}),
fieldMetadata,
relationIdentifierSubGqlFields: buildIdentifierGqlFields(
morphTargetObjectMetadataItem,
),
};
},
);
return {
@@ -156,8 +159,14 @@ export const generateDepthRecordGqlFieldsFromFields = ({
...morphGqlFields.reduce(
(morphGqlFields, morphGqlField) => ({
...morphGqlFields,
...(depth === 1
? { [`${morphGqlField.gqlField}`]: { id: true, name: true } }
...(depth === 1 && shouldOnlyLoadRelationIdentifiers
? {
[`${morphGqlField.gqlField}`]:
morphGqlField.relationIdentifierSubGqlFields,
}
: {}),
...(depth === 1 && !shouldOnlyLoadRelationIdentifiers
? { [`${morphGqlField.gqlField}`]: true }
: {}),
...(relationType === RelationType.MANY_TO_ONE
? { [`${morphGqlField.gqlField}Id`]: true }