Optimistic follow up (#15071)

Fixing optimistic rendering for the morph relation
This commit is contained in:
Guillim
2025-10-16 12:17:21 +02:00
committed by GitHub
parent d7e3208262
commit da77cd0cdc
7 changed files with 164 additions and 154 deletions
@@ -392,6 +392,7 @@ const extractTargetRecordsFromRelation = (
): RecordGqlNode[] => {
// TODO investigate on the root cause of array injection here, should never occurs
// Cache might be corrupted somewhere due to ObjectRecord and RecordGqlNode inclusion
if (!isDefined(value) || isArray(value)) {
return [];
}
@@ -1,10 +1,9 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById';
import { type FieldMorphRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { computeMorphRelationFieldName } from 'twenty-shared/utils';
export const getFieldMetadataMorphRelationFromGqlField = ({
export const getMorphRelationFromFieldMetadataAndGqlField = ({
objectMetadataItems,
fieldMetadata,
gqlField,
@@ -50,25 +49,10 @@ export const getFieldMetadataMorphRelationFromGqlField = ({
return {
morphRelation: gqlFieldMorphRelation.morphRelation,
sourceFieldMetadata: getFieldMetadataItemById({
fieldMetadataId:
gqlFieldMorphRelation.morphRelation.sourceFieldMetadata.id,
objectMetadataItems,
}),
targetFieldMetadata: getFieldMetadataItemById({
fieldMetadataId:
gqlFieldMorphRelation.morphRelation?.targetFieldMetadata.id,
objectMetadataItems,
}),
targetObjectMetadata: objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id ===
gqlFieldMorphRelation.morphRelation?.targetObjectMetadata.id,
),
sourceObjectMetadata: objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id ===
gqlFieldMorphRelation.morphRelation?.sourceObjectMetadata.id,
gqlFieldMorphRelation.morphRelation.targetObjectMetadata.id,
),
};
};
@@ -1,7 +1,9 @@
import { isNull, isUndefined } from '@sniptt/guards';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { computePossibleMorphGqlFieldForFieldName } from '@/object-record/cache/utils/computePossibleMorphGqlFieldForFieldName';
import { getFieldMetadataFromGqlField } from '@/object-record/cache/utils/getFieldMetadataFromGqlField';
import { getMorphRelationFromFieldMetadataAndGqlField } from '@/object-record/cache/utils/getMorphRelationFromFieldMetadataAndGqlField';
import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename';
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
@@ -44,14 +46,14 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
const nestedRecord = Object.fromEntries(
Object.entries(record)
.map(([fieldName, value]) => {
if (isDefined(recordGqlFields) && !recordGqlFields[fieldName]) {
.map(([gqlField, value]) => {
if (isDefined(recordGqlFields) && !recordGqlFields[gqlField]) {
return undefined;
}
const field = getFieldMetadataFromGqlField({
objectMetadataItem,
gqlField: fieldName,
gqlField: gqlField,
});
if (isUndefined(field)) {
@@ -73,16 +75,56 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
}
return [
fieldName,
gqlField,
getRecordConnectionFromRecords({
objectMetadataItems,
objectMetadataItem: oneToManyObjectMetadataItem,
records: value as ObjectRecord[],
recordGqlFields:
recordGqlFields?.[fieldName] === true ||
isUndefined(recordGqlFields?.[fieldName])
recordGqlFields?.[gqlField] === true ||
isUndefined(recordGqlFields?.[gqlField])
? undefined
: recordGqlFields?.[fieldName],
: recordGqlFields?.[gqlField],
withPageInfo: false,
isRootLevel: false,
computeReferences,
}),
];
}
if (
field.type === FieldMetadataType.MORPH_RELATION &&
field.settings?.relationType === RelationType.ONE_TO_MANY
) {
if (field.morphRelations?.length === 0) {
return undefined;
}
const morphRelation = getMorphRelationFromFieldMetadataAndGqlField({
objectMetadataItems,
fieldMetadata: { morphRelations: field.morphRelations ?? [] },
gqlField: gqlField,
});
if (isUndefined(morphRelation?.targetObjectMetadata?.nameSingular)) {
return undefined;
}
if (!morphRelation?.targetObjectMetadata?.nameSingular) {
return undefined;
}
return [
gqlField,
getRecordConnectionFromRecords({
objectMetadataItems,
objectMetadataItem: morphRelation?.targetObjectMetadata,
records: value as ObjectRecord[],
recordGqlFields:
recordGqlFields?.[gqlField] === true ||
isUndefined(recordGqlFields?.[gqlField])
? undefined
: recordGqlFields?.[gqlField],
withPageInfo: false,
isRootLevel: false,
computeReferences,
@@ -92,10 +134,9 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
switch (field.type) {
case FieldMetadataType.RELATION: {
const isJoinColumn = field.settings?.joinColumnName === fieldName;
const isJoinColumn = field.settings?.joinColumnName === gqlField;
if (isJoinColumn) {
return [fieldName, value];
return [gqlField, value];
}
if (
@@ -105,7 +146,7 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
}
if (isNull(value)) {
return [fieldName, null];
return [gqlField, null];
}
if (isUndefined(value?.id)) {
@@ -118,7 +159,7 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
if (computeReferences) {
return [
fieldName,
gqlField,
{
__ref: `${typeName}:${value.id}`,
},
@@ -126,7 +167,64 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
}
return [
fieldName,
gqlField,
{
__typename: typeName,
...value,
},
];
}
case FieldMetadataType.MORPH_RELATION: {
const possibleMorphRelationsJoinColumnNames =
computePossibleMorphGqlFieldForFieldName({
fieldMetadata: {
morphRelations: field.morphRelations ?? [],
fieldName: field.name,
},
}).map(
(possibleMorphRelationName) => `${possibleMorphRelationName}Id`,
);
const isJoinColumn =
possibleMorphRelationsJoinColumnNames.includes(gqlField);
if (isJoinColumn) {
return [gqlField, value];
}
if (field.morphRelations?.length === 0) {
return undefined;
}
const morphRelation = getMorphRelationFromFieldMetadataAndGqlField({
objectMetadataItems,
fieldMetadata: { morphRelations: field.morphRelations ?? [] },
gqlField: gqlField,
});
if (
isUndefined(morphRelation?.targetObjectMetadata?.nameSingular)
) {
return undefined;
}
const typeName = getObjectTypename(
morphRelation?.targetObjectMetadata?.nameSingular,
);
if (isNull(value)) {
return [gqlField, null];
}
if (isUndefined(value?.id)) {
return undefined;
}
if (computeReferences) {
return [gqlField, { __ref: `${typeName}:${value.id}` }];
}
return [
gqlField,
{
__typename: typeName,
...value,
@@ -138,7 +236,7 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
case FieldMetadataType.FULL_NAME:
case FieldMetadataType.CURRENCY: {
return [
fieldName,
gqlField,
{
...value,
__typename: pascalCase(field.type),
@@ -146,7 +244,7 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
];
}
default: {
return [fieldName, value];
return [gqlField, value];
}
}
})
@@ -5,6 +5,7 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
import { generateUpdateOneRecordMutation } from '@/object-metadata/utils/generateUpdateOneRecordMutation';
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { type RecordGqlFields } from '@/object-record/graphql/record-gql-fields/types/RecordGqlFields';
@@ -171,6 +172,11 @@ export const useUpdateOneRecordV2 = () => {
const record = data?.[mutationResponseField];
if (!isDefined(record)) return;
const recordToUpsert = getRecordFromRecordNode({
recordNode: record,
});
upsertRecordsInStore([recordToUpsert]);
triggerUpdateRecordOptimisticEffect({
cache,
objectMetadataItem,
@@ -26,16 +26,11 @@ import { isFieldSelect } from '@/object-record/record-field/ui/types/guards/isFi
import { isFieldSelectValue } from '@/object-record/record-field/ui/types/guards/isFieldSelectValue';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { isFieldArray } from '@/object-record/record-field/ui/types/guards/isFieldArray';
import { isFieldArrayValue } from '@/object-record/record-field/ui/types/guards/isFieldArrayValue';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne';
import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne';
import { isFieldRelationManyToOneValue } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOneValue';
@@ -45,7 +40,6 @@ import { isFieldRichTextValue } from '@/object-record/record-field/ui/types/guar
import { isFieldRichTextV2Value } from '@/object-record/record-field/ui/types/guards/isFieldRichTextValueV2';
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';
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue';
@@ -65,18 +59,15 @@ export const usePersistField = ({
}: {
objectMetadataItemId: string;
}) => {
const apolloCoreClient = useApolloCoreClient();
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataItemId,
});
const { objectMetadataItems } = useObjectMetadataItems();
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: objectMetadataItem?.nameSingular ?? '',
});
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const persistField = useRecoilCallback(
({ set, snapshot }) =>
@@ -222,68 +213,6 @@ export const usePersistField = ({
return;
}
if (fieldIsMorphRelationManyToOne) {
if (isFieldMorphRelation(fieldDefinition)) {
// TODO: this is hack, we should refactor the SingleRecordPicker to pass the updated object
const objectNameSingulars =
fieldDefinition.metadata.morphRelations.map(
(morphRelation) =>
morphRelation.targetObjectMetadata.nameSingular,
);
if (!isDefined(valueToPersist?.id)) {
// Handle detach
return;
}
for (const objectNameSingular of objectNameSingulars) {
const candidateObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular,
);
if (!isDefined(candidateObjectMetadataItem)) {
throw new Error(
`Object metadata item not found for ${objectNameSingular}`,
);
}
const cachedRecord = getRecordFromCache({
cache: apolloCoreClient.cache,
objectMetadataItem: candidateObjectMetadataItem,
objectMetadataItems,
recordId: valueToPersist.id,
objectPermissionsByObjectMetadataId,
});
if (!isDefined(cachedRecord)) {
continue;
}
const computedFieldName = computeMorphRelationFieldName({
fieldName,
relationType: fieldDefinition.metadata.relationType,
targetObjectMetadataNameSingular: objectNameSingular,
targetObjectMetadataNamePlural:
candidateObjectMetadataItem.namePlural,
});
const newRecord = await updateOneRecord?.({
idToUpdate: recordId,
updateOneRecordInput: {
[`${computedFieldName}Id`]: valueToPersist.id,
},
});
upsertRecordsInStore([
getRecordFromRecordNode({
recordNode: newRecord,
}),
]);
return;
}
}
}
if (isDeeplyEqual(valueToPersist, currentValue)) {
return;
}
@@ -308,13 +237,7 @@ export const usePersistField = ({
);
}
},
[
apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
updateOneRecord,
upsertRecordsInStore,
],
[updateOneRecord, upsertRecordsInStore],
);
return persistField;
@@ -5,7 +5,8 @@ import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldM
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useUpdateOneRecordV2 } from '@/object-record/hooks/useUpdateOneRecordV2';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
@@ -21,12 +22,10 @@ export const useMorphPersistManyToOne = ({
}: MorphPersistManyToOneProps) => {
const { objectMetadataItems } = useObjectMetadataItems();
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: objectMetadataNameSingular,
});
const { updateOneRecord } = useUpdateOneRecordV2();
const persistMorphManyToOne = useRecoilCallback(
({ set, snapshot }) =>
({ snapshot }) =>
async ({
recordId,
fieldDefinition,
@@ -84,22 +83,35 @@ export const useMorphPersistManyToOne = ({
return;
}
const allNullRecordInput: Record<string, null> =
fieldDefinition.metadata.morphRelations.reduce(
(acc, morphRelation) => {
const computedFieldName = computeMorphRelationFieldName({
fieldName,
relationType: fieldDefinition.metadata.relationType,
targetObjectMetadataNameSingular:
morphRelation.targetObjectMetadata.nameSingular,
targetObjectMetadataNamePlural:
morphRelation.targetObjectMetadata.namePlural,
});
acc[`${computedFieldName}Id`] = null;
return acc;
},
{} as Record<string, null>,
);
updateOneRecord?.({
objectNameSingular: objectMetadataNameSingular,
idToUpdate: recordId,
updateOneRecordInput: {
...allNullRecordInput,
[`${computedFieldName}Id`]: valueToPersist,
},
});
set(
recordStoreFamilySelector({
recordId,
fieldName: computedFieldName,
}),
valueToPersist,
);
return;
},
[updateOneRecord, objectMetadataItems],
[updateOneRecord, objectMetadataItems, objectMetadataNameSingular],
);
return { persistMorphManyToOne };
@@ -3,7 +3,7 @@ import { isNull, isUndefined } from '@sniptt/guards';
import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getFieldMetadataFromGqlField } from '@/object-record/cache/utils/getFieldMetadataFromGqlField';
import { getFieldMetadataMorphRelationFromGqlField } from '@/object-record/cache/utils/getFieldMetadataMorphRelationFromGqlField';
import { getMorphRelationFromFieldMetadataAndGqlField } from '@/object-record/cache/utils/getMorphRelationFromFieldMetadataAndGqlField';
import {
getRecordFromCache,
type GetRecordFromCacheArgs,
@@ -146,19 +146,19 @@ export const computeOptimisticRecordFromInput = ({
);
}
const relationFieldIdName = getForeignKeyNameFromRelationFieldName(
const relationGqlFieldWithId = getForeignKeyNameFromRelationFieldName(
fieldMetadataItem.name,
);
const recordInputFieldIdValue: string | null | undefined =
recordInput[relationFieldIdName];
recordInput[relationGqlFieldWithId];
if (isUndefined(recordInputFieldIdValue)) {
continue;
}
if (isNull(recordInputFieldIdValue)) {
optimisticRecord[relationFieldIdName] = null;
optimisticRecord[relationGqlFieldWithId] = null;
optimisticRecord[fieldMetadataItem.name] = null;
continue;
}
@@ -182,7 +182,7 @@ export const computeOptimisticRecordFromInput = ({
objectPermissionsByObjectMetadataId,
});
optimisticRecord[relationFieldIdName] = recordInputFieldIdValue;
optimisticRecord[relationGqlFieldWithId] = recordInputFieldIdValue;
if (!isDefined(cachedRecord) || Object.keys(cachedRecord).length <= 0) {
continue;
@@ -208,7 +208,7 @@ export const computeOptimisticRecordFromInput = ({
);
}
const relationFieldIdNames = fieldMetadataItem.morphRelations?.map(
const relationGqlFields = fieldMetadataItem.morphRelations?.map(
(morphRelation) => {
return computeMorphRelationFieldName({
fieldName: fieldMetadataItem.name,
@@ -221,39 +221,25 @@ export const computeOptimisticRecordFromInput = ({
},
);
const relationFieldName = relationFieldIdNames?.find(
(relationFieldIdName) => recordInput[`${relationFieldIdName}Id`],
const relationGqlField = relationGqlFields?.find(
(relationGqlField) => recordInput[`${relationGqlField}Id`],
);
const relationFieldIdName = `${relationFieldName}Id`;
const relationGqlFieldWithId = `${relationGqlField}Id`;
if (isUndefined(relationFieldName)) {
if (isUndefined(relationGqlField)) {
continue;
}
const recordInputFieldIdValue: string | null | undefined =
recordInput[relationFieldIdName];
recordInput[relationGqlFieldWithId];
if (isUndefined(recordInputFieldIdValue)) {
continue;
}
const relationIdFieldMetadataItem = getFieldMetadataFromGqlField({
objectMetadataItem,
gqlField: relationFieldName,
});
if (
!isDefined(relationIdFieldMetadataItem) &&
!isDefined(fieldMetadataItem.settings?.joinColumnName)
) {
throw new Error(
'Should never occur, encountered unknown relationId within relations definitions',
);
}
if (isNull(recordInputFieldIdValue)) {
optimisticRecord[relationFieldName] = null;
optimisticRecord[relationGqlField] = null;
optimisticRecord[fieldMetadataItem.name] = null;
continue;
}
@@ -265,10 +251,10 @@ export const computeOptimisticRecordFromInput = ({
}
const fieldMetadataMorphRelation =
getFieldMetadataMorphRelationFromGqlField({
getMorphRelationFromFieldMetadataAndGqlField({
objectMetadataItems,
fieldMetadata: { morphRelations: fieldMetadataItem.morphRelations },
gqlField: relationFieldName,
gqlField: relationGqlField,
});
if (!isDefined(fieldMetadataMorphRelation?.targetObjectMetadata)) {
@@ -285,13 +271,13 @@ export const computeOptimisticRecordFromInput = ({
objectPermissionsByObjectMetadataId,
});
optimisticRecord[relationFieldIdName] = recordInputFieldIdValue;
optimisticRecord[relationGqlFieldWithId] = recordInputFieldIdValue;
if (!isDefined(cachedRecord) || Object.keys(cachedRecord).length <= 0) {
continue;
}
optimisticRecord[relationFieldName] = cachedRecord;
optimisticRecord[relationGqlField] = cachedRecord;
}
}