Fix optimistic effect deletedAt (#7606)
In this PR, I'm fixing part of the impact of soft deletion on optimistic
rendering.
## Backend Vision
1) Backend endpoints will not return soft deleted records (having
deletedAt set) by default. To get the softDeleted records, we will pass
a { withSoftDelete: true } additional param in the query.
2) Record relations will NEVER contain softDeleted relations
## Backend current state
Right now, we have the following behavior:
- if the query filters do not mention deletedAt, we don't return
softDeletedRecords
- if the query filters mention deletedAt, we take it into consideration.
Meaning that if we want to have the softDeleted records in any way we
need to do { or: [ deletedAt: NULL, deletedAt: NOT_NULL] }
## Optimistic rendering strategy
1) useDestroyOne/Many is triggering destroyOptimisticEffects (previously
deleteOptimisticEffects)
2) UseDeleteOne/Many and useRestoreOne/Many are actually triggering
updateOptimisticEffects (as they only update deletedAt field) AND we
need updateOptimisticEffects to take into account deletedAt (future
withSoftDelete: true) filter.
This commit is contained in:
@ -18,11 +18,11 @@ export const useDeleteRecordFromCache = ({
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
return (recordToDelete: ObjectRecord) => {
|
||||
return (recordToDestroy: ObjectRecord) => {
|
||||
deleteRecordFromCache({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordToDelete,
|
||||
recordToDestroy,
|
||||
cache: apolloClient.cache,
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ApolloCache } from '@apollo/client';
|
||||
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
@ -8,21 +8,21 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
export const deleteRecordFromCache = ({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordToDelete,
|
||||
recordToDestroy,
|
||||
cache,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
recordToDelete: ObjectRecord;
|
||||
recordToDestroy: ObjectRecord;
|
||||
cache: ApolloCache<object>;
|
||||
}) => {
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
triggerDestroyRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordsToDelete: [
|
||||
recordsToDestroy: [
|
||||
{
|
||||
...recordToDelete,
|
||||
...recordToDestroy,
|
||||
__typename: getObjectTypename(objectMetadataItem.nameSingular),
|
||||
},
|
||||
],
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import {
|
||||
query,
|
||||
@ -6,9 +6,10 @@ import {
|
||||
variables,
|
||||
} from '@/object-record/hooks/__mocks__/useDeleteManyRecords';
|
||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
||||
import { act } from 'react';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
|
||||
const people = [
|
||||
const personIds = [
|
||||
'a7286b9a-c039-4a89-9567-2dfa7953cda9',
|
||||
'37faabcd-cb39-4a0a-8618-7e3fda9afca0',
|
||||
];
|
||||
@ -41,7 +42,7 @@ describe('useDeleteManyRecords', () => {
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const res = await result.current.deleteManyRecords(people);
|
||||
const res = await result.current.deleteManyRecords(personIds);
|
||||
expect(res).toBeDefined();
|
||||
expect(res[0]).toHaveProperty('id');
|
||||
});
|
||||
|
||||
@ -2,7 +2,7 @@ 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 { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
|
||||
@ -122,19 +122,19 @@ export const useCreateManyRecords = <
|
||||
},
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
recordsCreatedInCache.forEach((recordToDelete) => {
|
||||
recordsCreatedInCache.forEach((recordToDestroy) => {
|
||||
deleteRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloClient.cache,
|
||||
recordToDelete,
|
||||
recordToDestroy,
|
||||
});
|
||||
});
|
||||
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
triggerDestroyRecordsOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
recordsToDelete: recordsCreatedInCache,
|
||||
recordsToDestroy: recordsCreatedInCache,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ 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 { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
|
||||
@ -118,13 +118,13 @@ export const useCreateOneRecord = <
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloClient.cache,
|
||||
recordToDelete: recordCreatedInCache,
|
||||
recordToDestroy: recordCreatedInCache,
|
||||
});
|
||||
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
triggerDestroyRecordsOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
recordsToDelete: [recordCreatedInCache],
|
||||
recordsToDestroy: [recordCreatedInCache],
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
||||
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 { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||
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 { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -62,49 +63,74 @@ export const useDeleteManyRecords = ({
|
||||
const deletedRecords = [];
|
||||
|
||||
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
|
||||
const batchIds = idsToDelete.slice(
|
||||
const batchedIdsToDelete = idsToDelete.slice(
|
||||
batchIndex * mutationPageSize,
|
||||
(batchIndex + 1) * mutationPageSize,
|
||||
);
|
||||
|
||||
const currentTimestamp = new Date().toISOString();
|
||||
|
||||
const cachedRecords = batchedIdsToDelete
|
||||
.map((idToDelete) => getRecordFromCache(idToDelete, apolloClient.cache))
|
||||
.filter(isDefined);
|
||||
|
||||
if (!options?.skipOptimisticEffect) {
|
||||
cachedRecords.forEach((cachedRecord) => {
|
||||
if (!cachedRecord || !cachedRecord.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedRecordWithConnection =
|
||||
getRecordNodeFromRecord<ObjectRecord>({
|
||||
record: cachedRecord,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
computeReferences: true,
|
||||
});
|
||||
|
||||
const computedOptimisticRecord = {
|
||||
...cachedRecord,
|
||||
...{ id: cachedRecord.id, deletedAt: currentTimestamp },
|
||||
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
|
||||
};
|
||||
|
||||
const optimisticRecordWithConnection =
|
||||
getRecordNodeFromRecord<ObjectRecord>({
|
||||
record: computedOptimisticRecord,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
computeReferences: true,
|
||||
});
|
||||
|
||||
if (!optimisticRecordWithConnection || !cachedRecordWithConnection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloClient.cache,
|
||||
record: computedOptimisticRecord,
|
||||
});
|
||||
|
||||
triggerUpdateRecordOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
currentRecord: cachedRecordWithConnection,
|
||||
updatedRecord: optimisticRecordWithConnection,
|
||||
objectMetadataItems,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const deletedRecordsResponse = await apolloClient
|
||||
.mutate({
|
||||
mutation: deleteManyRecordsMutation,
|
||||
variables: {
|
||||
filter: { id: { in: batchIds } },
|
||||
filter: { id: { in: batchedIdsToDelete } },
|
||||
},
|
||||
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;
|
||||
@ -114,23 +140,45 @@ export const useDeleteManyRecords = ({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloClient.cache,
|
||||
record: {
|
||||
...cachedRecord,
|
||||
deletedAt: null,
|
||||
},
|
||||
record: cachedRecord,
|
||||
});
|
||||
});
|
||||
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordsToCreate: cachedRecords
|
||||
.filter(isDefined)
|
||||
.map((cachedRecord) => ({
|
||||
...cachedRecord,
|
||||
deletedAt: null,
|
||||
})),
|
||||
const cachedRecordWithConnection =
|
||||
getRecordNodeFromRecord<ObjectRecord>({
|
||||
record: cachedRecord,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
computeReferences: true,
|
||||
});
|
||||
|
||||
const computedOptimisticRecord = {
|
||||
...cachedRecord,
|
||||
...{ id: cachedRecord.id, deletedAt: currentTimestamp },
|
||||
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
|
||||
};
|
||||
|
||||
const optimisticRecordWithConnection =
|
||||
getRecordNodeFromRecord<ObjectRecord>({
|
||||
record: computedOptimisticRecord,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
computeReferences: true,
|
||||
});
|
||||
|
||||
if (
|
||||
!optimisticRecordWithConnection ||
|
||||
!cachedRecordWithConnection
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
triggerUpdateRecordOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
currentRecord: optimisticRecordWithConnection,
|
||||
updatedRecord: cachedRecordWithConnection,
|
||||
objectMetadataItems,
|
||||
});
|
||||
});
|
||||
|
||||
throw error;
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
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 { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||
import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
@ -41,66 +42,85 @@ export const useDeleteOneRecord = ({
|
||||
async (idToDelete: string) => {
|
||||
const currentTimestamp = new Date().toISOString();
|
||||
|
||||
const cachedRecord = getRecordFromCache(idToDelete, apolloClient.cache);
|
||||
|
||||
const cachedRecordWithConnection = getRecordNodeFromRecord<ObjectRecord>({
|
||||
record: cachedRecord,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
computeReferences: true,
|
||||
});
|
||||
|
||||
const computedOptimisticRecord = {
|
||||
...cachedRecord,
|
||||
...{ id: idToDelete, deletedAt: currentTimestamp },
|
||||
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
|
||||
};
|
||||
|
||||
const optimisticRecordWithConnection =
|
||||
getRecordNodeFromRecord<ObjectRecord>({
|
||||
record: computedOptimisticRecord,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
computeReferences: true,
|
||||
});
|
||||
|
||||
if (!optimisticRecordWithConnection || !cachedRecordWithConnection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloClient.cache,
|
||||
record: computedOptimisticRecord,
|
||||
});
|
||||
|
||||
triggerUpdateRecordOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
currentRecord: cachedRecordWithConnection,
|
||||
updatedRecord: optimisticRecordWithConnection,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
const deletedRecord = await apolloClient
|
||||
.mutate({
|
||||
mutation: deleteOneRecordMutation,
|
||||
variables: {
|
||||
idToDelete: idToDelete,
|
||||
},
|
||||
optimisticResponse: {
|
||||
[mutationResponseField]: {
|
||||
__typename: capitalize(objectNameSingular),
|
||||
id: idToDelete,
|
||||
deletedAt: currentTimestamp,
|
||||
},
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
const record = data?.[mutationResponseField];
|
||||
|
||||
if (!record) return;
|
||||
if (!record || !cachedRecord) return;
|
||||
|
||||
const cachedRecord = getRecordFromCache(record.id, cache);
|
||||
|
||||
if (!cachedRecord) return;
|
||||
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
triggerUpdateRecordOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToDelete: [cachedRecord],
|
||||
currentRecord: cachedRecord,
|
||||
updatedRecord: record,
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
const cachedRecord = getRecordFromCache(
|
||||
idToDelete,
|
||||
apolloClient.cache,
|
||||
);
|
||||
|
||||
if (!cachedRecord) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloClient.cache,
|
||||
record: {
|
||||
...cachedRecord,
|
||||
deletedAt: null,
|
||||
},
|
||||
record: cachedRecord,
|
||||
});
|
||||
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
triggerUpdateRecordOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
currentRecord: optimisticRecordWithConnection,
|
||||
updatedRecord: cachedRecordWithConnection,
|
||||
objectMetadataItems,
|
||||
recordsToCreate: [
|
||||
{
|
||||
...cachedRecord,
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
throw error;
|
||||
@ -114,7 +134,6 @@ export const useDeleteOneRecord = ({
|
||||
getRecordFromCache,
|
||||
mutationResponseField,
|
||||
objectMetadataItem,
|
||||
objectNameSingular,
|
||||
objectMetadataItems,
|
||||
],
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
|
||||
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
@ -61,12 +61,12 @@ export const useDestroyManyRecords = ({
|
||||
const destroyedRecords = [];
|
||||
|
||||
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
|
||||
const batchIds = idsToDestroy.slice(
|
||||
const batchedIdToDestroy = idsToDestroy.slice(
|
||||
batchIndex * mutationPageSize,
|
||||
(batchIndex + 1) * mutationPageSize,
|
||||
);
|
||||
|
||||
const originalRecords = idsToDestroy
|
||||
const originalRecords = batchedIdToDestroy
|
||||
.map((recordId) => getRecordFromCache(recordId, apolloClient.cache))
|
||||
.filter(isDefined);
|
||||
|
||||
@ -74,15 +74,17 @@ export const useDestroyManyRecords = ({
|
||||
.mutate({
|
||||
mutation: destroyManyRecordsMutation,
|
||||
variables: {
|
||||
filter: { id: { in: batchIds } },
|
||||
filter: { id: { in: batchedIdToDestroy } },
|
||||
},
|
||||
optimisticResponse: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: {
|
||||
[mutationResponseField]: batchIds.map((idToDestroy) => ({
|
||||
__typename: capitalize(objectNameSingular),
|
||||
id: idToDestroy,
|
||||
})),
|
||||
[mutationResponseField]: batchedIdToDestroy.map(
|
||||
(idToDestroy) => ({
|
||||
__typename: capitalize(objectNameSingular),
|
||||
id: idToDestroy,
|
||||
}),
|
||||
),
|
||||
},
|
||||
update: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
@ -95,10 +97,10 @@ export const useDestroyManyRecords = ({
|
||||
.map((record) => getRecordFromCache(record.id, cache))
|
||||
.filter(isDefined);
|
||||
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
triggerDestroyRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToDelete: cachedRecords,
|
||||
recordsToDestroy: cachedRecords,
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
|
||||
@ -2,7 +2,7 @@ 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 { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
@ -65,10 +65,10 @@ export const useDestroyOneRecord = ({
|
||||
|
||||
if (!cachedRecord) return;
|
||||
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
triggerDestroyRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToDelete: [cachedRecord],
|
||||
recordsToDestroy: [cachedRecord],
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
||||
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 { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
|
||||
import { useRestoreManyRecordsMutation } from '@/object-record/hooks/useRestoreManyRecordsMutation';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { getRestoreManyRecordsMutationResponseField } from '@/object-record/utils/getRestoreManyRecordsMutationResponseField';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -34,10 +40,16 @@ export const useRestoreManyRecords = ({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { restoreManyRecordsMutation } = useRestoreManyRecordsMutation({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const mutationResponseField = getRestoreManyRecordsMutationResponseField(
|
||||
objectMetadataItem.namePlural,
|
||||
);
|
||||
@ -51,36 +63,124 @@ export const useRestoreManyRecords = ({
|
||||
const restoredRecords = [];
|
||||
|
||||
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
|
||||
const batchIds = idsToRestore.slice(
|
||||
const batchedIdsToRestore = idsToRestore.slice(
|
||||
batchIndex * mutationPageSize,
|
||||
(batchIndex + 1) * mutationPageSize,
|
||||
);
|
||||
|
||||
// TODO: fix optimistic effect
|
||||
const findOneQueryName = `FindOne${capitalize(objectNameSingular)}`;
|
||||
const findManyQueryName = `FindMany${capitalize(
|
||||
objectMetadataItem.namePlural,
|
||||
)}`;
|
||||
const cachedRecords = batchedIdsToRestore
|
||||
.map((idToRestore) =>
|
||||
getRecordFromCache(idToRestore, apolloClient.cache),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
if (!options?.skipOptimisticEffect) {
|
||||
cachedRecords.forEach((cachedRecord) => {
|
||||
if (!cachedRecord || !cachedRecord.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedRecordWithConnection =
|
||||
getRecordNodeFromRecord<ObjectRecord>({
|
||||
record: cachedRecord,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
computeReferences: true,
|
||||
});
|
||||
|
||||
const computedOptimisticRecord = {
|
||||
...cachedRecord,
|
||||
...{ id: cachedRecord.id, deletedAt: null },
|
||||
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
|
||||
};
|
||||
|
||||
const optimisticRecordWithConnection =
|
||||
getRecordNodeFromRecord<ObjectRecord>({
|
||||
record: computedOptimisticRecord,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
computeReferences: true,
|
||||
});
|
||||
|
||||
if (!optimisticRecordWithConnection || !cachedRecordWithConnection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloClient.cache,
|
||||
record: computedOptimisticRecord,
|
||||
});
|
||||
|
||||
triggerUpdateRecordOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
currentRecord: cachedRecordWithConnection,
|
||||
updatedRecord: optimisticRecordWithConnection,
|
||||
objectMetadataItems,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const restoredRecordsResponse = await apolloClient
|
||||
.mutate({
|
||||
mutation: restoreManyRecordsMutation,
|
||||
refetchQueries: [findOneQueryName, findManyQueryName],
|
||||
variables: {
|
||||
filter: { id: { in: batchIds } },
|
||||
filter: { id: { in: batchedIdsToRestore } },
|
||||
},
|
||||
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)
|
||||
cachedRecords.forEach((cachedRecord) => {
|
||||
if (!cachedRecord) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloClient.cache,
|
||||
record: cachedRecord,
|
||||
});
|
||||
|
||||
const cachedRecordWithConnection =
|
||||
getRecordNodeFromRecord<ObjectRecord>({
|
||||
record: cachedRecord,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
computeReferences: true,
|
||||
});
|
||||
|
||||
const computedOptimisticRecord = {
|
||||
...cachedRecord,
|
||||
...{ id: cachedRecord.id, deletedAt: null },
|
||||
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
|
||||
};
|
||||
|
||||
const optimisticRecordWithConnection =
|
||||
getRecordNodeFromRecord<ObjectRecord>({
|
||||
record: computedOptimisticRecord,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
computeReferences: true,
|
||||
});
|
||||
|
||||
if (
|
||||
!optimisticRecordWithConnection ||
|
||||
!cachedRecordWithConnection
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
triggerUpdateRecordOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
currentRecord: optimisticRecordWithConnection,
|
||||
updatedRecord: cachedRecordWithConnection,
|
||||
objectMetadataItems,
|
||||
});
|
||||
});
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
EmailsFilter,
|
||||
FloatFilter,
|
||||
FullNameFilter,
|
||||
LeafObjectRecordFilter,
|
||||
LinksFilter,
|
||||
NotObjectRecordFilter,
|
||||
OrObjectRecordFilter,
|
||||
@ -29,6 +30,12 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { isEmptyObject } from '~/utils/isEmptyObject';
|
||||
|
||||
const isLeafFilter = (
|
||||
filter: RecordGqlOperationFilter,
|
||||
): filter is LeafObjectRecordFilter => {
|
||||
return !isAndFilter(filter) && !isOrFilter(filter) && !isNotFilter(filter);
|
||||
};
|
||||
|
||||
const isAndFilter = (
|
||||
filter: RecordGqlOperationFilter,
|
||||
): filter is AndObjectRecordFilter => 'and' in filter && !!filter.and;
|
||||
@ -50,7 +57,7 @@ export const isRecordMatchingFilter = ({
|
||||
filter: RecordGqlOperationFilter;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}): boolean => {
|
||||
if (Object.keys(filter).length === 0) {
|
||||
if (Object.keys(filter).length === 0 && record.deletedAt === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -120,6 +127,12 @@ export const isRecordMatchingFilter = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (isLeafFilter(filter)) {
|
||||
if (isDefined(record.deletedAt) && filter.deletedAt === undefined) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.entries(filter).every(([filterKey, filterValue]) => {
|
||||
if (!isDefined(filterValue)) {
|
||||
throw new Error(
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
||||
import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds';
|
||||
import { UseTableDataOptions } from '@/object-record/record-index/options/hooks/useTableData';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState';
|
||||
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
type UseDeleteTableDataOptions = Pick<
|
||||
UseTableDataOptions,
|
||||
@ -16,41 +12,16 @@ export const useDeleteTableData = ({
|
||||
objectNameSingular,
|
||||
recordIndexId,
|
||||
}: UseDeleteTableDataOptions) => {
|
||||
const { fetchAllRecordIds } = useFetchAllRecordIds({
|
||||
objectNameSingular,
|
||||
const { resetTableRowSelection } = useRecordTable({
|
||||
recordTableId: recordIndexId,
|
||||
});
|
||||
|
||||
const { resetTableRowSelection, hasUserSelectedAllRowsState } =
|
||||
useRecordTable({
|
||||
recordTableId: recordIndexId,
|
||||
});
|
||||
|
||||
const tableRowIds = useRecoilValue(
|
||||
tableRowIdsComponentState({
|
||||
scopeId: getScopeIdFromComponentId(recordIndexId),
|
||||
}),
|
||||
);
|
||||
|
||||
const { deleteManyRecords } = useDeleteManyRecords({
|
||||
objectNameSingular,
|
||||
});
|
||||
const { favorites, deleteFavorite } = useFavorites();
|
||||
|
||||
const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState);
|
||||
|
||||
const deleteRecords = async (recordIdsToDelete: string[]) => {
|
||||
if (hasUserSelectedAllRows) {
|
||||
const allRecordIds = await fetchAllRecordIds();
|
||||
|
||||
const unselectedRecordIds = tableRowIds.filter(
|
||||
(recordId) => !recordIdsToDelete.includes(recordId),
|
||||
);
|
||||
|
||||
recordIdsToDelete = allRecordIds.filter(
|
||||
(recordId) => !unselectedRecordIds.includes(recordId),
|
||||
);
|
||||
}
|
||||
|
||||
resetTableRowSelection();
|
||||
|
||||
for (const recordIdToDelete of recordIdsToDelete) {
|
||||
|
||||
Reference in New Issue
Block a user