[BUG] Handle optimistic cache deletion operation (#9914)
# Introduction This PR is highly related to previous optimistic cache refactor: https://github.com/twentyhq/twenty/pull/9881 Here we've added some logic within the `triggerUpdateRelationsOptimisticEffect` which will now run if given recordInput `deletedAt` field is defined. If deletion, we will iterate over all the fields searching for `RELATION` for which deletion might implies necessity to detach the relation ## Known troubleshooting ( also on main )  We might have to refactor the `prefillRecord` to spread and overrides`inputValue` over defaultOne as inputValue could be a partial one for more info please a look to # Conclusion Any suggestions are welcomed ! fixes https://github.com/twentyhq/twenty/issues/9580 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -1,3 +1,4 @@
|
|||||||
|
import { DeleteManyRecordsProps } from '@/object-record/hooks/useDeleteManyRecords';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { renderHook, waitFor } from '@testing-library/react';
|
import { renderHook, waitFor } from '@testing-library/react';
|
||||||
import { act } from 'react';
|
import { act } from 'react';
|
||||||
@ -75,13 +76,12 @@ describe('useDeleteMultipleRecordsAction', () => {
|
|||||||
result.current.ConfirmationModal?.props?.onConfirmClick();
|
result.current.ConfirmationModal?.props?.onConfirmClick();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const expectedParams: DeleteManyRecordsProps = {
|
||||||
|
recordIdsToDelete: [peopleMock[0].id, peopleMock[1].id],
|
||||||
|
};
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(resetTableRowSelectionMock).toHaveBeenCalled();
|
expect(resetTableRowSelectionMock).toHaveBeenCalled();
|
||||||
|
expect(deleteManyRecordsMock).toHaveBeenCalledWith(expectedParams);
|
||||||
expect(deleteManyRecordsMock).toHaveBeenCalledWith([
|
|
||||||
peopleMock[0].id,
|
|
||||||
peopleMock[1].id,
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -73,7 +73,9 @@ export const useDeleteMultipleRecordsAction: ActionHookWithObjectMetadataItem =
|
|||||||
|
|
||||||
resetTableRowSelection();
|
resetTableRowSelection();
|
||||||
|
|
||||||
await deleteManyRecords(recordIdsToDelete);
|
await deleteManyRecords({
|
||||||
|
recordIdsToDelete,
|
||||||
|
});
|
||||||
}, [deleteManyRecords, fetchAllRecordIds, resetTableRowSelection]);
|
}, [deleteManyRecords, fetchAllRecordIds, resetTableRowSelection]);
|
||||||
|
|
||||||
const isRemoteObject = objectMetadataItem.isRemote;
|
const isRemoteObject = objectMetadataItem.isRemote;
|
||||||
|
|||||||
@ -4,11 +4,13 @@ import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
|||||||
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
|
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||||
import { useCallback, useContext, useState } from 'react';
|
import { useCallback, useContext, useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
|
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||||
|
|
||||||
export const useDestroySingleRecordAction: ActionHookWithObjectMetadataItem = ({
|
export const useDestroySingleRecordAction: ActionHookWithObjectMetadataItem = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
@ -18,6 +20,8 @@ export const useDestroySingleRecordAction: ActionHookWithObjectMetadataItem = ({
|
|||||||
const [isDestroyRecordsModalOpen, setIsDestroyRecordsModalOpen] =
|
const [isDestroyRecordsModalOpen, setIsDestroyRecordsModalOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
|
const navigateApp = useNavigateApp();
|
||||||
|
|
||||||
const { resetTableRowSelection } = useRecordTable({
|
const { resetTableRowSelection } = useRecordTable({
|
||||||
recordTableId: objectMetadataItem.namePlural,
|
recordTableId: objectMetadataItem.namePlural,
|
||||||
});
|
});
|
||||||
@ -34,7 +38,16 @@ export const useDestroySingleRecordAction: ActionHookWithObjectMetadataItem = ({
|
|||||||
resetTableRowSelection();
|
resetTableRowSelection();
|
||||||
|
|
||||||
await destroyOneRecord(recordId);
|
await destroyOneRecord(recordId);
|
||||||
}, [resetTableRowSelection, destroyOneRecord, recordId]);
|
navigateApp(AppPath.RecordIndexPage, {
|
||||||
|
objectNamePlural: objectMetadataItem.namePlural,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
resetTableRowSelection,
|
||||||
|
destroyOneRecord,
|
||||||
|
recordId,
|
||||||
|
navigateApp,
|
||||||
|
objectMetadataItem.namePlural,
|
||||||
|
]);
|
||||||
|
|
||||||
const isRemoteObject = objectMetadataItem.isRemote;
|
const isRemoteObject = objectMetadataItem.isRemote;
|
||||||
|
|
||||||
|
|||||||
@ -92,28 +92,26 @@ export const useOpenCreateActivityDrawer = ({
|
|||||||
const targetableObjectRelationIdName = `${targetableObjects[0].targetObjectNameSingular}Id`;
|
const targetableObjectRelationIdName = `${targetableObjects[0].targetObjectNameSingular}Id`;
|
||||||
|
|
||||||
await createOneActivityTarget({
|
await createOneActivityTarget({
|
||||||
taskId:
|
...(activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||||
activityObjectNameSingular === CoreObjectNameSingular.Task
|
? {
|
||||||
? activity.id
|
taskId: activity.id,
|
||||||
: undefined,
|
}
|
||||||
noteId:
|
: {
|
||||||
activityObjectNameSingular === CoreObjectNameSingular.Note
|
noteId: activity.id,
|
||||||
? activity.id
|
}),
|
||||||
: undefined,
|
|
||||||
[targetableObjectRelationIdName]: targetableObjects[0].id,
|
[targetableObjectRelationIdName]: targetableObjects[0].id,
|
||||||
});
|
});
|
||||||
|
|
||||||
setActivityTargetableEntityArray(targetableObjects);
|
setActivityTargetableEntityArray(targetableObjects);
|
||||||
} else {
|
} else {
|
||||||
await createOneActivityTarget({
|
await createOneActivityTarget({
|
||||||
taskId:
|
...(activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||||
activityObjectNameSingular === CoreObjectNameSingular.Task
|
? {
|
||||||
? activity.id
|
taskId: activity.id,
|
||||||
: undefined,
|
}
|
||||||
noteId:
|
: {
|
||||||
activityObjectNameSingular === CoreObjectNameSingular.Note
|
noteId: activity.id,
|
||||||
? activity.id
|
}),
|
||||||
: undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setActivityTargetableEntityArray([]);
|
setActivityTargetableEntityArray([]);
|
||||||
|
|||||||
@ -15,8 +15,8 @@ import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameS
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useCreateManyRecordsInCache } from '@/object-record/cache/hooks/useCreateManyRecordsInCache';
|
import { useCreateManyRecordsInCache } from '@/object-record/cache/hooks/useCreateManyRecordsInCache';
|
||||||
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
|
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||||
import { activityTargetObjectRecordFamilyState } from '@/object-record/record-field/states/activityTargetObjectRecordFamilyState';
|
import { activityTargetObjectRecordFamilyState } from '@/object-record/record-field/states/activityTargetObjectRecordFamilyState';
|
||||||
import { objectRecordMultiSelectCheckedRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectCheckedRecordsIdsComponentState';
|
import { objectRecordMultiSelectCheckedRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectCheckedRecordsIdsComponentState';
|
||||||
import {
|
import {
|
||||||
@ -54,17 +54,15 @@ export const ActivityTargetInlineCellEditMode = ({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { createManyRecords: createManyActivityTargets } = useCreateManyRecords<
|
const { createOneRecord: createOneActivityTarget } = useCreateOneRecord<
|
||||||
NoteTarget | TaskTarget
|
NoteTarget | TaskTarget
|
||||||
>({
|
>({
|
||||||
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { deleteManyRecords: deleteManyActivityTargets } = useDeleteManyRecords(
|
const { deleteOneRecord: deleteOneActivityTarget } = useDeleteOneRecord({
|
||||||
{
|
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
||||||
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { closeInlineCell: closeEditableField } = useInlineCell();
|
const { closeInlineCell: closeEditableField } = useInlineCell();
|
||||||
|
|
||||||
@ -168,36 +166,21 @@ export const ActivityTargetInlineCellEditMode = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const newActivityTargetId = v4();
|
const newActivityTargetId = v4();
|
||||||
const fieldName = record.objectMetadataItem.nameSingular;
|
|
||||||
const fieldNameWithIdSuffix = getActivityTargetObjectFieldIdName({
|
const fieldNameWithIdSuffix = getActivityTargetObjectFieldIdName({
|
||||||
nameSingular: record.objectMetadataItem.nameSingular,
|
nameSingular: record.objectMetadataItem.nameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const newActivityTargetInput = {
|
||||||
|
id: newActivityTargetId,
|
||||||
|
...(activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||||
|
? { taskId: activity.id }
|
||||||
|
: { noteId: activity.id }),
|
||||||
|
[fieldNameWithIdSuffix]: recordId,
|
||||||
|
};
|
||||||
|
|
||||||
const newActivityTarget = prefillRecord<NoteTarget | TaskTarget>({
|
const newActivityTarget = prefillRecord<NoteTarget | TaskTarget>({
|
||||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||||
input: {
|
input: newActivityTargetInput,
|
||||||
id: newActivityTargetId,
|
|
||||||
taskId:
|
|
||||||
activityObjectNameSingular === CoreObjectNameSingular.Task
|
|
||||||
? activity.id
|
|
||||||
: null,
|
|
||||||
task:
|
|
||||||
activityObjectNameSingular === CoreObjectNameSingular.Task
|
|
||||||
? activity
|
|
||||||
: null,
|
|
||||||
noteId:
|
|
||||||
activityObjectNameSingular === CoreObjectNameSingular.Note
|
|
||||||
? activity.id
|
|
||||||
: null,
|
|
||||||
note:
|
|
||||||
activityObjectNameSingular === CoreObjectNameSingular.Note
|
|
||||||
? activity
|
|
||||||
: null,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
[fieldName]: record.record,
|
|
||||||
[fieldNameWithIdSuffix]: recordId,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
activityTargetsAfterUpdate.push(newActivityTarget);
|
activityTargetsAfterUpdate.push(newActivityTarget);
|
||||||
@ -215,7 +198,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await createManyActivityTargets([newActivityTarget]);
|
await createOneActivityTarget(newActivityTargetInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
set(activityTargetObjectRecordFamilyState(recordId), {
|
set(activityTargetObjectRecordFamilyState(recordId), {
|
||||||
@ -252,7 +235,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await deleteManyActivityTargets([activityTargetToDeleteId]);
|
await deleteOneActivityTarget(activityTargetToDeleteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
set(activityTargetObjectRecordFamilyState(recordId), {
|
set(activityTargetObjectRecordFamilyState(recordId), {
|
||||||
@ -263,9 +246,9 @@ export const ActivityTargetInlineCellEditMode = ({
|
|||||||
[
|
[
|
||||||
activity,
|
activity,
|
||||||
activityTargetWithTargetRecords,
|
activityTargetWithTargetRecords,
|
||||||
createManyActivityTargets,
|
createOneActivityTarget,
|
||||||
createManyActivityTargetsInCache,
|
createManyActivityTargetsInCache,
|
||||||
deleteManyActivityTargets,
|
deleteOneActivityTarget,
|
||||||
isActivityInCreateMode,
|
isActivityInCreateMode,
|
||||||
objectMetadataItemActivityTarget,
|
objectMetadataItemActivityTarget,
|
||||||
recordPickerInstanceId,
|
recordPickerInstanceId,
|
||||||
|
|||||||
@ -27,6 +27,10 @@ export const triggerUpdateRelationsOptimisticEffect = ({
|
|||||||
updatedSourceRecord,
|
updatedSourceRecord,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
}: triggerUpdateRelationsOptimisticEffectArgs) => {
|
}: triggerUpdateRelationsOptimisticEffectArgs) => {
|
||||||
|
const isDeletion =
|
||||||
|
isDefined(updatedSourceRecord) &&
|
||||||
|
isDefined(updatedSourceRecord['deletedAt']);
|
||||||
|
|
||||||
return sourceObjectMetadataItem.fields.forEach(
|
return sourceObjectMetadataItem.fields.forEach(
|
||||||
(fieldMetadataItemOnSourceRecord) => {
|
(fieldMetadataItemOnSourceRecord) => {
|
||||||
const notARelationField =
|
const notARelationField =
|
||||||
@ -72,15 +76,15 @@ export const triggerUpdateRelationsOptimisticEffect = ({
|
|||||||
| RecordGqlNode
|
| RecordGqlNode
|
||||||
| null = updatedSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
|
| null = updatedSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
|
||||||
|
|
||||||
if (
|
const noDiff = isDeeplyEqual(
|
||||||
isDeeplyEqual(
|
currentFieldValueOnSourceRecord,
|
||||||
currentFieldValueOnSourceRecord,
|
updatedFieldValueOnSourceRecord,
|
||||||
updatedFieldValueOnSourceRecord,
|
{ strict: true },
|
||||||
{ strict: true },
|
);
|
||||||
)
|
if (noDiff && !isDeletion) {
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractTargetRecordsFromRelation = (
|
const extractTargetRecordsFromRelation = (
|
||||||
value: RecordGqlConnection | RecordGqlNode | null,
|
value: RecordGqlConnection | RecordGqlNode | null,
|
||||||
): RecordGqlNode[] => {
|
): RecordGqlNode[] => {
|
||||||
@ -96,11 +100,12 @@ export const triggerUpdateRelationsOptimisticEffect = ({
|
|||||||
|
|
||||||
return [value];
|
return [value];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const recordToExtractDetachFrom = isDeletion
|
||||||
|
? updatedFieldValueOnSourceRecord
|
||||||
|
: currentFieldValueOnSourceRecord;
|
||||||
const targetRecordsToDetachFrom = extractTargetRecordsFromRelation(
|
const targetRecordsToDetachFrom = extractTargetRecordsFromRelation(
|
||||||
currentFieldValueOnSourceRecord,
|
recordToExtractDetachFrom,
|
||||||
);
|
|
||||||
const targetRecordsToAttachTo = extractTargetRecordsFromRelation(
|
|
||||||
updatedFieldValueOnSourceRecord,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: see if we can de-hardcode this, put cascade delete in relation metadata item
|
// TODO: see if we can de-hardcode this, put cascade delete in relation metadata item
|
||||||
@ -129,7 +134,11 @@ export const triggerUpdateRelationsOptimisticEffect = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDefined(updatedSourceRecord)) {
|
if (!isDeletion && isDefined(updatedSourceRecord)) {
|
||||||
|
const targetRecordsToAttachTo = extractTargetRecordsFromRelation(
|
||||||
|
updatedFieldValueOnSourceRecord,
|
||||||
|
);
|
||||||
|
|
||||||
targetRecordsToAttachTo.forEach((targetRecordToAttachTo) =>
|
targetRecordsToAttachTo.forEach((targetRecordToAttachTo) =>
|
||||||
triggerAttachRelationOptimisticEffect({
|
triggerAttachRelationOptimisticEffect({
|
||||||
cache,
|
cache,
|
||||||
|
|||||||
@ -22,7 +22,6 @@ export const useCreateFavoriteFolder = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await createFavoriteFolder({
|
await createFavoriteFolder({
|
||||||
workspaceMemberId: currentWorkspaceMemberId,
|
|
||||||
name,
|
name,
|
||||||
position: maxPosition + 1,
|
position: maxPosition + 1,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -52,7 +52,9 @@ describe('useDeleteManyRecords', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
const res = await result.current.deleteManyRecords(personIds);
|
const res = await result.current.deleteManyRecords({
|
||||||
|
recordIdsToDelete: personIds,
|
||||||
|
});
|
||||||
expect(res).toBeDefined();
|
expect(res).toBeDefined();
|
||||||
expect(res[0]).toHaveProperty('id');
|
expect(res[0]).toHaveProperty('id');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -135,7 +135,7 @@ export const useCreateManyRecords = <
|
|||||||
update: (cache, { data }) => {
|
update: (cache, { data }) => {
|
||||||
const records = data?.[mutationResponseField];
|
const records = data?.[mutationResponseField];
|
||||||
|
|
||||||
if (!records?.length || skipPostOptmisticEffect) return;
|
if (!isDefined(records?.length) || skipPostOptmisticEffect) return;
|
||||||
|
|
||||||
triggerCreateRecordsOptimisticEffect({
|
triggerCreateRecordsOptimisticEffect({
|
||||||
cache,
|
cache,
|
||||||
|
|||||||
@ -24,7 +24,8 @@ type useDeleteManyRecordProps = {
|
|||||||
refetchFindManyQuery?: boolean;
|
refetchFindManyQuery?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DeleteManyRecordsOptions = {
|
export type DeleteManyRecordsProps = {
|
||||||
|
recordIdsToDelete: string[];
|
||||||
skipOptimisticEffect?: boolean;
|
skipOptimisticEffect?: boolean;
|
||||||
delayInMsBetweenRequests?: number;
|
delayInMsBetweenRequests?: number;
|
||||||
};
|
};
|
||||||
@ -61,16 +62,18 @@ export const useDeleteManyRecords = ({
|
|||||||
objectMetadataItem.namePlural,
|
objectMetadataItem.namePlural,
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteManyRecords = async (
|
const deleteManyRecords = async ({
|
||||||
idsToDelete: string[],
|
recordIdsToDelete,
|
||||||
options?: DeleteManyRecordsOptions,
|
delayInMsBetweenRequests,
|
||||||
) => {
|
skipOptimisticEffect = false,
|
||||||
const numberOfBatches = Math.ceil(idsToDelete.length / mutationPageSize);
|
}: DeleteManyRecordsProps) => {
|
||||||
|
const numberOfBatches = Math.ceil(
|
||||||
|
recordIdsToDelete.length / mutationPageSize,
|
||||||
|
);
|
||||||
const deletedRecords = [];
|
const deletedRecords = [];
|
||||||
|
|
||||||
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
|
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
|
||||||
const batchedIdsToDelete = idsToDelete.slice(
|
const batchedIdsToDelete = recordIdsToDelete.slice(
|
||||||
batchIndex * mutationPageSize,
|
batchIndex * mutationPageSize,
|
||||||
(batchIndex + 1) * mutationPageSize,
|
(batchIndex + 1) * mutationPageSize,
|
||||||
);
|
);
|
||||||
@ -81,22 +84,21 @@ export const useDeleteManyRecords = ({
|
|||||||
.map((idToDelete) => getRecordFromCache(idToDelete, apolloClient.cache))
|
.map((idToDelete) => getRecordFromCache(idToDelete, apolloClient.cache))
|
||||||
.filter(isDefined);
|
.filter(isDefined);
|
||||||
|
|
||||||
const cachedRecordsWithConnection: RecordGqlNode[] = [];
|
if (!skipOptimisticEffect) {
|
||||||
const optimisticRecordsWithConnection: RecordGqlNode[] = [];
|
const cachedRecordsNode: RecordGqlNode[] = [];
|
||||||
|
const computedOptimisticRecordsNode: RecordGqlNode[] = [];
|
||||||
|
|
||||||
if (!options?.skipOptimisticEffect) {
|
|
||||||
cachedRecords.forEach((cachedRecord) => {
|
cachedRecords.forEach((cachedRecord) => {
|
||||||
if (!cachedRecord || !cachedRecord.id) {
|
if (!isDefined(cachedRecord) || !isDefined(cachedRecord.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedRecordWithConnection =
|
const cachedRecordNode = getRecordNodeFromRecord<ObjectRecord>({
|
||||||
getRecordNodeFromRecord<ObjectRecord>({
|
record: cachedRecord,
|
||||||
record: cachedRecord,
|
objectMetadataItem,
|
||||||
objectMetadataItem,
|
objectMetadataItems,
|
||||||
objectMetadataItems,
|
computeReferences: false,
|
||||||
computeReferences: true,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const computedOptimisticRecord = {
|
const computedOptimisticRecord = {
|
||||||
...cachedRecord,
|
...cachedRecord,
|
||||||
@ -104,34 +106,36 @@ export const useDeleteManyRecords = ({
|
|||||||
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
|
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
|
||||||
};
|
};
|
||||||
|
|
||||||
const optimisticRecordWithConnection =
|
const optimisticRecordNode = getRecordNodeFromRecord<ObjectRecord>({
|
||||||
getRecordNodeFromRecord<ObjectRecord>({
|
record: computedOptimisticRecord,
|
||||||
record: computedOptimisticRecord,
|
objectMetadataItem,
|
||||||
objectMetadataItem,
|
objectMetadataItems,
|
||||||
objectMetadataItems,
|
computeReferences: false,
|
||||||
computeReferences: true,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (!optimisticRecordWithConnection || !cachedRecordWithConnection) {
|
if (
|
||||||
return null;
|
!isDefined(optimisticRecordNode) ||
|
||||||
|
!isDefined(cachedRecordNode)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedRecordsWithConnection.push(cachedRecordWithConnection);
|
|
||||||
optimisticRecordsWithConnection.push(optimisticRecordWithConnection);
|
|
||||||
|
|
||||||
updateRecordFromCache({
|
updateRecordFromCache({
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
cache: apolloClient.cache,
|
cache: apolloClient.cache,
|
||||||
record: computedOptimisticRecord,
|
record: computedOptimisticRecord,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
computedOptimisticRecordsNode.push(optimisticRecordNode);
|
||||||
|
cachedRecordsNode.push(cachedRecordNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
triggerUpdateRecordOptimisticEffectByBatch({
|
triggerUpdateRecordOptimisticEffectByBatch({
|
||||||
cache: apolloClient.cache,
|
cache: apolloClient.cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
currentRecords: cachedRecordsWithConnection,
|
currentRecords: cachedRecordsNode,
|
||||||
updatedRecords: optimisticRecordsWithConnection,
|
updatedRecords: computedOptimisticRecordsNode,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -144,8 +148,8 @@ export const useDeleteManyRecords = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
const cachedRecordsWithConnection: RecordGqlNode[] = [];
|
const cachedRecordsNode: RecordGqlNode[] = [];
|
||||||
const optimisticRecordsWithConnection: RecordGqlNode[] = [];
|
const computedOptimisticRecordsNode: RecordGqlNode[] = [];
|
||||||
|
|
||||||
cachedRecords.forEach((cachedRecord) => {
|
cachedRecords.forEach((cachedRecord) => {
|
||||||
if (isUndefinedOrNull(cachedRecord?.id)) {
|
if (isUndefinedOrNull(cachedRecord?.id)) {
|
||||||
@ -164,7 +168,7 @@ export const useDeleteManyRecords = ({
|
|||||||
record: cachedRecord,
|
record: cachedRecord,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
computeReferences: true,
|
computeReferences: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const computedOptimisticRecord = {
|
const computedOptimisticRecord = {
|
||||||
@ -178,27 +182,25 @@ export const useDeleteManyRecords = ({
|
|||||||
record: computedOptimisticRecord,
|
record: computedOptimisticRecord,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
computeReferences: true,
|
computeReferences: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!optimisticRecordWithConnection ||
|
!isDefined(optimisticRecordWithConnection) ||
|
||||||
!cachedRecordWithConnection
|
!isDefined(cachedRecordWithConnection)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedRecordsWithConnection.push(cachedRecordWithConnection);
|
cachedRecordsNode.push(cachedRecordWithConnection);
|
||||||
optimisticRecordsWithConnection.push(
|
computedOptimisticRecordsNode.push(optimisticRecordWithConnection);
|
||||||
optimisticRecordWithConnection,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
triggerUpdateRecordOptimisticEffectByBatch({
|
triggerUpdateRecordOptimisticEffectByBatch({
|
||||||
cache: apolloClient.cache,
|
cache: apolloClient.cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
currentRecords: optimisticRecordsWithConnection,
|
currentRecords: computedOptimisticRecordsNode,
|
||||||
updatedRecords: cachedRecordsWithConnection,
|
updatedRecords: cachedRecordsNode,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -210,8 +212,8 @@ export const useDeleteManyRecords = ({
|
|||||||
|
|
||||||
deletedRecords.push(...deletedRecordsForThisBatch);
|
deletedRecords.push(...deletedRecordsForThisBatch);
|
||||||
|
|
||||||
if (isDefined(options?.delayInMsBetweenRequests)) {
|
if (isDefined(delayInMsBetweenRequests)) {
|
||||||
await sleep(options.delayInMsBetweenRequests);
|
await sleep(delayInMsBetweenRequests);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await refetchAggregateQueries();
|
await refetchAggregateQueries();
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggr
|
|||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField';
|
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField';
|
||||||
import { capitalize } from 'twenty-shared';
|
import { capitalize } from 'twenty-shared';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
type useDeleteOneRecordProps = {
|
type useDeleteOneRecordProps = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
@ -49,11 +50,11 @@ export const useDeleteOneRecord = ({
|
|||||||
|
|
||||||
const cachedRecord = getRecordFromCache(idToDelete, apolloClient.cache);
|
const cachedRecord = getRecordFromCache(idToDelete, apolloClient.cache);
|
||||||
|
|
||||||
const cachedRecordWithConnection = getRecordNodeFromRecord<ObjectRecord>({
|
const cachedRecordNode = getRecordNodeFromRecord<ObjectRecord>({
|
||||||
record: cachedRecord,
|
record: cachedRecord,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
computeReferences: true,
|
computeReferences: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const computedOptimisticRecord = {
|
const computedOptimisticRecord = {
|
||||||
@ -62,15 +63,14 @@ export const useDeleteOneRecord = ({
|
|||||||
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
|
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
|
||||||
};
|
};
|
||||||
|
|
||||||
const optimisticRecordWithConnection =
|
const optimisticRecordNode = getRecordNodeFromRecord<ObjectRecord>({
|
||||||
getRecordNodeFromRecord<ObjectRecord>({
|
record: computedOptimisticRecord,
|
||||||
record: computedOptimisticRecord,
|
objectMetadataItem,
|
||||||
objectMetadataItem,
|
objectMetadataItems,
|
||||||
objectMetadataItems,
|
computeReferences: false,
|
||||||
computeReferences: true,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (!optimisticRecordWithConnection || !cachedRecordWithConnection) {
|
if (!isDefined(optimisticRecordNode) || !isDefined(cachedRecordNode)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,8 +84,8 @@ export const useDeleteOneRecord = ({
|
|||||||
triggerUpdateRecordOptimisticEffect({
|
triggerUpdateRecordOptimisticEffect({
|
||||||
cache: apolloClient.cache,
|
cache: apolloClient.cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
currentRecord: cachedRecordWithConnection,
|
currentRecord: cachedRecordNode,
|
||||||
updatedRecord: optimisticRecordWithConnection,
|
updatedRecord: optimisticRecordNode,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -98,12 +98,13 @@ export const useDeleteOneRecord = ({
|
|||||||
update: (cache, { data }) => {
|
update: (cache, { data }) => {
|
||||||
const record = data?.[mutationResponseField];
|
const record = data?.[mutationResponseField];
|
||||||
|
|
||||||
if (!record || !cachedRecord) return;
|
if (!isDefined(record) || !isDefined(computedOptimisticRecord))
|
||||||
|
return;
|
||||||
|
|
||||||
triggerUpdateRecordOptimisticEffect({
|
triggerUpdateRecordOptimisticEffect({
|
||||||
cache,
|
cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
currentRecord: cachedRecord,
|
currentRecord: computedOptimisticRecord,
|
||||||
updatedRecord: record,
|
updatedRecord: record,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
});
|
});
|
||||||
@ -123,8 +124,8 @@ export const useDeleteOneRecord = ({
|
|||||||
triggerUpdateRecordOptimisticEffect({
|
triggerUpdateRecordOptimisticEffect({
|
||||||
cache: apolloClient.cache,
|
cache: apolloClient.cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
currentRecord: optimisticRecordWithConnection,
|
currentRecord: optimisticRecordNode,
|
||||||
updatedRecord: cachedRecordWithConnection,
|
updatedRecord: cachedRecordNode,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeO
|
|||||||
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
|
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
|
||||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||||
import { capitalize } from 'twenty-shared';
|
import { capitalize } from 'twenty-shared';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
type useUpdateOneRecordProps = {
|
type useUpdateOneRecordProps = {
|
||||||
@ -130,7 +131,8 @@ export const useUpdateOneRecord = <
|
|||||||
update: (cache, { data }) => {
|
update: (cache, { data }) => {
|
||||||
const record = data?.[mutationResponseField];
|
const record = data?.[mutationResponseField];
|
||||||
|
|
||||||
if (!record || !computedOptimisticRecord) return;
|
if (!isDefined(record) || !isDefined(computedOptimisticRecord))
|
||||||
|
return;
|
||||||
|
|
||||||
triggerUpdateRecordOptimisticEffect({
|
triggerUpdateRecordOptimisticEffect({
|
||||||
cache,
|
cache,
|
||||||
|
|||||||
@ -179,4 +179,28 @@ describe('computeOptimisticRecordFromInput', () => {
|
|||||||
`"Should never provide relation mutation through anything else than the fieldId e.g companyId"`,
|
`"Should never provide relation mutation through anything else than the fieldId e.g companyId"`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw an error if recordInput contains both the relationFieldId and relationField even if null', () => {
|
||||||
|
const cache = new InMemoryCache();
|
||||||
|
const personObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||||
|
(item) => item.nameSingular === 'person',
|
||||||
|
);
|
||||||
|
if (!personObjectMetadataItem) {
|
||||||
|
throw new Error('Person object metadata item not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
computeOptimisticRecordFromInput({
|
||||||
|
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||||
|
objectMetadataItem: personObjectMetadataItem,
|
||||||
|
recordInput: {
|
||||||
|
companyId: '123',
|
||||||
|
company: null,
|
||||||
|
},
|
||||||
|
cache,
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Should never provide relation mutation through anything else than the fieldId e.g companyId"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export const computeOptimisticRecordFromInput = ({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDefined(recordInputFieldValue)) {
|
if (!isUndefined(recordInputFieldValue)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Should never provide relation mutation through anything else than the fieldId e.g companyId',
|
'Should never provide relation mutation through anything else than the fieldId e.g companyId',
|
||||||
);
|
);
|
||||||
|
|||||||
@ -34,8 +34,10 @@ export const sanitizeRecordInput = ({
|
|||||||
(field) => field.name === relationIdFieldName,
|
(field) => field.name === relationIdFieldName,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const relationIdFieldValue = recordInput[relationIdFieldName];
|
||||||
|
|
||||||
return relationIdFieldMetadataItem
|
return relationIdFieldMetadataItem
|
||||||
? [relationIdFieldName, fieldValue?.id ?? null]
|
? [relationIdFieldName, relationIdFieldValue ?? null]
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user