Morph relation persist uses wrong FK naming, causing "unknown field" error
https://sonarly.com/issue/4056?type=bug Editing a morph relation field (e.g., "client" on opportunity) via the record detail UI throws an error because usePersistField generates the wrong foreign key name (`clientId`) that computeOptimisticRecordFromInput doesn't recognize. Fix: The morph relation persist code path in `usePersistField.ts` was using `getForeignKeyNameFromRelationFieldName(fieldName)` which simply appends `Id` to the field name (producing `clientId` from `client`). For morph relations, the correct naming convention uses target-specific names computed by `computeMorphRelationFieldName` (e.g., `clientCompanyId` or `clientPersonId`). The fix replaces the incorrect one-liner FK generation with the proper morph relation pattern already used by `useMorphPersistManyToOne`: 1. Extract `morphRelations` and `relationType` from the typed field definition 2. Build a record resetting all morph FK IDs to null (to clear any stale target) 3. Handle the null case explicitly (clear all morph FKs) 4. For a non-null selection: use `valueToPersist.objectMetadataId` (provided by the picker) to find the correct `targetMorphRelation` in `morphRelations` 5. Call `computeMorphRelationFieldName` to get the target-specific field name (e.g., `clientCompany`) 6. Pass `${computedFieldName}Id` (e.g., `clientCompanyId`) in the update input — which `computeOptimisticRecordFromInput` can correctly recognize ```typescript file=packages/twenty-front/src/modules/object-record/record-field/ui/hooks/usePersistField.ts lines=223-288 if (fieldIsMorphRelationManyToOne) { if (valueToPersist?.id === currentValue?.id) { return; } const morphFieldDefinition = fieldDefinition as FieldDefinition<FieldMorphRelationMetadata>; const morphRelations = morphFieldDefinition.metadata.morphRelations; const relationType = morphFieldDefinition.metadata.relationType; const recordWithAllMorphObjectIdsToNull = buildRecordWithAllMorphObjectIdsToNull({ morphRelations, fieldName, relationType, }); if (!isDefined(valueToPersist)) { await updateOneRecord({ objectNameSingular: objectMetadataItem.nameSingular, idToUpdate: recordId, updateOneRecordInput: recordWithAllMorphObjectIdsToNull, }); return; } const targetMorphRelation = morphRelations.find( (morphRelation) => morphRelation.targetObjectMetadata.id === (valueToPersist as ObjectRecord).objectMetadataId, ); if (!isDefined(targetMorphRelation)) { throw new Error( `Target morph relation not found for objectMetadataId ${(valueToPersist as ObjectRecord).objectMetadataId}`, ); } const computedFieldName = computeMorphRelationFieldName({ fieldName, relationType, targetObjectMetadataNameSingular: targetMorphRelation.targetObjectMetadata.nameSingular, targetObjectMetadataNamePlural: targetMorphRelation.targetObjectMetadata.namePlural, }); const newRecord = await updateOneRecord({ objectNameSingular: objectMetadataItem.nameSingular, idToUpdate: recordId, updateOneRecordInput: { ...recordWithAllMorphObjectIdsToNull, [`${computedFieldName}Id`]: valueToPersist?.id, }, }); upsertRecordsInStore({ partialRecords: [ getRecordFromRecordNode({ recordNode: newRecord, }), ], }); return; } ```
This commit is contained in:
+47
-2
@@ -53,8 +53,11 @@ import { isFieldRichTextValue } from '@/object-record/record-field/ui/types/guar
|
||||
import { isFieldRichTextV2Value } from '@/object-record/record-field/ui/types/guards/isFieldRichTextValueV2';
|
||||
import { isFieldText } from '@/object-record/record-field/ui/types/guards/isFieldText';
|
||||
import { isFieldTextValue } from '@/object-record/record-field/ui/types/guards/isFieldTextValue';
|
||||
import { buildRecordWithAllMorphObjectIdsToNull } from '@/object-record/record-field/ui/meta-types/input/utils/buildRecordWithAllMorphObjectIdsToNull';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const usePersistField = ({
|
||||
@@ -222,12 +225,54 @@ export const usePersistField = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const morphFieldDefinition =
|
||||
fieldDefinition as FieldDefinition<FieldMorphRelationMetadata>;
|
||||
const morphRelations = morphFieldDefinition.metadata.morphRelations;
|
||||
const relationType = morphFieldDefinition.metadata.relationType;
|
||||
|
||||
const recordWithAllMorphObjectIdsToNull =
|
||||
buildRecordWithAllMorphObjectIdsToNull({
|
||||
morphRelations,
|
||||
fieldName,
|
||||
relationType,
|
||||
});
|
||||
|
||||
if (!isDefined(valueToPersist)) {
|
||||
await updateOneRecord({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
idToUpdate: recordId,
|
||||
updateOneRecordInput: recordWithAllMorphObjectIdsToNull,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const targetMorphRelation = morphRelations.find(
|
||||
(morphRelation) =>
|
||||
morphRelation.targetObjectMetadata.id ===
|
||||
(valueToPersist as ObjectRecord).objectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(targetMorphRelation)) {
|
||||
throw new Error(
|
||||
`Target morph relation not found for objectMetadataId ${(valueToPersist as ObjectRecord).objectMetadataId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const computedFieldName = computeMorphRelationFieldName({
|
||||
fieldName,
|
||||
relationType,
|
||||
targetObjectMetadataNameSingular:
|
||||
targetMorphRelation.targetObjectMetadata.nameSingular,
|
||||
targetObjectMetadataNamePlural:
|
||||
targetMorphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
|
||||
const newRecord = await updateOneRecord({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
idToUpdate: recordId,
|
||||
updateOneRecordInput: {
|
||||
[getForeignKeyNameFromRelationFieldName(fieldName)]:
|
||||
valueToPersist?.id ?? null,
|
||||
...recordWithAllMorphObjectIdsToNull,
|
||||
[`${computedFieldName}Id`]: valueToPersist?.id,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@ export const addressSchema = z.object({
|
||||
addressState: z.string().nullable(),
|
||||
addressPostcode: z.string().nullable(),
|
||||
addressCountry: z.string().nullable(),
|
||||
addressLat: z.coerce.number().nullable(),
|
||||
addressLng: z.coerce.number().nullable(),
|
||||
addressLat: z.number().nullable(),
|
||||
addressLng: z.number().nullable(),
|
||||
});
|
||||
|
||||
export const isFieldAddressValue = (
|
||||
|
||||
Reference in New Issue
Block a user