🦣🦣🦣 Table virtualization (#14743)
This big PR implements table virtualization with an offset paging, allowing a way more fluid UX. It is a v1 that should be improved in the future with partial data loading and optimization of the browser display performance of a row. But with this PR we have the solid enough technical foundation, both frontend and backend, to get to a smooth table UX. Fixes and improvements after first successful round of development (needed to have main clean) : - [x] Delete should refresh virtualized portion only and reset all table - [x] Fix add new : top and bottom - [x] Table empty shouldn’t show when first loading - [x] Fix d&d - [x] Fix sorts - [x] Fix drag when scrolling after a full virtual page (it throws an error) - [x] Si update mais qu’on a un sort ou filter, alors il faut trigger le refresh - [x] Reset scroll position between tables - [x] Reset scroll shadows between tables - [x] Setup d&n for virtual list : https://github.com/hello-pangea/dnd/blob/main/docs/patterns/virtual-lists.md - [x] Full table re-render when entering edit mode - [x] Clean code and prepare for merge Fixes https://github.com/twentyhq/core-team-issues/issues/1613 that contains other bugs to be fixed before merge --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+6
-6
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
computeNewPositionOfRecordWithPosition,
|
||||
computeNewPositionOfDraggedRecord,
|
||||
type RecordWithPosition,
|
||||
} from '@/object-record/utils/computeNewPositionOfRecordWithPosition';
|
||||
} from '@/object-record/utils/computeNewPositionOfDraggedRecord';
|
||||
|
||||
const mockRecordsWithPosition: RecordWithPosition[] = [
|
||||
{
|
||||
@@ -24,7 +24,7 @@ const mockRecordsWithPosition: RecordWithPosition[] = [
|
||||
|
||||
describe('computeNewPositionOfRecordWithPosition', () => {
|
||||
it('should compute first position', () => {
|
||||
const newPosition = computeNewPositionOfRecordWithPosition({
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'B',
|
||||
idOfTargetItem: 'A',
|
||||
@@ -34,7 +34,7 @@ describe('computeNewPositionOfRecordWithPosition', () => {
|
||||
});
|
||||
|
||||
it('should compute last position', () => {
|
||||
const newPosition = computeNewPositionOfRecordWithPosition({
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'B',
|
||||
idOfTargetItem: 'D',
|
||||
@@ -44,7 +44,7 @@ describe('computeNewPositionOfRecordWithPosition', () => {
|
||||
});
|
||||
|
||||
it('should compute intermediary position after target item', () => {
|
||||
const newPosition = computeNewPositionOfRecordWithPosition({
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'A',
|
||||
idOfTargetItem: 'B',
|
||||
@@ -54,7 +54,7 @@ describe('computeNewPositionOfRecordWithPosition', () => {
|
||||
});
|
||||
|
||||
it('should compute intermediary position before target item', () => {
|
||||
const newPosition = computeNewPositionOfRecordWithPosition({
|
||||
const newPosition = computeNewPositionOfDraggedRecord({
|
||||
arrayOfRecordsWithPosition: mockRecordsWithPosition,
|
||||
idOfItemToMove: 'A',
|
||||
idOfTargetItem: 'C',
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
export const computeNewEvenlySpacedPositions = ({
|
||||
startingPosition,
|
||||
endingPosition,
|
||||
numberOfRecordsToInsertBetween,
|
||||
}: {
|
||||
startingPosition: number;
|
||||
endingPosition: number;
|
||||
numberOfRecordsToInsertBetween: number;
|
||||
}) => {
|
||||
const positionGapSize = endingPosition - startingPosition;
|
||||
|
||||
if (positionGapSize === 0) {
|
||||
return Array.from(
|
||||
{ length: numberOfRecordsToInsertBetween },
|
||||
() => endingPosition,
|
||||
);
|
||||
}
|
||||
|
||||
if (positionGapSize < 0) {
|
||||
throw new Error(
|
||||
`Cannot compute positions because starting position (${startingPosition}) is after ending position (${endingPosition})`,
|
||||
);
|
||||
}
|
||||
|
||||
const positionStep = positionGapSize / (numberOfRecordsToInsertBetween + 1);
|
||||
|
||||
let newPositionsBetween: number[] = [];
|
||||
|
||||
for (let i = 1; i <= numberOfRecordsToInsertBetween; i++) {
|
||||
newPositionsBetween.push(startingPosition + positionStep * i);
|
||||
}
|
||||
|
||||
return newPositionsBetween;
|
||||
};
|
||||
+19
-15
@@ -6,7 +6,7 @@ export type RecordWithPosition = {
|
||||
position: number;
|
||||
};
|
||||
|
||||
export const computeNewPositionOfRecordWithPosition = ({
|
||||
export const computeNewPositionOfDraggedRecord = ({
|
||||
arrayOfRecordsWithPosition,
|
||||
idOfItemToMove,
|
||||
idOfTargetItem,
|
||||
@@ -15,24 +15,16 @@ export const computeNewPositionOfRecordWithPosition = ({
|
||||
idOfItemToMove: string;
|
||||
idOfTargetItem: string;
|
||||
}) => {
|
||||
const itemToMove = arrayOfRecordsWithPosition.find(
|
||||
(recordToFind) => recordToFind.id === idOfItemToMove,
|
||||
);
|
||||
|
||||
const targetItem = arrayOfRecordsWithPosition.find(
|
||||
(recordToFind) => recordToFind.id === idOfTargetItem,
|
||||
);
|
||||
|
||||
if (!isDefined(itemToMove)) {
|
||||
throw new Error(`Cannot find item to move for id : ${idOfItemToMove}`);
|
||||
}
|
||||
|
||||
if (!isDefined(targetItem)) {
|
||||
throw new Error(`Cannot find item to move for id : ${idOfTargetItem}`);
|
||||
}
|
||||
|
||||
if (itemToMove.id === targetItem.id) {
|
||||
return itemToMove.position;
|
||||
if (idOfItemToMove === idOfTargetItem) {
|
||||
return targetItem.position;
|
||||
}
|
||||
|
||||
const targetPosition = targetItem.position;
|
||||
@@ -44,23 +36,35 @@ export const computeNewPositionOfRecordWithPosition = ({
|
||||
const indexOfItemToMove = sortedRecordsByAscendingPosition.findIndex(
|
||||
(recordToFind) => recordToFind.id === idOfItemToMove,
|
||||
);
|
||||
|
||||
const itemToMoveIsNotInTable = indexOfItemToMove === -1;
|
||||
|
||||
const indexOfTargetItem = sortedRecordsByAscendingPosition.findIndex(
|
||||
(recordToFind) => recordToFind.id === idOfTargetItem,
|
||||
);
|
||||
|
||||
const lastIndex = sortedRecordsByAscendingPosition.length - 1;
|
||||
|
||||
const shouldGoToFirstPosition =
|
||||
indexOfItemToMove > 0 && indexOfTargetItem === 0;
|
||||
const shouldGoToFirstPosition = indexOfTargetItem === 0;
|
||||
|
||||
const shouldGoToLastPosition =
|
||||
indexOfItemToMove < lastIndex && indexOfTargetItem === lastIndex;
|
||||
const shouldGoToLastPosition = indexOfTargetItem === lastIndex;
|
||||
|
||||
if (shouldGoToFirstPosition) {
|
||||
return targetPosition - 1;
|
||||
} else if (shouldGoToLastPosition) {
|
||||
return targetPosition + 1;
|
||||
} else {
|
||||
if (itemToMoveIsNotInTable) {
|
||||
const itemBeforeTargetItem =
|
||||
sortedRecordsByAscendingPosition[indexOfTargetItem - 1];
|
||||
|
||||
const intermediaryPosition =
|
||||
targetItem.position -
|
||||
(targetItem.position - itemBeforeTargetItem.position) / 2;
|
||||
|
||||
return intermediaryPosition;
|
||||
}
|
||||
|
||||
const shouldGoAfterTargetItem = indexOfItemToMove < indexOfTargetItem;
|
||||
|
||||
if (shouldGoAfterTargetItem) {
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import { computeNewEvenlySpacedPositions } from '@/object-record/utils/computeNewEvenlySpacedPositions';
|
||||
import { type RecordWithPosition } from '@/object-record/utils/computeNewPositionOfDraggedRecord';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
// TODO : refactor this
|
||||
export const computeNewPositionsOfDraggedRecords = ({
|
||||
arrayOfRecordsWithPosition,
|
||||
draggedRecordId,
|
||||
targetRecordId,
|
||||
sourceRecordIds,
|
||||
}: {
|
||||
arrayOfRecordsWithPosition: RecordWithPosition[];
|
||||
draggedRecordId: string;
|
||||
targetRecordId: string;
|
||||
sourceRecordIds: string[];
|
||||
}): RecordWithPosition[] | null => {
|
||||
const targetItem = arrayOfRecordsWithPosition.find(
|
||||
(recordToFind) => recordToFind.id === targetRecordId,
|
||||
);
|
||||
|
||||
if (!isDefined(targetItem)) {
|
||||
throw new Error(`Cannot find item to move for id : ${targetRecordId}`);
|
||||
}
|
||||
|
||||
if (targetRecordId === draggedRecordId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetIsInSourceRecordIds = sourceRecordIds.includes(targetRecordId);
|
||||
|
||||
if (targetIsInSourceRecordIds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetPosition = targetItem.position;
|
||||
|
||||
const indexOfItemToMove = arrayOfRecordsWithPosition.findIndex(
|
||||
(recordToFind) => recordToFind.id === draggedRecordId,
|
||||
);
|
||||
|
||||
const itemToMoveIsNotInTable = indexOfItemToMove === -1;
|
||||
|
||||
const indexOfTargetItem = arrayOfRecordsWithPosition.findIndex(
|
||||
(recordToFind) => recordToFind.id === targetRecordId,
|
||||
);
|
||||
|
||||
const lastIndex = arrayOfRecordsWithPosition.length - 1;
|
||||
|
||||
const shouldGoToFirstPosition = indexOfTargetItem === 0;
|
||||
|
||||
const shouldGoToLastPosition = indexOfTargetItem === lastIndex;
|
||||
|
||||
if (shouldGoToFirstPosition) {
|
||||
const newPositions = computeNewEvenlySpacedPositions({
|
||||
startingPosition: targetPosition - 1,
|
||||
endingPosition: targetPosition,
|
||||
numberOfRecordsToInsertBetween: sourceRecordIds.length,
|
||||
});
|
||||
|
||||
const newSourceRecordsWithPosition: RecordWithPosition[] =
|
||||
sourceRecordIds.map((recordId, index) => ({
|
||||
id: recordId,
|
||||
position: newPositions[index],
|
||||
}));
|
||||
|
||||
return newSourceRecordsWithPosition;
|
||||
} else if (shouldGoToLastPosition) {
|
||||
const newPositions = computeNewEvenlySpacedPositions({
|
||||
startingPosition: targetPosition,
|
||||
endingPosition: targetPosition + sourceRecordIds.length + 1,
|
||||
numberOfRecordsToInsertBetween: sourceRecordIds.length,
|
||||
});
|
||||
|
||||
const newSourceRecordsWithPosition: RecordWithPosition[] =
|
||||
sourceRecordIds.map((recordId, index) => ({
|
||||
id: recordId,
|
||||
position: newPositions[index],
|
||||
}));
|
||||
|
||||
return newSourceRecordsWithPosition;
|
||||
} else {
|
||||
if (itemToMoveIsNotInTable) {
|
||||
const itemBeforeTargetItem =
|
||||
arrayOfRecordsWithPosition[indexOfTargetItem - 1];
|
||||
|
||||
const newPositions = computeNewEvenlySpacedPositions({
|
||||
startingPosition: itemBeforeTargetItem.position,
|
||||
endingPosition: targetItem.position,
|
||||
numberOfRecordsToInsertBetween: sourceRecordIds.length,
|
||||
});
|
||||
|
||||
const newSourceRecordsWithPosition: RecordWithPosition[] =
|
||||
sourceRecordIds.map((recordId, index) => ({
|
||||
id: recordId,
|
||||
position: newPositions[index],
|
||||
}));
|
||||
|
||||
return newSourceRecordsWithPosition;
|
||||
}
|
||||
|
||||
const shouldGoAfterTargetItem = indexOfItemToMove < indexOfTargetItem;
|
||||
|
||||
if (shouldGoAfterTargetItem) {
|
||||
const itemAfterTargetItem =
|
||||
arrayOfRecordsWithPosition[indexOfTargetItem + 1];
|
||||
|
||||
const newPositions = computeNewEvenlySpacedPositions({
|
||||
startingPosition: targetItem.position,
|
||||
endingPosition: itemAfterTargetItem.position,
|
||||
numberOfRecordsToInsertBetween: sourceRecordIds.length,
|
||||
});
|
||||
|
||||
const newSourceRecordsWithPosition: RecordWithPosition[] =
|
||||
sourceRecordIds.map((recordId, index) => ({
|
||||
id: recordId,
|
||||
position: newPositions[index],
|
||||
}));
|
||||
|
||||
return newSourceRecordsWithPosition;
|
||||
} else {
|
||||
const itemBeforeTargetItem =
|
||||
arrayOfRecordsWithPosition[indexOfTargetItem - 1];
|
||||
|
||||
const newPositions = computeNewEvenlySpacedPositions({
|
||||
startingPosition: itemBeforeTargetItem.position,
|
||||
endingPosition: targetItem.position,
|
||||
numberOfRecordsToInsertBetween: sourceRecordIds.length,
|
||||
});
|
||||
|
||||
const newSourceRecordsWithPosition: RecordWithPosition[] =
|
||||
sourceRecordIds.map((recordId, index) => ({
|
||||
id: recordId,
|
||||
position: newPositions[index],
|
||||
}));
|
||||
|
||||
return newSourceRecordsWithPosition;
|
||||
}
|
||||
}
|
||||
};
|
||||
+2
-2
@@ -32,12 +32,12 @@ query FindMany${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}FilterInput, $orderBy: [${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}OrderByInput], $lastCursor: String, $limit: Int) {
|
||||
)}OrderByInput], $lastCursor: String, $limit: Int, $offset: Int) {
|
||||
${objectMetadataItem.namePlural}(filter: $filter, orderBy: $orderBy, ${
|
||||
cursorDirection === 'before'
|
||||
? 'last: $limit, before: $lastCursor'
|
||||
: 'first: $limit, after: $lastCursor'
|
||||
} ){
|
||||
}, offset: $offset){
|
||||
edges {
|
||||
node ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
|
||||
Reference in New Issue
Block a user