feat: order board cards by record position (#3902)

* feat: order board cards by record position

Closes #3848

* Fix tests

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Thaïs
2024-02-09 14:09:13 -03:00
committed by GitHub
parent 713ec9494d
commit d28843bb85
4 changed files with 82 additions and 63 deletions

View File

@ -9,21 +9,10 @@ const sortDefinition: SortDefinition = {
};
describe('turnSortsIntoOrderBy', () => {
it('should sort by createdAt if no sorts and createdAt field exists', () => {
it('should sort by recordPosition if no sorts', () => {
const fields = [{ id: 'field1', name: 'createdAt' }];
expect(turnSortsIntoOrderBy([], fields)).toEqual({
createdAt: 'DescNullsFirst',
});
});
it('should return empty OrderByField if no sorts and no createdAt field', () => {
expect(turnSortsIntoOrderBy([], [])).toEqual({});
});
it('should sort by first field if no sorts and createdAt field do not exists', () => {
const fields = [{ id: 'field1', name: 'field1' }];
expect(turnSortsIntoOrderBy([], fields)).toEqual({
field1: 'DescNullsFirst',
position: 'AscNullsFirst',
});
});
@ -38,6 +27,7 @@ describe('turnSortsIntoOrderBy', () => {
const fields = [{ id: 'field1', name: 'field1' }];
expect(turnSortsIntoOrderBy(sorts, fields)).toEqual({
field1: 'AscNullsFirst',
position: 'AscNullsFirst',
});
});
@ -61,6 +51,7 @@ describe('turnSortsIntoOrderBy', () => {
expect(turnSortsIntoOrderBy(sorts, fields)).toEqual({
field1: 'AscNullsFirst',
field2: 'DescNullsLast',
position: 'AscNullsFirst',
});
});

View File

@ -1,5 +1,7 @@
import { OrderBy } from '@/object-metadata/types/OrderBy';
import { OrderByField } from '@/object-metadata/types/OrderByField';
import { Field } from '~/generated/graphql';
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
import { Sort } from '../types/Sort';
@ -7,39 +9,26 @@ export const turnSortsIntoOrderBy = (
sorts: Sort[],
fields: Pick<Field, 'id' | 'name'>[],
): OrderByField => {
const sortsObject: Record<string, 'AscNullsFirst' | 'DescNullsLast'> = {};
const fieldsById = mapArrayToObject(fields, ({ id }) => id);
const sortsOrderBy = Object.fromEntries(
sorts.map((sort) => {
const correspondingField = fieldsById[sort.fieldMetadataId];
if (!sorts.length) {
const createdAtField = fields.find((field) => field.name === 'createdAt');
if (createdAtField) {
return {
createdAt: 'DescNullsFirst',
};
}
if (!correspondingField) {
throw new Error(
`Could not find field ${sort.fieldMetadataId} in metadata object`,
);
}
if (!fields.length) {
return {};
}
const direction: OrderBy =
sort.direction === 'asc' ? 'AscNullsFirst' : 'DescNullsLast';
return {
[fields[0].name]: 'DescNullsFirst',
};
}
return [correspondingField.name, direction];
}),
);
sorts.forEach((sort) => {
const correspondingField = fields.find(
(field) => field.id === sort.fieldMetadataId,
);
if (!correspondingField) {
throw new Error(
`Could not find field ${sort.fieldMetadataId} in metadata object`,
);
}
const direction =
sort.direction === 'asc' ? 'AscNullsFirst' : 'DescNullsLast';
sortsObject[correspondingField.name] = direction;
});
return sortsObject;
return {
...sortsOrderBy,
position: 'AscNullsFirst',
};
};

View File

@ -9,6 +9,7 @@ import { useRecordBoardStates } from '@/object-record/record-board/hooks/interna
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
import { RecordBoardColumn } from '@/object-record/record-board/record-board-column/components/RecordBoardColumn';
import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -48,8 +49,11 @@ export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => {
useContext(RecordBoardContext);
const boardRef = useRef<HTMLDivElement>(null);
const { getColumnIdsState, columnsFamilySelector } =
useRecordBoardStates(recordBoardId);
const {
getColumnIdsState,
columnsFamilySelector,
recordIdsByColumnIdFamilyState,
} = useRecordBoardStates(recordBoardId);
const columnIds = useRecoilValue(getColumnIdsState());
@ -66,34 +70,69 @@ export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => {
const onDragEnd: OnDragEndResponder = useRecoilCallback(
({ snapshot }) =>
async (result) => {
(result) => {
if (!result.destination) return;
const draggedRecordId = result.draggableId;
const destinationColumnId = result.destination?.droppableId;
const sourceColumnId = result.source.droppableId;
const destinationColumnId = result.destination.droppableId;
const destinationIndexInColumn = result.destination.index;
if (!destinationColumnId) {
return;
}
if (!destinationColumnId || !selectFieldMetadataItem) return;
const column = await snapshot
const column = snapshot
.getLoadable(columnsFamilySelector(destinationColumnId))
.getValue();
if (!column) {
return;
}
if (!column) return;
if (!selectFieldMetadataItem) {
return;
}
const destinationColumnRecordIds = snapshot
.getLoadable(recordIdsByColumnIdFamilyState(destinationColumnId))
.getValue();
const otherRecordsInDestinationColumn =
sourceColumnId === destinationColumnId
? destinationColumnRecordIds.filter(
(recordId) => recordId !== draggedRecordId,
)
: destinationColumnRecordIds;
const recordBeforeId =
otherRecordsInDestinationColumn[destinationIndexInColumn - 1];
const recordBefore = recordBeforeId
? snapshot
.getLoadable(recordStoreFamilyState(recordBeforeId))
.getValue()
: null;
const recordBeforePosition: number | undefined = recordBefore?.position;
const recordAfterId =
otherRecordsInDestinationColumn[destinationIndexInColumn];
const recordAfter = recordAfterId
? snapshot
.getLoadable(recordStoreFamilyState(recordAfterId))
.getValue()
: null;
const recordAfterPosition: number | undefined = recordAfter?.position;
const beforeBoundary = recordBeforePosition ?? 0;
const afterBoundary = recordAfterPosition ?? beforeBoundary + 1;
const draggedRecordPosition = (beforeBoundary + afterBoundary) / 2;
updateOneRecord({
idToUpdate: draggedRecordId,
updateOneRecordInput: {
[selectFieldMetadataItem.name]: column.value,
position: draggedRecordPosition,
},
});
},
[columnsFamilySelector, selectFieldMetadataItem, updateOneRecord],
[
columnsFamilySelector,
recordIdsByColumnIdFamilyState,
selectFieldMetadataItem,
updateOneRecord,
],
);
return (

View File

@ -32,7 +32,7 @@ export const seedOpportunity = async (
closeDate: new Date(),
probability: 0.5,
stage: 'NEW',
position: 0,
position: 1,
pipelineStepId: '6edf4ead-006a-46e1-9c6d-228f1d0143c9',
pointOfContactId: '86083141-1c0e-494c-a1b6-85b1c6fefaa5',
companyId: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
@ -45,7 +45,7 @@ export const seedOpportunity = async (
closeDate: new Date(),
probability: 0.5,
stage: 'MEETING',
position: 1,
position: 2,
pipelineStepId: 'd8361722-03fb-4e65-bd4f-ec9e52e5ec0a',
pointOfContactId: '93c72d2e-f517-42fd-80ae-14173b3b70ae',
companyId: '118995f3-5d81-46d6-bf83-f7fd33ea6102',
@ -58,7 +58,7 @@ export const seedOpportunity = async (
closeDate: new Date(),
probability: 0.5,
stage: 'PROPOSAL',
position: 2,
position: 3,
pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02',
pointOfContactId: '9b324a88-6784-4449-afdf-dc62cb8702f2',
companyId: '460b6fb1-ed89-413a-b31a-962986e67bb4',
@ -71,7 +71,7 @@ export const seedOpportunity = async (
closeDate: new Date(),
probability: 0.5,
stage: 'PROPOSAL',
position: 3,
position: 4,
pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02',
pointOfContactId: '98406e26-80f1-4dff-b570-a74942528de3',
companyId: '460b6fb1-ed89-413a-b31a-962986e67bb4',