diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx
index 29c46235d9..8234d7005f 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx
@@ -1,12 +1,6 @@
-import { RecordBoardAddGroupColumn } from '@/object-record/record-board/components/RecordBoardAddGroupColumn';
-import { RecordBoardColumnHeaderWrapper } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper';
-import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext';
-import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
+import { RecordBoardColumnDnd } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDnd';
import { RecordIndexGroupAggregatesDataLoader } from '@/object-record/record-index/components/RecordIndexGroupAggregatesDataLoader';
-import { useAtomComponentFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilySelectorValue';
-import { ViewType } from '@/views/types/ViewType';
import { styled } from '@linaria/react';
-import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledHeaderContainer = styled.div`
display: flex;
@@ -21,32 +15,12 @@ const StyledHeaderContainer = styled.div`
position: sticky;
top: 0;
}
-
- & > *:not(:first-of-type) {
- border-left: 1px solid ${themeCssVariables.border.color.light};
- }
`;
export const RecordBoardHeader = () => {
- const visibleRecordGroupIds = useAtomComponentFamilySelectorValue(
- visibleRecordGroupIdsComponentFamilySelector,
- ViewType.KANBAN,
- );
-
return (
);
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDnd.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDnd.tsx
new file mode 100644
index 0000000000..80b4079156
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDnd.tsx
@@ -0,0 +1,55 @@
+import { Fragment } from 'react';
+
+import { RecordBoardAddGroupColumn } from '@/object-record/record-board/components/RecordBoardAddGroupColumn';
+import { RecordBoardColumnHeaderWrapper } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper';
+import { RECORD_BOARD_COLUMN_DROPPABLE_ID } from '@/object-record/record-board/record-board-column/dnd/constants/RecordBoardColumnDroppableId';
+import { RecordBoardColumnDndKitProvider } from '@/object-record/record-board/record-board-column/dnd/providers/RecordBoardColumnDndKitProvider';
+import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
+import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext';
+import { DragDropColumnDropTarget } from '@/ui/utilities/drag-and-drop/components/DragDropColumnDropTarget';
+import { DragDropColumnDroppableSlot } from '@/ui/utilities/drag-and-drop/components/DragDropColumnDroppableSlot';
+import { DragDropColumnSortableCell } from '@/ui/utilities/drag-and-drop/components/DragDropColumnSortableCell';
+import { useAtomComponentFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilySelectorValue';
+import { ViewType } from '@/views/types/ViewType';
+
+export const RecordBoardColumnDnd = () => {
+ const visibleRecordGroupIds = useAtomComponentFamilySelectorValue(
+ visibleRecordGroupIdsComponentFamilySelector,
+ ViewType.KANBAN,
+ );
+
+ return (
+
+
+
+
+ {visibleRecordGroupIds.map((recordGroupId, index) => (
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx
index dc0832f35b..1d0a575db6 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx
@@ -6,7 +6,7 @@ import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPe
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu';
import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown';
-
+import { DragDropColumnSortableHandle } from '@/ui/utilities/drag-and-drop/components/DragDropColumnSortableHandle';
import { RECORD_BOARD_COLUMN_WIDTH } from '@/object-record/record-board/constants/RecordBoardColumnWidth';
import { RECORD_BOARD_COLUMN_WIDTH_CSS_VARIABLE_NAME } from '@/object-record/record-board/constants/RecordBoardColumnWidthCssVariableName';
import { RecordBoardColumnResizeHandler } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnResizeHandler';
@@ -19,6 +19,7 @@ import { recordIndexAggregateDisplayValueForGroupValueComponentFamilyState } fro
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { canCreateRecordsForObjectMetadataItem } from '@/object-record/utils/canCreateRecordsForObjectMetadataItem';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
+import { useDisableDragSelectOnPointerDown } from '@/ui/utilities/drag-select/hooks/useDisableDragSelectOnPointerDown';
import { useToggleDropdown } from '@/ui/layout/dropdown/hooks/useToggleDropdown';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
@@ -75,6 +76,10 @@ const StyledColumn = styled.div`
padding: ${themeCssVariables.spacing[2]};
position: relative;
+
+ &[data-has-left-border='true'] {
+ border-left: 1px solid ${themeCssVariables.border.color.light};
+ }
`;
const StyledTagContainer = styled.div`
@@ -89,7 +94,15 @@ const StyledDropdownContainer = styled.div`
`;
export const RecordBoardColumnHeader = () => {
- const { columnDefinition } = useContext(RecordBoardColumnContext);
+ const { columnDefinition, columnIndex } = useContext(
+ RecordBoardColumnContext,
+ );
+
+ const {
+ onPointerCancel: handlePointerCancel,
+ onPointerDown: handlePointerDown,
+ onPointerUp: handlePointerUp,
+ } = useDisableDragSelectOnPointerDown();
const [isHeaderHovered, setIsHeaderHovered] = useState(false);
@@ -136,64 +149,69 @@ export const RecordBoardColumnHeader = () => {
};
return (
-
- setIsHeaderHovered(true)}
- onMouseLeave={() => setIsHeaderHovered(false)}
- >
-
-
-
-
-
-
- }
- dropdownComponents={}
- />
-
-
-
-
-
- {isHeaderHovered && (
-
- {
- toggleDropdown({
- dropdownComponentInstanceIdFromProps: dropdownId,
- });
+ 0 ? 'true' : undefined}>
+
+ setIsHeaderHovered(true)}
+ onMouseLeave={() => setIsHeaderHovered(false)}
+ onPointerCancel={handlePointerCancel}
+ onPointerDown={handlePointerDown}
+ onPointerUp={handlePointerUp}
+ >
+
+
+
+
+
+
+ }
+ dropdownComponents={}
/>
- {canCreateRecords && !hasAnySoftDeleteFilterOnView && (
+
+
+
+
+
+ {isHeaderHovered && (
+
{
+ toggleDropdown({
+ dropdownComponentInstanceIdFromProps: dropdownId,
+ });
+ }}
/>
- )}
-
- )}
-
-
-
+ {canCreateRecords && !hasAnySoftDeleteFilterOnView && (
+
+ )}
+
+ )}
+
+
+
+
);
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/dnd/constants/RecordBoardColumnDroppableId.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/dnd/constants/RecordBoardColumnDroppableId.ts
new file mode 100644
index 0000000000..d726b339c3
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/dnd/constants/RecordBoardColumnDroppableId.ts
@@ -0,0 +1 @@
+export const RECORD_BOARD_COLUMN_DROPPABLE_ID = 'record-board-header';
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/dnd/hooks/useRecordBoardColumnDndKit.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/dnd/hooks/useRecordBoardColumnDndKit.ts
new file mode 100644
index 0000000000..af59de5579
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/dnd/hooks/useRecordBoardColumnDndKit.ts
@@ -0,0 +1,199 @@
+import { type DragDropProvider } from '@dnd-kit/react';
+import { type ComponentProps, useState } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+
+import { RECORD_GROUP_REORDER_CONFIRMATION_MODAL_ID } from '@/object-record/record-group/constants/RecordGroupReorderConfirmationModalId';
+import { useReorderRecordGroups } from '@/object-record/record-group/hooks/useReorderRecordGroups';
+import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
+import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
+import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
+import { recordIndexKanbanColumnWidthComponentState } from '@/object-record/record-index/states/recordIndexKanbanColumnWidthComponentState';
+import { recordIndexRecordGroupIsDraggableSortComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexRecordGroupIsDraggableSortComponentSelector';
+import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
+import { useModal } from '@/ui/layout/modal/hooks/useModal';
+import { type DragDropColumnData } from '@/ui/utilities/drag-and-drop/types/DragDropColumnData';
+import { resolveDragDropColumnDrop } from '@/ui/utilities/drag-and-drop/utils/resolveDragDropColumnDrop';
+import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
+import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
+import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
+import { useAtomComponentFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilySelectorValue';
+import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
+import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { ViewType } from '@/views/types/ViewType';
+
+type DragStartPayload = Parameters<
+ NonNullable<
+ ComponentProps>['onDragStart']
+ >
+>[0];
+type DragMovePayload = Parameters<
+ NonNullable<
+ ComponentProps>['onDragMove']
+ >
+>[0];
+type DragEndPayload = Parameters<
+ NonNullable<
+ ComponentProps>['onDragEnd']
+ >
+>[0];
+
+type PendingReorder = {
+ fromIndex: number;
+ toIndex: number;
+};
+
+export type RecordBoardColumnDndKitContextValues = {
+ activeDropTargetIndex: number | null;
+};
+
+export const useRecordBoardColumnDndKit = (): {
+ contextValues: RecordBoardColumnDndKitContextValues;
+ handlePendingReorderConfirmClick: () => void;
+ handlers: {
+ onDragStart: (event: DragStartPayload) => void;
+ onDragMove: (event: DragMovePayload) => void;
+ onDragEnd: (event: DragEndPayload) => void;
+ };
+} => {
+ const { openModal } = useModal();
+ const { recordIndexId } = useRecordIndexIdFromCurrentContextStore();
+ const { reorderRecordGroups } = useReorderRecordGroups({
+ recordIndexId,
+ viewType: ViewType.KANBAN,
+ });
+ const { setDragSelectionStartEnabled } = useDragSelect();
+ const { getScrollWrapperElement } = useScrollWrapperHTMLElement();
+
+ const isRecordGroupDraggableSort = useAtomComponentSelectorValue(
+ recordIndexRecordGroupIsDraggableSortComponentSelector,
+ recordIndexId,
+ );
+
+ const visibleRecordGroupIds = useAtomComponentFamilySelectorValue(
+ visibleRecordGroupIdsComponentFamilySelector,
+ ViewType.KANBAN,
+ );
+ const recordIndexKanbanColumnWidth = useAtomComponentStateValue(
+ recordIndexKanbanColumnWidthComponentState,
+ );
+
+ const [, setRecordIndexRecordGroupSort] = useAtomComponentState(
+ recordIndexRecordGroupSortComponentState,
+ );
+
+ const [activeDropTargetIndex, setActiveDropTargetIndex] = useState<
+ number | null
+ >(null);
+
+ const [pendingReorder, setPendingReorder] = useState(
+ null,
+ );
+
+ const resolveDropFromPointerX = ({
+ pointerX,
+ sourceIndex,
+ }: {
+ pointerX: number;
+ sourceIndex: number;
+ }) => {
+ const { scrollWrapperElement } = getScrollWrapperElement();
+ if (!isDefined(scrollWrapperElement)) return null;
+
+ const columnWidths = visibleRecordGroupIds.map(
+ () => recordIndexKanbanColumnWidth,
+ );
+
+ if (columnWidths.length === 0) {
+ return null;
+ }
+
+ return resolveDragDropColumnDrop({
+ pointerX,
+ sourceIndex,
+ scrollWrapperElement,
+ columnWidths,
+ });
+ };
+
+ const handleDragStart = (_event: DragStartPayload) => {
+ setActiveDropTargetIndex(null);
+ };
+
+ const handleDragMove = (event: DragMovePayload) => {
+ const { operation } = event;
+ const sourceIndex = operation.source?.data.index;
+
+ if (!isDefined(sourceIndex)) {
+ setActiveDropTargetIndex(null);
+ return;
+ }
+
+ const resolvedDrop = resolveDropFromPointerX({
+ pointerX: operation.position.current.x,
+ sourceIndex,
+ });
+
+ setActiveDropTargetIndex((currentActiveDropTargetIndex) => {
+ const nextActiveDropTargetIndex = resolvedDrop?.dropTargetIndex ?? null;
+
+ return currentActiveDropTargetIndex === nextActiveDropTargetIndex
+ ? currentActiveDropTargetIndex
+ : nextActiveDropTargetIndex;
+ });
+ };
+
+ const handleDragEnd = (event: DragEndPayload) => {
+ const { operation } = event;
+ const source = operation.source;
+
+ setActiveDropTargetIndex(null);
+ setDragSelectionStartEnabled(true);
+
+ if (event.canceled || !isDefined(source)) {
+ return;
+ }
+
+ const sourceIndex = source.data.index;
+ const resolvedDrop = resolveDropFromPointerX({
+ pointerX: operation.position.current.x,
+ sourceIndex,
+ });
+
+ if (!isDefined(resolvedDrop)) return;
+ if (resolvedDrop.sourceIndex === resolvedDrop.destinationIndex) return;
+
+ if (!isRecordGroupDraggableSort) {
+ setPendingReorder({
+ fromIndex: resolvedDrop.sourceIndex,
+ toIndex: resolvedDrop.destinationIndex,
+ });
+ openModal(RECORD_GROUP_REORDER_CONFIRMATION_MODAL_ID);
+ return;
+ }
+
+ reorderRecordGroups({
+ fromIndex: resolvedDrop.sourceIndex,
+ toIndex: resolvedDrop.destinationIndex,
+ });
+ };
+
+ const handlePendingReorderConfirmClick = () => {
+ if (!isDefined(pendingReorder)) return;
+
+ setRecordIndexRecordGroupSort(RecordGroupSort.Manual);
+ reorderRecordGroups(pendingReorder);
+ setPendingReorder(null);
+ };
+
+ return {
+ contextValues: {
+ activeDropTargetIndex,
+ },
+ handlePendingReorderConfirmClick,
+ handlers: {
+ onDragStart: handleDragStart,
+ onDragMove: handleDragMove,
+ onDragEnd: handleDragEnd,
+ },
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/dnd/providers/RecordBoardColumnDndKitProvider.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/dnd/providers/RecordBoardColumnDndKitProvider.tsx
new file mode 100644
index 0000000000..e20012a51c
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/dnd/providers/RecordBoardColumnDndKitProvider.tsx
@@ -0,0 +1,37 @@
+import { DragDropProvider } from '@dnd-kit/react';
+import type { ReactNode } from 'react';
+
+import { useRecordBoardColumnDndKit } from '@/object-record/record-board/record-board-column/dnd/hooks/useRecordBoardColumnDndKit';
+import { RecordGroupReorderConfirmationModal } from '@/object-record/record-group/components/RecordGroupReorderConfirmationModal';
+import { DragDropColumnDndContext } from '@/ui/utilities/drag-and-drop/context/DragDropColumnDndContext';
+import { DND_KIT_SENSORS } from '@/ui/utilities/drag-and-drop/constants/DndKitSensors';
+import { type DragDropColumnData } from '@/ui/utilities/drag-and-drop/types/DragDropColumnData';
+
+type RecordBoardColumnDndKitProviderProps = {
+ children: ReactNode;
+};
+
+export const RecordBoardColumnDndKitProvider = ({
+ children,
+}: RecordBoardColumnDndKitProviderProps) => {
+ const { contextValues, handlers, handlePendingReorderConfirmClick } =
+ useRecordBoardColumnDndKit();
+
+ return (
+ <>
+
+
+ sensors={DND_KIT_SENSORS}
+ onDragStart={handlers.onDragStart}
+ onDragMove={handlers.onDragMove}
+ onDragEnd={handlers.onDragEnd}
+ >
+ {children}
+
+
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx
index 086029e2ac..10b094168d 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx
@@ -6,7 +6,7 @@ import { RecordTableColumnHead } from '@/object-record/record-table/record-table
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
import { RecordTableHeaderCellContainer } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer';
import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler';
-import { RecordTableHeaderSortableHandle } from '@/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderSortableHandle';
+import { DragDropColumnSortableHandle } from '@/ui/utilities/drag-and-drop/components/DragDropColumnSortableHandle';
import { isRecordTableColumnHeadersReadOnlyComponentState } from '@/object-record/record-table/states/isRecordTableColumnHeadersReadOnlyComponentState';
import { isRecordTableColumnResizableComponentState } from '@/object-record/record-table/states/isRecordTableColumnResizableComponentState';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
@@ -15,7 +15,7 @@ import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/rec
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthClassName';
-import { useDisableDragSelectOnPointerDown } from '@/object-record/record-table/record-table-header/hooks/useDisableDragSelectOnPointerDown';
+import { useDisableDragSelectOnPointerDown } from '@/ui/utilities/drag-select/hooks/useDisableDragSelectOnPointerDown';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -104,7 +104,7 @@ export const RecordTableHeaderCell = ({
position="left"
/>
)}
-
+
{isRecordTableColumnHeadersReadOnly ? (
) : (
@@ -113,7 +113,7 @@ export const RecordTableHeaderCell = ({
objectMetadataId={objectMetadataItem.id}
/>
)}
-
+
{isRecordTableColumnResizable && (
{
return (
-
-
-
+
+
{isDefined(firstScrollableRecordField) && (
- {
-
+
)}
{recordFieldsWithoutFirstTwo.map((recordField, index) => (
-
-
-
-
+
+ {
recordField={recordField}
recordFieldIndex={index + 2}
/>
-
+
))}
-
-
-
+
{isRecordTableColumnHeadersReadOnly ? (
) : (
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderFirstScrollableCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderFirstScrollableCell.tsx
index a8e81f33fb..9a01379229 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderFirstScrollableCell.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderFirstScrollableCell.tsx
@@ -8,7 +8,7 @@ import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/rec
import { RecordTableHeaderCellContainer } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer';
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
-import { RecordTableHeaderSortableHandle } from '@/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderSortableHandle';
+import { DragDropColumnSortableHandle } from '@/ui/utilities/drag-and-drop/components/DragDropColumnSortableHandle';
import { isRecordTableColumnHeadersReadOnlyComponentState } from '@/object-record/record-table/states/isRecordTableColumnHeadersReadOnlyComponentState';
import { isRecordTableColumnResizableComponentState } from '@/object-record/record-table/states/isRecordTableColumnResizableComponentState';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
@@ -17,12 +17,12 @@ import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/rec
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthClassName';
+import { useDisableDragSelectOnPointerDown } from '@/ui/utilities/drag-select/hooks/useDisableDragSelectOnPointerDown';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { cx } from '@linaria/core';
import { isDefined } from 'twenty-shared/utils';
-import { useDisableDragSelectOnPointerDown } from '@/object-record/record-table/record-table-header/hooks/useDisableDragSelectOnPointerDown';
type RecordTableHeaderFirstScrollableCellProps = {
firstScrollableRecordField: RecordField;
@@ -99,7 +99,7 @@ export const RecordTableHeaderFirstScrollableCell = ({
{isRecordTableColumnResizable && (
)}
-
+
{isRecordTableColumnHeadersReadOnly ? (
) : (
@@ -108,7 +108,7 @@ export const RecordTableHeaderFirstScrollableCell = ({
objectMetadataId={objectMetadataItem.id}
/>
)}
-
+
{isRecordTableColumnResizable && (
)}
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderSortableHandle.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderSortableHandle.tsx
deleted file mode 100644
index 4a39702589..0000000000
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderSortableHandle.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { styled } from '@linaria/react';
-import { type ReactNode, useContext } from 'react';
-
-import { RecordTableHeaderSortableHandleRefContext } from '@/object-record/record-table/record-table-header/dnd/context/RecordTableHeaderSortableHandleRefContext';
-
-const StyledSortableHandle = styled.div`
- height: 100%;
- min-width: 0;
- outline: none;
-`;
-
-type RecordTableHeaderSortableHandleProps = {
- children: ReactNode;
-};
-
-export const RecordTableHeaderSortableHandle = ({
- children,
-}: RecordTableHeaderSortableHandleProps) => {
- const sortableHandleRef = useContext(
- RecordTableHeaderSortableHandleRefContext,
- );
-
- return (
-
- {children}
-
- );
-};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/context/RecordTableHeaderDndContext.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/context/RecordTableHeaderDndContext.tsx
deleted file mode 100644
index f566d8542c..0000000000
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/context/RecordTableHeaderDndContext.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { createContext } from 'react';
-
-type RecordTableHeaderDndContextValue = {
- activeDropTargetIndex: number | null;
-};
-
-export const RecordTableHeaderDndContext =
- createContext({
- activeDropTargetIndex: null,
- });
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/hooks/useRecordTableHeaderDndKit.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/hooks/useRecordTableHeaderDndKit.ts
index 8e31c800a6..7e6852fc51 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/hooks/useRecordTableHeaderDndKit.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/hooks/useRecordTableHeaderDndKit.ts
@@ -1,44 +1,35 @@
import { type DragDropProvider } from '@dnd-kit/react';
-import { useStore } from 'jotai';
-import { type ComponentProps, useCallback, useState } from 'react';
+import { type ComponentProps, useState } from 'react';
import { filterOutByProperty, isDefined } from 'twenty-shared/utils';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RECORD_TABLE_COLUMN_CHECKBOX_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnCheckboxWidth';
import { RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnDragAndDropWidth';
import { useReorderVisibleRecordFields } from '@/object-record/record-field/hooks/useReorderVisibleRecordFields';
-import { resolveRecordTableHeaderDrop } from '@/object-record/record-table/record-table-header/dnd/utils/resolveRecordTableHeaderDrop';
import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields';
import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField';
-import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
-import { isRecordTableHeaderDropProcessingComponentState } from '@/object-record/record-table/record-table-header/states/isRecordTableHeaderDropProcessingComponentState';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
-import { type RecordTableHeaderDndData } from '@/object-record/record-table/record-table-header/dnd/types/RecordTableHeaderDndData';
import { isRecordTableCheckboxColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableCheckboxColumnHiddenComponentState';
import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState';
+import { type DragDropColumnData } from '@/ui/utilities/drag-and-drop/types/DragDropColumnData';
+import { resolveDragDropColumnDrop } from '@/ui/utilities/drag-and-drop/utils/resolveDragDropColumnDrop';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
type DragStartPayload = Parameters<
NonNullable<
- ComponentProps<
- typeof DragDropProvider
- >['onDragStart']
+ ComponentProps>['onDragStart']
>
>[0];
type DragMovePayload = Parameters<
NonNullable<
- ComponentProps<
- typeof DragDropProvider
- >['onDragMove']
+ ComponentProps>['onDragMove']
>
>[0];
type DragEndPayload = Parameters<
NonNullable<
- ComponentProps<
- typeof DragDropProvider
- >['onDragEnd']
+ ComponentProps>['onDragEnd']
>
>[0];
@@ -54,8 +45,6 @@ export const useRecordTableHeaderDndKit = (): {
onDragEnd: (event: DragEndPayload) => void;
};
} => {
- const store = useStore();
-
const { recordTableId, visibleRecordFields } = useRecordTableContextOrThrow();
const { labelIdentifierFieldMetadataItem } = useRecordIndexContextOrThrow();
const { reorderVisibleRecordFields } =
@@ -64,10 +53,6 @@ export const useRecordTableHeaderDndKit = (): {
const { setDragSelectionStartEnabled } = useDragSelect();
const { getScrollWrapperElement } = useScrollWrapperHTMLElement();
- const isRecordTableHeaderDropProcessingCallbackState =
- useAtomComponentStateCallbackState(
- isRecordTableHeaderDropProcessingComponentState,
- );
const isRecordTableDragColumnHidden = useAtomComponentStateValue(
isRecordTableDragColumnHiddenComponentState,
);
@@ -100,57 +85,53 @@ export const useRecordTableHeaderDndKit = (): {
: RECORD_TABLE_COLUMN_CHECKBOX_WIDTH) +
(labelIdentifierRecordField?.size ?? 0);
- const resolveDropFromPointerX = useCallback(
- ({ pointerX, sourceIndex }: { pointerX: number; sourceIndex: number }) => {
- const { scrollWrapperElement } = getScrollWrapperElement();
- if (!isDefined(scrollWrapperElement)) return null;
+ const resolveDropFromPointerX = ({
+ pointerX,
+ sourceIndex,
+ }: {
+ pointerX: number;
+ sourceIndex: number;
+ }) => {
+ const { scrollWrapperElement } = getScrollWrapperElement();
+ if (!isDefined(scrollWrapperElement)) return null;
- return resolveRecordTableHeaderDrop({
- pointerX,
- sourceIndex,
- scrollWrapperElement,
- nonSortableColumnsWidth,
- recordFields: recordFieldsWithoutLabelIdentifier,
- });
- },
- [
- getScrollWrapperElement,
- nonSortableColumnsWidth,
- recordFieldsWithoutLabelIdentifier,
- ],
- );
+ return resolveDragDropColumnDrop({
+ pointerX,
+ sourceIndex,
+ scrollWrapperElement,
+ columnWidths: recordFieldsWithoutLabelIdentifier.map(
+ (recordField) => recordField.size,
+ ),
+ leadingOffset: nonSortableColumnsWidth,
+ });
+ };
const handleDragStart = (_event: DragStartPayload) => {
- store.set(isRecordTableHeaderDropProcessingCallbackState, true);
-
setActiveDropTargetIndex(null);
};
- const handleDragMove = useCallback(
- (event: DragMovePayload) => {
- const { operation } = event;
- const sourceIndex = operation.source?.data.index;
+ const handleDragMove = (event: DragMovePayload) => {
+ const { operation } = event;
+ const sourceIndex = operation.source?.data.index;
- if (!isDefined(sourceIndex)) {
- setActiveDropTargetIndex(null);
- return;
- }
+ if (!isDefined(sourceIndex)) {
+ setActiveDropTargetIndex(null);
+ return;
+ }
- const resolvedDrop = resolveDropFromPointerX({
- pointerX: operation.position.current.x,
- sourceIndex,
- });
+ const resolvedDrop = resolveDropFromPointerX({
+ pointerX: operation.position.current.x,
+ sourceIndex,
+ });
- setActiveDropTargetIndex((currentActiveDropTargetIndex) => {
- const nextActiveDropTargetIndex = resolvedDrop?.dropTargetIndex ?? null;
+ setActiveDropTargetIndex((currentActiveDropTargetIndex) => {
+ const nextActiveDropTargetIndex = resolvedDrop?.dropTargetIndex ?? null;
- return currentActiveDropTargetIndex === nextActiveDropTargetIndex
- ? currentActiveDropTargetIndex
- : nextActiveDropTargetIndex;
- });
- },
- [resolveDropFromPointerX, setActiveDropTargetIndex],
- );
+ return currentActiveDropTargetIndex === nextActiveDropTargetIndex
+ ? currentActiveDropTargetIndex
+ : nextActiveDropTargetIndex;
+ });
+ };
const handleDragEnd = (event: DragEndPayload) => {
const { operation } = event;
@@ -158,7 +139,6 @@ export const useRecordTableHeaderDndKit = (): {
setActiveDropTargetIndex(null);
setDragSelectionStartEnabled(true);
- store.set(isRecordTableHeaderDropProcessingCallbackState, false);
if (event.canceled) return;
if (!isDefined(source)) return;
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/providers/RecordTableHeaderDndKitProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/providers/RecordTableHeaderDndKitProvider.tsx
index ebcc1f139d..2ea9d55348 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/providers/RecordTableHeaderDndKitProvider.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/providers/RecordTableHeaderDndKitProvider.tsx
@@ -1,10 +1,10 @@
import { DragDropProvider } from '@dnd-kit/react';
import type { ReactNode } from 'react';
-import { type RecordTableHeaderDndData } from '@/object-record/record-table/record-table-header/dnd/types/RecordTableHeaderDndData';
-import { RecordTableHeaderDndContext } from '@/object-record/record-table/record-table-header/dnd/context/RecordTableHeaderDndContext';
import { useRecordTableHeaderDndKit } from '@/object-record/record-table/record-table-header/dnd/hooks/useRecordTableHeaderDndKit';
+import { DragDropColumnDndContext } from '@/ui/utilities/drag-and-drop/context/DragDropColumnDndContext';
import { DND_KIT_SENSORS } from '@/ui/utilities/drag-and-drop/constants/DndKitSensors';
+import { type DragDropColumnData } from '@/ui/utilities/drag-and-drop/types/DragDropColumnData';
type RecordTableHeaderDndKitProviderProps = {
children: ReactNode;
@@ -16,8 +16,8 @@ export const RecordTableHeaderDndKitProvider = ({
const { contextValues, handlers } = useRecordTableHeaderDndKit();
return (
-
-
+
+
sensors={DND_KIT_SENSORS}
onDragStart={handlers.onDragStart}
onDragMove={handlers.onDragMove}
@@ -25,6 +25,6 @@ export const RecordTableHeaderDndKitProvider = ({
>
{children}
-
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useDisableDragSelectOnPointerDown.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useDisableDragSelectOnPointerDown.ts
deleted file mode 100644
index a3b30bc54e..0000000000
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useDisableDragSelectOnPointerDown.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
-import { useCallback } from 'react';
-
-// Prevents the record table cell drag-select from starting when interacting with a column header.
-// Pointer capture is intentionally avoided: capturing on pointerdown retargets the pointerup/click
-// to the cell and stops the column header dropdown from opening. dnd-kit handles drag pointer tracking.
-export const useDisableDragSelectOnPointerDown = () => {
- const { setDragSelectionStartEnabled } = useDragSelect();
-
- const handlePointerDown = useCallback(() => {
- setDragSelectionStartEnabled(false);
- }, [setDragSelectionStartEnabled]);
-
- const handlePointerEnd = useCallback(() => {
- setDragSelectionStartEnabled(true);
- }, [setDragSelectionStartEnabled]);
-
- return {
- onPointerDown: handlePointerDown,
- onPointerUp: handlePointerEnd,
- onPointerCancel: handlePointerEnd,
- };
-};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/states/isRecordTableHeaderDropProcessingComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/states/isRecordTableHeaderDropProcessingComponentState.ts
deleted file mode 100644
index cf7a2abb86..0000000000
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/states/isRecordTableHeaderDropProcessingComponentState.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
-import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
-
-export const isRecordTableHeaderDropProcessingComponentState =
- createAtomComponentState({
- key: 'isRecordTableHeaderDropProcessingComponentState',
- defaultValue: false,
- componentInstanceContext: RecordTableComponentInstanceContext,
- });
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderDropTarget.tsx b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/components/DragDropColumnDropTarget.tsx
similarity index 71%
rename from packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderDropTarget.tsx
rename to packages/twenty-front/src/modules/ui/utilities/drag-and-drop/components/DragDropColumnDropTarget.tsx
index cfc0db2900..56df635e5e 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderDropTarget.tsx
+++ b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/components/DragDropColumnDropTarget.tsx
@@ -2,16 +2,16 @@ import { styled } from '@linaria/react';
import { type ReactNode, useContext } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
-import { RecordTableHeaderDndContext } from '@/object-record/record-table/record-table-header/dnd/context/RecordTableHeaderDndContext';
-import { isDefined } from 'twenty-shared/utils';
+import { DragDropColumnDndContext } from '@/ui/utilities/drag-and-drop/context/DragDropColumnDndContext';
const StyledDropTarget = styled.div<{
$compact?: boolean;
+ $overlay?: boolean;
}>`
height: 100%;
min-height: ${({ $compact }) =>
$compact ? '100%' : themeCssVariables.spacing[2]};
- position: relative;
+ position: ${({ $overlay }) => ($overlay ? 'absolute' : 'relative')};
transition: background-color 120ms ease-out;
width: 100%;
@@ -43,26 +43,27 @@ const StyledDropTarget = styled.div<{
}
`;
-type RecordTableHeaderDropTargetProps = {
- index: number;
+type DragDropColumnDropTargetProps = {
children?: ReactNode;
compact?: boolean;
+ index: number;
+ overlay?: boolean;
};
-export const RecordTableHeaderDropTarget = ({
- index,
+export const DragDropColumnDropTarget = ({
children,
compact = false,
-}: RecordTableHeaderDropTargetProps) => {
- const { activeDropTargetIndex } = useContext(RecordTableHeaderDndContext);
+ index,
+ overlay = false,
+}: DragDropColumnDropTargetProps) => {
+ const { activeDropTargetIndex } = useContext(DragDropColumnDndContext);
- const isDragOver = isDefined(activeDropTargetIndex)
- ? activeDropTargetIndex === index
- : false;
+ const isDragOver = activeDropTargetIndex === index;
return (
{children}
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderDroppableSlot.tsx b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/components/DragDropColumnDroppableSlot.tsx
similarity index 72%
rename from packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderDroppableSlot.tsx
rename to packages/twenty-front/src/modules/ui/utilities/drag-and-drop/components/DragDropColumnDroppableSlot.tsx
index a9d66285ea..358cdf1f4c 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderDroppableSlot.tsx
+++ b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/components/DragDropColumnDroppableSlot.tsx
@@ -1,9 +1,10 @@
-import { type RecordTableHeaderDndData } from '@/object-record/record-table/record-table-header/dnd/types/RecordTableHeaderDndData';
import { pointerIntersection } from '@dnd-kit/collision';
import { useDroppable } from '@dnd-kit/react';
import { styled } from '@linaria/react';
import { type ReactNode } from 'react';
+import { type DragDropColumnData } from '@/ui/utilities/drag-and-drop/types/DragDropColumnData';
+
const StyledSlotWrapper = styled.div`
align-self: stretch;
flex: 0 0 2px;
@@ -16,23 +17,24 @@ const StyledSlotWrapper = styled.div`
const SLOT_COLLISION_PRIORITY = 1;
-type RecordTableHeaderDroppableSlotProps = {
+type DragDropColumnDroppableSlotProps = {
+ children?: ReactNode;
+ collisionPriority?: number;
+ disabled?: boolean;
droppableId: string;
index: number;
- children?: ReactNode;
- disabled?: boolean;
- collisionPriority?: number;
};
-export const RecordTableHeaderDroppableSlot = ({
+export const DragDropColumnDroppableSlot = ({
+ children,
+ collisionPriority = SLOT_COLLISION_PRIORITY,
+ disabled = false,
droppableId,
index,
- children,
- disabled = false,
- collisionPriority = SLOT_COLLISION_PRIORITY,
-}: RecordTableHeaderDroppableSlotProps) => {
+}: DragDropColumnDroppableSlotProps) => {
const id = `${droppableId}::${index}`;
- const data: RecordTableHeaderDndData = { droppableId, index };
+ const data: DragDropColumnData = { droppableId, index };
+
const { ref } = useDroppable({
id,
disabled,
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderSortableCell.tsx b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/components/DragDropColumnSortableCell.tsx
similarity index 53%
rename from packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderSortableCell.tsx
rename to packages/twenty-front/src/modules/ui/utilities/drag-and-drop/components/DragDropColumnSortableCell.tsx
index 0b49f97270..b29e559b90 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderSortableCell.tsx
+++ b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/components/DragDropColumnSortableCell.tsx
@@ -4,43 +4,48 @@ import { useSortable } from '@dnd-kit/react/sortable';
import { styled } from '@linaria/react';
import { type ReactNode } from 'react';
-import { RecordTableHeaderSortableHandleRefContext } from '@/object-record/record-table/record-table-header/dnd/context/RecordTableHeaderSortableHandleRefContext';
+import { DragDropColumnSortableHandleRefContext } from '@/ui/utilities/drag-and-drop/context/DragDropColumnSortableHandleRefContext';
const SORTABLE_COLLISION_PRIORITY = 3;
const PLUGINS_WITHOUT_OPTIMISTIC = [SortableKeyboardPlugin];
// Columns can only be reordered horizontally, so keep the drag preview locked to the header row.
-const RECORD_TABLE_HEADER_SORTABLE_MODIFIERS = [RestrictToHorizontalAxis];
+const SORTABLE_MODIFIERS = [RestrictToHorizontalAxis];
-const RECORD_TABLE_HEADER_SORTABLE_TRANSITION = {
+const SORTABLE_TRANSITION = {
duration: 180,
easing: 'cubic-bezier(0.2, 0, 0, 1)',
idle: true,
};
-const StyledSortableRoot = styled.div`
+const StyledSortableRoot = styled.div<{ $fill?: boolean }>`
+ display: ${({ $fill }) => ($fill ? 'flex' : 'block')};
+ height: ${({ $fill }) => ($fill ? '100%' : 'auto')};
min-height: 0;
+ min-width: ${({ $fill }) => ($fill ? '0' : 'auto')};
outline: none;
position: relative;
will-change: transform;
`;
-type RecordTableHeaderSortableCellProps = {
+type DragDropColumnSortableCellProps = {
children: ReactNode;
disabled?: boolean;
+ fill?: boolean;
group: string;
id: string;
index: number;
};
-export const RecordTableHeaderSortableCell = ({
+export const DragDropColumnSortableCell = ({
+ children,
+ disabled = false,
+ fill = false,
+ group,
id,
index,
- group,
- disabled = false,
- children,
-}: RecordTableHeaderSortableCellProps) => {
+}: DragDropColumnSortableCellProps) => {
const { handleRef, ref } = useSortable({
id,
index,
@@ -48,18 +53,20 @@ export const RecordTableHeaderSortableCell = ({
collisionPriority: SORTABLE_COLLISION_PRIORITY,
data: {
droppableId: group,
- index: index,
+ index,
},
disabled,
- transition: RECORD_TABLE_HEADER_SORTABLE_TRANSITION,
+ transition: SORTABLE_TRANSITION,
plugins: PLUGINS_WITHOUT_OPTIMISTIC,
- modifiers: RECORD_TABLE_HEADER_SORTABLE_MODIFIERS,
+ modifiers: SORTABLE_MODIFIERS,
feedback: 'clone',
});
return (
-
- {children}
-
+
+
+ {children}
+
+
);
};
diff --git a/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/components/DragDropColumnSortableHandle.tsx b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/components/DragDropColumnSortableHandle.tsx
new file mode 100644
index 0000000000..f279547a48
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/components/DragDropColumnSortableHandle.tsx
@@ -0,0 +1,36 @@
+import { styled } from '@linaria/react';
+import { type ReactNode, useContext } from 'react';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+import { DragDropColumnSortableHandleRefContext } from '@/ui/utilities/drag-and-drop/context/DragDropColumnSortableHandleRefContext';
+
+const StyledSortableHandle = styled.div<{ $fill?: boolean }>`
+ display: ${({ $fill }) => ($fill ? 'flex' : 'block')};
+ height: 100%;
+ min-width: 0;
+ outline: none;
+ width: ${({ $fill }) => ($fill ? '100%' : 'auto')};
+
+ &:focus-visible {
+ outline: 2px solid ${themeCssVariables.color.blue};
+ outline-offset: -2px;
+ }
+`;
+
+type DragDropColumnSortableHandleProps = {
+ children: ReactNode;
+ fill?: boolean;
+};
+
+export const DragDropColumnSortableHandle = ({
+ children,
+ fill = false,
+}: DragDropColumnSortableHandleProps) => {
+ const sortableHandleRef = useContext(DragDropColumnSortableHandleRefContext);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/context/DragDropColumnDndContext.tsx b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/context/DragDropColumnDndContext.tsx
new file mode 100644
index 0000000000..f4e3245081
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/context/DragDropColumnDndContext.tsx
@@ -0,0 +1,10 @@
+import { createContext } from 'react';
+
+type DragDropColumnDndContextValue = {
+ activeDropTargetIndex: number | null;
+};
+
+export const DragDropColumnDndContext =
+ createContext({
+ activeDropTargetIndex: null,
+ });
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/context/RecordTableHeaderSortableHandleRefContext.tsx b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/context/DragDropColumnSortableHandleRefContext.tsx
similarity index 59%
rename from packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/context/RecordTableHeaderSortableHandleRefContext.tsx
rename to packages/twenty-front/src/modules/ui/utilities/drag-and-drop/context/DragDropColumnSortableHandleRefContext.tsx
index 40a42fe0e9..bc6b382423 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/context/RecordTableHeaderSortableHandleRefContext.tsx
+++ b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/context/DragDropColumnSortableHandleRefContext.tsx
@@ -1,5 +1,5 @@
import { createContext } from 'react';
-export const RecordTableHeaderSortableHandleRefContext = createContext<
+export const DragDropColumnSortableHandleRefContext = createContext<
((element: Element | null) => void) | undefined
>(undefined);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/types/RecordTableHeaderDndData.ts b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/types/DragDropColumnData.ts
similarity index 51%
rename from packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/types/RecordTableHeaderDndData.ts
rename to packages/twenty-front/src/modules/ui/utilities/drag-and-drop/types/DragDropColumnData.ts
index 27e43b235e..840f5270a5 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/types/RecordTableHeaderDndData.ts
+++ b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/types/DragDropColumnData.ts
@@ -1,4 +1,4 @@
-export type RecordTableHeaderDndData = {
- index: number;
+export type DragDropColumnData = {
droppableId: string;
+ index: number;
};
diff --git a/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/utils/__tests__/resolveDragDropColumnDrop.test.ts b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/utils/__tests__/resolveDragDropColumnDrop.test.ts
new file mode 100644
index 0000000000..d6cd3aa1c4
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/utils/__tests__/resolveDragDropColumnDrop.test.ts
@@ -0,0 +1,114 @@
+import { resolveDragDropColumnDrop } from '@/ui/utilities/drag-and-drop/utils/resolveDragDropColumnDrop';
+
+const createScrollWrapperElement = ({
+ left = 0,
+ scrollLeft = 0,
+}: {
+ left?: number;
+ scrollLeft?: number;
+}) =>
+ ({
+ getBoundingClientRect: () => ({ left }) as DOMRect,
+ scrollLeft,
+ }) as unknown as HTMLElement;
+
+const COLUMN_WIDTHS = [100, 100, 100];
+
+describe('resolveDragDropColumnDrop', () => {
+ it('should target the column under the pointer before its midpoint', () => {
+ const result = resolveDragDropColumnDrop({
+ pointerX: 40,
+ sourceIndex: 2,
+ scrollWrapperElement: createScrollWrapperElement({}),
+ columnWidths: COLUMN_WIDTHS,
+ });
+
+ expect(result).toEqual({
+ sourceIndex: 2,
+ dropTargetIndex: 0,
+ destinationIndex: 0,
+ });
+ });
+
+ it('should target the next slot once the pointer passes a column midpoint', () => {
+ const result = resolveDragDropColumnDrop({
+ pointerX: 120,
+ sourceIndex: 2,
+ scrollWrapperElement: createScrollWrapperElement({}),
+ columnWidths: COLUMN_WIDTHS,
+ });
+
+ expect(result.dropTargetIndex).toBe(1);
+ expect(result.destinationIndex).toBe(1);
+ });
+
+ it('should target the trailing slot when the pointer is past every column', () => {
+ const result = resolveDragDropColumnDrop({
+ pointerX: 400,
+ sourceIndex: 0,
+ scrollWrapperElement: createScrollWrapperElement({}),
+ columnWidths: COLUMN_WIDTHS,
+ });
+
+ expect(result.dropTargetIndex).toBe(COLUMN_WIDTHS.length);
+ expect(result.destinationIndex).toBe(COLUMN_WIDTHS.length - 1);
+ });
+
+ it('should decrement destinationIndex when dropping to the right of the source', () => {
+ const result = resolveDragDropColumnDrop({
+ pointerX: 160,
+ sourceIndex: 0,
+ scrollWrapperElement: createScrollWrapperElement({}),
+ columnWidths: COLUMN_WIDTHS,
+ });
+
+ expect(result.dropTargetIndex).toBe(2);
+ expect(result.destinationIndex).toBe(1);
+ });
+
+ it('should keep destinationIndex equal to dropTargetIndex when dropping to the left of the source', () => {
+ const result = resolveDragDropColumnDrop({
+ pointerX: 120,
+ sourceIndex: 2,
+ scrollWrapperElement: createScrollWrapperElement({}),
+ columnWidths: COLUMN_WIDTHS,
+ });
+
+ expect(result.dropTargetIndex).toBe(1);
+ expect(result.destinationIndex).toBe(1);
+ });
+
+ it('should offset the pointer by the scroll position', () => {
+ const result = resolveDragDropColumnDrop({
+ pointerX: 40,
+ sourceIndex: 2,
+ scrollWrapperElement: createScrollWrapperElement({ scrollLeft: 100 }),
+ columnWidths: COLUMN_WIDTHS,
+ });
+
+ expect(result.dropTargetIndex).toBe(1);
+ });
+
+ it('should offset the pointer by the container left', () => {
+ const result = resolveDragDropColumnDrop({
+ pointerX: 90,
+ sourceIndex: 2,
+ scrollWrapperElement: createScrollWrapperElement({ left: 50 }),
+ columnWidths: COLUMN_WIDTHS,
+ });
+
+ expect(result.dropTargetIndex).toBe(0);
+ });
+
+ it('should subtract the leading offset before resolving the column', () => {
+ const result = resolveDragDropColumnDrop({
+ pointerX: 140,
+ sourceIndex: 2,
+ scrollWrapperElement: createScrollWrapperElement({}),
+ columnWidths: COLUMN_WIDTHS,
+ leadingOffset: 100,
+ });
+
+ expect(result.dropTargetIndex).toBe(0);
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/utils/resolveRecordTableHeaderDrop.ts b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/utils/resolveDragDropColumnDrop.ts
similarity index 63%
rename from packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/utils/resolveRecordTableHeaderDrop.ts
rename to packages/twenty-front/src/modules/ui/utilities/drag-and-drop/utils/resolveDragDropColumnDrop.ts
index 2569581a80..d1c1f52ab2 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/dnd/utils/resolveRecordTableHeaderDrop.ts
+++ b/packages/twenty-front/src/modules/ui/utilities/drag-and-drop/utils/resolveDragDropColumnDrop.ts
@@ -1,14 +1,12 @@
-import { type RecordField } from '@/object-record/record-field/types/RecordField';
-
-type ResolveRecordTableHeaderDropArgs = {
+type ResolveDragDropColumnDropArgs = {
pointerX: number;
sourceIndex: number;
scrollWrapperElement: HTMLElement;
- nonSortableColumnsWidth: number;
- recordFields: RecordField[];
+ columnWidths: number[];
+ leadingOffset?: number;
};
-type ResolvedRecordTableHeaderDrop = {
+type ResolvedDragDropColumnDrop = {
sourceIndex: number;
dropTargetIndex: number;
destinationIndex: number;
@@ -22,26 +20,25 @@ const getDestinationIndexFromDropTargetIndex = ({
dropTargetIndex: number;
}) => (dropTargetIndex > sourceIndex ? dropTargetIndex - 1 : dropTargetIndex);
-export const resolveRecordTableHeaderDrop = ({
+export const resolveDragDropColumnDrop = ({
pointerX,
sourceIndex,
scrollWrapperElement,
- nonSortableColumnsWidth,
- recordFields,
-}: ResolveRecordTableHeaderDropArgs): ResolvedRecordTableHeaderDrop => {
+ columnWidths,
+ leadingOffset = 0,
+}: ResolveDragDropColumnDropArgs): ResolvedDragDropColumnDrop => {
const scrollContainerRect = scrollWrapperElement.getBoundingClientRect();
const contentX =
pointerX -
scrollContainerRect.left +
scrollWrapperElement.scrollLeft -
- nonSortableColumnsWidth;
+ leadingOffset;
let left = 0;
- for (const [index, field] of recordFields.entries()) {
- const width = field.size;
- const midpoint = left + width / 2;
+ for (const [index, columnWidth] of columnWidths.entries()) {
+ const midpoint = left + columnWidth / 2;
if (contentX < midpoint) {
return {
@@ -54,10 +51,10 @@ export const resolveRecordTableHeaderDrop = ({
};
}
- left += width;
+ left += columnWidth;
}
- const dropTargetIndex = recordFields.length;
+ const dropTargetIndex = columnWidths.length;
return {
sourceIndex,
diff --git a/packages/twenty-front/src/modules/ui/utilities/drag-select/hooks/useDisableDragSelectOnPointerDown.ts b/packages/twenty-front/src/modules/ui/utilities/drag-select/hooks/useDisableDragSelectOnPointerDown.ts
new file mode 100644
index 0000000000..7c6cf407c7
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/utilities/drag-select/hooks/useDisableDragSelectOnPointerDown.ts
@@ -0,0 +1,14 @@
+import { useDragSelect } from './useDragSelect';
+
+export const useDisableDragSelectOnPointerDown = () => {
+ const { setDragSelectionStartEnabled } = useDragSelect();
+
+ const handlePointerDown = () => setDragSelectionStartEnabled(false);
+ const handlePointerEnd = () => setDragSelectionStartEnabled(true);
+
+ return {
+ onPointerDown: handlePointerDown,
+ onPointerUp: handlePointerEnd,
+ onPointerCancel: handlePointerEnd,
+ };
+};