Skip to content

Commit 2ffe2d8

Browse files
authored
Add delete and restore event handling for table and board (#17489)
This PR adds what is required to handle soft-delete and restore SSE events in virtualized table and board. Restore is not handled in board for now as it requires respecting sorts when inserting record ids. Since virtualized table is refetching small chunks, we just refetch for now. The long term goal is to handle event handling without refetching in all main components, and also handle SSE events that have the same origin that the current tab. But for now we implement what is easily doable. # QA Delete between table and board (delete only) : https://github.com/user-attachments/assets/715dd44a-007a-44ab-bf49-5ef039cd57c3 Delete and restore between table and table : https://github.com/user-attachments/assets/f2122519-e969-491f-b71c-018d0f85bd86
1 parent 6b38686 commit 2ffe2d8

15 files changed

+441
-28
lines changed

packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { triggerUpdateRecordOptimisticEffectByBatch } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffectByBatch';
22
import { apiConfigState } from '@/client-config/states/apiConfigState';
3+
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/hooks/useRemoveNavigationMenuItemByTargetRecordId';
34
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
45
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
56
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
@@ -16,11 +17,10 @@ import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useU
1617
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
1718
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
1819
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
19-
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/hooks/useRemoveNavigationMenuItemByTargetRecordId';
2020
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
2121
import { useRecoilValue } from 'recoil';
22-
import { FeatureFlagKey } from '~/generated/graphql';
2322
import { isDefined } from 'twenty-shared/utils';
23+
import { FeatureFlagKey } from '~/generated/graphql';
2424
import { sleep } from '~/utils/sleep';
2525

2626
type useDeleteManyRecordProps = {
@@ -239,6 +239,7 @@ export const useDeleteManyRecords = ({
239239
objectMetadataItem,
240240
operation: {
241241
type: 'delete-many',
242+
deletedRecordIds: recordIdsToDelete,
242243
},
243244
});
244245

packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export const useDeleteOneRecord = ({
168168
objectMetadataItem,
169169
operation: {
170170
type: 'delete-one',
171+
deletedRecordId: idToDelete,
171172
},
172173
});
173174

packages/twenty-front/src/modules/object-record/hooks/useIncrementalDeleteManyRecords.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { triggerUpdateRecordOptimisticEffectByBatch } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffectByBatch';
2+
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/hooks/useRemoveNavigationMenuItemByTargetRecordId';
23
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
34
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
45
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
@@ -16,11 +17,10 @@ import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggr
1617
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
1718
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
1819
import { dispatchObjectRecordOperationBrowserEvent } from '@/object-record/utils/dispatchObjectRecordOperationBrowserEvent';
19-
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/hooks/useRemoveNavigationMenuItemByTargetRecordId';
2020
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
2121
import { useCallback } from 'react';
22-
import { FeatureFlagKey } from '~/generated/graphql';
2322
import { isDefined } from 'twenty-shared/utils';
23+
import { FeatureFlagKey } from '~/generated/graphql';
2424
import { sleep } from '~/utils/sleep';
2525

2626
const DEFAULT_DELAY_BETWEEN_MUTATIONS_MS = 50;
@@ -252,6 +252,7 @@ export const useIncrementalDeleteManyRecords = <T>({
252252
objectMetadataItem,
253253
operation: {
254254
type: 'delete-many',
255+
deletedRecordIds: allDeletedRecordIds,
255256
},
256257
});
257258

packages/twenty-front/src/modules/object-record/hooks/useRestoreManyRecords.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ export const useRestoreManyRecords = ({
203203
objectMetadataItem,
204204
operation: {
205205
type: 'restore-many',
206+
restoredRecords: restoredRecordsForThisBatch,
206207
},
207208
});
208209

packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDataChangedEffect.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useListenToObjectRecordOperationBrowserEvent } from '@/object-record/hooks/useListenToObjectRecordOperationBrowserEvent';
22
import { useGetShouldInitializeRecordBoardForUpdateInputs } from '@/object-record/record-board/hooks/useGetShouldInitializeRecordBoardForUpdateInputs';
3+
import { useRemoveRecordsFromBoard } from '@/object-record/record-board/hooks/useRemoveRecordsFromBoard';
34
import { useTriggerRecordBoardInitialQuery } from '@/object-record/record-board/hooks/useTriggerRecordBoardInitialQuery';
45
import { recordGroupFromGroupValueComponentFamilySelector } from '@/object-record/record-group/states/selectors/recordGroupFromGroupValueComponentFamilySelector';
56
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
@@ -32,6 +33,8 @@ export const RecordBoardDataChangedEffect = () => {
3233
recordIndexRecordIdsByGroupComponentFamilyState,
3334
);
3435

36+
const { removeRecordsFromBoard } = useRemoveRecordsFromBoard();
37+
3538
const handleObjectRecordOperation = useRecoilCallback(
3639
({ snapshot }) =>
3740
(
@@ -126,6 +129,26 @@ export const RecordBoardDataChangedEffect = () => {
126129
}
127130
break;
128131
}
132+
case 'delete-one': {
133+
const removedRecordId = objectRecordOperation.deletedRecordId;
134+
135+
removeRecordsFromBoard({
136+
recordIdsToRemove: [removedRecordId],
137+
});
138+
return;
139+
}
140+
case 'delete-many': {
141+
const removedRecordIds = objectRecordOperation.deletedRecordIds;
142+
143+
removeRecordsFromBoard({
144+
recordIdsToRemove: removedRecordIds,
145+
});
146+
return;
147+
}
148+
case 'restore-many':
149+
case 'restore-one': {
150+
return;
151+
}
129152
default: {
130153
triggerRecordBoardInitialQuery();
131154
}
@@ -137,6 +160,7 @@ export const RecordBoardDataChangedEffect = () => {
137160
recordIndexGroupFieldMetadataItemCallbackState,
138161
recordGroupFromGroupValueCallbackState,
139162
recordIndexRecordIdsByGroupCallbackState,
163+
removeRecordsFromBoard,
140164
],
141165
);
142166

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { recordGroupFromGroupValueComponentFamilySelector } from '@/object-record/record-group/states/selectors/recordGroupFromGroupValueComponentFamilySelector';
2+
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
3+
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
4+
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
5+
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
6+
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
7+
import { useRecoilCallback } from 'recoil';
8+
import { isDefined } from 'twenty-shared/utils';
9+
10+
export const useRemoveRecordsFromBoard = () => {
11+
const recordIndexGroupFieldMetadataItemCallbackState =
12+
useRecoilComponentCallbackState(
13+
recordIndexGroupFieldMetadataItemComponentState,
14+
);
15+
16+
const recordGroupFromGroupValueFamilyCallbackState =
17+
useRecoilComponentCallbackState(
18+
recordGroupFromGroupValueComponentFamilySelector,
19+
);
20+
21+
const recordIndexRecordIdsByGroupFamilyCallbackState =
22+
useRecoilComponentCallbackState(
23+
recordIndexRecordIdsByGroupComponentFamilyState,
24+
);
25+
26+
const removeRecordsFromBoard = useRecoilCallback(
27+
({ snapshot, set }) =>
28+
({ recordIdsToRemove }: { recordIdsToRemove: string[] }) => {
29+
const recordIdsToRemoveByGroup = new Map<string, string[]>();
30+
31+
for (const recordIdToRemove of recordIdsToRemove) {
32+
const recordToRemove = getSnapshotValue(
33+
snapshot,
34+
recordStoreFamilyState(recordIdToRemove),
35+
);
36+
37+
if (!isDefined(recordToRemove)) {
38+
continue;
39+
}
40+
41+
const recordIndexGroupFieldMetadataItem = getSnapshotValue(
42+
snapshot,
43+
recordIndexGroupFieldMetadataItemCallbackState,
44+
);
45+
46+
if (!isDefined(recordIndexGroupFieldMetadataItem)) {
47+
continue;
48+
}
49+
50+
const recordGroupValue =
51+
recordToRemove[recordIndexGroupFieldMetadataItem.name];
52+
53+
const recordGroupDefinitionFromGroupValue = getSnapshotValue(
54+
snapshot,
55+
recordGroupFromGroupValueFamilyCallbackState({ recordGroupValue }),
56+
);
57+
58+
if (!isDefined(recordGroupDefinitionFromGroupValue)) {
59+
continue;
60+
}
61+
62+
const groupId = recordGroupDefinitionFromGroupValue.id;
63+
64+
if (!recordIdsToRemoveByGroup.has(groupId)) {
65+
recordIdsToRemoveByGroup.set(groupId, []);
66+
}
67+
68+
recordIdsToRemoveByGroup.get(groupId)?.push(recordIdToRemove);
69+
}
70+
71+
for (const [
72+
groupId,
73+
recordIdsToRemoveInGroup,
74+
] of recordIdsToRemoveByGroup) {
75+
const currentRecordIdsForGroup = getSnapshotValue(
76+
snapshot,
77+
recordIndexRecordIdsByGroupFamilyCallbackState(groupId),
78+
);
79+
80+
const recordIdsWithoutRemovedRecords =
81+
currentRecordIdsForGroup.filter(
82+
(recordId) => !recordIdsToRemoveInGroup.includes(recordId),
83+
);
84+
85+
set(
86+
recordIndexRecordIdsByGroupFamilyCallbackState(groupId),
87+
recordIdsWithoutRemovedRecords,
88+
);
89+
}
90+
},
91+
[
92+
recordIndexGroupFieldMetadataItemCallbackState,
93+
recordGroupFromGroupValueFamilyCallbackState,
94+
recordIndexRecordIdsByGroupFamilyCallbackState,
95+
],
96+
);
97+
98+
return {
99+
removeRecordsFromBoard,
100+
};
101+
};

packages/twenty-front/src/modules/object-record/record-table/virtualization/components/RecordTableVirtualizedDataChangedEffect.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { useListenToObjectRecordOperationBrowserEvent } from '@/object-record/hooks/useListenToObjectRecordOperationBrowserEvent';
22
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
33
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
4+
import { SSE_TABLE_DEBOUNCE_TIME_IN_MS_TO_AVOID_SSE_OWN_EVENTS_RACE_CONDITION } from '@/object-record/record-table/virtualization/constants/SseTableDebounceTimeInMsToAvoidSseOwnEventsRaceCondition';
45
import { useGetShouldResetTableVirtualizationForUpdateInputs } from '@/object-record/record-table/virtualization/hooks/useGetShouldResetTableVirtualizationForUpdateInputs';
56
import { useResetVirtualizationBecauseDataChanged } from '@/object-record/record-table/virtualization/hooks/useResetVirtualizationBecauseDataChanged';
67
import { type ObjectRecordOperationBrowserEventDetail } from '@/object-record/types/ObjectRecordOperationBrowserEventDetail';
8+
import { useDebouncedCallback } from 'use-debounce';
79

810
export const RecordTableVirtualizedDataChangedEffect = () => {
911
const { objectMetadataItem } = useRecordIndexContextOrThrow();
@@ -15,6 +17,14 @@ export const RecordTableVirtualizedDataChangedEffect = () => {
1517
const { getShouldResetTableVirtualizationForUpdateInputs } =
1618
useGetShouldResetTableVirtualizationForUpdateInputs();
1719

20+
const debouncedResertVirtualizationBecauseDataChanged = useDebouncedCallback(
21+
resetVirtualizationBecauseDataChanged,
22+
SSE_TABLE_DEBOUNCE_TIME_IN_MS_TO_AVOID_SSE_OWN_EVENTS_RACE_CONDITION,
23+
{
24+
leading: false,
25+
},
26+
);
27+
1828
const handleObjectRecordOperation = (
1929
objectRecordOperationEventDetail: ObjectRecordOperationBrowserEventDetail,
2030
) => {
@@ -34,10 +44,10 @@ export const RecordTableVirtualizedDataChangedEffect = () => {
3444
getShouldResetTableVirtualizationForUpdateInputs(updateInputs);
3545

3646
if (shouldResetForUpdateOperation) {
37-
resetVirtualizationBecauseDataChanged();
47+
debouncedResertVirtualizationBecauseDataChanged();
3848
}
3949
} else {
40-
resetVirtualizationBecauseDataChanged();
50+
debouncedResertVirtualizationBecauseDataChanged();
4151
}
4252
};
4353

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const SSE_TABLE_DEBOUNCE_TIME_IN_MS_TO_AVOID_SSE_OWN_EVENTS_RACE_CONDITION = 50;

packages/twenty-front/src/modules/object-record/types/ObjectRecordOperation.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,21 @@ export type ObjectRecordOperation =
1919
createdRecord: ObjectRecord;
2020
}
2121
| {
22-
type:
23-
| 'create-many'
24-
| 'destroy-one'
25-
| 'destroy-many'
26-
| 'delete-one'
27-
| 'delete-many'
28-
| 'restore-one'
29-
| 'restore-many'
30-
| 'merge-records';
22+
type: 'delete-one';
23+
deletedRecordId: string;
24+
}
25+
| {
26+
type: 'delete-many';
27+
deletedRecordIds: string[];
28+
}
29+
| {
30+
type: 'restore-one';
31+
restoredRecord: ObjectRecord;
32+
}
33+
| {
34+
type: 'restore-many';
35+
restoredRecords: ObjectRecord[];
36+
}
37+
| {
38+
type: 'create-many' | 'merge-records' | 'destroy-one' | 'destroy-many';
3139
};

0 commit comments

Comments
 (0)