Fix dragging behavior below the last card when dragging below the new CTA button (#11781)

- 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 <paul@twenty.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Abdul Rahman
2025-04-30 13:04:56 +05:30
committed by GitHub
parent e6c1b70d9c
commit ea25498625
4 changed files with 132 additions and 11 deletions

View File

@ -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;

View File

@ -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}
>
<StyledNewButtonContainer>
<RecordBoardColumnNewRecordButton />
</StyledNewButtonContainer>
</div>
></div>
)}
</Draggable>
{droppableProvided?.placeholder}
<StyledNewButtonContainer>
<RecordBoardColumnNewRecordButton />
</StyledNewButtonContainer>
</StyledColumnCardsContainer>
);
};

View File

@ -0,0 +1,93 @@
import { EachTestingContext } from 'twenty-shared/testing';
import { getIndexNeighboursElementsFromArray } from '../getIndexNeighboursElementsFromArray';
type TestCase = {
expected: ReturnType<typeof getIndexNeighboursElementsFromArray>;
} & Parameters<typeof getIndexNeighboursElementsFromArray>[0];
describe('getIndexNeighboursElementsFromArray', () => {
const testCases: EachTestingContext<TestCase>[] = [
{
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);
});
});

View File

@ -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),
};
};