Morph relation persist uses wrong foreign key naming, producing invalid field parentObjectId
https://sonarly.com/issue/8116?type=bug When editing a morph relation field on a task record via the Field Widget, `usePersistField.ts` constructs the wrong foreign key name (`parentObjectId` instead of e.g. `parentObjectCompanyId`), causing the optimistic cache validation to throw. Fix: The morph relation branch in `usePersistField.ts` (lines 220–243, introduced by regression commit `4e767799c6`) used `getForeignKeyNameFromRelationFieldName(fieldName)` to construct the update key, producing `parentObjectId` instead of the required target-specific key like `parentObjectCompanyId`. This caused `computeOptimisticRecordFromInput` to throw because `parentObjectId` doesn't match any known field pattern for morph relations. The fix replaces the broken morph relation branch with the correct approach that mirrors `useMorphPersistManyToOne`: 1. **Extracts `morphRelations` and `relationType`** from the field definition metadata (cast as `FieldMorphRelationMetadata`). 2. **Builds the null-out record** using `buildRecordWithAllMorphObjectIdsToNull` — this correctly zeroes out all morph target IDs (e.g., both `parentObjectCompanyId` and `parentObjectPersonId`) before setting the new one. 3. **For null values**: sends the all-null record to clear the relation. 4. **For non-null values**: finds the matching `morphRelation` by comparing `valueToPersist.__typename` (e.g., `"Company"`) against each `targetObjectMetadata.nameSingular` (e.g., `"company"`), then uses `computeMorphRelationFieldName()` to compute the correct key (e.g., `parentObjectCompany`), and sends `{ ...allNull, parentObjectCompanyId: valueToPersist.id }`. ```typescript file=packages/twenty-front/src/modules/object-record/record-field/ui/hooks/usePersistField.ts lines=222-292 if (fieldIsMorphRelationManyToOne) { if (valueToPersist?.id === currentValue?.id) { return; } const morphFieldDefinition = fieldDefinition as FieldDefinition<FieldMorphRelationMetadata>; const { morphRelations, relationType } = morphFieldDefinition.metadata; const recordWithAllMorphObjectIdsToNull = buildRecordWithAllMorphObjectIdsToNull({ morphRelations, fieldName, relationType, }); if (!valueToPersist) { // null out all morph IDs to clear the relation const newRecord = await updateOneRecord({ ... }); upsertRecordsInStore({ ... }); return; } const targetMorphRelation = morphRelations.find( (morphRelation) => morphRelation.targetObjectMetadata.nameSingular.toLowerCase() === valueToPersist.__typename?.toLowerCase(), ); const computedFieldName = computeMorphRelationFieldName({ fieldName, relationType, targetObjectMetadataNameSingular: targetMorphRelation.targetObjectMetadata.nameSingular, targetObjectMetadataNamePlural: targetMorphRelation.targetObjectMetadata.namePlural, }); // Produces e.g. "parentObjectCompanyId" ✓ instead of "parentObjectId" ✗ const newRecord = await updateOneRecord({ updateOneRecordInput: { ...recordWithAllMorphObjectIdsToNull, [`${computedFieldName}Id`]: valueToPersist.id, }, }); ... } ``` Two additional imports were added: - `computeMorphRelationFieldName` from `twenty-shared/utils` - `buildRecordWithAllMorphObjectIdsToNull` from the local utils path
This commit is contained in:
+54
-5
@@ -53,9 +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 { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { computeMorphRelationFieldName } from 'twenty-shared/utils';
|
||||
|
||||
export const usePersistField = ({
|
||||
objectMetadataItemId,
|
||||
@@ -222,20 +224,67 @@ export const usePersistField = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const morphFieldDefinition =
|
||||
fieldDefinition as FieldDefinition<FieldMorphRelationMetadata>;
|
||||
const { morphRelations, relationType } =
|
||||
morphFieldDefinition.metadata;
|
||||
|
||||
const recordWithAllMorphObjectIdsToNull =
|
||||
buildRecordWithAllMorphObjectIdsToNull({
|
||||
morphRelations,
|
||||
fieldName,
|
||||
relationType,
|
||||
});
|
||||
|
||||
if (!valueToPersist) {
|
||||
const newRecord = await updateOneRecord({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
idToUpdate: recordId,
|
||||
updateOneRecordInput: recordWithAllMorphObjectIdsToNull,
|
||||
});
|
||||
|
||||
upsertRecordsInStore({
|
||||
partialRecords: [
|
||||
getRecordFromRecordNode({ recordNode: newRecord }),
|
||||
],
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const targetMorphRelation = morphRelations.find(
|
||||
(morphRelation) =>
|
||||
morphRelation.targetObjectMetadata.nameSingular.toLowerCase() ===
|
||||
valueToPersist.__typename?.toLowerCase(),
|
||||
);
|
||||
|
||||
if (!targetMorphRelation) {
|
||||
throw new Error(
|
||||
`Could not find morph relation target for __typename: ${valueToPersist.__typename}`,
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
upsertRecordsInStore({
|
||||
partialRecords: [
|
||||
getRecordFromRecordNode({
|
||||
recordNode: newRecord,
|
||||
}),
|
||||
getRecordFromRecordNode({ recordNode: newRecord }),
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user