From ea254986258c4ba72870a494968eefb355ab318f Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:04:56 +0530 Subject: [PATCH] Fix dragging behavior below the last card when dragging below the new CTA button (#11781) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed an issue where dragging an item below the last card didn't work when the card was dragged below the new CTA button (`destination.index === items.length` case). - Moved the "new record" button outside of the draggable list ### Demo https://github.com/user-attachments/assets/370f2c1f-4bb2-403b-b8ed-4afda064c98d Closes #10197 --------- Co-authored-by: prastoin Co-authored-by: Félix Malfait --- .../record-board/components/RecordBoard.tsx | 10 +- .../RecordBoardColumnCardsContainer.tsx | 11 +-- ...etIndexNeighboursElementsFromArray.test.ts | 93 +++++++++++++++++++ .../getIndexNeighboursElementsFromArray.ts | 29 ++++++ 4 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 packages/twenty-front/src/utils/array/__tests__/getIndexNeighboursElementsFromArray.test.ts create mode 100644 packages/twenty-front/src/utils/array/getIndexNeighboursElementsFromArray.ts diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index 3c28e9ca1..c87033b9b 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -32,6 +32,7 @@ import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component- import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { ViewType } from '@/views/types/ViewType'; +import { getIndexNeighboursElementsFromArray } from '~/utils/array/getIndexNeighboursElementsFromArray'; const StyledContainer = styled.div` display: flex; @@ -173,14 +174,15 @@ export const RecordBoard = () => { ) : destinationRecordByGroupIds; - const recordBeforeId = - otherRecordIdsInDestinationColumn[destinationIndexInColumn - 1]; + const { before: recordBeforeId, after: recordAfterId } = + getIndexNeighboursElementsFromArray({ + index: destinationIndexInColumn, + array: otherRecordIdsInDestinationColumn, + }); const recordBefore = recordBeforeId ? getSnapshotValue(snapshot, recordStoreFamilyState(recordBeforeId)) : null; - const recordAfterId = - otherRecordIdsInDestinationColumn[destinationIndexInColumn]; const recordAfter = recordAfterId ? getSnapshotValue(snapshot, recordStoreFamilyState(recordAfterId)) : null; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx index 93b5fb572..7e60cef4b 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx @@ -13,7 +13,6 @@ import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/re import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; import { isRecordIndexBoardColumnLoadingFamilyState } from '@/object-record/states/isRecordBoardColumnLoadingFamilyState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; - const StyledColumnCardsContainer = styled.div` display: flex; flex: 1; @@ -23,7 +22,6 @@ const StyledColumnCardsContainer = styled.div` const StyledNewButtonContainer = styled.div` padding-bottom: ${({ theme }) => theme.spacing(4)}; `; - // eslint-disable-next-line @nx/workspace-no-hardcoded-colors const StyledSkeletonCardContainer = styled.div` background-color: ${({ theme }) => theme.background.secondary}; @@ -102,14 +100,13 @@ export const RecordBoardColumnCardsContainer = ({ ref={draggableProvided?.innerRef} // eslint-disable-next-line react/jsx-props-no-spreading {...draggableProvided?.draggableProps} - > - - - - + > )} {droppableProvided?.placeholder} + + + ); }; diff --git a/packages/twenty-front/src/utils/array/__tests__/getIndexNeighboursElementsFromArray.test.ts b/packages/twenty-front/src/utils/array/__tests__/getIndexNeighboursElementsFromArray.test.ts new file mode 100644 index 000000000..0106c54b2 --- /dev/null +++ b/packages/twenty-front/src/utils/array/__tests__/getIndexNeighboursElementsFromArray.test.ts @@ -0,0 +1,93 @@ +import { EachTestingContext } from 'twenty-shared/testing'; +import { getIndexNeighboursElementsFromArray } from '../getIndexNeighboursElementsFromArray'; + +type TestCase = { + expected: ReturnType; +} & Parameters[0]; + +describe('getIndexNeighboursElementsFromArray', () => { + const testCases: EachTestingContext[] = [ + { + title: + 'should return undefined for before and first element for after when index is 0', + context: { + index: 0, + array: [ + 'a1b2c3d4-e5f6-4a5b-8c7d-9e0f1a2b3c4d', + 'b2c3d4e5-f6a7-5b6c-9d8e-0f1a2b3c4d5e', + 'c3d4e5f6-a7b8-6c7d-0e9f-1a2b3c4d5e6f', + ], + expected: { + before: undefined, + after: 'a1b2c3d4-e5f6-4a5b-8c7d-9e0f1a2b3c4d', + }, + }, + }, + { + title: + 'should return last element for before and undefined for after when index is at end', + context: { + index: 3, + array: [ + 'a1b2c3d4-e5f6-4a5b-8c7d-9e0f1a2b3c4d', + 'b2c3d4e5-f6a7-5b6c-9d8e-0f1a2b3c4d5e', + 'c3d4e5f6-a7b8-6c7d-0e9f-1a2b3c4d5e6f', + ], + expected: { + before: 'c3d4e5f6-a7b8-6c7d-0e9f-1a2b3c4d5e6f', + after: undefined, + }, + }, + }, + { + title: + 'should return last element for before and undefined for after when index is above the end', + context: { + index: 42, + array: [ + 'a1b2c3d4-e5f6-4a5b-8c7d-9e0f1a2b3c4d', + 'b2c3d4e5-f6a7-5b6c-9d8e-0f1a2b3c4d5e', + 'c3d4e5f6-a7b8-6c7d-0e9f-1a2b3c4d5e6f', + ], + expected: { + before: 'c3d4e5f6-a7b8-6c7d-0e9f-1a2b3c4d5e6f', + after: undefined, + }, + }, + }, + { + title: 'should return correct before and after elements for middle index', + context: { + index: 1, + array: [ + 'a1b2c3d4-e5f6-4a5b-8c7d-9e0f1a2b3c4d', + 'b2c3d4e5-f6a7-5b6c-9d8e-0f1a2b3c4d5e', + 'c3d4e5f6-a7b8-6c7d-0e9f-1a2b3c4d5e6f', + ], + expected: { + before: 'a1b2c3d4-e5f6-4a5b-8c7d-9e0f1a2b3c4d', + after: 'b2c3d4e5-f6a7-5b6c-9d8e-0f1a2b3c4d5e', + }, + }, + }, + { + title: 'should handle empty array array', + context: { + index: 0, + array: [], + expected: { + before: undefined, + after: undefined, + }, + }, + }, + ]; + + it.each(testCases)('$title', ({ context }) => { + const result = getIndexNeighboursElementsFromArray({ + index: context.index, + array: context.array, + }); + expect(result).toEqual(context.expected); + }); +}); diff --git a/packages/twenty-front/src/utils/array/getIndexNeighboursElementsFromArray.ts b/packages/twenty-front/src/utils/array/getIndexNeighboursElementsFromArray.ts new file mode 100644 index 000000000..7422ebdee --- /dev/null +++ b/packages/twenty-front/src/utils/array/getIndexNeighboursElementsFromArray.ts @@ -0,0 +1,29 @@ +type GetIndexNeighboursElementsFromArrayArgs = { + index: number; + array: string[]; +}; +type GetIndexNeighboursElementsFromArrayReturnType = { + before?: string; + after?: string; +}; +export const getIndexNeighboursElementsFromArray = ({ + index, + array, +}: GetIndexNeighboursElementsFromArrayArgs): GetIndexNeighboursElementsFromArrayReturnType => { + if (index === 0) { + return { + after: array.at(0), + }; + } + + if (index >= array.length) { + return { + before: array.at(-1), + }; + } + + return { + before: array.at(index - 1), + after: array.at(index), + }; +};