diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts index b9313fae3ecc..017dad72de49 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts @@ -2,9 +2,11 @@ import { useApolloClient } from '@apollo/client'; import { v4 } from 'uuid'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; +import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache'; +import { deleteRecordFromCache } from '@/object-record/cache/utils/deleteRecordFromCache'; import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; @@ -67,7 +69,7 @@ export const useCreateManyRecords = < }, ); - const recordsCreatedInCache = []; + const recordsCreatedInCache: ObjectRecord[] = []; for (const recordToCreate of sanitizedCreateManyRecordsInput) { if (recordToCreate.id === null) { @@ -98,26 +100,46 @@ export const useCreateManyRecords = < objectMetadataItem.namePlural, ); - const createdObjects = await apolloClient.mutate({ - mutation: createManyRecordsMutation, - variables: { - data: sanitizedCreateManyRecordsInput, - upsert: upsert, - }, - update: (cache, { data }) => { - const records = data?.[mutationResponseField]; + const createdObjects = await apolloClient + .mutate({ + mutation: createManyRecordsMutation, + variables: { + data: sanitizedCreateManyRecordsInput, + upsert: upsert, + }, + update: (cache, { data }) => { + const records = data?.[mutationResponseField]; + + if (!records?.length || skipPostOptmisticEffect) return; - if (!records?.length || skipPostOptmisticEffect) return; + triggerCreateRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToCreate: records, + objectMetadataItems, + shouldMatchRootQueryFilter, + }); + }, + }) + .catch((error: Error) => { + recordsCreatedInCache.forEach((recordToDelete) => { + deleteRecordFromCache({ + objectMetadataItems, + objectMetadataItem, + cache: apolloClient.cache, + recordToDelete, + }); + }); - triggerCreateRecordsOptimisticEffect({ - cache, + triggerDeleteRecordsOptimisticEffect({ + cache: apolloClient.cache, objectMetadataItem, - recordsToCreate: records, + recordsToDelete: recordsCreatedInCache, objectMetadataItems, - shouldMatchRootQueryFilter, }); - }, - }); + + throw error; + }); return createdObjects.data?.[mutationResponseField] ?? []; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts index f34ce0692f7a..2e9d79239094 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts @@ -3,9 +3,11 @@ import { useState } from 'react'; import { v4 } from 'uuid'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; +import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache'; +import { deleteRecordFromCache } from '@/object-record/cache/utils/deleteRecordFromCache'; import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; @@ -85,27 +87,49 @@ export const useCreateOneRecord = < const mutationResponseField = getCreateOneRecordMutationResponseField(objectNameSingular); - const createdObject = await apolloClient.mutate({ - mutation: createOneRecordMutation, - variables: { - input: sanitizedInput, - }, - update: (cache, { data }) => { - const record = data?.[mutationResponseField]; - - if (!record || skipPostOptmisticEffect) return; + const createdObject = await apolloClient + .mutate({ + mutation: createOneRecordMutation, + variables: { + input: sanitizedInput, + }, + update: (cache, { data }) => { + const record = data?.[mutationResponseField]; + + if (!record || skipPostOptmisticEffect) return; + + triggerCreateRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToCreate: [record], + objectMetadataItems, + shouldMatchRootQueryFilter, + }); + + setLoading(false); + }, + }) + .catch((error: Error) => { + if (!recordCreatedInCache) { + throw error; + } + + deleteRecordFromCache({ + objectMetadataItems, + objectMetadataItem, + cache: apolloClient.cache, + recordToDelete: recordCreatedInCache, + }); - triggerCreateRecordsOptimisticEffect({ - cache, + triggerDeleteRecordsOptimisticEffect({ + cache: apolloClient.cache, objectMetadataItem, - recordsToCreate: [record], + recordsToDelete: [recordCreatedInCache], objectMetadataItems, - shouldMatchRootQueryFilter, }); - setLoading(false); - }, - }); + throw error; + }); return createdObject.data?.[mutationResponseField] ?? null; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts index 35a65a507765..38bd825d55de 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts @@ -1,10 +1,12 @@ import { useApolloClient } from '@apollo/client'; +import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { apiConfigState } from '@/client-config/states/apiConfigState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; +import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation'; import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField'; @@ -65,38 +67,74 @@ export const useDeleteManyRecords = ({ (batchIndex + 1) * mutationPageSize, ); - const deletedRecordsResponse = await apolloClient.mutate({ - mutation: deleteManyRecordsMutation, - variables: { - filter: { id: { in: batchIds } }, - }, - optimisticResponse: options?.skipOptimisticEffect - ? undefined - : { - [mutationResponseField]: batchIds.map((idToDelete) => ({ - __typename: capitalize(objectNameSingular), - id: idToDelete, + const deletedRecordsResponse = await apolloClient + .mutate({ + mutation: deleteManyRecordsMutation, + variables: { + filter: { id: { in: batchIds } }, + }, + optimisticResponse: options?.skipOptimisticEffect + ? undefined + : { + [mutationResponseField]: batchIds.map((idToDelete) => ({ + __typename: capitalize(objectNameSingular), + id: idToDelete, + })), + }, + update: options?.skipOptimisticEffect + ? undefined + : (cache, { data }) => { + const records = data?.[mutationResponseField]; + + if (!records?.length) return; + + const cachedRecords = records + .map((record) => getRecordFromCache(record.id, cache)) + .filter(isDefined); + + triggerDeleteRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToDelete: cachedRecords, + objectMetadataItems, + }); + }, + }) + .catch((error: Error) => { + const cachedRecords = batchIds.map((idToDelete) => + getRecordFromCache(idToDelete, apolloClient.cache), + ); + + cachedRecords.forEach((cachedRecord) => { + if (!cachedRecord) { + return; + } + + updateRecordFromCache({ + objectMetadataItems, + objectMetadataItem, + cache: apolloClient.cache, + record: { + ...cachedRecord, + deletedAt: null, + }, + }); + }); + + triggerCreateRecordsOptimisticEffect({ + cache: apolloClient.cache, + objectMetadataItem, + objectMetadataItems, + recordsToCreate: cachedRecords + .filter(isDefined) + .map((cachedRecord) => ({ + ...cachedRecord, + deletedAt: null, })), - }, - update: options?.skipOptimisticEffect - ? undefined - : (cache, { data }) => { - const records = data?.[mutationResponseField]; - - if (!records?.length) return; - - const cachedRecords = records - .map((record) => getRecordFromCache(record.id, cache)) - .filter(isDefined); - - triggerDeleteRecordsOptimisticEffect({ - cache, - objectMetadataItem, - recordsToDelete: cachedRecords, - objectMetadataItems, - }); - }, - }); + }); + + throw error; + }); const deletedRecordsForThisBatch = deletedRecordsResponse.data?.[mutationResponseField] ?? []; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts index 3b33fe3e7ed1..b39871ba2753 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts @@ -1,10 +1,12 @@ import { useApolloClient } from '@apollo/client'; import { useCallback } from 'react'; +import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; +import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation'; import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField'; import { capitalize } from '~/utils/string/capitalize'; @@ -39,35 +41,70 @@ export const useDeleteOneRecord = ({ async (idToDelete: string) => { const currentTimestamp = new Date().toISOString(); - const deletedRecord = await apolloClient.mutate({ - mutation: deleteOneRecordMutation, - variables: { - idToDelete: idToDelete, - }, - optimisticResponse: { - [mutationResponseField]: { - __typename: capitalize(objectNameSingular), - id: idToDelete, - deletedAt: currentTimestamp, + const deletedRecord = await apolloClient + .mutate({ + mutation: deleteOneRecordMutation, + variables: { + idToDelete: idToDelete, }, - }, - update: (cache, { data }) => { - const record = data?.[mutationResponseField]; + optimisticResponse: { + [mutationResponseField]: { + __typename: capitalize(objectNameSingular), + id: idToDelete, + deletedAt: currentTimestamp, + }, + }, + update: (cache, { data }) => { + const record = data?.[mutationResponseField]; - if (!record) return; + if (!record) return; - const cachedRecord = getRecordFromCache(record.id, cache); + const cachedRecord = getRecordFromCache(record.id, cache); - if (!cachedRecord) return; + if (!cachedRecord) return; - triggerDeleteRecordsOptimisticEffect({ - cache, + triggerDeleteRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToDelete: [cachedRecord], + objectMetadataItems, + }); + }, + }) + .catch((error: Error) => { + const cachedRecord = getRecordFromCache( + idToDelete, + apolloClient.cache, + ); + + if (!cachedRecord) { + throw error; + } + + updateRecordFromCache({ + objectMetadataItems, + objectMetadataItem, + cache: apolloClient.cache, + record: { + ...cachedRecord, + deletedAt: null, + }, + }); + + triggerCreateRecordsOptimisticEffect({ + cache: apolloClient.cache, objectMetadataItem, - recordsToDelete: [cachedRecord], objectMetadataItems, + recordsToCreate: [ + { + ...cachedRecord, + deletedAt: null, + }, + ], }); - }, - }); + + throw error; + }); return deletedRecord.data?.[mutationResponseField] ?? null; }, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts index 08f4d092a135..3ba7283b9666 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts @@ -1,5 +1,6 @@ import { useApolloClient } from '@apollo/client'; +import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { apiConfigState } from '@/client-config/states/apiConfigState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; @@ -65,38 +66,54 @@ export const useDestroyManyRecords = ({ (batchIndex + 1) * mutationPageSize, ); - const destroyedRecordsResponse = await apolloClient.mutate({ - mutation: destroyManyRecordsMutation, - variables: { - filter: { id: { in: batchIds } }, - }, - optimisticResponse: options?.skipOptimisticEffect - ? undefined - : { - [mutationResponseField]: batchIds.map((idToDestroy) => ({ - __typename: capitalize(objectNameSingular), - id: idToDestroy, - })), - }, - update: options?.skipOptimisticEffect - ? undefined - : (cache, { data }) => { - const records = data?.[mutationResponseField]; - - if (!records?.length) return; - - const cachedRecords = records - .map((record) => getRecordFromCache(record.id, cache)) - .filter(isDefined); - - triggerDeleteRecordsOptimisticEffect({ - cache, - objectMetadataItem, - recordsToDelete: cachedRecords, - objectMetadataItems, - }); - }, - }); + const originalRecords = idsToDestroy + .map((recordId) => getRecordFromCache(recordId, apolloClient.cache)) + .filter(isDefined); + + const destroyedRecordsResponse = await apolloClient + .mutate({ + mutation: destroyManyRecordsMutation, + variables: { + filter: { id: { in: batchIds } }, + }, + optimisticResponse: options?.skipOptimisticEffect + ? undefined + : { + [mutationResponseField]: batchIds.map((idToDestroy) => ({ + __typename: capitalize(objectNameSingular), + id: idToDestroy, + })), + }, + update: options?.skipOptimisticEffect + ? undefined + : (cache, { data }) => { + const records = data?.[mutationResponseField]; + + if (!records?.length) return; + + const cachedRecords = records + .map((record) => getRecordFromCache(record.id, cache)) + .filter(isDefined); + + triggerDeleteRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToDelete: cachedRecords, + objectMetadataItems, + }); + }, + }) + .catch((error: Error) => { + if (originalRecords.length > 0) { + triggerCreateRecordsOptimisticEffect({ + cache: apolloClient.cache, + objectMetadataItem, + recordsToCreate: originalRecords, + objectMetadataItems, + }); + } + throw error; + }); const destroyedRecordsForThisBatch = destroyedRecordsResponse.data?.[mutationResponseField] ?? []; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecord.ts index fc5d75d0a42f..91446a87262a 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecord.ts @@ -1,12 +1,15 @@ import { useApolloClient } from '@apollo/client'; import { useCallback } from 'react'; +import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getDestroyOneRecordMutationResponseField } from '@/object-record/utils/getDestroyOneRecordMutationResponseField'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { capitalize } from '~/utils/string/capitalize'; type useDestroyOneRecordProps = { @@ -38,32 +41,49 @@ export const useDestroyOneRecord = ({ const destroyOneRecord = useCallback( async (idToDestroy: string) => { - const deletedRecord = await apolloClient.mutate({ - mutation: destroyOneRecordMutation, - variables: { idToDestroy }, - optimisticResponse: { - [mutationResponseField]: { - __typename: capitalize(objectNameSingular), - id: idToDestroy, + const originalRecord: ObjectRecord | null = getRecordFromCache( + idToDestroy, + apolloClient.cache, + ); + + const deletedRecord = await apolloClient + .mutate({ + mutation: destroyOneRecordMutation, + variables: { idToDestroy }, + optimisticResponse: { + [mutationResponseField]: { + __typename: capitalize(objectNameSingular), + id: idToDestroy, + }, }, - }, - update: (cache, { data }) => { - const record = data?.[mutationResponseField]; + update: (cache, { data }) => { + const record = data?.[mutationResponseField]; - if (!record) return; + if (!record) return; - const cachedRecord = getRecordFromCache(record.id, cache); + const cachedRecord = getRecordFromCache(record.id, cache); - if (!cachedRecord) return; + if (!cachedRecord) return; - triggerDeleteRecordsOptimisticEffect({ - cache, - objectMetadataItem, - recordsToDelete: [cachedRecord], - objectMetadataItems, - }); - }, - }); + triggerDeleteRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToDelete: [cachedRecord], + objectMetadataItems, + }); + }, + }) + .catch((error: Error) => { + if (!isUndefinedOrNull(originalRecord)) { + triggerCreateRecordsOptimisticEffect({ + cache: apolloClient.cache, + objectMetadataItem, + recordsToCreate: [originalRecord], + objectMetadataItems, + }); + } + throw error; + }); return deletedRecord.data?.[mutationResponseField] ?? null; }, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRestoreManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useRestoreManyRecords.ts index 55bd5cc5e865..66af0949fef0 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useRestoreManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useRestoreManyRecords.ts @@ -62,22 +62,27 @@ export const useRestoreManyRecords = ({ objectMetadataItem.namePlural, )}`; - const restoredRecordsResponse = await apolloClient.mutate({ - mutation: restoreManyRecordsMutation, - refetchQueries: [findOneQueryName, findManyQueryName], - variables: { - filter: { id: { in: batchIds } }, - }, - optimisticResponse: options?.skipOptimisticEffect - ? undefined - : { - [mutationResponseField]: batchIds.map((idToRestore) => ({ - __typename: capitalize(objectNameSingular), - id: idToRestore, - deletedAt: null, - })), - }, - }); + const restoredRecordsResponse = await apolloClient + .mutate({ + mutation: restoreManyRecordsMutation, + refetchQueries: [findOneQueryName, findManyQueryName], + variables: { + filter: { id: { in: batchIds } }, + }, + optimisticResponse: options?.skipOptimisticEffect + ? undefined + : { + [mutationResponseField]: batchIds.map((idToRestore) => ({ + __typename: capitalize(objectNameSingular), + id: idToRestore, + deletedAt: null, + })), + }, + }) + .catch((error: Error) => { + // TODO: revert optimistic effect (once optimistic effect is fixed) + throw error; + }); const restoredRecordsForThisBatch = restoredRecordsResponse.data?.[mutationResponseField] ?? []; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts index 427c48547172..c87cbf9246a9 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts @@ -108,26 +108,48 @@ export const useUpdateOneRecord = < const mutationResponseField = getUpdateOneRecordMutationResponseField(objectNameSingular); - const updatedRecord = await apolloClient.mutate({ - mutation: updateOneRecordMutation, - variables: { - idToUpdate, - input: sanitizedInput, - }, - update: (cache, { data }) => { - const record = data?.[mutationResponseField]; - - if (!record || !cachedRecord) return; + const updatedRecord = await apolloClient + .mutate({ + mutation: updateOneRecordMutation, + variables: { + idToUpdate, + input: sanitizedInput, + }, + update: (cache, { data }) => { + const record = data?.[mutationResponseField]; + + if (!record || !cachedRecord) return; + + triggerUpdateRecordOptimisticEffect({ + cache, + objectMetadataItem, + currentRecord: cachedRecord, + updatedRecord: record, + objectMetadataItems, + }); + }, + }) + .catch((error: Error) => { + if (!cachedRecord) { + throw error; + } + updateRecordFromCache({ + objectMetadataItems, + objectMetadataItem, + cache: apolloClient.cache, + record: cachedRecord, + }); triggerUpdateRecordOptimisticEffect({ - cache, + cache: apolloClient.cache, objectMetadataItem, - currentRecord: cachedRecord, - updatedRecord: record, + currentRecord: optimisticRecordWithConnection, + updatedRecord: cachedRecordWithConnection, objectMetadataItems, }); - }, - }); + + throw error; + }); return updatedRecord?.data?.[mutationResponseField] ?? null; };