Multi drag on tables + Refactor (#13570)
https://github.com/user-attachments/assets/32d20ca0-fa50-427c-92e8-638884d86de3 - code needs refactoring - ~~missing stack behaviour (I dont think its easy to add something similar here, but will give it a try) - I suspect it will look crowded~~ - ~~the notification counter - boards have it on top right, but on tables we cant do that since the rows could be longer than the scrolling container , therefore I added it on top left corner~~ - ~~needs to test with large set of data for both boards and tables (pagination)~~ --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
+7
-7
@@ -15,17 +15,17 @@ import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/comp
|
||||
import { RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-board/constants/RecordBoardClickOutsideListenerId';
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/useActiveRecordBoardCard';
|
||||
import { useBoardCardDragState } from '@/object-record/record-board/hooks/useBoardCardDragState';
|
||||
import { useEndBoardCardDrag } from '@/object-record/record-board/hooks/useEndBoardCardDrag';
|
||||
import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard';
|
||||
import { useRecordBoardDragOperations } from '@/object-record/record-board/hooks/useRecordBoardDragOperations';
|
||||
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
||||
import { useStartBoardCardDrag } from '@/object-record/record-board/hooks/useStartBoardCardDrag';
|
||||
import { RecordBoardDeactivateBoardCardEffect } from '@/object-record/record-board/record-board-card/components/RecordBoardDeactivateBoardCardEffect';
|
||||
import { RECORD_BOARD_CARD_CLICK_OUTSIDE_ID } from '@/object-record/record-board/record-board-card/constants/RecordBoardCardClickOutsideId';
|
||||
import { RecordBoardColumn } from '@/object-record/record-board/record-board-column/components/RecordBoardColumn';
|
||||
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
|
||||
import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector';
|
||||
import { useRecordBoardDragOperations } from '@/object-record/record-drag/board/hooks/useRecordBoardDragOperations';
|
||||
import { useEndRecordDrag } from '@/object-record/record-drag/shared/hooks/useEndRecordDrag';
|
||||
import { useRecordDragState } from '@/object-record/record-drag/shared/hooks/useRecordDragState';
|
||||
import { useStartRecordDrag } from '@/object-record/record-drag/shared/hooks/useStartRecordDrag';
|
||||
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
|
||||
import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
@@ -113,9 +113,9 @@ export const RecordBoard = () => {
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const startDrag = useStartBoardCardDrag(recordBoardId);
|
||||
const endDrag = useEndBoardCardDrag(recordBoardId);
|
||||
const multiDragState = useBoardCardDragState(recordBoardId);
|
||||
const { startDrag } = useStartRecordDrag('board', recordBoardId);
|
||||
const { endDrag } = useEndRecordDrag('board', recordBoardId);
|
||||
const multiDragState = useRecordDragState('board', recordBoardId);
|
||||
|
||||
const { processDragOperation } = useRecordBoardDragOperations();
|
||||
|
||||
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
import { draggedRecordIdsComponentState } from '@/object-record/record-board/states/draggedRecordIdsComponentState';
|
||||
import { isMultiDragActiveComponentState } from '@/object-record/record-board/states/isMultiDragActiveComponentState';
|
||||
import { originalSelectionComponentState } from '@/object-record/record-board/states/originalSelectionComponentState';
|
||||
import { primaryDraggedRecordIdComponentState } from '@/object-record/record-board/states/primaryDraggedRecordIdComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
export const useBoardCardDragState = (recordBoardId?: string) => {
|
||||
const isDragging = useRecoilComponentValue(
|
||||
isMultiDragActiveComponentState,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const draggedRecordIds = useRecoilComponentValue(
|
||||
draggedRecordIdsComponentState,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const primaryDraggedRecordId = useRecoilComponentValue(
|
||||
primaryDraggedRecordIdComponentState,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const originalSelection = useRecoilComponentValue(
|
||||
originalSelectionComponentState,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
return {
|
||||
isDragging,
|
||||
draggedRecordIds,
|
||||
primaryDraggedRecordId,
|
||||
originalSelection,
|
||||
};
|
||||
};
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { draggedRecordIdsComponentState } from '@/object-record/record-board/states/draggedRecordIdsComponentState';
|
||||
import { isMultiDragActiveComponentState } from '@/object-record/record-board/states/isMultiDragActiveComponentState';
|
||||
import { originalSelectionComponentState } from '@/object-record/record-board/states/originalSelectionComponentState';
|
||||
import { primaryDraggedRecordIdComponentState } from '@/object-record/record-board/states/primaryDraggedRecordIdComponentState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
|
||||
export const useEndBoardCardDrag = (recordBoardId?: string) => {
|
||||
const isMultiDragActiveState = useRecoilComponentCallbackState(
|
||||
isMultiDragActiveComponentState,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const draggedRecordIdsState = useRecoilComponentCallbackState(
|
||||
draggedRecordIdsComponentState,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const primaryDraggedRecordIdState = useRecoilComponentCallbackState(
|
||||
primaryDraggedRecordIdComponentState,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const originalSelectionState = useRecoilComponentCallbackState(
|
||||
originalSelectionComponentState,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
return useRecoilCallback(
|
||||
({ set }) =>
|
||||
() => {
|
||||
set(isMultiDragActiveState, false);
|
||||
set(draggedRecordIdsState, []);
|
||||
set(primaryDraggedRecordIdState, null);
|
||||
set(originalSelectionState, []);
|
||||
},
|
||||
[
|
||||
isMultiDragActiveState,
|
||||
draggedRecordIdsState,
|
||||
primaryDraggedRecordIdState,
|
||||
originalSelectionState,
|
||||
],
|
||||
);
|
||||
};
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { getDragOperationType } from '../utils/getDragOperationType';
|
||||
import { processMultiDrag } from '../utils/processMultiDrag';
|
||||
import { processSingleDrag } from '../utils/processSingleDrag';
|
||||
|
||||
export const useRecordBoardDragOperations = () => {
|
||||
const { updateOneRecord, selectFieldMetadataItem } =
|
||||
useContext(RecordBoardContext);
|
||||
|
||||
const recordIndexRecordIdsByGroupFamilyState =
|
||||
useRecoilComponentCallbackState(
|
||||
recordIndexRecordIdsByGroupComponentFamilyState,
|
||||
);
|
||||
|
||||
const processDragOperation = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(result: DropResult, selectedRecordIds: string[]) => {
|
||||
if (!result.destination || !selectFieldMetadataItem) return;
|
||||
|
||||
const draggedRecordId = result.draggableId;
|
||||
const destinationGroupId = result.destination.droppableId;
|
||||
|
||||
const recordGroup = getSnapshotValue(
|
||||
snapshot,
|
||||
recordGroupDefinitionFamilyState(destinationGroupId),
|
||||
);
|
||||
|
||||
if (!recordGroup) return;
|
||||
|
||||
const destinationRecordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
recordIndexRecordIdsByGroupFamilyState(destinationGroupId),
|
||||
) as string[];
|
||||
|
||||
const recordPositionData = destinationRecordIds.map((recordId) => {
|
||||
const record = getSnapshotValue(
|
||||
snapshot,
|
||||
recordStoreFamilyState(recordId),
|
||||
);
|
||||
return {
|
||||
recordId,
|
||||
position: record?.position,
|
||||
};
|
||||
});
|
||||
|
||||
const operationType = getDragOperationType({
|
||||
draggedRecordId,
|
||||
selectedRecordIds,
|
||||
});
|
||||
|
||||
if (operationType === 'single') {
|
||||
const singleDragResult = processSingleDrag({
|
||||
result,
|
||||
recordPositionData,
|
||||
destinationRecordIds,
|
||||
groupValue: recordGroup.value,
|
||||
selectFieldName: selectFieldMetadataItem.name,
|
||||
});
|
||||
|
||||
updateOneRecord({
|
||||
idToUpdate: singleDragResult.recordId,
|
||||
updateOneRecordInput: {
|
||||
[selectFieldMetadataItem.name]: singleDragResult.groupValue,
|
||||
position: singleDragResult.position,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const multiDragResult = processMultiDrag({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData,
|
||||
destinationRecordIds,
|
||||
groupValue: recordGroup.value,
|
||||
selectFieldName: selectFieldMetadataItem.name,
|
||||
});
|
||||
|
||||
for (const update of multiDragResult.recordUpdates) {
|
||||
updateOneRecord({
|
||||
idToUpdate: update.recordId,
|
||||
updateOneRecordInput: {
|
||||
[selectFieldMetadataItem.name]: update.groupValue,
|
||||
position: update.position,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
updateOneRecord,
|
||||
selectFieldMetadataItem,
|
||||
recordIndexRecordIdsByGroupFamilyState,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
processDragOperation,
|
||||
};
|
||||
};
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
import { type DragStart } from '@hello-pangea/dnd';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { draggedRecordIdsComponentState } from '@/object-record/record-board/states/draggedRecordIdsComponentState';
|
||||
import { isMultiDragActiveComponentState } from '@/object-record/record-board/states/isMultiDragActiveComponentState';
|
||||
import { originalSelectionComponentState } from '@/object-record/record-board/states/originalSelectionComponentState';
|
||||
import { primaryDraggedRecordIdComponentState } from '@/object-record/record-board/states/primaryDraggedRecordIdComponentState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { getDragOperationType } from '../utils/getDragOperationType';
|
||||
|
||||
export const useStartBoardCardDrag = (recordBoardId?: string) => {
|
||||
const isMultiDragActiveState = useRecoilComponentCallbackState(
|
||||
isMultiDragActiveComponentState,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const draggedRecordIdsState = useRecoilComponentCallbackState(
|
||||
draggedRecordIdsComponentState,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const primaryDraggedRecordIdState = useRecoilComponentCallbackState(
|
||||
primaryDraggedRecordIdComponentState,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
const originalSelectionState = useRecoilComponentCallbackState(
|
||||
originalSelectionComponentState,
|
||||
recordBoardId,
|
||||
);
|
||||
|
||||
return useRecoilCallback(
|
||||
({ set }) =>
|
||||
(start: DragStart, selectedRecordIds: string[]) => {
|
||||
const draggedRecordId = start.draggableId;
|
||||
|
||||
const operationType = getDragOperationType({
|
||||
draggedRecordId,
|
||||
selectedRecordIds,
|
||||
});
|
||||
|
||||
if (operationType === 'multi') {
|
||||
set(isMultiDragActiveState, true);
|
||||
set(draggedRecordIdsState, selectedRecordIds);
|
||||
set(primaryDraggedRecordIdState, draggedRecordId);
|
||||
set(originalSelectionState, selectedRecordIds);
|
||||
} else {
|
||||
set(isMultiDragActiveState, true);
|
||||
set(draggedRecordIdsState, [draggedRecordId]);
|
||||
set(primaryDraggedRecordIdState, draggedRecordId);
|
||||
set(originalSelectionState, [draggedRecordId]);
|
||||
}
|
||||
},
|
||||
[
|
||||
isMultiDragActiveState,
|
||||
draggedRecordIdsState,
|
||||
primaryDraggedRecordIdState,
|
||||
originalSelectionState,
|
||||
],
|
||||
);
|
||||
};
|
||||
+7
-8
@@ -1,7 +1,7 @@
|
||||
import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState';
|
||||
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
|
||||
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
|
||||
import { useBoardCardDragState } from '@/object-record/record-board/hooks/useBoardCardDragState';
|
||||
import { useRecordDragState } from '@/object-record/record-drag/shared/hooks/useRecordDragState';
|
||||
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
|
||||
import { isRecordBoardCardActiveComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardActiveComponentFamilyState';
|
||||
import { isRecordBoardCardFocusedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState';
|
||||
@@ -124,11 +124,14 @@ export const RecordBoardCard = () => {
|
||||
RecordBoardCardContext,
|
||||
);
|
||||
|
||||
const multiDragState = useBoardCardDragState();
|
||||
const recordBoardId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordBoardComponentInstanceContext,
|
||||
);
|
||||
|
||||
const multiDragState = useRecordDragState('board', recordBoardId);
|
||||
|
||||
const isPrimaryMultiDrag =
|
||||
multiDragState &&
|
||||
multiDragState.isDragging &&
|
||||
multiDragState?.isDragging &&
|
||||
recordId === multiDragState.primaryDraggedRecordId &&
|
||||
multiDragState.originalSelection.length > 1;
|
||||
|
||||
@@ -169,10 +172,6 @@ export const RecordBoardCard = () => {
|
||||
},
|
||||
);
|
||||
|
||||
const recordBoardId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordBoardComponentInstanceContext,
|
||||
);
|
||||
|
||||
const actionMenuId = getActionMenuIdFromRecordIndexId(recordBoardId);
|
||||
|
||||
const actionMenuDropdownId =
|
||||
|
||||
+7
-2
@@ -1,5 +1,7 @@
|
||||
import { useBoardCardDragState } from '@/object-record/record-board/hooks/useBoardCardDragState';
|
||||
import { useRecordDragState } from '@/object-record/record-drag/shared/hooks/useRecordDragState';
|
||||
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
|
||||
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext } from 'react';
|
||||
import { NotificationCounter } from 'twenty-ui/navigation';
|
||||
@@ -19,7 +21,10 @@ export const RecordBoardCardMultiDragPreview = ({
|
||||
isDragging,
|
||||
}: RecordBoardCardMultiDragPreviewProps) => {
|
||||
const { recordId } = useContext(RecordBoardCardContext);
|
||||
const multiDragState = useBoardCardDragState();
|
||||
const recordBoardId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordBoardComponentInstanceContext,
|
||||
);
|
||||
const multiDragState = useRecordDragState('board', recordBoardId);
|
||||
|
||||
const isCurrentCardSelected =
|
||||
multiDragState?.originalSelection.includes(recordId) || false;
|
||||
|
||||
-446
@@ -1,446 +0,0 @@
|
||||
import { calculateRecordPositions } from '../calculateRecordPositions';
|
||||
|
||||
import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition';
|
||||
import { getIndexNeighboursElementsFromArray } from '~/utils/array/getIndexNeighboursElementsFromArray';
|
||||
|
||||
jest.mock(
|
||||
'@/object-record/record-board/utils/getDraggedRecordPosition',
|
||||
() => ({
|
||||
getDraggedRecordPosition: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('~/utils/array/getIndexNeighboursElementsFromArray', () => ({
|
||||
getIndexNeighboursElementsFromArray: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockGetDraggedRecordPosition = getDraggedRecordPosition as jest.Mock;
|
||||
const mockGetIndexNeighboursElementsFromArray =
|
||||
getIndexNeighboursElementsFromArray as jest.Mock;
|
||||
|
||||
describe('calculateRecordPositions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockRecordPositionData = [
|
||||
{ recordId: 'record-1', position: 1 },
|
||||
{ recordId: 'record-2', position: 2 },
|
||||
{ recordId: 'record-3', position: 3 },
|
||||
{ recordId: 'record-4', position: 4 },
|
||||
{ recordId: 'record-5', position: 5 },
|
||||
];
|
||||
|
||||
it('should calculate single record position correctly', () => {
|
||||
const basePosition = 2.5;
|
||||
|
||||
mockGetIndexNeighboursElementsFromArray.mockReturnValue({
|
||||
before: 'record-2',
|
||||
after: 'record-3',
|
||||
});
|
||||
|
||||
mockGetDraggedRecordPosition.mockReturnValue(basePosition);
|
||||
|
||||
const result = calculateRecordPositions({
|
||||
destinationRecordIds: [
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
'record-4',
|
||||
'record-5',
|
||||
],
|
||||
recordsToMove: ['record-1'],
|
||||
destinationIndex: 2,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
'record-1': basePosition,
|
||||
});
|
||||
expect(mockGetDraggedRecordPosition).toHaveBeenCalledWith(2, 3);
|
||||
});
|
||||
|
||||
it('should calculate multiple record positions with proportional spacing', () => {
|
||||
const basePosition = 2.5;
|
||||
|
||||
mockGetIndexNeighboursElementsFromArray.mockReturnValue({
|
||||
before: 'record-2',
|
||||
after: 'record-5',
|
||||
});
|
||||
|
||||
mockGetDraggedRecordPosition.mockReturnValue(basePosition);
|
||||
|
||||
const result = calculateRecordPositions({
|
||||
destinationRecordIds: ['record-2', 'record-3', 'record-4', 'record-5'],
|
||||
recordsToMove: ['record-1', 'record-6', 'record-7'],
|
||||
destinationIndex: 1,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
'record-1': 3.125,
|
||||
'record-6': 3.75,
|
||||
'record-7': 4.375,
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter out records being moved from destination column', () => {
|
||||
const basePosition = 2.5;
|
||||
|
||||
mockGetIndexNeighboursElementsFromArray.mockReturnValue({
|
||||
before: 'record-2',
|
||||
after: 'record-5',
|
||||
});
|
||||
|
||||
mockGetDraggedRecordPosition.mockReturnValue(basePosition);
|
||||
|
||||
calculateRecordPositions({
|
||||
destinationRecordIds: [
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
'record-4',
|
||||
'record-5',
|
||||
],
|
||||
recordsToMove: ['record-1', 'record-3'],
|
||||
destinationIndex: 1,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(mockGetIndexNeighboursElementsFromArray).toHaveBeenCalledWith({
|
||||
index: 1,
|
||||
array: ['record-2', 'record-3', 'record-4', 'record-5'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle same group single drag by filtering source record', () => {
|
||||
const basePosition = 2.5;
|
||||
|
||||
mockGetIndexNeighboursElementsFromArray.mockReturnValue({
|
||||
before: 'record-2',
|
||||
after: 'record-4',
|
||||
});
|
||||
|
||||
mockGetDraggedRecordPosition.mockReturnValue(basePosition);
|
||||
|
||||
calculateRecordPositions({
|
||||
destinationRecordIds: [
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
'record-4',
|
||||
'record-5',
|
||||
],
|
||||
recordsToMove: ['record-3'],
|
||||
destinationIndex: 1,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(mockGetIndexNeighboursElementsFromArray).toHaveBeenCalledWith({
|
||||
index: 1,
|
||||
array: ['record-1', 'record-2', 'record-4', 'record-5'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle case with no before record', () => {
|
||||
const basePosition = 0.5;
|
||||
|
||||
mockGetIndexNeighboursElementsFromArray.mockReturnValue({
|
||||
before: null,
|
||||
after: 'record-2',
|
||||
});
|
||||
|
||||
mockGetDraggedRecordPosition.mockReturnValue(basePosition);
|
||||
|
||||
const result = calculateRecordPositions({
|
||||
destinationRecordIds: ['record-1', 'record-2', 'record-3'],
|
||||
recordsToMove: ['record-new'],
|
||||
destinationIndex: 0,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(mockGetDraggedRecordPosition).toHaveBeenCalledWith(undefined, 2);
|
||||
expect(result).toEqual({
|
||||
'record-new': basePosition,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle case with no after record', () => {
|
||||
const basePosition = 5.5;
|
||||
|
||||
mockGetIndexNeighboursElementsFromArray.mockReturnValue({
|
||||
before: 'record-5',
|
||||
after: null,
|
||||
});
|
||||
|
||||
mockGetDraggedRecordPosition.mockReturnValue(basePosition);
|
||||
|
||||
const result = calculateRecordPositions({
|
||||
destinationRecordIds: [
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
'record-4',
|
||||
'record-5',
|
||||
],
|
||||
recordsToMove: ['record-new'],
|
||||
destinationIndex: 5,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(mockGetDraggedRecordPosition).toHaveBeenCalledWith(5, undefined);
|
||||
expect(result).toEqual({
|
||||
'record-new': basePosition,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multi-drag with proportional spacing when after record exists', () => {
|
||||
const basePosition = 2.5;
|
||||
const afterPosition = 5.0;
|
||||
|
||||
mockGetIndexNeighboursElementsFromArray.mockReturnValue({
|
||||
before: 'record-2',
|
||||
after: 'record-5',
|
||||
});
|
||||
|
||||
mockGetDraggedRecordPosition.mockReturnValue(basePosition);
|
||||
|
||||
const recordPositionDataWithAfter = [
|
||||
...mockRecordPositionData,
|
||||
{ recordId: 'record-5', position: afterPosition },
|
||||
];
|
||||
|
||||
const result = calculateRecordPositions({
|
||||
destinationRecordIds: [
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
'record-4',
|
||||
'record-5',
|
||||
],
|
||||
recordsToMove: ['record-a', 'record-b'],
|
||||
destinationIndex: 1,
|
||||
recordPositionData: recordPositionDataWithAfter,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
'record-a': 3.3333333333333335,
|
||||
'record-b': 4.166666666666667,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multi-drag with default spacing when no after record exists', () => {
|
||||
const basePosition = 5.5;
|
||||
|
||||
mockGetIndexNeighboursElementsFromArray.mockReturnValue({
|
||||
before: 'record-5',
|
||||
after: null,
|
||||
});
|
||||
|
||||
mockGetDraggedRecordPosition.mockReturnValue(basePosition);
|
||||
|
||||
const result = calculateRecordPositions({
|
||||
destinationRecordIds: [
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
'record-4',
|
||||
'record-5',
|
||||
],
|
||||
recordsToMove: ['record-a', 'record-b'],
|
||||
destinationIndex: 5,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
'record-a': 5.833333333333333,
|
||||
'record-b': 6.166666666666667,
|
||||
});
|
||||
});
|
||||
|
||||
describe('Single drag scenarios', () => {
|
||||
it('should handle single drag within same column', () => {
|
||||
mockGetIndexNeighboursElementsFromArray.mockReturnValue({
|
||||
before: 'record-2',
|
||||
after: 'record-4',
|
||||
});
|
||||
mockGetDraggedRecordPosition.mockReturnValue(2.5);
|
||||
|
||||
const result = calculateRecordPositions({
|
||||
destinationRecordIds: ['record-1', 'record-2', 'record-3', 'record-4'],
|
||||
recordsToMove: ['record-3'],
|
||||
destinationIndex: 2,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(mockGetIndexNeighboursElementsFromArray).toHaveBeenCalledWith({
|
||||
index: 2,
|
||||
array: ['record-1', 'record-2', 'record-4'],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
'record-3': 2.5,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle single drag to empty column', () => {
|
||||
mockGetIndexNeighboursElementsFromArray.mockReturnValue({
|
||||
before: undefined,
|
||||
after: undefined,
|
||||
});
|
||||
mockGetDraggedRecordPosition.mockReturnValue(1);
|
||||
|
||||
const result = calculateRecordPositions({
|
||||
destinationRecordIds: [],
|
||||
recordsToMove: ['record-1'],
|
||||
destinationIndex: 0,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(mockGetIndexNeighboursElementsFromArray).toHaveBeenCalledWith({
|
||||
index: 0,
|
||||
array: [],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
'record-1': 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multi-drag scenarios', () => {
|
||||
it('should handle multi-drag with secondary records already in destination', () => {
|
||||
mockGetIndexNeighboursElementsFromArray.mockReturnValue({
|
||||
before: 'record-1',
|
||||
after: 'record-4',
|
||||
});
|
||||
mockGetDraggedRecordPosition.mockReturnValue(2);
|
||||
|
||||
const result = calculateRecordPositions({
|
||||
destinationRecordIds: ['record-1', 'record-2', 'record-3', 'record-4'],
|
||||
recordsToMove: ['record-2', 'record-3', 'record-5'],
|
||||
destinationIndex: 1,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(mockGetIndexNeighboursElementsFromArray).toHaveBeenCalledWith({
|
||||
index: 1,
|
||||
array: ['record-1', 'record-3', 'record-4'],
|
||||
});
|
||||
|
||||
const availableSpace = 4 - 2;
|
||||
const increment = availableSpace / (3 + 1);
|
||||
|
||||
expect(result).toEqual({
|
||||
'record-2': 2 + 1 * increment,
|
||||
'record-3': 2 + 2 * increment,
|
||||
'record-5': 2 + 3 * increment,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multi-drag where all records are from different columns', () => {
|
||||
mockGetIndexNeighboursElementsFromArray.mockReturnValue({
|
||||
before: 'record-1',
|
||||
after: 'record-2',
|
||||
});
|
||||
mockGetDraggedRecordPosition.mockReturnValue(1.5);
|
||||
|
||||
const result = calculateRecordPositions({
|
||||
destinationRecordIds: ['record-1', 'record-2'],
|
||||
recordsToMove: ['record-3', 'record-4'],
|
||||
destinationIndex: 1,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(mockGetIndexNeighboursElementsFromArray).toHaveBeenCalledWith({
|
||||
index: 1,
|
||||
array: ['record-1', 'record-2'],
|
||||
});
|
||||
|
||||
const availableSpace = 2 - 1.5;
|
||||
const increment = availableSpace / 3;
|
||||
|
||||
expect(result).toEqual({
|
||||
'record-3': 1.5 + increment,
|
||||
'record-4': 1.5 + 2 * increment,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multi-drag to empty column', () => {
|
||||
mockGetIndexNeighboursElementsFromArray.mockReturnValue({
|
||||
before: undefined,
|
||||
after: undefined,
|
||||
});
|
||||
mockGetDraggedRecordPosition.mockReturnValue(1);
|
||||
|
||||
const result = calculateRecordPositions({
|
||||
destinationRecordIds: [],
|
||||
recordsToMove: ['record-1', 'record-2'],
|
||||
destinationIndex: 0,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(mockGetIndexNeighboursElementsFromArray).toHaveBeenCalledWith({
|
||||
index: 0,
|
||||
array: [],
|
||||
});
|
||||
|
||||
const increment = 1 / 3;
|
||||
|
||||
expect(result).toEqual({
|
||||
'record-1': 1 + increment,
|
||||
'record-2': 1 + 2 * increment,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle moving to beginning of column (index 0)', () => {
|
||||
mockGetIndexNeighboursElementsFromArray.mockReturnValue({
|
||||
before: undefined,
|
||||
after: 'record-1',
|
||||
});
|
||||
mockGetDraggedRecordPosition.mockReturnValue(0.5);
|
||||
|
||||
const result = calculateRecordPositions({
|
||||
destinationRecordIds: ['record-1', 'record-2'],
|
||||
recordsToMove: ['record-3'],
|
||||
destinationIndex: 0,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(mockGetIndexNeighboursElementsFromArray).toHaveBeenCalledWith({
|
||||
index: 0,
|
||||
array: ['record-1', 'record-2'],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
'record-3': 0.5,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle moving to end of column', () => {
|
||||
mockGetIndexNeighboursElementsFromArray.mockReturnValue({
|
||||
before: 'record-2',
|
||||
after: undefined,
|
||||
});
|
||||
mockGetDraggedRecordPosition.mockReturnValue(2.5);
|
||||
|
||||
const result = calculateRecordPositions({
|
||||
destinationRecordIds: ['record-1', 'record-2'],
|
||||
recordsToMove: ['record-3'],
|
||||
destinationIndex: 2,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(mockGetIndexNeighboursElementsFromArray).toHaveBeenCalledWith({
|
||||
index: 2,
|
||||
array: ['record-1', 'record-2'],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
'record-3': 2.5,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
-180
@@ -1,180 +0,0 @@
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { processMultiDrag } from '../processMultiDrag';
|
||||
|
||||
import { calculateRecordPositions } from '../calculateRecordPositions';
|
||||
|
||||
jest.mock('../calculateRecordPositions', () => ({
|
||||
calculateRecordPositions: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockCalculateRecordPositions = calculateRecordPositions as jest.Mock;
|
||||
|
||||
describe('processMultiDrag', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockDropResult: DropResult = {
|
||||
draggableId: 'record-1',
|
||||
type: 'DEFAULT',
|
||||
source: {
|
||||
droppableId: 'group-1',
|
||||
index: 0,
|
||||
},
|
||||
destination: {
|
||||
droppableId: 'group-2',
|
||||
index: 1,
|
||||
},
|
||||
reason: 'DROP',
|
||||
mode: 'FLUID',
|
||||
combine: null,
|
||||
};
|
||||
|
||||
const mockRecordPositionData = [
|
||||
{ recordId: 'record-1', position: 1 },
|
||||
{ recordId: 'record-2', position: 2 },
|
||||
{ recordId: 'record-3', position: 3 },
|
||||
];
|
||||
|
||||
it('should process multi-drag operation correctly', () => {
|
||||
const selectedRecordIds = ['record-1', 'record-3'];
|
||||
|
||||
mockCalculateRecordPositions.mockReturnValue({
|
||||
'record-1': 2.5,
|
||||
'record-3': 2.5001,
|
||||
});
|
||||
|
||||
const result = processMultiDrag({
|
||||
result: mockDropResult,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
destinationRecordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: 'new-group-value',
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
recordUpdates: [
|
||||
{
|
||||
recordId: 'record-1',
|
||||
position: 2.5,
|
||||
groupValue: 'new-group-value',
|
||||
selectFieldName: 'status',
|
||||
},
|
||||
{
|
||||
recordId: 'record-3',
|
||||
position: 2.5001,
|
||||
groupValue: 'new-group-value',
|
||||
selectFieldName: 'status',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockCalculateRecordPositions).toHaveBeenCalledWith({
|
||||
destinationRecordIds: ['record-1', 'record-2', 'record-3'],
|
||||
recordsToMove: selectedRecordIds,
|
||||
destinationIndex: 1,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle single record in multi-drag correctly', () => {
|
||||
const selectedRecordIds = ['record-2'];
|
||||
|
||||
mockCalculateRecordPositions.mockReturnValue({
|
||||
'record-2': 3.5,
|
||||
});
|
||||
|
||||
const result = processMultiDrag({
|
||||
result: mockDropResult,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
destinationRecordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: 'single-record-group',
|
||||
selectFieldName: 'priority',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
recordUpdates: [
|
||||
{
|
||||
recordId: 'record-2',
|
||||
position: 3.5,
|
||||
groupValue: 'single-record-group',
|
||||
selectFieldName: 'priority',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null group value', () => {
|
||||
const selectedRecordIds = ['record-1', 'record-2'];
|
||||
|
||||
mockCalculateRecordPositions.mockReturnValue({
|
||||
'record-1': 1.5,
|
||||
'record-2': 1.5001,
|
||||
});
|
||||
|
||||
const result = processMultiDrag({
|
||||
result: mockDropResult,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
destinationRecordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: null,
|
||||
selectFieldName: 'category',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
recordUpdates: [
|
||||
{
|
||||
recordId: 'record-1',
|
||||
position: 1.5,
|
||||
groupValue: null,
|
||||
selectFieldName: 'category',
|
||||
},
|
||||
{
|
||||
recordId: 'record-2',
|
||||
position: 1.5001,
|
||||
groupValue: null,
|
||||
selectFieldName: 'category',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when destination is null', () => {
|
||||
const dropResultWithoutDestination = {
|
||||
...mockDropResult,
|
||||
destination: null,
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
processMultiDrag({
|
||||
result: dropResultWithoutDestination,
|
||||
selectedRecordIds: ['record-1', 'record-2'],
|
||||
recordPositionData: mockRecordPositionData,
|
||||
destinationRecordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: 'new-group-value',
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
}).toThrow('Destination is required for drag operation');
|
||||
});
|
||||
|
||||
it('should handle empty selectedRecordIds', () => {
|
||||
const selectedRecordIds: string[] = [];
|
||||
|
||||
mockCalculateRecordPositions.mockReturnValue({});
|
||||
|
||||
const result = processMultiDrag({
|
||||
result: mockDropResult,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
destinationRecordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: 'empty-selection',
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
recordUpdates: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
-143
@@ -1,143 +0,0 @@
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { processSingleDrag } from '../processSingleDrag';
|
||||
|
||||
import { calculateRecordPositions } from '../calculateRecordPositions';
|
||||
|
||||
jest.mock('../calculateRecordPositions', () => ({
|
||||
calculateRecordPositions: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockCalculateRecordPositions = calculateRecordPositions as jest.Mock;
|
||||
|
||||
describe('processSingleDrag', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockDropResult: DropResult = {
|
||||
draggableId: 'record-1',
|
||||
type: 'DEFAULT',
|
||||
source: {
|
||||
droppableId: 'group-1',
|
||||
index: 0,
|
||||
},
|
||||
destination: {
|
||||
droppableId: 'group-2',
|
||||
index: 1,
|
||||
},
|
||||
reason: 'DROP',
|
||||
mode: 'FLUID',
|
||||
combine: null,
|
||||
};
|
||||
|
||||
const mockRecordPositionData = [
|
||||
{ recordId: 'record-1', position: 1 },
|
||||
{ recordId: 'record-2', position: 2 },
|
||||
{ recordId: 'record-3', position: 3 },
|
||||
];
|
||||
|
||||
it('should process single drag operation correctly', () => {
|
||||
const expectedPosition = 2.5;
|
||||
|
||||
mockCalculateRecordPositions.mockReturnValue({
|
||||
'record-1': expectedPosition,
|
||||
});
|
||||
|
||||
const result = processSingleDrag({
|
||||
result: mockDropResult,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
destinationRecordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: 'new-group-value',
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
recordId: 'record-1',
|
||||
position: expectedPosition,
|
||||
groupValue: 'new-group-value',
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
|
||||
expect(mockCalculateRecordPositions).toHaveBeenCalledWith({
|
||||
destinationRecordIds: ['record-1', 'record-2', 'record-3'],
|
||||
recordsToMove: ['record-1'],
|
||||
destinationIndex: 1,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle different record IDs correctly', () => {
|
||||
const dropResult = {
|
||||
...mockDropResult,
|
||||
draggableId: 'record-5',
|
||||
};
|
||||
|
||||
const expectedPosition = 3.5;
|
||||
|
||||
mockCalculateRecordPositions.mockReturnValue({
|
||||
'record-5': expectedPosition,
|
||||
});
|
||||
|
||||
const result = processSingleDrag({
|
||||
result: dropResult,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
destinationRecordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: 'another-group-value',
|
||||
selectFieldName: 'priority',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
recordId: 'record-5',
|
||||
position: expectedPosition,
|
||||
groupValue: 'another-group-value',
|
||||
selectFieldName: 'priority',
|
||||
});
|
||||
|
||||
expect(mockCalculateRecordPositions).toHaveBeenCalledWith({
|
||||
destinationRecordIds: ['record-1', 'record-2', 'record-3'],
|
||||
recordsToMove: ['record-5'],
|
||||
destinationIndex: 1,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null group value', () => {
|
||||
const expectedPosition = 1.5;
|
||||
|
||||
mockCalculateRecordPositions.mockReturnValue({
|
||||
'record-1': expectedPosition,
|
||||
});
|
||||
|
||||
const result = processSingleDrag({
|
||||
result: mockDropResult,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
destinationRecordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: null,
|
||||
selectFieldName: 'category',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
recordId: 'record-1',
|
||||
position: expectedPosition,
|
||||
groupValue: null,
|
||||
selectFieldName: 'category',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when destination is null', () => {
|
||||
const dropResultWithoutDestination = {
|
||||
...mockDropResult,
|
||||
destination: null,
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
processSingleDrag({
|
||||
result: dropResultWithoutDestination,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
destinationRecordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: 'new-group-value',
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
}).toThrow('Destination is required for drag operation');
|
||||
});
|
||||
});
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import {
|
||||
calculateRecordPositions,
|
||||
type RecordPositionData,
|
||||
} from './calculateRecordPositions';
|
||||
|
||||
type MultiDragContext = {
|
||||
result: DropResult;
|
||||
selectedRecordIds: string[];
|
||||
recordPositionData: RecordPositionData[];
|
||||
destinationRecordIds: string[];
|
||||
groupValue: string | null;
|
||||
selectFieldName: string;
|
||||
};
|
||||
|
||||
type MultiDragResult = {
|
||||
recordUpdates: Array<{
|
||||
recordId: string;
|
||||
position: number;
|
||||
groupValue: string | null;
|
||||
selectFieldName: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export const processMultiDrag = ({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData,
|
||||
destinationRecordIds,
|
||||
groupValue,
|
||||
selectFieldName,
|
||||
}: MultiDragContext): MultiDragResult => {
|
||||
if (!result.destination) {
|
||||
throw new Error('Destination is required for drag operation');
|
||||
}
|
||||
|
||||
const destinationIndex = result.destination.index;
|
||||
|
||||
const recordsToMove = selectedRecordIds;
|
||||
|
||||
const positions = calculateRecordPositions({
|
||||
destinationRecordIds,
|
||||
recordsToMove,
|
||||
destinationIndex,
|
||||
recordPositionData,
|
||||
});
|
||||
|
||||
const recordUpdates = recordsToMove.map((recordId) => ({
|
||||
recordId,
|
||||
position: positions[recordId],
|
||||
groupValue,
|
||||
selectFieldName,
|
||||
}));
|
||||
|
||||
return {
|
||||
recordUpdates,
|
||||
};
|
||||
};
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import {
|
||||
calculateRecordPositions,
|
||||
type RecordPositionData,
|
||||
} from './calculateRecordPositions';
|
||||
|
||||
type SingleDragContext = {
|
||||
result: DropResult;
|
||||
recordPositionData: RecordPositionData[];
|
||||
destinationRecordIds: string[];
|
||||
groupValue: string | null;
|
||||
selectFieldName: string;
|
||||
};
|
||||
|
||||
type SingleDragResult = {
|
||||
recordId: string;
|
||||
position: number;
|
||||
groupValue: string | null;
|
||||
selectFieldName: string;
|
||||
};
|
||||
|
||||
export const processSingleDrag = ({
|
||||
result,
|
||||
recordPositionData,
|
||||
destinationRecordIds,
|
||||
groupValue,
|
||||
selectFieldName,
|
||||
}: SingleDragContext): SingleDragResult => {
|
||||
const draggedRecordId = result.draggableId;
|
||||
|
||||
if (!result.destination) {
|
||||
throw new Error('Destination is required for drag operation');
|
||||
}
|
||||
|
||||
const destinationIndex = result.destination.index;
|
||||
|
||||
const recordsToMove = [draggedRecordId];
|
||||
|
||||
const positions = calculateRecordPositions({
|
||||
destinationRecordIds,
|
||||
recordsToMove,
|
||||
destinationIndex,
|
||||
recordPositionData,
|
||||
});
|
||||
|
||||
return {
|
||||
recordId: draggedRecordId,
|
||||
position: positions[draggedRecordId],
|
||||
groupValue,
|
||||
selectFieldName,
|
||||
};
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { processGroupDragOperation } from '@/object-record/record-drag/shared/utils/processGroupDragOperation';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
|
||||
export const useRecordBoardDragOperations = () => {
|
||||
const { updateOneRecord, selectFieldMetadataItem } =
|
||||
useContext(RecordBoardContext);
|
||||
|
||||
const recordIndexRecordIdsByGroupFamilyState =
|
||||
useRecoilComponentCallbackState(
|
||||
recordIndexRecordIdsByGroupComponentFamilyState,
|
||||
);
|
||||
|
||||
const processDragOperation = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(result: DropResult, selectedRecordIds: string[]) => {
|
||||
if (!selectFieldMetadataItem) return;
|
||||
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot,
|
||||
selectedRecordIds,
|
||||
selectFieldName: selectFieldMetadataItem.name,
|
||||
recordIdsByGroupFamilyState: recordIndexRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: ({ recordId, position, groupValue }) => {
|
||||
updateOneRecord({
|
||||
idToUpdate: recordId,
|
||||
updateOneRecordInput: {
|
||||
[selectFieldMetadataItem.name]: groupValue,
|
||||
position,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
updateOneRecord,
|
||||
selectFieldMetadataItem,
|
||||
recordIndexRecordIdsByGroupFamilyState,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
processDragOperation,
|
||||
};
|
||||
};
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { draggedRecordIdsComponentState } from '@/object-record/record-drag/board/states/draggedRecordIdsComponentState';
|
||||
import { isMultiDragActiveComponentState } from '@/object-record/record-drag/board/states/isMultiDragActiveComponentState';
|
||||
import { originalSelectionComponentState } from '@/object-record/record-drag/board/states/originalSelectionComponentState';
|
||||
import { primaryDraggedRecordIdComponentState } from '@/object-record/record-drag/board/states/primaryDraggedRecordIdComponentState';
|
||||
import { useEndRecordDrag } from '@/object-record/record-drag/shared/hooks/useEndRecordDrag';
|
||||
import { draggedRecordIdsTableComponentState } from '@/object-record/record-drag/table/states/draggedRecordIdsTableComponentState';
|
||||
import { isMultiDragActiveTableComponentState } from '@/object-record/record-drag/table/states/isMultiDragActiveTableComponentState';
|
||||
import { originalSelectionTableComponentState } from '@/object-record/record-drag/table/states/originalSelectionTableComponentState';
|
||||
import { primaryDraggedRecordIdTableComponentState } from '@/object-record/record-drag/table/states/primaryDraggedRecordIdTableComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
describe('useEndRecordDrag', () => {
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('Board context', () => {
|
||||
it('should clear all board drag states', () => {
|
||||
const instanceId = 'test-instance';
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const isMultiDragActive = useRecoilComponentValue(
|
||||
isMultiDragActiveComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const draggedRecordIds = useRecoilComponentValue(
|
||||
draggedRecordIdsComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const primaryDraggedRecordId = useRecoilComponentValue(
|
||||
primaryDraggedRecordIdComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const originalSelection = useRecoilComponentValue(
|
||||
originalSelectionComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const setIsMultiDragActive = useSetRecoilState(
|
||||
isMultiDragActiveComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
const setDraggedRecordIds = useSetRecoilState(
|
||||
draggedRecordIdsComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
const setPrimaryDraggedRecordId = useSetRecoilState(
|
||||
primaryDraggedRecordIdComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
const setOriginalSelection = useSetRecoilState(
|
||||
originalSelectionComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
const { endDrag } = useEndRecordDrag('board', instanceId);
|
||||
|
||||
return {
|
||||
endDrag,
|
||||
isMultiDragActive,
|
||||
draggedRecordIds,
|
||||
primaryDraggedRecordId,
|
||||
originalSelection,
|
||||
setIsMultiDragActive,
|
||||
setDraggedRecordIds,
|
||||
setPrimaryDraggedRecordId,
|
||||
setOriginalSelection,
|
||||
};
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.setIsMultiDragActive(true);
|
||||
result.current.setDraggedRecordIds(['record-1', 'record-2']);
|
||||
result.current.setPrimaryDraggedRecordId('record-1');
|
||||
result.current.setOriginalSelection([
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
]);
|
||||
});
|
||||
|
||||
expect(result.current.isMultiDragActive).toBe(true);
|
||||
expect(result.current.draggedRecordIds).toEqual(['record-1', 'record-2']);
|
||||
expect(result.current.primaryDraggedRecordId).toBe('record-1');
|
||||
expect(result.current.originalSelection).toEqual([
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
]);
|
||||
|
||||
act(() => {
|
||||
result.current.endDrag();
|
||||
});
|
||||
|
||||
expect(result.current.isMultiDragActive).toBe(false);
|
||||
expect(result.current.draggedRecordIds).toEqual([]);
|
||||
expect(result.current.primaryDraggedRecordId).toBeNull();
|
||||
expect(result.current.originalSelection).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Table context', () => {
|
||||
it('should clear all table drag states', () => {
|
||||
const instanceId = 'test-instance';
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const isMultiDragActive = useRecoilComponentValue(
|
||||
isMultiDragActiveTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const draggedRecordIds = useRecoilComponentValue(
|
||||
draggedRecordIdsTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const primaryDraggedRecordId = useRecoilComponentValue(
|
||||
primaryDraggedRecordIdTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const originalSelection = useRecoilComponentValue(
|
||||
originalSelectionTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const setIsMultiDragActive = useSetRecoilState(
|
||||
isMultiDragActiveTableComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
const setDraggedRecordIds = useSetRecoilState(
|
||||
draggedRecordIdsTableComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
const setPrimaryDraggedRecordId = useSetRecoilState(
|
||||
primaryDraggedRecordIdTableComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
const setOriginalSelection = useSetRecoilState(
|
||||
originalSelectionTableComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
const { endDrag } = useEndRecordDrag('table', instanceId);
|
||||
|
||||
return {
|
||||
endDrag,
|
||||
isMultiDragActive,
|
||||
draggedRecordIds,
|
||||
primaryDraggedRecordId,
|
||||
originalSelection,
|
||||
setIsMultiDragActive,
|
||||
setDraggedRecordIds,
|
||||
setPrimaryDraggedRecordId,
|
||||
setOriginalSelection,
|
||||
};
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.setIsMultiDragActive(true);
|
||||
result.current.setDraggedRecordIds(['record-1', 'record-2']);
|
||||
result.current.setPrimaryDraggedRecordId('record-1');
|
||||
result.current.setOriginalSelection([
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
]);
|
||||
});
|
||||
|
||||
expect(result.current.isMultiDragActive).toBe(true);
|
||||
expect(result.current.draggedRecordIds).toEqual(['record-1', 'record-2']);
|
||||
expect(result.current.primaryDraggedRecordId).toBe('record-1');
|
||||
expect(result.current.originalSelection).toEqual([
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
]);
|
||||
|
||||
act(() => {
|
||||
result.current.endDrag();
|
||||
});
|
||||
|
||||
expect(result.current.isMultiDragActive).toBe(false);
|
||||
expect(result.current.draggedRecordIds).toEqual([]);
|
||||
expect(result.current.primaryDraggedRecordId).toBeNull();
|
||||
expect(result.current.originalSelection).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { type DragStart } from '@hello-pangea/dnd';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
|
||||
import { useMultiDragState } from '@/object-record/record-drag/shared/hooks/useMultiDragState';
|
||||
|
||||
const createDragStart = (draggableId: string, index: number): DragStart => ({
|
||||
draggableId,
|
||||
type: 'record',
|
||||
source: {
|
||||
droppableId: 'test-droppable',
|
||||
index,
|
||||
},
|
||||
mode: 'FLUID',
|
||||
});
|
||||
|
||||
describe('useMultiDragState', () => {
|
||||
it('should have initial state with no drag', () => {
|
||||
const { result } = renderHook(() => useMultiDragState());
|
||||
|
||||
expect(result.current.multiDragState.isDragging).toBe(false);
|
||||
expect(result.current.multiDragState.draggedRecordIds).toEqual([]);
|
||||
expect(result.current.multiDragState.primaryDraggedRecordId).toBeNull();
|
||||
expect(result.current.multiDragState.originalSelection).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle single drag when record is not in selection', () => {
|
||||
const { result } = renderHook(() => useMultiDragState());
|
||||
|
||||
const dragStart = createDragStart('record-1', 0);
|
||||
const selectedRecordIds = ['record-2', 'record-3'];
|
||||
|
||||
act(() => {
|
||||
result.current.startDrag(dragStart, selectedRecordIds);
|
||||
});
|
||||
|
||||
expect(result.current.multiDragState.isDragging).toBe(true);
|
||||
expect(result.current.multiDragState.draggedRecordIds).toEqual([
|
||||
'record-1',
|
||||
]);
|
||||
expect(result.current.multiDragState.primaryDraggedRecordId).toBe(
|
||||
'record-1',
|
||||
);
|
||||
expect(result.current.multiDragState.originalSelection).toEqual([
|
||||
'record-1',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle single drag when only one record is selected', () => {
|
||||
const { result } = renderHook(() => useMultiDragState());
|
||||
|
||||
const dragStart = createDragStart('record-1', 0);
|
||||
const selectedRecordIds = ['record-1'];
|
||||
|
||||
act(() => {
|
||||
result.current.startDrag(dragStart, selectedRecordIds);
|
||||
});
|
||||
|
||||
expect(result.current.multiDragState.isDragging).toBe(true);
|
||||
expect(result.current.multiDragState.draggedRecordIds).toEqual([
|
||||
'record-1',
|
||||
]);
|
||||
expect(result.current.multiDragState.primaryDraggedRecordId).toBe(
|
||||
'record-1',
|
||||
);
|
||||
expect(result.current.multiDragState.originalSelection).toEqual([
|
||||
'record-1',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle multi drag when multiple records are selected', () => {
|
||||
const { result } = renderHook(() => useMultiDragState());
|
||||
|
||||
const dragStart = createDragStart('record-2', 1);
|
||||
const selectedRecordIds = ['record-1', 'record-2', 'record-3'];
|
||||
|
||||
act(() => {
|
||||
result.current.startDrag(dragStart, selectedRecordIds);
|
||||
});
|
||||
|
||||
expect(result.current.multiDragState.isDragging).toBe(true);
|
||||
expect(result.current.multiDragState.draggedRecordIds).toEqual([
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
]);
|
||||
expect(result.current.multiDragState.primaryDraggedRecordId).toBe(
|
||||
'record-2',
|
||||
);
|
||||
expect(result.current.multiDragState.originalSelection).toEqual([
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should clear state when endDrag is called', () => {
|
||||
const { result } = renderHook(() => useMultiDragState());
|
||||
|
||||
const dragStart = createDragStart('record-1', 0);
|
||||
const selectedRecordIds = ['record-1', 'record-2'];
|
||||
|
||||
act(() => {
|
||||
result.current.startDrag(dragStart, selectedRecordIds);
|
||||
});
|
||||
|
||||
expect(result.current.multiDragState.isDragging).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.endDrag();
|
||||
});
|
||||
|
||||
expect(result.current.multiDragState.isDragging).toBe(false);
|
||||
expect(result.current.multiDragState.draggedRecordIds).toEqual([]);
|
||||
expect(result.current.multiDragState.primaryDraggedRecordId).toBeNull();
|
||||
expect(result.current.multiDragState.originalSelection).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle empty selection', () => {
|
||||
const { result } = renderHook(() => useMultiDragState());
|
||||
|
||||
const dragStart = createDragStart('record-1', 0);
|
||||
const selectedRecordIds: string[] = [];
|
||||
|
||||
act(() => {
|
||||
result.current.startDrag(dragStart, selectedRecordIds);
|
||||
});
|
||||
|
||||
expect(result.current.multiDragState.isDragging).toBe(true);
|
||||
expect(result.current.multiDragState.draggedRecordIds).toEqual([
|
||||
'record-1',
|
||||
]);
|
||||
expect(result.current.multiDragState.primaryDraggedRecordId).toBe(
|
||||
'record-1',
|
||||
);
|
||||
expect(result.current.multiDragState.originalSelection).toEqual([
|
||||
'record-1',
|
||||
]);
|
||||
});
|
||||
});
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { draggedRecordIdsComponentState } from '@/object-record/record-drag/board/states/draggedRecordIdsComponentState';
|
||||
import { isMultiDragActiveComponentState } from '@/object-record/record-drag/board/states/isMultiDragActiveComponentState';
|
||||
import { originalSelectionComponentState } from '@/object-record/record-drag/board/states/originalSelectionComponentState';
|
||||
import { primaryDraggedRecordIdComponentState } from '@/object-record/record-drag/board/states/primaryDraggedRecordIdComponentState';
|
||||
import { useRecordDragState } from '@/object-record/record-drag/shared/hooks/useRecordDragState';
|
||||
import { draggedRecordIdsTableComponentState } from '@/object-record/record-drag/table/states/draggedRecordIdsTableComponentState';
|
||||
import { isMultiDragActiveTableComponentState } from '@/object-record/record-drag/table/states/isMultiDragActiveTableComponentState';
|
||||
import { originalSelectionTableComponentState } from '@/object-record/record-drag/table/states/originalSelectionTableComponentState';
|
||||
import { primaryDraggedRecordIdTableComponentState } from '@/object-record/record-drag/table/states/primaryDraggedRecordIdTableComponentState';
|
||||
|
||||
describe('useRecordDragState', () => {
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('Board context', () => {
|
||||
it('should return current board drag state', () => {
|
||||
const instanceId = 'test-instance';
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const dragState = useRecordDragState('board', instanceId);
|
||||
|
||||
const setIsMultiDragActive = useSetRecoilState(
|
||||
isMultiDragActiveComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
const setDraggedRecordIds = useSetRecoilState(
|
||||
draggedRecordIdsComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
const setPrimaryDraggedRecordId = useSetRecoilState(
|
||||
primaryDraggedRecordIdComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
const setOriginalSelection = useSetRecoilState(
|
||||
originalSelectionComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
dragState,
|
||||
setIsMultiDragActive,
|
||||
setDraggedRecordIds,
|
||||
setPrimaryDraggedRecordId,
|
||||
setOriginalSelection,
|
||||
};
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.dragState.isDragging).toBe(false);
|
||||
expect(result.current.dragState.draggedRecordIds).toEqual([]);
|
||||
expect(result.current.dragState.primaryDraggedRecordId).toBeNull();
|
||||
expect(result.current.dragState.originalSelection).toEqual([]);
|
||||
|
||||
act(() => {
|
||||
result.current.setIsMultiDragActive(true);
|
||||
result.current.setDraggedRecordIds(['record-1', 'record-2']);
|
||||
result.current.setPrimaryDraggedRecordId('record-1');
|
||||
result.current.setOriginalSelection([
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
]);
|
||||
});
|
||||
|
||||
expect(result.current.dragState.isDragging).toBe(true);
|
||||
expect(result.current.dragState.draggedRecordIds).toEqual([
|
||||
'record-1',
|
||||
'record-2',
|
||||
]);
|
||||
expect(result.current.dragState.primaryDraggedRecordId).toBe('record-1');
|
||||
expect(result.current.dragState.originalSelection).toEqual([
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Table context', () => {
|
||||
it('should return current table drag state', () => {
|
||||
const instanceId = 'test-instance';
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const dragState = useRecordDragState('table', instanceId);
|
||||
|
||||
const setIsMultiDragActive = useSetRecoilState(
|
||||
isMultiDragActiveTableComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
const setDraggedRecordIds = useSetRecoilState(
|
||||
draggedRecordIdsTableComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
const setPrimaryDraggedRecordId = useSetRecoilState(
|
||||
primaryDraggedRecordIdTableComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
const setOriginalSelection = useSetRecoilState(
|
||||
originalSelectionTableComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
dragState,
|
||||
setIsMultiDragActive,
|
||||
setDraggedRecordIds,
|
||||
setPrimaryDraggedRecordId,
|
||||
setOriginalSelection,
|
||||
};
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.dragState.isDragging).toBe(false);
|
||||
expect(result.current.dragState.draggedRecordIds).toEqual([]);
|
||||
expect(result.current.dragState.primaryDraggedRecordId).toBeNull();
|
||||
expect(result.current.dragState.originalSelection).toEqual([]);
|
||||
|
||||
act(() => {
|
||||
result.current.setIsMultiDragActive(true);
|
||||
result.current.setDraggedRecordIds(['record-1', 'record-2']);
|
||||
result.current.setPrimaryDraggedRecordId('record-1');
|
||||
result.current.setOriginalSelection([
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
]);
|
||||
});
|
||||
|
||||
expect(result.current.dragState.isDragging).toBe(true);
|
||||
expect(result.current.dragState.draggedRecordIds).toEqual([
|
||||
'record-1',
|
||||
'record-2',
|
||||
]);
|
||||
expect(result.current.dragState.primaryDraggedRecordId).toBe('record-1');
|
||||
expect(result.current.dragState.originalSelection).toEqual([
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
import { type DragStart } from '@hello-pangea/dnd';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { draggedRecordIdsComponentState } from '@/object-record/record-drag/board/states/draggedRecordIdsComponentState';
|
||||
import { isMultiDragActiveComponentState } from '@/object-record/record-drag/board/states/isMultiDragActiveComponentState';
|
||||
import { originalSelectionComponentState } from '@/object-record/record-drag/board/states/originalSelectionComponentState';
|
||||
import { primaryDraggedRecordIdComponentState } from '@/object-record/record-drag/board/states/primaryDraggedRecordIdComponentState';
|
||||
import { useStartRecordDrag } from '@/object-record/record-drag/shared/hooks/useStartRecordDrag';
|
||||
import { draggedRecordIdsTableComponentState } from '@/object-record/record-drag/table/states/draggedRecordIdsTableComponentState';
|
||||
import { isMultiDragActiveTableComponentState } from '@/object-record/record-drag/table/states/isMultiDragActiveTableComponentState';
|
||||
import { originalSelectionTableComponentState } from '@/object-record/record-drag/table/states/originalSelectionTableComponentState';
|
||||
import { primaryDraggedRecordIdTableComponentState } from '@/object-record/record-drag/table/states/primaryDraggedRecordIdTableComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
const createDragStart = (draggableId: string, index: number): DragStart => ({
|
||||
draggableId,
|
||||
type: 'record',
|
||||
source: {
|
||||
droppableId: 'test-droppable',
|
||||
index,
|
||||
},
|
||||
mode: 'FLUID',
|
||||
});
|
||||
|
||||
describe('useStartRecordDrag', () => {
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('Board context', () => {
|
||||
it('should set single drag state when dragged record is not in selection', () => {
|
||||
const instanceId = 'test-instance';
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const isMultiDragActive = useRecoilComponentValue(
|
||||
isMultiDragActiveComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const draggedRecordIds = useRecoilComponentValue(
|
||||
draggedRecordIdsComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const primaryDraggedRecordId = useRecoilComponentValue(
|
||||
primaryDraggedRecordIdComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const originalSelection = useRecoilComponentValue(
|
||||
originalSelectionComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const { startDrag } = useStartRecordDrag('board', instanceId);
|
||||
|
||||
return {
|
||||
startDrag,
|
||||
isMultiDragActive,
|
||||
draggedRecordIds,
|
||||
primaryDraggedRecordId,
|
||||
originalSelection,
|
||||
};
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
const dragStart = createDragStart('record-1', 0);
|
||||
const selectedRecordIds = ['record-2', 'record-3'];
|
||||
|
||||
act(() => {
|
||||
result.current.startDrag(dragStart, selectedRecordIds);
|
||||
});
|
||||
|
||||
expect(result.current.isMultiDragActive).toBe(true);
|
||||
expect(result.current.draggedRecordIds).toEqual(['record-1']);
|
||||
expect(result.current.primaryDraggedRecordId).toBe('record-1');
|
||||
expect(result.current.originalSelection).toEqual(['record-1']);
|
||||
});
|
||||
|
||||
it('should set single drag state when only one record is selected', () => {
|
||||
const instanceId = 'test-instance';
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const isMultiDragActive = useRecoilComponentValue(
|
||||
isMultiDragActiveComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const draggedRecordIds = useRecoilComponentValue(
|
||||
draggedRecordIdsComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const primaryDraggedRecordId = useRecoilComponentValue(
|
||||
primaryDraggedRecordIdComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const originalSelection = useRecoilComponentValue(
|
||||
originalSelectionComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const { startDrag } = useStartRecordDrag('board', instanceId);
|
||||
|
||||
return {
|
||||
startDrag,
|
||||
isMultiDragActive,
|
||||
draggedRecordIds,
|
||||
primaryDraggedRecordId,
|
||||
originalSelection,
|
||||
};
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
const dragStart = createDragStart('record-1', 0);
|
||||
const selectedRecordIds = ['record-1'];
|
||||
|
||||
act(() => {
|
||||
result.current.startDrag(dragStart, selectedRecordIds);
|
||||
});
|
||||
|
||||
expect(result.current.isMultiDragActive).toBe(true);
|
||||
expect(result.current.draggedRecordIds).toEqual(['record-1']);
|
||||
expect(result.current.primaryDraggedRecordId).toBe('record-1');
|
||||
expect(result.current.originalSelection).toEqual(['record-1']);
|
||||
});
|
||||
|
||||
it('should set multi drag state when multiple records are selected', () => {
|
||||
const instanceId = 'test-instance';
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const isMultiDragActive = useRecoilComponentValue(
|
||||
isMultiDragActiveComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const draggedRecordIds = useRecoilComponentValue(
|
||||
draggedRecordIdsComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const primaryDraggedRecordId = useRecoilComponentValue(
|
||||
primaryDraggedRecordIdComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const originalSelection = useRecoilComponentValue(
|
||||
originalSelectionComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const { startDrag } = useStartRecordDrag('board', instanceId);
|
||||
|
||||
return {
|
||||
startDrag,
|
||||
isMultiDragActive,
|
||||
draggedRecordIds,
|
||||
primaryDraggedRecordId,
|
||||
originalSelection,
|
||||
};
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
const dragStart = createDragStart('record-2', 1);
|
||||
const selectedRecordIds = ['record-1', 'record-2', 'record-3'];
|
||||
|
||||
act(() => {
|
||||
result.current.startDrag(dragStart, selectedRecordIds);
|
||||
});
|
||||
|
||||
expect(result.current.isMultiDragActive).toBe(true);
|
||||
expect(result.current.draggedRecordIds).toEqual([
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
]);
|
||||
expect(result.current.primaryDraggedRecordId).toBe('record-2');
|
||||
expect(result.current.originalSelection).toEqual([
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty selection', () => {
|
||||
const instanceId = 'test-instance';
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const isMultiDragActive = useRecoilComponentValue(
|
||||
isMultiDragActiveComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const draggedRecordIds = useRecoilComponentValue(
|
||||
draggedRecordIdsComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const primaryDraggedRecordId = useRecoilComponentValue(
|
||||
primaryDraggedRecordIdComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const originalSelection = useRecoilComponentValue(
|
||||
originalSelectionComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const { startDrag } = useStartRecordDrag('board', instanceId);
|
||||
|
||||
return {
|
||||
startDrag,
|
||||
isMultiDragActive,
|
||||
draggedRecordIds,
|
||||
primaryDraggedRecordId,
|
||||
originalSelection,
|
||||
};
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
const dragStart = createDragStart('record-1', 0);
|
||||
const selectedRecordIds: string[] = [];
|
||||
|
||||
act(() => {
|
||||
result.current.startDrag(dragStart, selectedRecordIds);
|
||||
});
|
||||
|
||||
expect(result.current.isMultiDragActive).toBe(true);
|
||||
expect(result.current.draggedRecordIds).toEqual(['record-1']);
|
||||
expect(result.current.primaryDraggedRecordId).toBe('record-1');
|
||||
expect(result.current.originalSelection).toEqual(['record-1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Table context', () => {
|
||||
it('should set single drag state when dragged record is not in selection', () => {
|
||||
const instanceId = 'test-instance';
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const isMultiDragActive = useRecoilComponentValue(
|
||||
isMultiDragActiveTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const draggedRecordIds = useRecoilComponentValue(
|
||||
draggedRecordIdsTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const primaryDraggedRecordId = useRecoilComponentValue(
|
||||
primaryDraggedRecordIdTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const originalSelection = useRecoilComponentValue(
|
||||
originalSelectionTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const { startDrag } = useStartRecordDrag('table', instanceId);
|
||||
|
||||
return {
|
||||
startDrag,
|
||||
isMultiDragActive,
|
||||
draggedRecordIds,
|
||||
primaryDraggedRecordId,
|
||||
originalSelection,
|
||||
};
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
const dragStart = createDragStart('record-1', 0);
|
||||
const selectedRecordIds = ['record-2', 'record-3'];
|
||||
|
||||
act(() => {
|
||||
result.current.startDrag(dragStart, selectedRecordIds);
|
||||
});
|
||||
|
||||
expect(result.current.isMultiDragActive).toBe(true);
|
||||
expect(result.current.draggedRecordIds).toEqual(['record-1']);
|
||||
expect(result.current.primaryDraggedRecordId).toBe('record-1');
|
||||
expect(result.current.originalSelection).toEqual(['record-1']);
|
||||
});
|
||||
|
||||
it('should set multi drag state when multiple records are selected', () => {
|
||||
const instanceId = 'test-instance';
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const isMultiDragActive = useRecoilComponentValue(
|
||||
isMultiDragActiveTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const draggedRecordIds = useRecoilComponentValue(
|
||||
draggedRecordIdsTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const primaryDraggedRecordId = useRecoilComponentValue(
|
||||
primaryDraggedRecordIdTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
const originalSelection = useRecoilComponentValue(
|
||||
originalSelectionTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const { startDrag } = useStartRecordDrag('table', instanceId);
|
||||
|
||||
return {
|
||||
startDrag,
|
||||
isMultiDragActive,
|
||||
draggedRecordIds,
|
||||
primaryDraggedRecordId,
|
||||
originalSelection,
|
||||
};
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
const dragStart = createDragStart('record-2', 1);
|
||||
const selectedRecordIds = ['record-1', 'record-2', 'record-3'];
|
||||
|
||||
act(() => {
|
||||
result.current.startDrag(dragStart, selectedRecordIds);
|
||||
});
|
||||
|
||||
expect(result.current.isMultiDragActive).toBe(true);
|
||||
expect(result.current.draggedRecordIds).toEqual([
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
]);
|
||||
expect(result.current.primaryDraggedRecordId).toBe('record-2');
|
||||
expect(result.current.originalSelection).toEqual([
|
||||
'record-1',
|
||||
'record-2',
|
||||
'record-3',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { type RecordDragContext } from '@/object-record/record-drag/shared/types/RecordDragContext';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
|
||||
import { draggedRecordIdsComponentState } from '@/object-record/record-drag/board/states/draggedRecordIdsComponentState';
|
||||
import { isMultiDragActiveComponentState } from '@/object-record/record-drag/board/states/isMultiDragActiveComponentState';
|
||||
import { originalSelectionComponentState } from '@/object-record/record-drag/board/states/originalSelectionComponentState';
|
||||
import { primaryDraggedRecordIdComponentState } from '@/object-record/record-drag/board/states/primaryDraggedRecordIdComponentState';
|
||||
|
||||
import { draggedRecordIdsTableComponentState } from '@/object-record/record-drag/table/states/draggedRecordIdsTableComponentState';
|
||||
import { isMultiDragActiveTableComponentState } from '@/object-record/record-drag/table/states/isMultiDragActiveTableComponentState';
|
||||
import { originalSelectionTableComponentState } from '@/object-record/record-drag/table/states/originalSelectionTableComponentState';
|
||||
import { primaryDraggedRecordIdTableComponentState } from '@/object-record/record-drag/table/states/primaryDraggedRecordIdTableComponentState';
|
||||
|
||||
export const useEndRecordDrag = (
|
||||
context: RecordDragContext,
|
||||
instanceId?: string,
|
||||
) => {
|
||||
const isMultiDragActiveCallbackState = useRecoilComponentCallbackState(
|
||||
context === 'board'
|
||||
? isMultiDragActiveComponentState
|
||||
: isMultiDragActiveTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const draggedRecordIdsCallbackState = useRecoilComponentCallbackState(
|
||||
context === 'board'
|
||||
? draggedRecordIdsComponentState
|
||||
: draggedRecordIdsTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const primaryDraggedRecordIdCallbackState = useRecoilComponentCallbackState(
|
||||
context === 'board'
|
||||
? primaryDraggedRecordIdComponentState
|
||||
: primaryDraggedRecordIdTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const originalSelectionCallbackState = useRecoilComponentCallbackState(
|
||||
context === 'board'
|
||||
? originalSelectionComponentState
|
||||
: originalSelectionTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const endDrag = useRecoilCallback(
|
||||
({ set }) =>
|
||||
() => {
|
||||
set(isMultiDragActiveCallbackState, false);
|
||||
set(draggedRecordIdsCallbackState, []);
|
||||
set(primaryDraggedRecordIdCallbackState, null);
|
||||
set(originalSelectionCallbackState, []);
|
||||
},
|
||||
[
|
||||
isMultiDragActiveCallbackState,
|
||||
draggedRecordIdsCallbackState,
|
||||
primaryDraggedRecordIdCallbackState,
|
||||
originalSelectionCallbackState,
|
||||
],
|
||||
);
|
||||
|
||||
return { endDrag };
|
||||
};
|
||||
+2
-8
@@ -1,13 +1,7 @@
|
||||
import { type MultiDragState } from '@/object-record/record-drag/shared/types/MultiDragState';
|
||||
import { getDragOperationType } from '@/object-record/record-drag/shared/utils/getDragOperationType';
|
||||
import { type DragStart } from '@hello-pangea/dnd';
|
||||
import { useState } from 'react';
|
||||
import { getDragOperationType } from '../utils/getDragOperationType';
|
||||
|
||||
export interface MultiDragState {
|
||||
isDragging: boolean;
|
||||
draggedRecordIds: string[];
|
||||
primaryDraggedRecordId: string | null;
|
||||
originalSelection: string[];
|
||||
}
|
||||
|
||||
export const useMultiDragState = () => {
|
||||
const [multiDragState, setMultiDragState] = useState<MultiDragState>({
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { type RecordDragContext } from '@/object-record/record-drag/shared/types/RecordDragContext';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
import { draggedRecordIdsComponentState } from '@/object-record/record-drag/board/states/draggedRecordIdsComponentState';
|
||||
import { isMultiDragActiveComponentState } from '@/object-record/record-drag/board/states/isMultiDragActiveComponentState';
|
||||
import { originalSelectionComponentState } from '@/object-record/record-drag/board/states/originalSelectionComponentState';
|
||||
import { primaryDraggedRecordIdComponentState } from '@/object-record/record-drag/board/states/primaryDraggedRecordIdComponentState';
|
||||
|
||||
import { draggedRecordIdsTableComponentState } from '@/object-record/record-drag/table/states/draggedRecordIdsTableComponentState';
|
||||
import { isMultiDragActiveTableComponentState } from '@/object-record/record-drag/table/states/isMultiDragActiveTableComponentState';
|
||||
import { originalSelectionTableComponentState } from '@/object-record/record-drag/table/states/originalSelectionTableComponentState';
|
||||
import { primaryDraggedRecordIdTableComponentState } from '@/object-record/record-drag/table/states/primaryDraggedRecordIdTableComponentState';
|
||||
|
||||
export const useRecordDragState = (
|
||||
context: RecordDragContext,
|
||||
instanceId?: string,
|
||||
) => {
|
||||
const isDragging = useRecoilComponentValue(
|
||||
context === 'board'
|
||||
? isMultiDragActiveComponentState
|
||||
: isMultiDragActiveTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const draggedRecordIds = useRecoilComponentValue(
|
||||
context === 'board'
|
||||
? draggedRecordIdsComponentState
|
||||
: draggedRecordIdsTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const primaryDraggedRecordId = useRecoilComponentValue(
|
||||
context === 'board'
|
||||
? primaryDraggedRecordIdComponentState
|
||||
: primaryDraggedRecordIdTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const originalSelection = useRecoilComponentValue(
|
||||
context === 'board'
|
||||
? originalSelectionComponentState
|
||||
: originalSelectionTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
return {
|
||||
isDragging,
|
||||
draggedRecordIds,
|
||||
primaryDraggedRecordId,
|
||||
originalSelection,
|
||||
};
|
||||
};
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { type DragStart } from '@hello-pangea/dnd';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { type RecordDragContext } from '@/object-record/record-drag/shared/types/RecordDragContext';
|
||||
import { getDragOperationType } from '@/object-record/record-drag/shared/utils/getDragOperationType';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
|
||||
import { draggedRecordIdsComponentState } from '@/object-record/record-drag/board/states/draggedRecordIdsComponentState';
|
||||
import { isMultiDragActiveComponentState } from '@/object-record/record-drag/board/states/isMultiDragActiveComponentState';
|
||||
import { originalSelectionComponentState } from '@/object-record/record-drag/board/states/originalSelectionComponentState';
|
||||
import { primaryDraggedRecordIdComponentState } from '@/object-record/record-drag/board/states/primaryDraggedRecordIdComponentState';
|
||||
|
||||
import { draggedRecordIdsTableComponentState } from '@/object-record/record-drag/table/states/draggedRecordIdsTableComponentState';
|
||||
import { isMultiDragActiveTableComponentState } from '@/object-record/record-drag/table/states/isMultiDragActiveTableComponentState';
|
||||
import { originalSelectionTableComponentState } from '@/object-record/record-drag/table/states/originalSelectionTableComponentState';
|
||||
import { primaryDraggedRecordIdTableComponentState } from '@/object-record/record-drag/table/states/primaryDraggedRecordIdTableComponentState';
|
||||
|
||||
export const useStartRecordDrag = (
|
||||
context: RecordDragContext,
|
||||
instanceId?: string,
|
||||
) => {
|
||||
const isMultiDragActiveCallbackState = useRecoilComponentCallbackState(
|
||||
context === 'board'
|
||||
? isMultiDragActiveComponentState
|
||||
: isMultiDragActiveTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const draggedRecordIdsCallbackState = useRecoilComponentCallbackState(
|
||||
context === 'board'
|
||||
? draggedRecordIdsComponentState
|
||||
: draggedRecordIdsTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const primaryDraggedRecordIdCallbackState = useRecoilComponentCallbackState(
|
||||
context === 'board'
|
||||
? primaryDraggedRecordIdComponentState
|
||||
: primaryDraggedRecordIdTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const originalSelectionCallbackState = useRecoilComponentCallbackState(
|
||||
context === 'board'
|
||||
? originalSelectionComponentState
|
||||
: originalSelectionTableComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const startDrag = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(start: DragStart, selectedRecordIds: string[]) => {
|
||||
const draggedRecordId = start.draggableId;
|
||||
|
||||
const dragOperationType = getDragOperationType({
|
||||
draggedRecordId,
|
||||
selectedRecordIds,
|
||||
});
|
||||
|
||||
if (dragOperationType === 'multi') {
|
||||
set(isMultiDragActiveCallbackState, true);
|
||||
set(draggedRecordIdsCallbackState, selectedRecordIds);
|
||||
set(primaryDraggedRecordIdCallbackState, draggedRecordId);
|
||||
set(originalSelectionCallbackState, selectedRecordIds);
|
||||
} else {
|
||||
set(isMultiDragActiveCallbackState, true);
|
||||
set(draggedRecordIdsCallbackState, [draggedRecordId]);
|
||||
set(primaryDraggedRecordIdCallbackState, draggedRecordId);
|
||||
set(originalSelectionCallbackState, [draggedRecordId]);
|
||||
}
|
||||
},
|
||||
[
|
||||
isMultiDragActiveCallbackState,
|
||||
draggedRecordIdsCallbackState,
|
||||
primaryDraggedRecordIdCallbackState,
|
||||
originalSelectionCallbackState,
|
||||
],
|
||||
);
|
||||
|
||||
return { startDrag };
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { type RecordDragUpdate } from '@/object-record/record-drag/shared/types/RecordDragUpdate';
|
||||
|
||||
export type MultiDragResult = {
|
||||
recordUpdates: RecordDragUpdate[];
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export type MultiDragState = {
|
||||
isDragging: boolean;
|
||||
draggedRecordIds: string[];
|
||||
primaryDraggedRecordId: string | null;
|
||||
originalSelection: string[];
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export type RecordDragContext = 'board' | 'table';
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export type RecordDragPositionData = {
|
||||
recordId: string;
|
||||
position?: number;
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export type RecordDragUpdate = {
|
||||
recordId: string;
|
||||
position: number;
|
||||
groupValue?: string | null;
|
||||
selectFieldName?: string;
|
||||
};
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
import { type RecordDragPositionData } from '@/object-record/record-drag/shared/types/RecordDragPositionData';
|
||||
import { calculateDragPositions } from '@/object-record/record-drag/shared/utils/calculateDragPositions';
|
||||
|
||||
describe('calculateDragPositions', () => {
|
||||
const mockRecordPositionData: RecordDragPositionData[] = [
|
||||
{ recordId: 'record-1', position: 1 },
|
||||
{ recordId: 'record-2', position: 2 },
|
||||
{ recordId: 'record-3', position: 3 },
|
||||
{ recordId: 'record-4', position: 4 },
|
||||
{ recordId: 'record-5', position: 5 },
|
||||
];
|
||||
|
||||
describe('Single record drag', () => {
|
||||
it('should calculate position when moving record forward', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3', 'record-4', 'record-5'],
|
||||
recordsToMove: ['record-2'],
|
||||
destinationIndex: 3,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(result['record-2']).toBeGreaterThan(3);
|
||||
expect(result['record-2']).toBeLessThan(5);
|
||||
});
|
||||
|
||||
it('should calculate position when moving record backward', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3', 'record-4'],
|
||||
recordsToMove: ['record-4'],
|
||||
destinationIndex: 1,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(result['record-4']).toBeGreaterThan(1);
|
||||
expect(result['record-4']).toBeLessThan(2);
|
||||
});
|
||||
|
||||
it('should handle moving to the beginning', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
recordsToMove: ['record-3'],
|
||||
destinationIndex: 0,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(result['record-3']).toBeLessThan(1);
|
||||
});
|
||||
|
||||
it('should handle moving to the end', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
recordsToMove: ['record-1'],
|
||||
destinationIndex: 2,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(result['record-1']).toBeGreaterThan(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multi record drag', () => {
|
||||
it('should calculate positions for multiple records with proper spacing', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3', 'record-4', 'record-5'],
|
||||
recordsToMove: ['record-1', 'record-3'],
|
||||
destinationIndex: 3,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(result['record-1']).toBeGreaterThan(4);
|
||||
expect(result['record-1']).toBeLessThan(5);
|
||||
expect(result['record-3']).toBeGreaterThan(result['record-1']);
|
||||
expect(result['record-3']).toBeLessThan(5);
|
||||
});
|
||||
|
||||
it('should maintain order of dragged records', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3', 'record-4', 'record-5'],
|
||||
recordsToMove: ['record-2', 'record-3', 'record-4'],
|
||||
destinationIndex: 0,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(result['record-2']).toBeLessThan(result['record-3']);
|
||||
expect(result['record-3']).toBeLessThan(result['record-4']);
|
||||
expect(result['record-4']).toBeLessThan(1);
|
||||
});
|
||||
|
||||
it('should handle moving multiple records to the end', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3', 'record-4'],
|
||||
recordsToMove: ['record-1', 'record-2'],
|
||||
destinationIndex: 2,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
expect(typeof result['record-1']).toBe('number');
|
||||
expect(result['record-2']).toBeGreaterThan(result['record-1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle undefined positions', () => {
|
||||
const dataWithUndefined: RecordDragPositionData[] = [
|
||||
{ recordId: 'record-1', position: undefined },
|
||||
{ recordId: 'record-2', position: 2 },
|
||||
{ recordId: 'record-3', position: undefined },
|
||||
];
|
||||
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
recordsToMove: ['record-1'],
|
||||
destinationIndex: 1,
|
||||
recordPositionData: dataWithUndefined,
|
||||
});
|
||||
|
||||
expect(typeof result['record-1']).toBe('number');
|
||||
expect(isNaN(result['record-1'])).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle empty record arrays', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: [],
|
||||
recordsToMove: ['record-1'],
|
||||
destinationIndex: 0,
|
||||
recordPositionData: [],
|
||||
});
|
||||
|
||||
expect(typeof result['record-1']).toBe('number');
|
||||
});
|
||||
|
||||
it('should handle negative positions', () => {
|
||||
const negativeData: RecordDragPositionData[] = [
|
||||
{ recordId: 'record-1', position: -5 },
|
||||
{ recordId: 'record-2', position: -3 },
|
||||
{ recordId: 'record-3', position: -1 },
|
||||
];
|
||||
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
recordsToMove: ['record-1'],
|
||||
destinationIndex: 2,
|
||||
recordPositionData: negativeData,
|
||||
});
|
||||
|
||||
expect(result['record-1']).toBeGreaterThan(-1);
|
||||
});
|
||||
|
||||
it('should handle very small gaps between positions', () => {
|
||||
const tightData: RecordDragPositionData[] = [
|
||||
{ recordId: 'record-1', position: 1 },
|
||||
{ recordId: 'record-2', position: 1.0001 },
|
||||
{ recordId: 'record-3', position: 1.0002 },
|
||||
];
|
||||
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
recordsToMove: ['record-3'],
|
||||
destinationIndex: 0,
|
||||
recordPositionData: tightData,
|
||||
});
|
||||
|
||||
expect(result['record-3']).toBeLessThan(1);
|
||||
});
|
||||
|
||||
it('should handle single record in list', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1'],
|
||||
recordsToMove: ['record-1'],
|
||||
destinationIndex: 0,
|
||||
recordPositionData: [{ recordId: 'record-1', position: 1 }],
|
||||
});
|
||||
|
||||
expect(typeof result['record-1']).toBe('number');
|
||||
});
|
||||
|
||||
it('should preserve relative order when moving multiple non-consecutive records', () => {
|
||||
const result = calculateDragPositions({
|
||||
recordIds: ['record-1', 'record-2', 'record-3', 'record-4', 'record-5'],
|
||||
recordsToMove: ['record-1', 'record-3', 'record-5'],
|
||||
destinationIndex: 1,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
});
|
||||
|
||||
const positions = ['record-1', 'record-3', 'record-5'].map(
|
||||
(id) => result[id],
|
||||
);
|
||||
|
||||
expect(positions[0]).toBeLessThan(positions[1]);
|
||||
expect(positions[1]).toBeLessThan(positions[2]);
|
||||
expect(typeof positions[2]).toBe('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import { type Snapshot } from 'recoil';
|
||||
|
||||
import { extractRecordPositions } from '@/object-record/record-drag/shared/utils/extractRecordPositions';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
describe('extractRecordPositions', () => {
|
||||
const createMockSnapshot = (recordData: Record<string, any>): Snapshot => {
|
||||
return {
|
||||
getLoadable: (atom: any) => {
|
||||
const match = atom.key.match(/recordStoreFamilyState__"([^"]+)"/);
|
||||
const recordId = match ? match[1] : atom.key;
|
||||
if (isDefined(recordData[recordId])) {
|
||||
return {
|
||||
getValue: () => recordData[recordId],
|
||||
state: 'hasValue',
|
||||
};
|
||||
}
|
||||
return {
|
||||
getValue: () => undefined,
|
||||
state: 'hasValue',
|
||||
};
|
||||
},
|
||||
} as unknown as Snapshot;
|
||||
};
|
||||
|
||||
it('should extract position data from records in snapshot', () => {
|
||||
const mockRecordData = {
|
||||
'record-1': { id: 'record-1', position: 1, name: 'Record 1' },
|
||||
'record-2': { id: 'record-2', position: 2, name: 'Record 2' },
|
||||
'record-3': { id: 'record-3', position: 3, name: 'Record 3' },
|
||||
};
|
||||
|
||||
const snapshot = createMockSnapshot(mockRecordData);
|
||||
const allRecordIds = ['record-1', 'record-2', 'record-3'];
|
||||
|
||||
const result = extractRecordPositions(allRecordIds, snapshot);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ recordId: 'record-1', position: 1 },
|
||||
{ recordId: 'record-2', position: 2 },
|
||||
{ recordId: 'record-3', position: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle records with undefined positions', () => {
|
||||
const mockRecordData = {
|
||||
'record-1': { id: 'record-1', position: 1 },
|
||||
'record-2': { id: 'record-2' },
|
||||
'record-3': { id: 'record-3', position: undefined },
|
||||
};
|
||||
|
||||
const snapshot = createMockSnapshot(mockRecordData);
|
||||
const allRecordIds = ['record-1', 'record-2', 'record-3'];
|
||||
|
||||
const result = extractRecordPositions(allRecordIds, snapshot);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ recordId: 'record-1', position: 1 },
|
||||
{ recordId: 'record-2', position: undefined },
|
||||
{ recordId: 'record-3', position: undefined },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle non-existent records', () => {
|
||||
const mockRecordData = {
|
||||
'record-1': { id: 'record-1', position: 1 },
|
||||
};
|
||||
|
||||
const snapshot = createMockSnapshot(mockRecordData);
|
||||
const allRecordIds = ['record-1', 'record-2', 'non-existent'];
|
||||
|
||||
const result = extractRecordPositions(allRecordIds, snapshot);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ recordId: 'record-1', position: 1 },
|
||||
{ recordId: 'record-2', position: undefined },
|
||||
{ recordId: 'non-existent', position: undefined },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty record list', () => {
|
||||
const snapshot = createMockSnapshot({});
|
||||
const allRecordIds: string[] = [];
|
||||
|
||||
const result = extractRecordPositions(allRecordIds, snapshot);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should preserve order of input record IDs', () => {
|
||||
const mockRecordData = {
|
||||
'record-1': { id: 'record-1', position: 100 },
|
||||
'record-2': { id: 'record-2', position: 50 },
|
||||
'record-3': { id: 'record-3', position: 75 },
|
||||
};
|
||||
|
||||
const snapshot = createMockSnapshot(mockRecordData);
|
||||
const allRecordIds = ['record-3', 'record-1', 'record-2'];
|
||||
|
||||
const result = extractRecordPositions(allRecordIds, snapshot);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ recordId: 'record-3', position: 75 },
|
||||
{ recordId: 'record-1', position: 100 },
|
||||
{ recordId: 'record-2', position: 50 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle null positions', () => {
|
||||
const mockRecordData = {
|
||||
'record-1': { id: 'record-1', position: null },
|
||||
'record-2': { id: 'record-2', position: 2 },
|
||||
};
|
||||
|
||||
const snapshot = createMockSnapshot(mockRecordData);
|
||||
const allRecordIds = ['record-1', 'record-2'];
|
||||
|
||||
const result = extractRecordPositions(allRecordIds, snapshot);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ recordId: 'record-1', position: null },
|
||||
{ recordId: 'record-2', position: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle negative positions', () => {
|
||||
const mockRecordData = {
|
||||
'record-1': { id: 'record-1', position: -5 },
|
||||
'record-2': { id: 'record-2', position: -10 },
|
||||
};
|
||||
|
||||
const snapshot = createMockSnapshot(mockRecordData);
|
||||
const allRecordIds = ['record-1', 'record-2'];
|
||||
|
||||
const result = extractRecordPositions(allRecordIds, snapshot);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ recordId: 'record-1', position: -5 },
|
||||
{ recordId: 'record-2', position: -10 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle decimal positions', () => {
|
||||
const mockRecordData = {
|
||||
'record-1': { id: 'record-1', position: 1.5 },
|
||||
'record-2': { id: 'record-2', position: 2.75 },
|
||||
};
|
||||
|
||||
const snapshot = createMockSnapshot(mockRecordData);
|
||||
const allRecordIds = ['record-1', 'record-2'];
|
||||
|
||||
const result = extractRecordPositions(allRecordIds, snapshot);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ recordId: 'record-1', position: 1.5 },
|
||||
{ recordId: 'record-2', position: 2.75 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { getDragOperationType } from '../getDragOperationType';
|
||||
import { getDragOperationType } from '@/object-record/record-drag/shared/utils/getDragOperationType';
|
||||
|
||||
describe('getDragOperationType', () => {
|
||||
it('should return single when dragged record is not selected', () => {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { getDraggedRecordPosition } from '../getDraggedRecordPosition';
|
||||
import { getDraggedRecordPosition } from '@/object-record/record-drag/shared/utils/getDraggedRecordPosition';
|
||||
|
||||
describe('getDraggedRecordPosition', () => {
|
||||
it('when both records defined and positive, should return the average of the two positions', () => {
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { type Snapshot } from 'recoil';
|
||||
|
||||
import { processGroupDragOperation } from '../processGroupDragOperation';
|
||||
|
||||
jest.mock('../getDragOperationType');
|
||||
jest.mock('../extractRecordPositions');
|
||||
jest.mock('../processSingleDrag');
|
||||
jest.mock('../processMultiDrag');
|
||||
jest.mock('@/ui/utilities/state/utils/getSnapshotValue');
|
||||
|
||||
import { getDragOperationType } from '../getDragOperationType';
|
||||
import { extractRecordPositions } from '../extractRecordPositions';
|
||||
import { processSingleDrag } from '../processSingleDrag';
|
||||
import { processMultiDrag } from '../processMultiDrag';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
|
||||
const mockGetDragOperationType = getDragOperationType as jest.Mock;
|
||||
const mockExtractRecordPositions = extractRecordPositions as jest.Mock;
|
||||
const mockProcessSingleDrag = processSingleDrag as jest.Mock;
|
||||
const mockProcessMultiDrag = processMultiDrag as jest.Mock;
|
||||
const mockGetSnapshotValue = getSnapshotValue as jest.Mock;
|
||||
|
||||
describe('processGroupDragOperation', () => {
|
||||
const mockSnapshot = {} as Snapshot;
|
||||
const mockOnUpdateRecord = jest.fn();
|
||||
const mockRecordIdsByGroupFamilyState = jest.fn();
|
||||
|
||||
const createDropResult = (destination?: {
|
||||
droppableId: string;
|
||||
index: number;
|
||||
}): DropResult => ({
|
||||
draggableId: 'record-1',
|
||||
type: 'DEFAULT',
|
||||
source: {
|
||||
droppableId: 'group-1',
|
||||
index: 0,
|
||||
},
|
||||
destination: destination ?? null,
|
||||
reason: 'DROP',
|
||||
mode: 'FLUID',
|
||||
combine: null,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Early returns', () => {
|
||||
it('should return early when destination is not defined', () => {
|
||||
const result = createDropResult();
|
||||
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot: mockSnapshot,
|
||||
selectedRecordIds: ['record-1'],
|
||||
selectFieldName: 'status',
|
||||
recordIdsByGroupFamilyState: mockRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: mockOnUpdateRecord,
|
||||
});
|
||||
|
||||
expect(mockGetSnapshotValue).not.toHaveBeenCalled();
|
||||
expect(mockOnUpdateRecord).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when record group is not defined', () => {
|
||||
const result = createDropResult({ droppableId: 'group-2', index: 1 });
|
||||
|
||||
mockGetSnapshotValue.mockReturnValueOnce(undefined);
|
||||
|
||||
expect(() => {
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot: mockSnapshot,
|
||||
selectedRecordIds: ['record-1'],
|
||||
selectFieldName: 'status',
|
||||
recordIdsByGroupFamilyState: mockRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: mockOnUpdateRecord,
|
||||
});
|
||||
}).toThrow('Record group is not defined');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Single drag operation', () => {
|
||||
it('should process single drag operation successfully', () => {
|
||||
const result = createDropResult({ droppableId: 'group-2', index: 1 });
|
||||
const mockRecordGroup = {
|
||||
value: 'in-progress',
|
||||
fieldMetadataId: 'field-1',
|
||||
};
|
||||
const mockDestinationRecordIds = ['record-2', 'record-3'];
|
||||
const mockRecordPositionData = [
|
||||
{ recordId: 'record-2', position: 1 },
|
||||
{ recordId: 'record-3', position: 2 },
|
||||
];
|
||||
|
||||
mockGetSnapshotValue
|
||||
.mockReturnValueOnce(mockRecordGroup)
|
||||
.mockReturnValueOnce(mockDestinationRecordIds);
|
||||
|
||||
mockExtractRecordPositions.mockReturnValue(mockRecordPositionData);
|
||||
mockGetDragOperationType.mockReturnValue('single');
|
||||
mockProcessSingleDrag.mockReturnValue({
|
||||
recordId: 'record-1',
|
||||
position: 1.5,
|
||||
groupValue: 'in-progress',
|
||||
});
|
||||
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot: mockSnapshot,
|
||||
selectedRecordIds: ['record-1'],
|
||||
selectFieldName: 'status',
|
||||
recordIdsByGroupFamilyState: mockRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: mockOnUpdateRecord,
|
||||
});
|
||||
|
||||
expect(mockGetDragOperationType).toHaveBeenCalledWith({
|
||||
draggedRecordId: 'record-1',
|
||||
selectedRecordIds: ['record-1'],
|
||||
});
|
||||
|
||||
expect(mockProcessSingleDrag).toHaveBeenCalledWith({
|
||||
result,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: mockDestinationRecordIds,
|
||||
groupValue: 'in-progress',
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
|
||||
expect(mockOnUpdateRecord).toHaveBeenCalledWith({
|
||||
recordId: 'record-1',
|
||||
position: 1.5,
|
||||
groupValue: 'in-progress',
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return early when single drag has no position', () => {
|
||||
const result = createDropResult({ droppableId: 'group-2', index: 1 });
|
||||
const mockRecordGroup = { value: 'done', fieldMetadataId: 'field-1' };
|
||||
const mockDestinationRecordIds = ['record-2'];
|
||||
|
||||
mockGetSnapshotValue
|
||||
.mockReturnValueOnce(mockRecordGroup)
|
||||
.mockReturnValueOnce(mockDestinationRecordIds);
|
||||
|
||||
mockExtractRecordPositions.mockReturnValue([]);
|
||||
mockGetDragOperationType.mockReturnValue('single');
|
||||
mockProcessSingleDrag.mockReturnValue({
|
||||
recordId: 'record-1',
|
||||
position: undefined,
|
||||
groupValue: 'done',
|
||||
});
|
||||
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot: mockSnapshot,
|
||||
selectedRecordIds: ['record-1'],
|
||||
selectFieldName: 'status',
|
||||
recordIdsByGroupFamilyState: mockRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: mockOnUpdateRecord,
|
||||
});
|
||||
|
||||
expect(mockOnUpdateRecord).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multi drag operation', () => {
|
||||
it('should process multi drag operation successfully', () => {
|
||||
const result = createDropResult({ droppableId: 'group-2', index: 1 });
|
||||
const mockRecordGroup = {
|
||||
value: 'in-review',
|
||||
fieldMetadataId: 'field-1',
|
||||
};
|
||||
const mockDestinationRecordIds = ['record-4', 'record-5'];
|
||||
const mockRecordPositionData = [
|
||||
{ recordId: 'record-4', position: 1 },
|
||||
{ recordId: 'record-5', position: 2 },
|
||||
];
|
||||
const selectedRecordIds = ['record-1', 'record-2', 'record-3'];
|
||||
|
||||
mockGetSnapshotValue
|
||||
.mockReturnValueOnce(mockRecordGroup)
|
||||
.mockReturnValueOnce(mockDestinationRecordIds);
|
||||
|
||||
mockExtractRecordPositions.mockReturnValue(mockRecordPositionData);
|
||||
mockGetDragOperationType.mockReturnValue('multi');
|
||||
mockProcessMultiDrag.mockReturnValue({
|
||||
recordUpdates: [
|
||||
{ recordId: 'record-1', position: 1.3 },
|
||||
{ recordId: 'record-2', position: 1.6 },
|
||||
{ recordId: 'record-3', position: 1.9 },
|
||||
],
|
||||
});
|
||||
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot: mockSnapshot,
|
||||
selectedRecordIds,
|
||||
selectFieldName: 'priority',
|
||||
recordIdsByGroupFamilyState: mockRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: mockOnUpdateRecord,
|
||||
});
|
||||
|
||||
expect(mockGetDragOperationType).toHaveBeenCalledWith({
|
||||
draggedRecordId: 'record-1',
|
||||
selectedRecordIds,
|
||||
});
|
||||
|
||||
expect(mockProcessMultiDrag).toHaveBeenCalledWith({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: mockDestinationRecordIds,
|
||||
groupValue: 'in-review',
|
||||
selectFieldName: 'priority',
|
||||
});
|
||||
|
||||
expect(mockOnUpdateRecord).toHaveBeenCalledTimes(3);
|
||||
expect(mockOnUpdateRecord).toHaveBeenNthCalledWith(1, {
|
||||
recordId: 'record-1',
|
||||
position: 1.3,
|
||||
groupValue: 'in-review',
|
||||
selectFieldName: 'priority',
|
||||
});
|
||||
expect(mockOnUpdateRecord).toHaveBeenNthCalledWith(2, {
|
||||
recordId: 'record-2',
|
||||
position: 1.6,
|
||||
groupValue: 'in-review',
|
||||
selectFieldName: 'priority',
|
||||
});
|
||||
expect(mockOnUpdateRecord).toHaveBeenNthCalledWith(3, {
|
||||
recordId: 'record-3',
|
||||
position: 1.9,
|
||||
groupValue: 'in-review',
|
||||
selectFieldName: 'priority',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty multi drag result', () => {
|
||||
const result = createDropResult({ droppableId: 'group-2', index: 0 });
|
||||
const mockRecordGroup = { value: null, fieldMetadataId: 'field-1' };
|
||||
const mockDestinationRecordIds: string[] = [];
|
||||
|
||||
mockGetSnapshotValue
|
||||
.mockReturnValueOnce(mockRecordGroup)
|
||||
.mockReturnValueOnce(mockDestinationRecordIds);
|
||||
|
||||
mockExtractRecordPositions.mockReturnValue([]);
|
||||
mockGetDragOperationType.mockReturnValue('multi');
|
||||
mockProcessMultiDrag.mockReturnValue({
|
||||
recordUpdates: [],
|
||||
});
|
||||
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot: mockSnapshot,
|
||||
selectedRecordIds: ['record-1', 'record-2'],
|
||||
selectFieldName: 'category',
|
||||
recordIdsByGroupFamilyState: mockRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: mockOnUpdateRecord,
|
||||
});
|
||||
|
||||
expect(mockOnUpdateRecord).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Group value handling', () => {
|
||||
it('should handle null group value correctly', () => {
|
||||
const result = createDropResult({ droppableId: 'no-group', index: 0 });
|
||||
const mockRecordGroup = { value: null, fieldMetadataId: 'field-1' };
|
||||
const mockDestinationRecordIds = ['record-1'];
|
||||
|
||||
mockGetSnapshotValue
|
||||
.mockReturnValueOnce(mockRecordGroup)
|
||||
.mockReturnValueOnce(mockDestinationRecordIds);
|
||||
|
||||
mockExtractRecordPositions.mockReturnValue([]);
|
||||
mockGetDragOperationType.mockReturnValue('single');
|
||||
mockProcessSingleDrag.mockReturnValue({
|
||||
recordId: 'record-1',
|
||||
position: 1,
|
||||
groupValue: null,
|
||||
});
|
||||
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot: mockSnapshot,
|
||||
selectedRecordIds: ['record-1'],
|
||||
selectFieldName: 'status',
|
||||
recordIdsByGroupFamilyState: mockRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: mockOnUpdateRecord,
|
||||
});
|
||||
|
||||
expect(mockOnUpdateRecord).toHaveBeenCalledWith({
|
||||
recordId: 'record-1',
|
||||
position: 1,
|
||||
groupValue: null,
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass correct group value to processors', () => {
|
||||
const result = createDropResult({
|
||||
droppableId: 'custom-group',
|
||||
index: 2,
|
||||
});
|
||||
const mockRecordGroup = {
|
||||
value: 'custom-value',
|
||||
fieldMetadataId: 'field-custom',
|
||||
};
|
||||
const mockDestinationRecordIds = ['record-a', 'record-b', 'record-c'];
|
||||
|
||||
mockGetSnapshotValue
|
||||
.mockReturnValueOnce(mockRecordGroup)
|
||||
.mockReturnValueOnce(mockDestinationRecordIds);
|
||||
|
||||
mockExtractRecordPositions.mockReturnValue([]);
|
||||
mockGetDragOperationType.mockReturnValue('single');
|
||||
mockProcessSingleDrag.mockReturnValue({
|
||||
recordId: 'record-1',
|
||||
position: 2.5,
|
||||
});
|
||||
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot: mockSnapshot,
|
||||
selectedRecordIds: ['record-1'],
|
||||
selectFieldName: 'customField',
|
||||
recordIdsByGroupFamilyState: mockRecordIdsByGroupFamilyState,
|
||||
onUpdateRecord: mockOnUpdateRecord,
|
||||
});
|
||||
|
||||
expect(mockProcessSingleDrag).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
groupValue: 'custom-value',
|
||||
selectFieldName: 'customField',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
import { type RecordDragPositionData } from '@/object-record/record-drag/shared/types/RecordDragPositionData';
|
||||
import { processMultiDrag } from '@/object-record/record-drag/shared/utils/processMultiDrag';
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
|
||||
jest.mock(
|
||||
'@/object-record/record-drag/shared/utils/calculateDragPositions',
|
||||
() => ({
|
||||
calculateDragPositions: jest.fn(({ recordsToMove }) => {
|
||||
const positions: Record<string, number> = {};
|
||||
recordsToMove.forEach((id: string, index: number) => {
|
||||
positions[id] = 1500 + index * 500;
|
||||
});
|
||||
return positions;
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
describe('processMultiDrag', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockRecordPositionData: RecordDragPositionData[] = [
|
||||
{ recordId: 'record-1', position: 1000 },
|
||||
{ recordId: 'record-2', position: 2000 },
|
||||
{ recordId: 'record-3', position: 3000 },
|
||||
{ recordId: 'record-4', position: 4000 },
|
||||
];
|
||||
|
||||
const createDropResult = (
|
||||
draggableId: string,
|
||||
destinationIndex: number | null,
|
||||
): DropResult => ({
|
||||
draggableId,
|
||||
type: 'record',
|
||||
source: {
|
||||
droppableId: 'source',
|
||||
index: 0,
|
||||
},
|
||||
destination:
|
||||
destinationIndex !== null
|
||||
? {
|
||||
droppableId: 'destination',
|
||||
index: destinationIndex,
|
||||
}
|
||||
: null,
|
||||
reason: 'DROP',
|
||||
mode: 'FLUID',
|
||||
combine: null,
|
||||
});
|
||||
|
||||
describe('Table context (without group fields)', () => {
|
||||
it('should process multi drag for table', () => {
|
||||
const result = createDropResult('record-2', 3);
|
||||
const selectedRecordIds = ['record-1', 'record-2', 'record-3'];
|
||||
|
||||
const dragResult = processMultiDrag({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3', 'record-4'],
|
||||
});
|
||||
|
||||
expect(dragResult.recordUpdates).toEqual([
|
||||
{ recordId: 'record-1', position: 1500 },
|
||||
{ recordId: 'record-2', position: 2000 },
|
||||
{ recordId: 'record-3', position: 2500 },
|
||||
]);
|
||||
|
||||
dragResult.recordUpdates.forEach((update) => {
|
||||
expect(update).not.toHaveProperty('groupValue');
|
||||
expect(update).not.toHaveProperty('selectFieldName');
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when destination is null', () => {
|
||||
const result = createDropResult('record-1', null);
|
||||
const selectedRecordIds = ['record-1', 'record-2'];
|
||||
|
||||
expect(() => {
|
||||
processMultiDrag({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
});
|
||||
}).toThrow('Destination is required for drag operation');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Board context (with group fields)', () => {
|
||||
it('should process multi drag for board with group fields', () => {
|
||||
const result = createDropResult('record-2', 3);
|
||||
const selectedRecordIds = ['record-1', 'record-2', 'record-3'];
|
||||
|
||||
const dragResult = processMultiDrag({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3', 'record-4'],
|
||||
groupValue: 'in-progress',
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
|
||||
expect(dragResult.recordUpdates).toEqual([
|
||||
{
|
||||
recordId: 'record-1',
|
||||
position: 1500,
|
||||
groupValue: 'in-progress',
|
||||
selectFieldName: 'status',
|
||||
},
|
||||
{
|
||||
recordId: 'record-2',
|
||||
position: 2000,
|
||||
groupValue: 'in-progress',
|
||||
selectFieldName: 'status',
|
||||
},
|
||||
{
|
||||
recordId: 'record-3',
|
||||
position: 2500,
|
||||
groupValue: 'in-progress',
|
||||
selectFieldName: 'status',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle null group value', () => {
|
||||
const result = createDropResult('record-1', 1);
|
||||
const selectedRecordIds = ['record-1', 'record-2'];
|
||||
|
||||
const dragResult = processMultiDrag({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: null,
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
|
||||
expect(dragResult.recordUpdates).toEqual([
|
||||
{
|
||||
recordId: 'record-1',
|
||||
position: 1500,
|
||||
groupValue: null,
|
||||
selectFieldName: 'status',
|
||||
},
|
||||
{
|
||||
recordId: 'record-2',
|
||||
position: 2000,
|
||||
groupValue: null,
|
||||
selectFieldName: 'status',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle single record in selection', () => {
|
||||
const result = createDropResult('record-1', 2);
|
||||
const selectedRecordIds = ['record-1'];
|
||||
|
||||
const dragResult = processMultiDrag({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
});
|
||||
|
||||
expect(dragResult.recordUpdates).toEqual([
|
||||
{ recordId: 'record-1', position: 1500 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty selection', () => {
|
||||
const result = createDropResult('record-1', 2);
|
||||
const selectedRecordIds: string[] = [];
|
||||
|
||||
const dragResult = processMultiDrag({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
});
|
||||
|
||||
expect(dragResult.recordUpdates).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not include group fields when only one is provided', () => {
|
||||
const result = createDropResult('record-1', 2);
|
||||
const selectedRecordIds = ['record-1', 'record-2'];
|
||||
|
||||
const dragResult = processMultiDrag({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: 'group-1',
|
||||
});
|
||||
|
||||
expect(dragResult.recordUpdates).toEqual([
|
||||
{ recordId: 'record-1', position: 1500 },
|
||||
{ recordId: 'record-2', position: 2000 },
|
||||
]);
|
||||
|
||||
dragResult.recordUpdates.forEach((update) => {
|
||||
expect(update).not.toHaveProperty('groupValue');
|
||||
expect(update).not.toHaveProperty('selectFieldName');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
import { type RecordDragPositionData } from '@/object-record/record-drag/shared/types/RecordDragPositionData';
|
||||
import { processSingleDrag } from '@/object-record/record-drag/shared/utils/processSingleDrag';
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
|
||||
jest.mock(
|
||||
'@/object-record/record-drag/shared/utils/calculateDragPositions',
|
||||
() => ({
|
||||
calculateDragPositions: jest.fn(({ recordsToMove }) => {
|
||||
const positions: Record<string, number> = {};
|
||||
recordsToMove.forEach((id: string, index: number) => {
|
||||
positions[id] = 1000 + index * 1000;
|
||||
});
|
||||
return positions;
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
describe('processSingleDrag', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockRecordPositionData: RecordDragPositionData[] = [
|
||||
{ recordId: 'record-1', position: 1000 },
|
||||
{ recordId: 'record-2', position: 2000 },
|
||||
{ recordId: 'record-3', position: 3000 },
|
||||
];
|
||||
|
||||
const createDropResult = (
|
||||
draggableId: string,
|
||||
destinationIndex: number | null,
|
||||
): DropResult => ({
|
||||
draggableId,
|
||||
type: 'record',
|
||||
source: {
|
||||
droppableId: 'source',
|
||||
index: 0,
|
||||
},
|
||||
destination:
|
||||
destinationIndex !== null
|
||||
? {
|
||||
droppableId: 'destination',
|
||||
index: destinationIndex,
|
||||
}
|
||||
: null,
|
||||
reason: 'DROP',
|
||||
mode: 'FLUID',
|
||||
combine: null,
|
||||
});
|
||||
|
||||
describe('Table context (without group fields)', () => {
|
||||
it('should process single drag for table', () => {
|
||||
const result = createDropResult('record-1', 2);
|
||||
|
||||
const dragResult = processSingleDrag({
|
||||
result,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
recordId: 'record-1',
|
||||
position: 1000,
|
||||
});
|
||||
expect(dragResult).not.toHaveProperty('groupValue');
|
||||
expect(dragResult).not.toHaveProperty('selectFieldName');
|
||||
});
|
||||
|
||||
it('should throw error when destination is null', () => {
|
||||
const result = createDropResult('record-1', null);
|
||||
|
||||
expect(() => {
|
||||
processSingleDrag({
|
||||
result,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
});
|
||||
}).toThrow('Destination is required for drag operation');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Board context (with group fields)', () => {
|
||||
it('should process single drag for board with group fields', () => {
|
||||
const result = createDropResult('record-1', 2);
|
||||
|
||||
const dragResult = processSingleDrag({
|
||||
result,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: 'group-1',
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
recordId: 'record-1',
|
||||
position: 1000,
|
||||
groupValue: 'group-1',
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null group value', () => {
|
||||
const result = createDropResult('record-1', 2);
|
||||
|
||||
const dragResult = processSingleDrag({
|
||||
result,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: null,
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
recordId: 'record-1',
|
||||
position: 1000,
|
||||
groupValue: null,
|
||||
selectFieldName: 'status',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle empty record list', () => {
|
||||
const result = createDropResult('record-1', 0);
|
||||
|
||||
const dragResult = processSingleDrag({
|
||||
result,
|
||||
recordPositionData: [],
|
||||
recordIds: [],
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
recordId: 'record-1',
|
||||
position: 1000,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not include group fields when only one is provided', () => {
|
||||
const result = createDropResult('record-1', 2);
|
||||
|
||||
const dragResult = processSingleDrag({
|
||||
result,
|
||||
recordPositionData: mockRecordPositionData,
|
||||
recordIds: ['record-1', 'record-2', 'record-3'],
|
||||
groupValue: 'group-1',
|
||||
});
|
||||
|
||||
expect(dragResult).toEqual({
|
||||
recordId: 'record-1',
|
||||
position: 1000,
|
||||
});
|
||||
expect(dragResult).not.toHaveProperty('groupValue');
|
||||
expect(dragResult).not.toHaveProperty('selectFieldName');
|
||||
});
|
||||
});
|
||||
});
|
||||
+11
-17
@@ -1,34 +1,28 @@
|
||||
import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition';
|
||||
import { type RecordDragPositionData } from '@/object-record/record-drag/shared/types/RecordDragPositionData';
|
||||
import { getDraggedRecordPosition } from '@/object-record/record-drag/shared/utils/getDraggedRecordPosition';
|
||||
import { getIndexNeighboursElementsFromArray } from '~/utils/array/getIndexNeighboursElementsFromArray';
|
||||
|
||||
export type RecordPositionData = {
|
||||
recordId: string;
|
||||
position?: number;
|
||||
};
|
||||
|
||||
type PositionCalculationContext = {
|
||||
destinationRecordIds: string[];
|
||||
type DragPositionCalculationParams = {
|
||||
recordIds: string[];
|
||||
recordsToMove: string[];
|
||||
destinationIndex: number;
|
||||
recordPositionData: RecordPositionData[];
|
||||
recordPositionData: RecordDragPositionData[];
|
||||
};
|
||||
|
||||
export const calculateRecordPositions = ({
|
||||
destinationRecordIds,
|
||||
export const calculateDragPositions = ({
|
||||
recordIds,
|
||||
recordsToMove,
|
||||
destinationIndex,
|
||||
recordPositionData,
|
||||
}: PositionCalculationContext): Record<string, number> => {
|
||||
const otherRecordIdsInDestinationColumn = destinationRecordIds.filter(
|
||||
}: DragPositionCalculationParams): Record<string, number> => {
|
||||
const otherRecordIds = recordIds.filter(
|
||||
(recordId: string) => !recordsToMove.includes(recordId),
|
||||
);
|
||||
|
||||
const filteredRecordIds =
|
||||
recordsToMove.length === 1
|
||||
? otherRecordIdsInDestinationColumn
|
||||
: destinationRecordIds.filter(
|
||||
(recordId) => recordId !== recordsToMove[0],
|
||||
);
|
||||
? otherRecordIds
|
||||
: recordIds.filter((recordId) => recordId !== recordsToMove[0]);
|
||||
|
||||
const { before: recordBeforeId, after: recordAfterId } =
|
||||
getIndexNeighboursElementsFromArray({
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { type Snapshot } from 'recoil';
|
||||
|
||||
export const extractRecordPositions = (
|
||||
recordIds: string[],
|
||||
snapshot: Snapshot,
|
||||
) => {
|
||||
return recordIds.map((recordId) => {
|
||||
const record = snapshot
|
||||
.getLoadable(recordStoreFamilyState(recordId))
|
||||
.getValue();
|
||||
return {
|
||||
recordId,
|
||||
position: record?.position,
|
||||
};
|
||||
});
|
||||
};
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { type Snapshot } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
|
||||
import { extractRecordPositions } from './extractRecordPositions';
|
||||
import { getDragOperationType } from './getDragOperationType';
|
||||
import { processMultiDrag } from './processMultiDrag';
|
||||
import { processSingleDrag } from './processSingleDrag';
|
||||
|
||||
type ProcessGroupDragOperationParams = {
|
||||
result: DropResult;
|
||||
snapshot: Snapshot;
|
||||
selectedRecordIds: string[];
|
||||
selectFieldName: string;
|
||||
recordIdsByGroupFamilyState: any;
|
||||
onUpdateRecord: (update: {
|
||||
recordId: string;
|
||||
position?: number;
|
||||
groupValue?: string | null;
|
||||
selectFieldName: string;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export const processGroupDragOperation = ({
|
||||
result,
|
||||
snapshot,
|
||||
selectedRecordIds,
|
||||
selectFieldName,
|
||||
recordIdsByGroupFamilyState,
|
||||
onUpdateRecord,
|
||||
}: ProcessGroupDragOperationParams) => {
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
const destinationGroupId = result.destination.droppableId;
|
||||
|
||||
const recordGroup = getSnapshotValue(
|
||||
snapshot,
|
||||
recordGroupDefinitionFamilyState(destinationGroupId),
|
||||
);
|
||||
|
||||
if (!isDefined(recordGroup)) {
|
||||
throw new Error('Record group is not defined');
|
||||
}
|
||||
|
||||
const destinationRecordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
recordIdsByGroupFamilyState(destinationGroupId),
|
||||
) as string[];
|
||||
|
||||
const recordPositionData = extractRecordPositions(
|
||||
destinationRecordIds,
|
||||
snapshot,
|
||||
);
|
||||
|
||||
const draggedRecordId = result.draggableId;
|
||||
const dragOperationType = getDragOperationType({
|
||||
draggedRecordId,
|
||||
selectedRecordIds,
|
||||
});
|
||||
|
||||
if (dragOperationType === 'single') {
|
||||
const singleDragResult = processSingleDrag({
|
||||
result,
|
||||
recordPositionData,
|
||||
recordIds: destinationRecordIds,
|
||||
groupValue: recordGroup.value,
|
||||
selectFieldName,
|
||||
});
|
||||
|
||||
if (!isDefined(singleDragResult.position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
onUpdateRecord({
|
||||
recordId: singleDragResult.recordId,
|
||||
position: singleDragResult.position,
|
||||
groupValue: recordGroup.value,
|
||||
selectFieldName,
|
||||
});
|
||||
} else {
|
||||
const multiDragResult = processMultiDrag({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData,
|
||||
recordIds: destinationRecordIds,
|
||||
groupValue: recordGroup.value,
|
||||
selectFieldName,
|
||||
});
|
||||
|
||||
for (const update of multiDragResult.recordUpdates) {
|
||||
onUpdateRecord({
|
||||
recordId: update.recordId,
|
||||
position: update.position,
|
||||
groupValue: recordGroup.value,
|
||||
selectFieldName,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { type MultiDragResult } from '@/object-record/record-drag/shared/types/MultiDragResult';
|
||||
import { type RecordDragPositionData } from '@/object-record/record-drag/shared/types/RecordDragPositionData';
|
||||
import { calculateDragPositions } from '@/object-record/record-drag/shared/utils/calculateDragPositions';
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type MultiDragContext = {
|
||||
result: DropResult;
|
||||
selectedRecordIds: string[];
|
||||
recordPositionData: RecordDragPositionData[];
|
||||
recordIds: string[];
|
||||
groupValue?: string | null;
|
||||
selectFieldName?: string;
|
||||
};
|
||||
|
||||
export const processMultiDrag = ({
|
||||
result,
|
||||
selectedRecordIds,
|
||||
recordPositionData,
|
||||
recordIds,
|
||||
groupValue,
|
||||
selectFieldName,
|
||||
}: MultiDragContext): MultiDragResult => {
|
||||
if (!result.destination) {
|
||||
throw new Error('Destination is required for drag operation');
|
||||
}
|
||||
|
||||
const destinationIndex = result.destination.index;
|
||||
|
||||
const positions = calculateDragPositions({
|
||||
recordIds,
|
||||
recordsToMove: selectedRecordIds,
|
||||
destinationIndex,
|
||||
recordPositionData,
|
||||
});
|
||||
|
||||
const recordUpdates = selectedRecordIds.map((recordId) => {
|
||||
const baseUpdate = {
|
||||
recordId,
|
||||
position: positions[recordId],
|
||||
};
|
||||
|
||||
const shouldIncludeGroupFields =
|
||||
isDefined(selectFieldName) &&
|
||||
(isDefined(groupValue) || isNull(groupValue));
|
||||
|
||||
if (shouldIncludeGroupFields) {
|
||||
return {
|
||||
...baseUpdate,
|
||||
groupValue,
|
||||
selectFieldName,
|
||||
};
|
||||
}
|
||||
|
||||
return baseUpdate;
|
||||
});
|
||||
|
||||
return {
|
||||
recordUpdates,
|
||||
};
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { type RecordDragPositionData } from '@/object-record/record-drag/shared/types/RecordDragPositionData';
|
||||
import { type RecordDragUpdate } from '@/object-record/record-drag/shared/types/RecordDragUpdate';
|
||||
import { calculateDragPositions } from '@/object-record/record-drag/shared/utils/calculateDragPositions';
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type SingleDragContext = {
|
||||
result: DropResult;
|
||||
recordPositionData: RecordDragPositionData[];
|
||||
recordIds: string[];
|
||||
groupValue?: string | null;
|
||||
selectFieldName?: string;
|
||||
};
|
||||
|
||||
export const processSingleDrag = ({
|
||||
result,
|
||||
recordPositionData,
|
||||
recordIds,
|
||||
groupValue,
|
||||
selectFieldName,
|
||||
}: SingleDragContext): RecordDragUpdate => {
|
||||
const draggedRecordId = result.draggableId;
|
||||
|
||||
if (!result.destination) {
|
||||
throw new Error('Destination is required for drag operation');
|
||||
}
|
||||
|
||||
const destinationIndex = result.destination.index;
|
||||
const recordsToMove = [draggedRecordId];
|
||||
|
||||
const positions = calculateDragPositions({
|
||||
recordIds,
|
||||
recordsToMove,
|
||||
destinationIndex,
|
||||
recordPositionData,
|
||||
});
|
||||
|
||||
const baseResult = {
|
||||
recordId: draggedRecordId,
|
||||
position: positions[draggedRecordId],
|
||||
};
|
||||
|
||||
const shouldIncludeGroupFields =
|
||||
isDefined(selectFieldName) && (isDefined(groupValue) || isNull(groupValue));
|
||||
|
||||
if (shouldIncludeGroupFields) {
|
||||
return {
|
||||
...baseResult,
|
||||
groupValue,
|
||||
selectFieldName,
|
||||
};
|
||||
}
|
||||
|
||||
return baseResult;
|
||||
};
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { getDragOperationType } from '@/object-record/record-drag/shared/utils/getDragOperationType';
|
||||
import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId';
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useRecordDragState } from '@/object-record/record-drag/shared/hooks/useRecordDragState';
|
||||
import { extractRecordPositions } from '@/object-record/record-drag/shared/utils/extractRecordPositions';
|
||||
import { processSingleDrag } from '@/object-record/record-drag/shared/utils/processSingleDrag';
|
||||
import { processMultiDrag } from '@/object-record/record-drag/shared/utils/processMultiDrag';
|
||||
import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useRecordTableDragOperations = () => {
|
||||
const { objectNameSingular, recordTableId } = useRecordTableContextOrThrow();
|
||||
|
||||
const { updateOneRecord: updateOneRow } = useUpdateOneRecord({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const recordIndexAllRecordIdsSelector = useRecoilComponentCallbackState(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
);
|
||||
|
||||
const selectedRowIdsSelector = useRecoilComponentCallbackState(
|
||||
selectedRowIdsComponentSelector,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const currentRecordSorts = useRecoilComponentValue(
|
||||
currentRecordSortsComponentState,
|
||||
);
|
||||
|
||||
const { openModal } = useModal();
|
||||
const multiDragState = useRecordDragState('table', recordTableId);
|
||||
|
||||
const processDragOperation = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(result: DropResult) => {
|
||||
if (!result.destination) return;
|
||||
|
||||
if (currentRecordSorts.length > 0) {
|
||||
openModal(RECORD_INDEX_REMOVE_SORTING_MODAL_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
const allRecordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
recordIndexAllRecordIdsSelector,
|
||||
);
|
||||
|
||||
const draggedRecordId = result.draggableId;
|
||||
const selectedRecordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
selectedRowIdsSelector,
|
||||
);
|
||||
|
||||
const recordPositionData = extractRecordPositions(
|
||||
allRecordIds,
|
||||
snapshot,
|
||||
);
|
||||
|
||||
const dragOperationType = getDragOperationType({
|
||||
draggedRecordId,
|
||||
selectedRecordIds,
|
||||
});
|
||||
|
||||
if (dragOperationType === 'single') {
|
||||
const singleDragResult = processSingleDrag({
|
||||
result,
|
||||
recordPositionData,
|
||||
recordIds: allRecordIds,
|
||||
});
|
||||
|
||||
if (!isDefined(singleDragResult.position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateOneRow({
|
||||
idToUpdate: singleDragResult.recordId,
|
||||
updateOneRecordInput: {
|
||||
position: singleDragResult.position,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const multiDragResult = processMultiDrag({
|
||||
result,
|
||||
selectedRecordIds: multiDragState.originalSelection,
|
||||
recordPositionData,
|
||||
recordIds: allRecordIds,
|
||||
});
|
||||
|
||||
for (const update of multiDragResult.recordUpdates) {
|
||||
updateOneRow({
|
||||
idToUpdate: update.recordId,
|
||||
updateOneRecordInput: {
|
||||
position: update.position,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
selectedRowIdsSelector,
|
||||
updateOneRow,
|
||||
openModal,
|
||||
currentRecordSorts,
|
||||
multiDragState.originalSelection,
|
||||
recordIndexAllRecordIdsSelector,
|
||||
],
|
||||
);
|
||||
|
||||
return { processDragOperation };
|
||||
};
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { useRecordDragState } from '@/object-record/record-drag/shared/hooks/useRecordDragState';
|
||||
import { processGroupDragOperation } from '@/object-record/record-drag/shared/utils/processGroupDragOperation';
|
||||
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
|
||||
import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
|
||||
export const useRecordTableGroupDragOperations = () => {
|
||||
const { objectNameSingular, objectMetadataItem, recordTableId } =
|
||||
useRecordTableContextOrThrow();
|
||||
|
||||
const { updateOneRecord: updateOneRow } = useUpdateOneRecord({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { openModal } = useModal();
|
||||
|
||||
const recordIdsByGroupFamilyState = useRecoilComponentCallbackState(
|
||||
recordIndexRecordIdsByGroupComponentFamilyState,
|
||||
);
|
||||
|
||||
const currentRecordSortsCallbackState = useRecoilComponentCallbackState(
|
||||
currentRecordSortsComponentState,
|
||||
);
|
||||
|
||||
const selectedRowIdsSelector = useRecoilComponentCallbackState(
|
||||
selectedRowIdsComponentSelector,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const multiDragState = useRecordDragState('table', recordTableId);
|
||||
|
||||
const processDragOperation = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(result: DropResult) => {
|
||||
if (!result.destination) return;
|
||||
|
||||
const destinationRecordGroupId = result.destination.droppableId;
|
||||
const destinationRecordGroup = getSnapshotValue(
|
||||
snapshot,
|
||||
recordGroupDefinitionFamilyState(destinationRecordGroupId),
|
||||
);
|
||||
|
||||
if (!isDefined(destinationRecordGroup)) {
|
||||
throw new Error('Record group is not defined');
|
||||
}
|
||||
|
||||
const fieldMetadata = objectMetadataItem.fields.find(
|
||||
(field) => field.id === destinationRecordGroup.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldMetadata)) {
|
||||
throw new Error('Field metadata is not defined');
|
||||
}
|
||||
|
||||
const selectedRecordIds = multiDragState.isDragging
|
||||
? multiDragState.originalSelection
|
||||
: getSnapshotValue(snapshot, selectedRowIdsSelector);
|
||||
|
||||
const currentRecordSorts = snapshot
|
||||
.getLoadable(currentRecordSortsCallbackState)
|
||||
.getValue();
|
||||
|
||||
if (currentRecordSorts.length > 0) {
|
||||
openModal(RECORD_INDEX_REMOVE_SORTING_MODAL_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
processGroupDragOperation({
|
||||
result,
|
||||
snapshot,
|
||||
selectedRecordIds,
|
||||
selectFieldName: fieldMetadata.name,
|
||||
recordIdsByGroupFamilyState: recordIdsByGroupFamilyState,
|
||||
onUpdateRecord: ({ recordId, position }) => {
|
||||
updateOneRow({
|
||||
idToUpdate: recordId,
|
||||
updateOneRecordInput: {
|
||||
position,
|
||||
[fieldMetadata.name]: destinationRecordGroup.value,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
currentRecordSortsCallbackState,
|
||||
objectMetadataItem.fields,
|
||||
recordIdsByGroupFamilyState,
|
||||
updateOneRow,
|
||||
openModal,
|
||||
selectedRowIdsSelector,
|
||||
multiDragState.originalSelection,
|
||||
multiDragState.isDragging,
|
||||
],
|
||||
);
|
||||
|
||||
return { processDragOperation };
|
||||
};
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const draggedRecordIdsTableComponentState = createComponentState<
|
||||
string[]
|
||||
>({
|
||||
key: 'draggedRecordIdsTableComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
});
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const isMultiDragActiveTableComponentState =
|
||||
createComponentState<boolean>({
|
||||
key: 'isMultiDragActiveTableComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
});
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const originalSelectionTableComponentState = createComponentState<
|
||||
string[]
|
||||
>({
|
||||
key: 'originalSelectionTableComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
});
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const primaryDraggedRecordIdTableComponentState = createComponentState<
|
||||
string | null
|
||||
>({
|
||||
key: 'primaryDraggedRecordIdTableComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
});
|
||||
+37
-89
@@ -1,112 +1,60 @@
|
||||
import { DragDropContext, type DropResult } from '@hello-pangea/dnd';
|
||||
import {
|
||||
DragDropContext,
|
||||
type DragStart,
|
||||
type DropResult,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { type ReactNode } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition';
|
||||
import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId';
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useEndRecordDrag } from '@/object-record/record-drag/shared/hooks/useEndRecordDrag';
|
||||
import { useStartRecordDrag } from '@/object-record/record-drag/shared/hooks/useStartRecordDrag';
|
||||
import { useRecordTableDragOperations } from '@/object-record/record-drag/table/hooks/useRecordTableDragOperations';
|
||||
import { selectedRowIdsComponentSelector } from '../../states/selectors/selectedRowIdsComponentSelector';
|
||||
|
||||
export const RecordTableBodyDragDropContextProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const { objectNameSingular } = useRecordTableContextOrThrow();
|
||||
const { recordTableId } = useRecordTableContextOrThrow();
|
||||
|
||||
const { updateOneRecord: updateOneRow } = useUpdateOneRecord({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const recordIndexAllRecordIdsSelector = useRecoilComponentCallbackState(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
const selectedRowIdsSelector = useRecoilComponentCallbackState(
|
||||
selectedRowIdsComponentSelector,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const currentRecordSorts = useRecoilComponentValue(
|
||||
currentRecordSortsComponentState,
|
||||
);
|
||||
const { startDrag } = useStartRecordDrag('table', recordTableId);
|
||||
const { endDrag } = useEndRecordDrag('table', recordTableId);
|
||||
const { processDragOperation } = useRecordTableDragOperations();
|
||||
|
||||
const { openModal } = useModal();
|
||||
const handleDragStart = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(start: DragStart) => {
|
||||
const currentSelectedRecordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
selectedRowIdsSelector,
|
||||
);
|
||||
|
||||
startDrag(start, currentSelectedRecordIds);
|
||||
},
|
||||
[selectedRowIdsSelector, startDrag],
|
||||
);
|
||||
|
||||
const handleDragEnd = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(result: DropResult) => {
|
||||
if (currentRecordSorts.length > 0) {
|
||||
openModal(RECORD_INDEX_REMOVE_SORTING_MODAL_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(result.destination)) {
|
||||
throw new Error('Drop Destination is not defined');
|
||||
}
|
||||
|
||||
const allRecordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
recordIndexAllRecordIdsSelector,
|
||||
);
|
||||
|
||||
const isSourceIndexBeforeDestinationIndex =
|
||||
result.source.index < result.destination.index;
|
||||
|
||||
const recordBeforeDestinationId =
|
||||
allRecordIds[
|
||||
isSourceIndexBeforeDestinationIndex
|
||||
? result.destination.index
|
||||
: result.destination.index - 1
|
||||
];
|
||||
|
||||
const recordBeforeDestination = recordBeforeDestinationId
|
||||
? snapshot
|
||||
.getLoadable(recordStoreFamilyState(recordBeforeDestinationId))
|
||||
.getValue()
|
||||
: null;
|
||||
|
||||
const recordAfterDestinationId =
|
||||
allRecordIds[
|
||||
isSourceIndexBeforeDestinationIndex
|
||||
? result.destination.index + 1
|
||||
: result.destination.index
|
||||
];
|
||||
|
||||
const recordAfterDestination = recordAfterDestinationId
|
||||
? snapshot
|
||||
.getLoadable(recordStoreFamilyState(recordAfterDestinationId))
|
||||
.getValue()
|
||||
: null;
|
||||
|
||||
const newPosition = getDraggedRecordPosition(
|
||||
recordBeforeDestination?.position,
|
||||
recordAfterDestination?.position,
|
||||
);
|
||||
|
||||
if (!isDefined(newPosition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateOneRow({
|
||||
idToUpdate: result.draggableId,
|
||||
updateOneRecordInput: {
|
||||
position: newPosition,
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
currentRecordSorts.length,
|
||||
recordIndexAllRecordIdsSelector,
|
||||
updateOneRow,
|
||||
openModal,
|
||||
],
|
||||
() => (result: DropResult) => {
|
||||
processDragOperation(result);
|
||||
endDrag();
|
||||
},
|
||||
[endDrag, processDragOperation],
|
||||
);
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={handleDragEnd}>{children}</DragDropContext>
|
||||
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
{children}
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
|
||||
+36
-120
@@ -1,143 +1,59 @@
|
||||
import { DragDropContext, type DropResult } from '@hello-pangea/dnd';
|
||||
import {
|
||||
DragDropContext,
|
||||
type DragStart,
|
||||
type DropResult,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { type ReactNode } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition';
|
||||
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
|
||||
import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useEndRecordDrag } from '@/object-record/record-drag/shared/hooks/useEndRecordDrag';
|
||||
import { useStartRecordDrag } from '@/object-record/record-drag/shared/hooks/useStartRecordDrag';
|
||||
import { useRecordTableGroupDragOperations } from '@/object-record/record-drag/table/hooks/useRecordTableGroupDragOperations';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const RecordTableBodyRecordGroupDragDropContextProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const { objectNameSingular, objectMetadataItem } =
|
||||
useRecordTableContextOrThrow();
|
||||
const { recordTableId } = useRecordTableContextOrThrow();
|
||||
|
||||
const { updateOneRecord: updateOneRow } = useUpdateOneRecord({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { openModal } = useModal();
|
||||
|
||||
const recordIdsByGroupFamilyState = useRecoilComponentCallbackState(
|
||||
recordIndexRecordIdsByGroupComponentFamilyState,
|
||||
const selectedRowIdsSelector = useRecoilComponentCallbackState(
|
||||
selectedRowIdsComponentSelector,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const currentRecordSortsCallbackState = useRecoilComponentCallbackState(
|
||||
currentRecordSortsComponentState,
|
||||
const { startDrag } = useStartRecordDrag('table', recordTableId);
|
||||
const { endDrag } = useEndRecordDrag('table', recordTableId);
|
||||
const { processDragOperation } = useRecordTableGroupDragOperations();
|
||||
|
||||
const handleDragStart = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(start: DragStart) => {
|
||||
const currentSelectedRecordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
selectedRowIdsSelector,
|
||||
);
|
||||
|
||||
startDrag(start, currentSelectedRecordIds);
|
||||
},
|
||||
[selectedRowIdsSelector, startDrag],
|
||||
);
|
||||
|
||||
const handleDragEnd = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(result: DropResult) => {
|
||||
const destinationRecordGroupId = result.destination?.droppableId;
|
||||
|
||||
if (!isDefined(result.destination)) {
|
||||
throw new Error('Drop Destination is not defined');
|
||||
}
|
||||
|
||||
if (!isDefined(destinationRecordGroupId)) {
|
||||
throw new Error('Record group id is not defined');
|
||||
}
|
||||
|
||||
const destinationRecordGroup = getSnapshotValue(
|
||||
snapshot,
|
||||
recordGroupDefinitionFamilyState(destinationRecordGroupId),
|
||||
);
|
||||
|
||||
const currentRecordSorts = snapshot
|
||||
.getLoadable(currentRecordSortsCallbackState)
|
||||
.getValue();
|
||||
|
||||
if (!isDefined(destinationRecordGroup)) {
|
||||
throw new Error('Record group is not defined');
|
||||
}
|
||||
|
||||
const fieldMetadata = objectMetadataItem.fields.find(
|
||||
(field) => field.id === destinationRecordGroup.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldMetadata)) {
|
||||
throw new Error('Field metadata is not defined');
|
||||
}
|
||||
|
||||
if (currentRecordSorts.length > 0) {
|
||||
openModal(RECORD_INDEX_REMOVE_SORTING_MODAL_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
const isSourceIndexBeforeDestinationIndexInSameGroup =
|
||||
result.source.index < result.destination.index &&
|
||||
result.source.droppableId === result.destination.droppableId;
|
||||
|
||||
const destinationGroupRecordIds = getSnapshotValue(
|
||||
snapshot,
|
||||
recordIdsByGroupFamilyState(destinationRecordGroupId),
|
||||
);
|
||||
|
||||
const recordBeforeDestinationId =
|
||||
destinationGroupRecordIds[
|
||||
isSourceIndexBeforeDestinationIndexInSameGroup
|
||||
? result.destination.index
|
||||
: result.destination.index - 1
|
||||
];
|
||||
|
||||
const recordBeforeDestination = recordBeforeDestinationId
|
||||
? snapshot
|
||||
.getLoadable(recordStoreFamilyState(recordBeforeDestinationId))
|
||||
.getValue()
|
||||
: null;
|
||||
|
||||
const recordAfterDestinationId =
|
||||
destinationGroupRecordIds[
|
||||
isSourceIndexBeforeDestinationIndexInSameGroup
|
||||
? result.destination.index + 1
|
||||
: result.destination.index
|
||||
];
|
||||
|
||||
const recordAfterDestination = recordAfterDestinationId
|
||||
? snapshot
|
||||
.getLoadable(recordStoreFamilyState(recordAfterDestinationId))
|
||||
.getValue()
|
||||
: null;
|
||||
|
||||
const newPosition = getDraggedRecordPosition(
|
||||
recordBeforeDestination?.position,
|
||||
recordAfterDestination?.position,
|
||||
);
|
||||
|
||||
if (!isDefined(newPosition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateOneRow({
|
||||
idToUpdate: result.draggableId,
|
||||
updateOneRecordInput: {
|
||||
position: newPosition,
|
||||
[fieldMetadata.name]: destinationRecordGroup.value,
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
currentRecordSortsCallbackState,
|
||||
objectMetadataItem.fields,
|
||||
recordIdsByGroupFamilyState,
|
||||
updateOneRow,
|
||||
openModal,
|
||||
],
|
||||
() => (result: DropResult) => {
|
||||
processDragOperation(result);
|
||||
endDrag();
|
||||
},
|
||||
[endDrag, processDragOperation],
|
||||
);
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={handleDragEnd}>{children}</DragDropContext>
|
||||
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
{children}
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
|
||||
+14
@@ -2,7 +2,10 @@ import { useTheme } from '@emotion/react';
|
||||
import { Draggable } from '@hello-pangea/dnd';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
|
||||
import { useRecordDragState } from '@/object-record/record-drag/shared/hooks/useRecordDragState';
|
||||
import { RecordTableRowMultiDragPreview } from '@/object-record/record-table/record-table-row/components/RecordTableRowMultiDragPreview';
|
||||
import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr';
|
||||
import { RecordTableTrEffect } from '@/object-record/record-table/record-table-row/components/RecordTableTrEffect';
|
||||
|
||||
@@ -26,6 +29,13 @@ export const RecordTableDraggableTr = ({
|
||||
children,
|
||||
}: RecordTableDraggableTrProps) => {
|
||||
const theme = useTheme();
|
||||
const { recordTableId } = useRecordTableContextOrThrow();
|
||||
const multiDragState = useRecordDragState('table', recordTableId);
|
||||
|
||||
const isSecondaryDragged =
|
||||
multiDragState?.isDragging &&
|
||||
multiDragState.originalSelection.includes(recordId) &&
|
||||
recordId !== multiDragState.primaryDraggedRecordId;
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
@@ -51,6 +61,7 @@ export const RecordTableDraggableTr = ({
|
||||
borderColor: draggableSnapshot.isDragging
|
||||
? `${theme.border.color.medium}`
|
||||
: 'transparent',
|
||||
opacity: isSecondaryDragged ? 0.3 : 1,
|
||||
}}
|
||||
isDragging={draggableSnapshot.isDragging}
|
||||
data-testid={`row-id-${recordId}`}
|
||||
@@ -65,6 +76,9 @@ export const RecordTableDraggableTr = ({
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<RecordTableRowMultiDragPreview
|
||||
isDragging={draggableSnapshot.isDragging}
|
||||
/>
|
||||
</RecordTableRowDraggableContextProvider>
|
||||
</RecordTableTr>
|
||||
</>
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { NotificationCounter } from 'twenty-ui/navigation';
|
||||
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { useRecordDragState } from '@/object-record/record-drag/shared/hooks/useRecordDragState';
|
||||
|
||||
const StyledNotificationCounter = styled(NotificationCounter)`
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: -7px;
|
||||
z-index: 1000;
|
||||
`;
|
||||
|
||||
type RecordTableRowMultiDragPreviewProps = {
|
||||
isDragging: boolean;
|
||||
};
|
||||
|
||||
export const RecordTableRowMultiDragPreview = ({
|
||||
isDragging,
|
||||
}: RecordTableRowMultiDragPreviewProps) => {
|
||||
const { recordId } = useRecordTableRowContextOrThrow();
|
||||
const { recordTableId } = useRecordTableContextOrThrow();
|
||||
const multiDragState = useRecordDragState('table', recordTableId);
|
||||
|
||||
const isCurrentRowSelected =
|
||||
multiDragState?.originalSelection.includes(recordId) || false;
|
||||
const selectedCount = multiDragState?.originalSelection.length ?? 0;
|
||||
|
||||
const shouldShow = isDragging && isCurrentRowSelected && selectedCount > 1;
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <StyledNotificationCounter count={selectedCount} />;
|
||||
};
|
||||
Reference in New Issue
Block a user