diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx index 844388d1a..b46ee87ac 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx @@ -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', }); }); diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts index 90adec1f6..fde1df253 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts @@ -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[], ): OrderByField => { - const sortsObject: Record = {}; + 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', + }; }; 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 3fff6daa5..f8aa66909 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 @@ -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(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 ( diff --git a/packages/twenty-server/src/database/typeorm-seeds/workspace/opportunity.ts b/packages/twenty-server/src/database/typeorm-seeds/workspace/opportunity.ts index e2a201865..49f76de5e 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/workspace/opportunity.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/workspace/opportunity.ts @@ -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',