From bf92db70fc73edd21781bd62ebeab5ca0bddfb61 Mon Sep 17 00:00:00 2001 From: Priyanshu Bartwal Date: Sat, 30 May 2026 13:48:48 +0530 Subject: [PATCH 1/6] Init --- .../components/RecordBoardDragDropContext.tsx | 2 + .../components/RecordTableContent.tsx | 16 +-- .../components/RecordTableHeader.tsx | 59 +++++---- .../components/RecordTableHeaderCell.tsx | 71 ++++++----- .../RecordTableHeaderDragDropContext.tsx | 116 ++++++++++++++++++ 5 files changed, 205 insertions(+), 59 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropContext.tsx diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDragDropContext.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDragDropContext.tsx index 8d086c637efc7..fe54b7d8beb34 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDragDropContext.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDragDropContext.tsx @@ -28,11 +28,13 @@ export const RecordBoardDragDropContext = ({ currentRecordSortsComponentState, recordBoardId, ); + console.log(currentRecordSorts); const recordBoardSelectedRecordIds = useAtomComponentSelectorCallbackState( recordBoardSelectedRecordIdsComponentSelector, recordBoardId, ); + console.log(recordBoardSelectedRecordIds); const store = useStore(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContent.tsx index 7c4b091701e31..119dd1aea1653 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContent.tsx @@ -173,14 +173,14 @@ export const RecordTableContent = ({ - + {/* */} ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx index 01ba0b11814f1..6ab3a81ff6564 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx @@ -5,6 +5,7 @@ import { RecordTableHeaderAddColumnButton } from '@/object-record/record-table/r import { RecordTableHeaderCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCell'; import { RecordTableHeaderCheckboxColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn'; import { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn'; +import { RecordTableHeaderDragDropContext } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropContext'; import { RecordTableHeaderEmptyLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderEmptyLastColumn'; import { RecordTableHeaderFirstCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderFirstCell'; import { RecordTableHeaderFirstScrollableCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderFirstScrollableCell'; @@ -16,6 +17,7 @@ import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/rec import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { styled } from '@linaria/react'; import { filterOutByProperty } from 'twenty-shared/utils'; +import { Droppable } from '@hello-pangea/dnd'; const StyledHeaderContainer = styled.div` display: flex; @@ -53,28 +55,39 @@ export const RecordTableHeader = () => { useResizeTableHeader(); return ( - - {!isRecordTableDragColumnHidden && } - {!isRecordTableCheckboxColumnHidden && ( - - )} - - - {recordFieldsWithoutLabelIdentifierAndFirstOne.map( - (recordField, index) => ( - - ), - )} - {isRecordTableColumnHeadersReadOnly ? ( - - ) : ( - - )} - - + + + {(droppableProvied) => ( + + {!isRecordTableDragColumnHidden && ( + + )} + {!isRecordTableCheckboxColumnHidden && ( + + )} + + + {recordFieldsWithoutLabelIdentifierAndFirstOne.map( + (recordField, index) => ( + + ), + )} + {isRecordTableColumnHeadersReadOnly ? ( + + ) : ( + + )} + + + )} + + ); }; 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 346235c8328b0..f47ec36b7510c 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 @@ -17,6 +17,7 @@ import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record- 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 { Draggable } from '@hello-pangea/dnd'; import { cx } from '@linaria/core'; import { isDefined } from 'twenty-shared/utils'; @@ -77,36 +78,50 @@ export const RecordTableHeaderCell = ({ isRecordTableScrolledVertically; return ( - - {isRecordTableColumnResizable && ( - - )} - {isRecordTableColumnHeadersReadOnly ? ( - - ) : ( - - )} - {isRecordTableColumnResizable && ( - + {(draggableProvided) => ( + + {isRecordTableColumnResizable && ( + + )} + {isRecordTableColumnHeadersReadOnly ? ( + + ) : ( + + )} + {isRecordTableColumnResizable && ( + + )} + )} - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropContext.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropContext.tsx new file mode 100644 index 0000000000000..d3d90f2237266 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropContext.tsx @@ -0,0 +1,116 @@ +import { isRecordBoardDropProcessingComponentState } from '@/object-record/record-board/states/isRecordBoardDropProcessingComponentState'; +import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector'; +import { useEndRecordDrag } from '@/object-record/record-drag/hooks/useEndRecordDrag'; +import { useProcessBoardCardDrop } from '@/object-record/record-drag/hooks/useProcessBoardCardDrop'; +import { useStartRecordDrag } from '@/object-record/record-drag/hooks/useStartRecordDrag'; +import { originalDragSelectionComponentState } from '@/object-record/record-drag/states/originalDragSelectionComponentState'; + +import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId'; +import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState'; +import { useModal } from '@/ui/layout/modal/hooks/useModal'; +import { useAtomComponentSelectorCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorCallbackState'; +import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; +import { + DragDropContext, + type DragStart, + type OnDragEndResponder, +} from '@hello-pangea/dnd'; +import { useStore } from 'jotai'; +import { useCallback } from 'react'; + +export const RecordTableHeaderDragDropContext = ({ + children, +}: React.PropsWithChildren) => { + const { recordTableId } = useRecordTableContextOrThrow(); + + // const currentRecordSorts = useAtomComponentStateCallbackState( + // currentRecordSortsComponentState, + // recordTableId, + // ); + + const store = useStore(); + + const originalDragSelectionCallbackState = useAtomComponentStateCallbackState( + originalDragSelectionComponentState, + recordTableId, + ); + + const { startRecordDrag } = useStartRecordDrag(recordTableId); + const { endRecordDrag } = useEndRecordDrag(recordTableId); + + // const { processBoardCardDrop } = useProcessBoardCardDrop(); + + // const isRecordBoardDropProcessingCallbackState = + // useAtomComponentStateCallbackState( + // isRecordBoardDropProcessingComponentState, + // ); + + const { openModal } = useModal(); + + // const handleDragStart = useCallback( + // (start: DragStart) => { + // + // // store.set(isRecordBoardDropProcessingCallbackState, true); + // + // // startRecordDrag(start, currentSelectedRecordIds); + // }, + // [ + // // recordBoardSelectedRecordIds, + // startRecordDrag, + // store, + // // isRecordBoardDropProcessingCallbackState, + // ], + // ); + + const handleDragEnd: OnDragEndResponder = useCallback( + (result) => { + const originalDragSelection = store.get( + originalDragSelectionCallbackState, + ); + + if (!result.destination) { + // store.set(isRecordBoardDropProcessingCallbackState, false); + endRecordDrag(); + return; + } + + // const existingRecordSorts = store.get(currentRecordSorts); + + // if (existingRecordSorts.length > 0) { + // // store.set(isRecordBoardDropProcessingCallbackState, false); + // endRecordDrag(); + // openModal(RECORD_INDEX_REMOVE_SORTING_MODAL_ID); + // return; + // } + + try { + // processBoardCardDrop(result, originalDragSelection); + } catch (error) { + // store.set(isRecordBoardDropProcessingCallbackState, false); + endRecordDrag(); + + throw error; + } + + // store.set(isRecordBoardDropProcessingCallbackState, false); + endRecordDrag(); + }, + [ + // processBoardCardDrop, + endRecordDrag, + // currentRecordSorts, + openModal, + store, + originalDragSelectionCallbackState, + // isRecordBoardDropProcessingCallbackState, + ], + ); + + return ( + + {children} + + ); +}; From 5f1fc5cd2d4cde6c812faba394d6a4fbe8e5f7d0 Mon Sep 17 00:00:00 2001 From: Priyanshu Bartwal Date: Wed, 3 Jun 2026 23:32:50 +0530 Subject: [PATCH 2/6] Feat: Record table header drag & drop --- .../components/RecordBoardDragDropContext.tsx | 2 - .../components/RecordTableHeader.tsx | 13 ++- .../components/RecordTableHeaderCell.tsx | 45 +++++--- .../RecordTableHeaderDragDropContext.tsx | 102 +++--------------- .../RecordTableHeaderFirstScrollableCell.tsx | 80 ++++++++++---- .../hooks/useProcessTableColumnDrop.ts | 47 ++++++++ .../hooks/useReorderColumns.ts | 96 +++++++++++++++++ ...cordTableHeaderProcessingComponentState.ts | 9 ++ 8 files changed, 268 insertions(+), 126 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useReorderColumns.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-header/states/isRecordTableHeaderProcessingComponentState.ts diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDragDropContext.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDragDropContext.tsx index fe54b7d8beb34..8d086c637efc7 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDragDropContext.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDragDropContext.tsx @@ -28,13 +28,11 @@ export const RecordBoardDragDropContext = ({ currentRecordSortsComponentState, recordBoardId, ); - console.log(currentRecordSorts); const recordBoardSelectedRecordIds = useAtomComponentSelectorCallbackState( recordBoardSelectedRecordIdsComponentSelector, recordBoardId, ); - console.log(recordBoardSelectedRecordIds); const store = useStore(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx index 6ab3a81ff6564..c7f7bdf39e286 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx @@ -56,11 +56,12 @@ export const RecordTableHeader = () => { return ( - - {(droppableProvied) => ( - + {(droppableProvided) => ( + {!isRecordTableDragColumnHidden && ( @@ -75,6 +76,7 @@ export const RecordTableHeader = () => { ), @@ -85,6 +87,7 @@ export const RecordTableHeader = () => { )} + {droppableProvided.placeholder} )} 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 f47ec36b7510c..6ae40c4f6b613 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,6 +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 { isRecordTableHeaderProcessingComponentState } from '@/object-record/record-table/record-table-header/states/isRecordTableHeaderProcessingComponentState'; 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'; @@ -19,15 +20,23 @@ import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { Draggable } from '@hello-pangea/dnd'; import { cx } from '@linaria/core'; +import { styled } from '@linaria/react'; import { isDefined } from 'twenty-shared/utils'; +const StyledDragHandle = styled.div` + height: 100%; + width: 100%; +`; + type RecordTableHeaderCellProps = { recordField: RecordField; + draggableIndex: number; recordFieldIndex: number; }; export const RecordTableHeaderCell = ({ recordField, + draggableIndex, recordFieldIndex, }: RecordTableHeaderCellProps) => { const { objectMetadataItem } = useRecordTableContextOrThrow(); @@ -70,6 +79,10 @@ export const RecordTableHeaderCell = ({ resizedFieldMetadataIdComponentState, ); + const isRecordTableHeaderDropProcessing = useAtomComponentStateValue( + isRecordTableHeaderProcessingComponentState, + ); + const isResizingAnyColumn = isDefined(resizedFieldMetadataId); const shouldDisplayBorderBottom = @@ -81,13 +94,15 @@ export const RecordTableHeaderCell = ({ {(draggableProvided) => ( {isRecordTableColumnResizable && ( )} - {isRecordTableColumnHeadersReadOnly ? ( - - ) : ( - - )} + + {isRecordTableColumnHeadersReadOnly ? ( + + ) : ( + + )} + {isRecordTableColumnResizable && ( { - const { recordTableId } = useRecordTableContextOrThrow(); - - // const currentRecordSorts = useAtomComponentStateCallbackState( - // currentRecordSortsComponentState, - // recordTableId, - // ); - const store = useStore(); - const originalDragSelectionCallbackState = useAtomComponentStateCallbackState( - originalDragSelectionComponentState, - recordTableId, - ); - - const { startRecordDrag } = useStartRecordDrag(recordTableId); - const { endRecordDrag } = useEndRecordDrag(recordTableId); - - // const { processBoardCardDrop } = useProcessBoardCardDrop(); + const { processTableColumnDrop } = useProcessTableColumnDrop(); - // const isRecordBoardDropProcessingCallbackState = - // useAtomComponentStateCallbackState( - // isRecordBoardDropProcessingComponentState, - // ); + const isRecordTableHeaderProcessingCallbackState = + useAtomComponentStateCallbackState( + isRecordTableHeaderProcessingComponentState, + ); - const { openModal } = useModal(); - - // const handleDragStart = useCallback( - // (start: DragStart) => { - // - // // store.set(isRecordBoardDropProcessingCallbackState, true); - // - // // startRecordDrag(start, currentSelectedRecordIds); - // }, - // [ - // // recordBoardSelectedRecordIds, - // startRecordDrag, - // store, - // // isRecordBoardDropProcessingCallbackState, - // ], - // ); + const handleDragStart = useCallback(() => { + store.set(isRecordTableHeaderProcessingCallbackState, true); + }, [store, isRecordTableHeaderProcessingCallbackState]); const handleDragEnd: OnDragEndResponder = useCallback( (result) => { - const originalDragSelection = store.get( - originalDragSelectionCallbackState, - ); - if (!result.destination) { - // store.set(isRecordBoardDropProcessingCallbackState, false); - endRecordDrag(); + store.set(isRecordTableHeaderProcessingCallbackState, false); return; } - // const existingRecordSorts = store.get(currentRecordSorts); - - // if (existingRecordSorts.length > 0) { - // // store.set(isRecordBoardDropProcessingCallbackState, false); - // endRecordDrag(); - // openModal(RECORD_INDEX_REMOVE_SORTING_MODAL_ID); - // return; - // } - try { - // processBoardCardDrop(result, originalDragSelection); + processTableColumnDrop(result); } catch (error) { - // store.set(isRecordBoardDropProcessingCallbackState, false); - endRecordDrag(); - + store.set(isRecordTableHeaderProcessingCallbackState, false); throw error; } - - // store.set(isRecordBoardDropProcessingCallbackState, false); - endRecordDrag(); }, - [ - // processBoardCardDrop, - endRecordDrag, - // currentRecordSorts, - openModal, - store, - originalDragSelectionCallbackState, - // isRecordBoardDropProcessingCallbackState, - ], + [processTableColumnDrop, store, isRecordTableHeaderProcessingCallbackState], ); return ( - + {children} ); 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 320086b7aa026..e006e1188dcee 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 @@ -9,6 +9,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 { isRecordTableHeaderProcessingComponentState } from '@/object-record/record-table/record-table-header/states/isRecordTableHeaderProcessingComponentState'; 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'; @@ -20,9 +21,16 @@ import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record- 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 { Draggable } from '@hello-pangea/dnd'; import { cx } from '@linaria/core'; +import { styled } from '@linaria/react'; import { filterOutByProperty, isDefined } from 'twenty-shared/utils'; +const StyledDragHandle = styled.div` + height: 100%; + width: 100%; +`; + export const RecordTableHeaderFirstScrollableCell = () => { const { objectMetadataItem, visibleRecordFields } = useRecordTableContextOrThrow(); @@ -81,33 +89,65 @@ export const RecordTableHeaderFirstScrollableCell = () => { const isResizingAnyColumn = isDefined(resizedFieldMetadataId); + const isRecordTableHeaderDropProcessing = useAtomComponentStateValue( + isRecordTableHeaderProcessingComponentState, + ); + if (!recordField) { return <>; } return ( - - {isRecordTableColumnResizable && ( - - )} - {isRecordTableColumnHeadersReadOnly ? ( - - ) : ( - - )} - {isRecordTableColumnResizable && ( - + {(draggableProvided) => ( + + {isRecordTableColumnResizable && ( + + )} + + {isRecordTableColumnHeadersReadOnly ? ( + + ) : ( + + )} + + {isRecordTableColumnResizable && ( + + )} + )} - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop.ts new file mode 100644 index 0000000000000..c102eb9241d9e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop.ts @@ -0,0 +1,47 @@ +import { type DropResult } from '@hello-pangea/dnd'; +import { useStore } from 'jotai'; +import { useCallback } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; +import { useDebouncedCallback } from 'use-debounce'; +import { isRecordTableHeaderProcessingComponentState } from '@/object-record/record-table/record-table-header/states/isRecordTableHeaderProcessingComponentState'; +import { useReorderColumns } from '@/object-record/record-table/record-table-header/hooks/useReorderColumns'; + +export const useProcessTableColumnDrop = () => { + const store = useStore(); + + const { reorderColumns } = useReorderColumns(); + + const isRecordTableHeaderProcessingCallbackState = + useAtomComponentStateCallbackState( + isRecordTableHeaderProcessingComponentState, + ); + + // TODO: this is necessary to avoid race conditions when dragging right after a previous drag (~200ms to 500ms) + // A way to fix this would be to have a proper optimistic logic on drop that doesn't just resets the whole board with trigger initial query but updates everything without waiting for the request return + // Which is the problem here because it kind of destroys the existing columns that have more records than page size, and dnd library has issues computing drag when the underlying data change. + const debouncedUpdateDropProcessing = useDebouncedCallback( + (isPending: boolean) => { + store.set(isRecordTableHeaderProcessingCallbackState, isPending); + }, + 500, + ); + + const processTableColumnDrop = useCallback( + (headerColumnDropResult: DropResult) => { + const source = headerColumnDropResult.source; + const destination = headerColumnDropResult.destination; + + if (!isDefined(source) || !isDefined(destination)) return; + reorderColumns({ source, destination }); + + debouncedUpdateDropProcessing(false); + }, + [reorderColumns, debouncedUpdateDropProcessing], + ); + + return { + processTableColumnDrop, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useReorderColumns.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useReorderColumns.ts new file mode 100644 index 0000000000000..cb32e20b17985 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useReorderColumns.ts @@ -0,0 +1,96 @@ +import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField'; +import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; +import { useAtomComponentSelectorCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorCallbackState'; +import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; +import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField'; +import { type DraggableLocation } from '@hello-pangea/dnd'; +import { useStore } from 'jotai'; +import { useCallback } from 'react'; +import { filterOutByProperty } from 'twenty-shared/utils'; + +export const useReorderColumns = (recordTableId?: string) => { + const store = useStore(); + + const { labelIdentifierFieldMetadataItem } = useRecordIndexContextOrThrow(); + + const visibleRecordFieldsCallbackState = + useAtomComponentSelectorCallbackState( + visibleRecordFieldsComponentSelector, + recordTableId, + ); + + const { saveViewFields } = useSaveCurrentViewFields(); + + const { updateRecordField } = useUpdateRecordField(recordTableId); + + const reorderColumns = useCallback( + async ({ + source, + destination, + }: { + source: DraggableLocation; + destination: DraggableLocation; + }) => { + if (source.index === destination.index) { + return; + } + + const draggableFields = store + .get(visibleRecordFieldsCallbackState) + .filter( + filterOutByProperty( + 'fieldMetadataItemId', + labelIdentifierFieldMetadataItem?.id, + ), + ); + + if ( + source.index < 0 || + destination.index < 0 || + source.index >= draggableFields.length || + destination.index >= draggableFields.length + ) { + return; + } + + const reorderedFields = [...draggableFields]; + const [movedField] = reorderedFields.splice(source.index, 1); + reorderedFields.splice(destination.index, 0, movedField); + + + const orderedPositions = draggableFields.map((field) => field.position); + + const updatedViewFields = reorderedFields + .map((field, index) => { + const newPosition = orderedPositions[index]; + + if (field.position === newPosition) { + return null; + } + + const updatedField = updateRecordField(field.fieldMetadataItemId, { + position: newPosition, + }); + + return mapRecordFieldToViewField(updatedField); + }) + .filter((viewField) => viewField !== null); + + if (updatedViewFields.length === 0) { + return; + } + + await saveViewFields(updatedViewFields); + }, + [ + store, + visibleRecordFieldsCallbackState, + labelIdentifierFieldMetadataItem?.id, + updateRecordField, + saveViewFields, + ], + ); + + return { reorderColumns }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/states/isRecordTableHeaderProcessingComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/states/isRecordTableHeaderProcessingComponentState.ts new file mode 100644 index 0000000000000..78d457c3e2889 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/states/isRecordTableHeaderProcessingComponentState.ts @@ -0,0 +1,9 @@ +import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; +import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState'; + +export const isRecordTableHeaderProcessingComponentState = + createAtomComponentState({ + key: 'isRecordTableHeaderProcessingComponentState', + defaultValue: false, + componentInstanceContext: RecordTableComponentInstanceContext, + }); From e6691eace32dad7b15ab6b399e802d5eddb0ac7f Mon Sep 17 00:00:00 2001 From: Priyanshu Bartwal Date: Thu, 4 Jun 2026 21:13:19 +0530 Subject: [PATCH 3/6] Fix: Format, Lint & naming convention --- .../components/RecordTableHeader.tsx | 12 +++++++---- .../components/RecordTableHeaderCell.tsx | 4 ++-- .../RecordTableHeaderDragDropContext.tsx | 20 +++++++++++------- .../RecordTableHeaderFirstScrollableCell.tsx | 4 ++-- .../hooks/useProcessTableColumnDrop.ts | 21 +++++-------------- .../hooks/useReorderColumns.ts | 3 +-- ...ableHeaderDropProcessingComponentState.ts} | 4 ++-- 7 files changed, 32 insertions(+), 36 deletions(-) rename packages/twenty-front/src/modules/object-record/record-table/record-table-header/states/{isRecordTableHeaderProcessingComponentState.ts => isRecordTableHeaderDropProcessingComponentState.ts} (75%) diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx index c7f7bdf39e286..65c4187c9f0f1 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx @@ -56,12 +56,16 @@ export const RecordTableHeader = () => { return ( - + {(droppableProvided) => ( {!isRecordTableDragColumnHidden && ( 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 6ae40c4f6b613..ba1e0881d37aa 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 { isRecordTableHeaderProcessingComponentState } from '@/object-record/record-table/record-table-header/states/isRecordTableHeaderProcessingComponentState'; +import { isRecordTableHeaderDropProcessingComponentState } from '@/object-record/record-table/record-table-header/states/isRecordTableHeaderDropProcessingComponentState'; 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'; @@ -80,7 +80,7 @@ export const RecordTableHeaderCell = ({ ); const isRecordTableHeaderDropProcessing = useAtomComponentStateValue( - isRecordTableHeaderProcessingComponentState, + isRecordTableHeaderDropProcessingComponentState, ); const isResizingAnyColumn = isDefined(resizedFieldMetadataId); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropContext.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropContext.tsx index 771e707e45aa8..04557d0d29165 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropContext.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropContext.tsx @@ -1,5 +1,5 @@ import { useProcessTableColumnDrop } from '@/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop'; -import { isRecordTableHeaderProcessingComponentState } from '@/object-record/record-table/record-table-header/states/isRecordTableHeaderProcessingComponentState'; +import { isRecordTableHeaderDropProcessingComponentState } from '@/object-record/record-table/record-table-header/states/isRecordTableHeaderDropProcessingComponentState'; import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; import { DragDropContext, type OnDragEndResponder } from '@hello-pangea/dnd'; import { useStore } from 'jotai'; @@ -12,30 +12,34 @@ export const RecordTableHeaderDragDropContext = ({ const { processTableColumnDrop } = useProcessTableColumnDrop(); - const isRecordTableHeaderProcessingCallbackState = + const isRecordTableHeaderDropProcessingCallbackState = useAtomComponentStateCallbackState( - isRecordTableHeaderProcessingComponentState, + isRecordTableHeaderDropProcessingComponentState, ); const handleDragStart = useCallback(() => { - store.set(isRecordTableHeaderProcessingCallbackState, true); - }, [store, isRecordTableHeaderProcessingCallbackState]); + store.set(isRecordTableHeaderDropProcessingCallbackState, true); + }, [store, isRecordTableHeaderDropProcessingCallbackState]); const handleDragEnd: OnDragEndResponder = useCallback( (result) => { if (!result.destination) { - store.set(isRecordTableHeaderProcessingCallbackState, false); + store.set(isRecordTableHeaderDropProcessingCallbackState, false); return; } try { processTableColumnDrop(result); } catch (error) { - store.set(isRecordTableHeaderProcessingCallbackState, false); + store.set(isRecordTableHeaderDropProcessingCallbackState, false); throw error; } }, - [processTableColumnDrop, store, isRecordTableHeaderProcessingCallbackState], + [ + processTableColumnDrop, + store, + isRecordTableHeaderDropProcessingCallbackState, + ], ); return ( 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 e006e1188dcee..a7f0cd880bcc3 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 @@ -9,7 +9,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 { isRecordTableHeaderProcessingComponentState } from '@/object-record/record-table/record-table-header/states/isRecordTableHeaderProcessingComponentState'; +import { isRecordTableHeaderDropProcessingComponentState } from '@/object-record/record-table/record-table-header/states/isRecordTableHeaderDropProcessingComponentState'; 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'; @@ -90,7 +90,7 @@ export const RecordTableHeaderFirstScrollableCell = () => { const isResizingAnyColumn = isDefined(resizedFieldMetadataId); const isRecordTableHeaderDropProcessing = useAtomComponentStateValue( - isRecordTableHeaderProcessingComponentState, + isRecordTableHeaderDropProcessingComponentState, ); if (!recordField) { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop.ts index c102eb9241d9e..c452776d4b0f1 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop.ts @@ -4,8 +4,7 @@ import { useCallback } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; -import { useDebouncedCallback } from 'use-debounce'; -import { isRecordTableHeaderProcessingComponentState } from '@/object-record/record-table/record-table-header/states/isRecordTableHeaderProcessingComponentState'; +import { isRecordTableHeaderDropProcessingComponentState } from '@/object-record/record-table/record-table-header/states/isRecordTableHeaderDropProcessingComponentState'; import { useReorderColumns } from '@/object-record/record-table/record-table-header/hooks/useReorderColumns'; export const useProcessTableColumnDrop = () => { @@ -13,21 +12,11 @@ export const useProcessTableColumnDrop = () => { const { reorderColumns } = useReorderColumns(); - const isRecordTableHeaderProcessingCallbackState = + const isRecordTableHeaderDropProcessingCallbackState = useAtomComponentStateCallbackState( - isRecordTableHeaderProcessingComponentState, + isRecordTableHeaderDropProcessingComponentState, ); - // TODO: this is necessary to avoid race conditions when dragging right after a previous drag (~200ms to 500ms) - // A way to fix this would be to have a proper optimistic logic on drop that doesn't just resets the whole board with trigger initial query but updates everything without waiting for the request return - // Which is the problem here because it kind of destroys the existing columns that have more records than page size, and dnd library has issues computing drag when the underlying data change. - const debouncedUpdateDropProcessing = useDebouncedCallback( - (isPending: boolean) => { - store.set(isRecordTableHeaderProcessingCallbackState, isPending); - }, - 500, - ); - const processTableColumnDrop = useCallback( (headerColumnDropResult: DropResult) => { const source = headerColumnDropResult.source; @@ -36,9 +25,9 @@ export const useProcessTableColumnDrop = () => { if (!isDefined(source) || !isDefined(destination)) return; reorderColumns({ source, destination }); - debouncedUpdateDropProcessing(false); + store.set(isRecordTableHeaderDropProcessingCallbackState, false); }, - [reorderColumns, debouncedUpdateDropProcessing], + [reorderColumns, store, isRecordTableHeaderDropProcessingCallbackState], ); return { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useReorderColumns.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useReorderColumns.ts index cb32e20b17985..a52bb60947ce2 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useReorderColumns.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useReorderColumns.ts @@ -58,9 +58,8 @@ export const useReorderColumns = (recordTableId?: string) => { const [movedField] = reorderedFields.splice(source.index, 1); reorderedFields.splice(destination.index, 0, movedField); - const orderedPositions = draggableFields.map((field) => field.position); - + const updatedViewFields = reorderedFields .map((field, index) => { const newPosition = orderedPositions[index]; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/states/isRecordTableHeaderProcessingComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/states/isRecordTableHeaderDropProcessingComponentState.ts similarity index 75% rename from packages/twenty-front/src/modules/object-record/record-table/record-table-header/states/isRecordTableHeaderProcessingComponentState.ts rename to packages/twenty-front/src/modules/object-record/record-table/record-table-header/states/isRecordTableHeaderDropProcessingComponentState.ts index 78d457c3e2889..cf7a2abb8655c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/states/isRecordTableHeaderProcessingComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/states/isRecordTableHeaderDropProcessingComponentState.ts @@ -1,9 +1,9 @@ import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState'; -export const isRecordTableHeaderProcessingComponentState = +export const isRecordTableHeaderDropProcessingComponentState = createAtomComponentState({ - key: 'isRecordTableHeaderProcessingComponentState', + key: 'isRecordTableHeaderDropProcessingComponentState', defaultValue: false, componentInstanceContext: RecordTableComponentInstanceContext, }); From 7c82d5fcf5078e1523a98f30888af1f99dadc116 Mon Sep 17 00:00:00 2001 From: Priyanshu Bartwal Date: Mon, 8 Jun 2026 13:51:33 +0530 Subject: [PATCH 4/6] Fix: Conflicting Drag selection of rows and Header drag and drop column reorder --- .../components/RecordTableContent.tsx | 16 +++++----- .../components/RecordTableHeaderCell.tsx | 14 +++++++++ .../RecordTableHeaderDragDropContext.tsx | 18 ++++++------ .../RecordTableHeaderFirstScrollableCell.tsx | 14 +++++++++ .../hooks/useProcessTableColumnDrop.ts | 20 +++++-------- .../useResetRecordTableHeaderDragState.ts | 29 +++++++++++++++++++ 6 files changed, 81 insertions(+), 30 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useResetRecordTableHeaderDragState.ts diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContent.tsx index 119dd1aea1653..7c4b091701e31 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContent.tsx @@ -173,14 +173,14 @@ export const RecordTableContent = ({ - {/* */} + ); }; 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 ba1e0881d37aa..7b265406e48b7 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 @@ -15,12 +15,14 @@ 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 { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; 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 { Draggable } from '@hello-pangea/dnd'; import { cx } from '@linaria/core'; import { styled } from '@linaria/react'; +import { useCallback } from 'react'; import { isDefined } from 'twenty-shared/utils'; const StyledDragHandle = styled.div` @@ -41,6 +43,8 @@ export const RecordTableHeaderCell = ({ }: RecordTableHeaderCellProps) => { const { objectMetadataItem } = useRecordTableContextOrThrow(); + const { setDragSelectionStartEnabled } = useDragSelect(); + const isRecordTableColumnHeadersReadOnly = useAtomComponentStateValue( isRecordTableColumnHeadersReadOnlyComponentState, ); @@ -90,6 +94,14 @@ export const RecordTableHeaderCell = ({ !isFirstRowActiveOrFocused || isRecordTableScrolledVertically; + const handlePointerDown = useCallback(() => { + setDragSelectionStartEnabled(false); + }, [setDragSelectionStartEnabled]); + + const handlePointerUp = useCallback(() => { + setDragSelectionStartEnabled(true); + }, [setDragSelectionStartEnabled]); + return ( diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropContext.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropContext.tsx index 04557d0d29165..ed2bf7abe4df1 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropContext.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropContext.tsx @@ -1,4 +1,5 @@ import { useProcessTableColumnDrop } from '@/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop'; +import { useResetRecordTableHeaderDragStates } from '@/object-record/record-table/record-table-header/hooks/useResetRecordTableHeaderDragState'; import { isRecordTableHeaderDropProcessingComponentState } from '@/object-record/record-table/record-table-header/states/isRecordTableHeaderDropProcessingComponentState'; import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; import { DragDropContext, type OnDragEndResponder } from '@hello-pangea/dnd'; @@ -10,13 +11,16 @@ export const RecordTableHeaderDragDropContext = ({ }: React.PropsWithChildren) => { const store = useStore(); - const { processTableColumnDrop } = useProcessTableColumnDrop(); - const isRecordTableHeaderDropProcessingCallbackState = useAtomComponentStateCallbackState( isRecordTableHeaderDropProcessingComponentState, ); + const { processTableColumnDrop } = useProcessTableColumnDrop(); + + const { resetRecordTableHeaderDragStates } = + useResetRecordTableHeaderDragStates(); + const handleDragStart = useCallback(() => { store.set(isRecordTableHeaderDropProcessingCallbackState, true); }, [store, isRecordTableHeaderDropProcessingCallbackState]); @@ -24,22 +28,18 @@ export const RecordTableHeaderDragDropContext = ({ const handleDragEnd: OnDragEndResponder = useCallback( (result) => { if (!result.destination) { - store.set(isRecordTableHeaderDropProcessingCallbackState, false); + resetRecordTableHeaderDragStates(); return; } try { processTableColumnDrop(result); } catch (error) { - store.set(isRecordTableHeaderDropProcessingCallbackState, false); + resetRecordTableHeaderDragStates(); throw error; } }, - [ - processTableColumnDrop, - store, - isRecordTableHeaderDropProcessingCallbackState, - ], + [processTableColumnDrop, resetRecordTableHeaderDragStates], ); return ( 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 a7f0cd880bcc3..9d95c3b00f414 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 @@ -25,6 +25,8 @@ import { Draggable } from '@hello-pangea/dnd'; import { cx } from '@linaria/core'; import { styled } from '@linaria/react'; import { filterOutByProperty, isDefined } from 'twenty-shared/utils'; +import { useCallback } from 'react'; +import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; const StyledDragHandle = styled.div` height: 100%; @@ -35,6 +37,8 @@ export const RecordTableHeaderFirstScrollableCell = () => { const { objectMetadataItem, visibleRecordFields } = useRecordTableContextOrThrow(); + const { setDragSelectionStartEnabled } = useDragSelect(); + const isRecordTableColumnHeadersReadOnly = useAtomComponentStateValue( isRecordTableColumnHeadersReadOnlyComponentState, ); @@ -93,6 +97,14 @@ export const RecordTableHeaderFirstScrollableCell = () => { isRecordTableHeaderDropProcessingComponentState, ); + const handlePointerDown = useCallback(() => { + setDragSelectionStartEnabled(false); + }, [setDragSelectionStartEnabled]); + + const handlePointerUp = useCallback(() => { + setDragSelectionStartEnabled(true); + }, [setDragSelectionStartEnabled]); + if (!recordField) { return <>; } @@ -118,6 +130,8 @@ export const RecordTableHeaderFirstScrollableCell = () => { zIndex={TABLE_Z_INDEX.headerColumns.headerColumnsNormal} isResizing={isResizingAnyColumn} isReadOnly={isRecordTableColumnHeadersReadOnly} + onPointerDown={handlePointerDown} + onPointerUp={handlePointerUp} // oxlint-disable-next-line react/jsx-props-no-spreading {...draggableProvided.draggableProps} > diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop.ts index c452776d4b0f1..6702cf60d51f9 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop.ts @@ -1,33 +1,27 @@ import { type DropResult } from '@hello-pangea/dnd'; -import { useStore } from 'jotai'; import { useCallback } from 'react'; import { isDefined } from 'twenty-shared/utils'; -import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; -import { isRecordTableHeaderDropProcessingComponentState } from '@/object-record/record-table/record-table-header/states/isRecordTableHeaderDropProcessingComponentState'; import { useReorderColumns } from '@/object-record/record-table/record-table-header/hooks/useReorderColumns'; +import { useResetRecordTableHeaderDragStates } from '@/object-record/record-table/record-table-header/hooks/useResetRecordTableHeaderDragState'; export const useProcessTableColumnDrop = () => { - const store = useStore(); - const { reorderColumns } = useReorderColumns(); - const isRecordTableHeaderDropProcessingCallbackState = - useAtomComponentStateCallbackState( - isRecordTableHeaderDropProcessingComponentState, - ); + const { resetRecordTableHeaderDragStates } = + useResetRecordTableHeaderDragStates(); const processTableColumnDrop = useCallback( - (headerColumnDropResult: DropResult) => { + async (headerColumnDropResult: DropResult) => { const source = headerColumnDropResult.source; const destination = headerColumnDropResult.destination; if (!isDefined(source) || !isDefined(destination)) return; - reorderColumns({ source, destination }); + await reorderColumns({ source, destination }); - store.set(isRecordTableHeaderDropProcessingCallbackState, false); + resetRecordTableHeaderDragStates(); }, - [reorderColumns, store, isRecordTableHeaderDropProcessingCallbackState], + [reorderColumns, resetRecordTableHeaderDragStates], ); return { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useResetRecordTableHeaderDragState.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useResetRecordTableHeaderDragState.ts new file mode 100644 index 0000000000000..b16ce3bfdea33 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useResetRecordTableHeaderDragState.ts @@ -0,0 +1,29 @@ +import { isRecordTableHeaderDropProcessingComponentState } from '@/object-record/record-table/record-table-header/states/isRecordTableHeaderDropProcessingComponentState'; +import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; +import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; +import { useCallback } from 'react'; +import { useStore } from 'jotai'; + +export const useResetRecordTableHeaderDragStates = () => { + const store = useStore(); + + const isRecordTableHeaderDropProcessingCallbackState = + useAtomComponentStateCallbackState( + isRecordTableHeaderDropProcessingComponentState, + ); + + const { setDragSelectionStartEnabled } = useDragSelect(); + + const resetRecordTableHeaderDragStates = useCallback(() => { + store.set(isRecordTableHeaderDropProcessingCallbackState, false); + setDragSelectionStartEnabled(true); + }, [ + store, + isRecordTableHeaderDropProcessingCallbackState, + setDragSelectionStartEnabled, + ]); + + return { + resetRecordTableHeaderDragStates, + }; +}; From 367b403b4347af514e2b5d37001cf0dc565e3e4e Mon Sep 17 00:00:00 2001 From: Priyanshu Bartwal Date: Mon, 8 Jun 2026 14:13:38 +0530 Subject: [PATCH 5/6] chore: Reusing moveArrayItem --- .../record-table-header/hooks/useReorderColumns.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useReorderColumns.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useReorderColumns.ts index a52bb60947ce2..60470577e67a3 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useReorderColumns.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useReorderColumns.ts @@ -8,6 +8,7 @@ import { type DraggableLocation } from '@hello-pangea/dnd'; import { useStore } from 'jotai'; import { useCallback } from 'react'; import { filterOutByProperty } from 'twenty-shared/utils'; +import { moveArrayItem } from '~/utils/array/moveArrayItem'; export const useReorderColumns = (recordTableId?: string) => { const store = useStore(); @@ -54,9 +55,10 @@ export const useReorderColumns = (recordTableId?: string) => { return; } - const reorderedFields = [...draggableFields]; - const [movedField] = reorderedFields.splice(source.index, 1); - reorderedFields.splice(destination.index, 0, movedField); + const reorderedFields = moveArrayItem(draggableFields, { + fromIndex: source.index, + toIndex: destination.index, + }); const orderedPositions = draggableFields.map((field) => field.position); From b900f4e418e56fbb1bfbfad9ae4d8ffe7f9c3a6f Mon Sep 17 00:00:00 2001 From: Priyanshu Bartwal Date: Mon, 8 Jun 2026 14:59:39 +0530 Subject: [PATCH 6/6] Fix: Missing try/finally block --- .../record-table-header/hooks/useProcessTableColumnDrop.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop.ts index 6702cf60d51f9..f413f270db110 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/hooks/useProcessTableColumnDrop.ts @@ -17,9 +17,12 @@ export const useProcessTableColumnDrop = () => { const destination = headerColumnDropResult.destination; if (!isDefined(source) || !isDefined(destination)) return; - await reorderColumns({ source, destination }); - resetRecordTableHeaderDragStates(); + try { + await reorderColumns({ source, destination }); + } finally { + resetRecordTableHeaderDragStates(); + } }, [reorderColumns, resetRecordTableHeaderDragStates], );