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:
@ -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',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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',
|
||||
};
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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',
|
||||
|
||||
Reference in New Issue
Block a user