fix: guard board drag initiation during data re-render to prevent stale draggable lookup
https://sonarly.com/issue/19024?type=bug Users on the Kanban board view encounter an unhandled "Invariant failed" crash when attempting to drag a card while the board is re-rendering after a record deletion or data refresh. Fix: Added `disableDragDuringBoardUpdate()` calls in `RecordBoardDataChangedEffect` to temporarily set `isRecordBoardDropProcessing` to `true` before any board data mutation (delete-one, delete-many, updates triggering refetch, creates triggering refetch, and the default case). This disables the `isDragDisabled` prop on all `<Draggable>` board cards for 500ms, preventing `@hello-pangea/dnd` from attempting to lift a draggable that may be unmounting/remounting during the React reconciliation triggered by the data change. This uses the exact same mechanism already established in PR #19005 (`8ef99671e2`): - Same `isRecordBoardDropProcessingComponentState` Jotai atom - Same `useDebouncedCallback` from `use-debounce` with 500ms timeout - Same pattern of `store.set(state, true)` then debounced `store.set(state, false)` The key gap in the original fix was that `isRecordBoardDropProcessing` was only set during active drag-and-drop operations, not during board re-renders triggered by CRUD operations. When a user deleted a record and then tried to drag a card, the board would re-render (unmounting/remounting Draggable components) without the drag guard, causing the `getById` invariant to fail when the DnD library tried to look up a deregistered draggable.
This commit is contained in:
+27
@@ -5,6 +5,7 @@ import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/ty
|
||||
import { useGetShouldInitializeRecordBoardForUpdateInputs } from '@/object-record/record-board/hooks/useGetShouldInitializeRecordBoardForUpdateInputs';
|
||||
import { useRemoveRecordsFromBoard } from '@/object-record/record-board/hooks/useRemoveRecordsFromBoard';
|
||||
import { useTriggerRecordBoardInitialQuery } from '@/object-record/record-board/hooks/useTriggerRecordBoardInitialQuery';
|
||||
import { isRecordBoardDropProcessingComponentState } from '@/object-record/record-board/states/isRecordBoardDropProcessingComponentState';
|
||||
import { recordGroupFromGroupValueComponentFamilySelector } from '@/object-record/record-group/states/selectors/recordGroupFromGroupValueComponentFamilySelector';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
@@ -15,6 +16,7 @@ import { useAtomComponentFamilyStateCallbackState } from '@/ui/utilities/state/j
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
export const RecordBoardDataChangedEffect = () => {
|
||||
const store = useStore();
|
||||
@@ -38,6 +40,23 @@ export const RecordBoardDataChangedEffect = () => {
|
||||
|
||||
const { removeRecordsFromBoard } = useRemoveRecordsFromBoard();
|
||||
|
||||
const isRecordBoardDropProcessingCallbackState =
|
||||
useAtomComponentStateCallbackState(
|
||||
isRecordBoardDropProcessingComponentState,
|
||||
);
|
||||
|
||||
// Temporarily disable drag during board data changes to prevent
|
||||
// @hello-pangea/dnd invariant errors when draggable components
|
||||
// unmount/remount during re-renders triggered by data mutations
|
||||
const debouncedReEnableDrag = useDebouncedCallback(() => {
|
||||
store.set(isRecordBoardDropProcessingCallbackState, false);
|
||||
}, 500);
|
||||
|
||||
const disableDragDuringBoardUpdate = useCallback(() => {
|
||||
store.set(isRecordBoardDropProcessingCallbackState, true);
|
||||
debouncedReEnableDrag();
|
||||
}, [store, isRecordBoardDropProcessingCallbackState, debouncedReEnableDrag]);
|
||||
|
||||
const handleObjectRecordOperation = useCallback(
|
||||
(
|
||||
objectRecordOperationEventDetail: ObjectRecordOperationBrowserEventDetail,
|
||||
@@ -57,12 +76,14 @@ export const RecordBoardDataChangedEffect = () => {
|
||||
getShouldInitializeRecordBoardForUpdateInputs(updateInputs);
|
||||
|
||||
if (shouldInitializeForUpdateOperation) {
|
||||
disableDragDuringBoardUpdate();
|
||||
triggerRecordBoardInitialQuery();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'create-one': {
|
||||
if (objectRecordOperation.createdRecord.position === 'first') {
|
||||
disableDragDuringBoardUpdate();
|
||||
triggerRecordBoardInitialQuery();
|
||||
} else {
|
||||
const createdRecordPosition =
|
||||
@@ -106,6 +127,7 @@ export const RecordBoardDataChangedEffect = () => {
|
||||
const groupIsEmpty = recordIdsWithoutCreatedRecord.length === 0;
|
||||
|
||||
if (groupIsEmpty) {
|
||||
disableDragDuringBoardUpdate();
|
||||
triggerRecordBoardInitialQuery();
|
||||
return;
|
||||
}
|
||||
@@ -122,6 +144,7 @@ export const RecordBoardDataChangedEffect = () => {
|
||||
if (
|
||||
createdRecordPosition < (firstExistingRecordInGroup.position ?? 0)
|
||||
) {
|
||||
disableDragDuringBoardUpdate();
|
||||
triggerRecordBoardInitialQuery();
|
||||
}
|
||||
}
|
||||
@@ -130,6 +153,7 @@ export const RecordBoardDataChangedEffect = () => {
|
||||
case 'delete-one': {
|
||||
const removedRecordId = objectRecordOperation.deletedRecordId;
|
||||
|
||||
disableDragDuringBoardUpdate();
|
||||
removeRecordsFromBoard({
|
||||
recordIdsToRemove: [removedRecordId],
|
||||
});
|
||||
@@ -138,6 +162,7 @@ export const RecordBoardDataChangedEffect = () => {
|
||||
case 'delete-many': {
|
||||
const removedRecordIds = objectRecordOperation.deletedRecordIds;
|
||||
|
||||
disableDragDuringBoardUpdate();
|
||||
removeRecordsFromBoard({
|
||||
recordIdsToRemove: removedRecordIds,
|
||||
});
|
||||
@@ -148,6 +173,7 @@ export const RecordBoardDataChangedEffect = () => {
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
disableDragDuringBoardUpdate();
|
||||
triggerRecordBoardInitialQuery();
|
||||
}
|
||||
}
|
||||
@@ -160,6 +186,7 @@ export const RecordBoardDataChangedEffect = () => {
|
||||
recordGroupFromGroupValueCallbackState,
|
||||
recordIndexRecordIdsByGroupCallbackState,
|
||||
removeRecordsFromBoard,
|
||||
disableDragDuringBoardUpdate,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user