[Twenty-Front]: Record board column drag and drop functionality (#22323)
Closes #22321 - Used `@dnd-kit` library for core drag and drop logic. - Tried to keep as much similar to #21304 as possible. https://github.com/user-attachments/assets/bff100cf-d727-4281-a5fa-b010a373f189 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22323?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com> Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
committed by
GitHub
parent
ab9e6f30b8
commit
7babc049f5
+2
-28
@@ -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 (
|
||||
<StyledHeaderContainer id="record-board-header">
|
||||
{visibleRecordGroupIds.map((recordGroupId, index) => (
|
||||
<RecordGroupContext.Provider
|
||||
key={recordGroupId}
|
||||
value={{ recordGroupId }}
|
||||
>
|
||||
<RecordBoardColumnHeaderWrapper
|
||||
columnId={recordGroupId}
|
||||
columnIndex={index}
|
||||
/>
|
||||
</RecordGroupContext.Provider>
|
||||
))}
|
||||
<RecordBoardAddGroupColumn />
|
||||
<RecordBoardColumnDnd />
|
||||
<RecordIndexGroupAggregatesDataLoader />
|
||||
</StyledHeaderContainer>
|
||||
);
|
||||
|
||||
+55
@@ -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 (
|
||||
<RecordBoardColumnDndKitProvider>
|
||||
<DragDropColumnDroppableSlot
|
||||
droppableId={RECORD_BOARD_COLUMN_DROPPABLE_ID}
|
||||
index={0}
|
||||
>
|
||||
<DragDropColumnDropTarget index={0} compact overlay />
|
||||
</DragDropColumnDroppableSlot>
|
||||
{visibleRecordGroupIds.map((recordGroupId, index) => (
|
||||
<Fragment key={recordGroupId}>
|
||||
<DragDropColumnSortableCell
|
||||
id={recordGroupId}
|
||||
index={index}
|
||||
group={RECORD_BOARD_COLUMN_DROPPABLE_ID}
|
||||
fill
|
||||
>
|
||||
<RecordGroupContext.Provider value={{ recordGroupId }}>
|
||||
<RecordBoardColumnHeaderWrapper
|
||||
columnId={recordGroupId}
|
||||
columnIndex={index}
|
||||
/>
|
||||
</RecordGroupContext.Provider>
|
||||
</DragDropColumnSortableCell>
|
||||
<DragDropColumnDroppableSlot
|
||||
droppableId={RECORD_BOARD_COLUMN_DROPPABLE_ID}
|
||||
index={index + 1}
|
||||
>
|
||||
<DragDropColumnDropTarget index={index + 1} compact overlay />
|
||||
</DragDropColumnDroppableSlot>
|
||||
</Fragment>
|
||||
))}
|
||||
<RecordBoardAddGroupColumn />
|
||||
</RecordBoardColumnDndKitProvider>
|
||||
);
|
||||
};
|
||||
+73
-55
@@ -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 (
|
||||
<StyledColumn>
|
||||
<StyledHeader
|
||||
onMouseEnter={() => setIsHeaderHovered(true)}
|
||||
onMouseLeave={() => setIsHeaderHovered(false)}
|
||||
>
|
||||
<StyledHeaderContainer>
|
||||
<StyledLeftContainer>
|
||||
<StyledDropdownContainer>
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownOffset={{
|
||||
x: 0,
|
||||
y: 10,
|
||||
}}
|
||||
clickableComponent={
|
||||
<StyledTagContainer>
|
||||
<RecordGroupChip
|
||||
recordGroupDefinition={columnDefinition}
|
||||
fieldMetadataItem={selectFieldMetadataItem}
|
||||
/>
|
||||
</StyledTagContainer>
|
||||
}
|
||||
dropdownComponents={<RecordBoardColumnDropdownMenu />}
|
||||
/>
|
||||
</StyledDropdownContainer>
|
||||
|
||||
<RecordBoardColumnHeaderAggregateDropdown
|
||||
aggregateValue={recordIndexAggregateDisplayValueForGroupValue}
|
||||
dropdownId={`record-board-column-aggregate-dropdown-${columnDefinition.id}`}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
aggregateLabel={recordIndexAggregateDisplayLabel}
|
||||
/>
|
||||
</StyledLeftContainer>
|
||||
<StyledRightContainer>
|
||||
{isHeaderHovered && (
|
||||
<StyledHeaderActions>
|
||||
<LightIconButton
|
||||
accent="tertiary"
|
||||
Icon={IconDotsVertical}
|
||||
onClick={() => {
|
||||
toggleDropdown({
|
||||
dropdownComponentInstanceIdFromProps: dropdownId,
|
||||
});
|
||||
<StyledColumn data-has-left-border={columnIndex > 0 ? 'true' : undefined}>
|
||||
<DragDropColumnSortableHandle fill>
|
||||
<StyledHeader
|
||||
onMouseEnter={() => setIsHeaderHovered(true)}
|
||||
onMouseLeave={() => setIsHeaderHovered(false)}
|
||||
onPointerCancel={handlePointerCancel}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerUp={handlePointerUp}
|
||||
>
|
||||
<StyledHeaderContainer>
|
||||
<StyledLeftContainer>
|
||||
<StyledDropdownContainer>
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownOffset={{
|
||||
x: 0,
|
||||
y: 10,
|
||||
}}
|
||||
clickableComponent={
|
||||
<StyledTagContainer>
|
||||
<RecordGroupChip
|
||||
recordGroupDefinition={columnDefinition}
|
||||
fieldMetadataItem={selectFieldMetadataItem}
|
||||
/>
|
||||
</StyledTagContainer>
|
||||
}
|
||||
dropdownComponents={<RecordBoardColumnDropdownMenu />}
|
||||
/>
|
||||
{canCreateRecords && !hasAnySoftDeleteFilterOnView && (
|
||||
</StyledDropdownContainer>
|
||||
|
||||
<RecordBoardColumnHeaderAggregateDropdown
|
||||
aggregateValue={recordIndexAggregateDisplayValueForGroupValue}
|
||||
dropdownId={`record-board-column-aggregate-dropdown-${columnDefinition.id}`}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
aggregateLabel={recordIndexAggregateDisplayLabel}
|
||||
/>
|
||||
</StyledLeftContainer>
|
||||
<StyledRightContainer>
|
||||
{isHeaderHovered && (
|
||||
<StyledHeaderActions>
|
||||
<LightIconButton
|
||||
accent="tertiary"
|
||||
Icon={IconPlus}
|
||||
onClick={handleCreateNewRecordClick}
|
||||
Icon={IconDotsVertical}
|
||||
onClick={() => {
|
||||
toggleDropdown({
|
||||
dropdownComponentInstanceIdFromProps: dropdownId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</StyledHeaderActions>
|
||||
)}
|
||||
</StyledRightContainer>
|
||||
</StyledHeaderContainer>
|
||||
</StyledHeader>
|
||||
{canCreateRecords && !hasAnySoftDeleteFilterOnView && (
|
||||
<LightIconButton
|
||||
accent="tertiary"
|
||||
Icon={IconPlus}
|
||||
onClick={handleCreateNewRecordClick}
|
||||
/>
|
||||
)}
|
||||
</StyledHeaderActions>
|
||||
)}
|
||||
</StyledRightContainer>
|
||||
</StyledHeaderContainer>
|
||||
</StyledHeader>
|
||||
</DragDropColumnSortableHandle>
|
||||
<RecordBoardColumnResizeHandler />
|
||||
</StyledColumn>
|
||||
);
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const RECORD_BOARD_COLUMN_DROPPABLE_ID = 'record-board-header';
|
||||
+199
@@ -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<typeof DragDropProvider<DragDropColumnData>>['onDragStart']
|
||||
>
|
||||
>[0];
|
||||
type DragMovePayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<typeof DragDropProvider<DragDropColumnData>>['onDragMove']
|
||||
>
|
||||
>[0];
|
||||
type DragEndPayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<typeof DragDropProvider<DragDropColumnData>>['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<PendingReorder | null>(
|
||||
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,
|
||||
},
|
||||
};
|
||||
};
|
||||
+37
@@ -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 (
|
||||
<>
|
||||
<DragDropColumnDndContext.Provider value={contextValues}>
|
||||
<DragDropProvider<DragDropColumnData>
|
||||
sensors={DND_KIT_SENSORS}
|
||||
onDragStart={handlers.onDragStart}
|
||||
onDragMove={handlers.onDragMove}
|
||||
onDragEnd={handlers.onDragEnd}
|
||||
>
|
||||
{children}
|
||||
</DragDropProvider>
|
||||
</DragDropColumnDndContext.Provider>
|
||||
<RecordGroupReorderConfirmationModal
|
||||
onConfirmClick={handlePendingReorderConfirmClick}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+4
-4
@@ -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"
|
||||
/>
|
||||
)}
|
||||
<RecordTableHeaderSortableHandle>
|
||||
<DragDropColumnSortableHandle>
|
||||
{isRecordTableColumnHeadersReadOnly ? (
|
||||
<RecordTableColumnHead recordField={recordField} />
|
||||
) : (
|
||||
@@ -113,7 +113,7 @@ export const RecordTableHeaderCell = ({
|
||||
objectMetadataId={objectMetadataItem.id}
|
||||
/>
|
||||
)}
|
||||
</RecordTableHeaderSortableHandle>
|
||||
</DragDropColumnSortableHandle>
|
||||
{isRecordTableColumnResizable && (
|
||||
<RecordTableHeaderResizeHandler
|
||||
recordFieldIndex={recordFieldIndex}
|
||||
|
||||
+18
-18
@@ -6,12 +6,12 @@ import { RecordTableHeaderCell } from '@/object-record/record-table/record-table
|
||||
import { RecordTableHeaderEmptyLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderEmptyLastColumn';
|
||||
import { RecordTableHeaderFirstScrollableCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderFirstScrollableCell';
|
||||
import { RecordTableHeaderLastEmptyColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastEmptyColumn';
|
||||
import { RecordTableHeaderDroppableSlot } from '@/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderDroppableSlot';
|
||||
import { RecordTableHeaderDropTarget } from '@/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderDropTarget';
|
||||
import { RecordTableHeaderSortableCell } from '@/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderSortableCell';
|
||||
import { RECORD_TABLE_HEADER_DROPPABLE_ID } from '@/object-record/record-table/record-table-header/dnd/constants/RecordTableHeaderDroppableId';
|
||||
import { RecordTableHeaderDndKitProvider } from '@/object-record/record-table/record-table-header/dnd/providers/RecordTableHeaderDndKitProvider';
|
||||
import { isRecordTableColumnHeadersReadOnlyComponentState } from '@/object-record/record-table/states/isRecordTableColumnHeadersReadOnlyComponentState';
|
||||
import { DragDropColumnDroppableSlot } from '@/ui/utilities/drag-and-drop/components/DragDropColumnDroppableSlot';
|
||||
import { DragDropColumnDropTarget } from '@/ui/utilities/drag-and-drop/components/DragDropColumnDropTarget';
|
||||
import { DragDropColumnSortableCell } from '@/ui/utilities/drag-and-drop/components/DragDropColumnSortableCell';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -28,16 +28,16 @@ export const RecordTableHeaderDnd = () => {
|
||||
|
||||
return (
|
||||
<RecordTableHeaderDndKitProvider>
|
||||
<RecordTableHeaderDroppableSlot
|
||||
<DragDropColumnDroppableSlot
|
||||
droppableId={RECORD_TABLE_HEADER_DROPPABLE_ID}
|
||||
index={0}
|
||||
disabled={isRecordTableColumnHeadersReadOnly}
|
||||
>
|
||||
<RecordTableHeaderDropTarget index={0} compact />
|
||||
</RecordTableHeaderDroppableSlot>
|
||||
<DragDropColumnDropTarget index={0} compact />
|
||||
</DragDropColumnDroppableSlot>
|
||||
|
||||
{isDefined(firstScrollableRecordField) && (
|
||||
<RecordTableHeaderSortableCell
|
||||
<DragDropColumnSortableCell
|
||||
key={firstScrollableRecordField.fieldMetadataItemId}
|
||||
id={firstScrollableRecordField.fieldMetadataItemId}
|
||||
index={0}
|
||||
@@ -47,19 +47,19 @@ export const RecordTableHeaderDnd = () => {
|
||||
<RecordTableHeaderFirstScrollableCell
|
||||
firstScrollableRecordField={firstScrollableRecordField}
|
||||
/>
|
||||
</RecordTableHeaderSortableCell>
|
||||
</DragDropColumnSortableCell>
|
||||
)}
|
||||
|
||||
{recordFieldsWithoutFirstTwo.map((recordField, index) => (
|
||||
<React.Fragment key={recordField.fieldMetadataItemId}>
|
||||
<RecordTableHeaderDroppableSlot
|
||||
<DragDropColumnDroppableSlot
|
||||
droppableId={RECORD_TABLE_HEADER_DROPPABLE_ID}
|
||||
index={index + 1}
|
||||
disabled={isRecordTableColumnHeadersReadOnly}
|
||||
>
|
||||
<RecordTableHeaderDropTarget index={index + 1} compact />
|
||||
</RecordTableHeaderDroppableSlot>
|
||||
<RecordTableHeaderSortableCell
|
||||
<DragDropColumnDropTarget index={index + 1} compact />
|
||||
</DragDropColumnDroppableSlot>
|
||||
<DragDropColumnSortableCell
|
||||
id={recordField.fieldMetadataItemId}
|
||||
index={index + 1}
|
||||
group={RECORD_TABLE_HEADER_DROPPABLE_ID}
|
||||
@@ -70,19 +70,19 @@ export const RecordTableHeaderDnd = () => {
|
||||
recordField={recordField}
|
||||
recordFieldIndex={index + 2}
|
||||
/>
|
||||
</RecordTableHeaderSortableCell>
|
||||
</DragDropColumnSortableCell>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<RecordTableHeaderDroppableSlot
|
||||
<DragDropColumnDroppableSlot
|
||||
droppableId={RECORD_TABLE_HEADER_DROPPABLE_ID}
|
||||
index={visibleRecordFields.length}
|
||||
index={visibleRecordFields.length - 1}
|
||||
disabled={isRecordTableColumnHeadersReadOnly}
|
||||
>
|
||||
<RecordTableHeaderDropTarget
|
||||
index={visibleRecordFields.length}
|
||||
<DragDropColumnDropTarget
|
||||
index={visibleRecordFields.length - 1}
|
||||
compact
|
||||
/>
|
||||
</RecordTableHeaderDroppableSlot>
|
||||
</DragDropColumnDroppableSlot>
|
||||
{isRecordTableColumnHeadersReadOnly ? (
|
||||
<RecordTableHeaderEmptyLastColumn />
|
||||
) : (
|
||||
|
||||
+4
-4
@@ -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 && (
|
||||
<RecordTableHeaderResizeHandler recordFieldIndex={1} position="left" />
|
||||
)}
|
||||
<RecordTableHeaderSortableHandle>
|
||||
<DragDropColumnSortableHandle>
|
||||
{isRecordTableColumnHeadersReadOnly ? (
|
||||
<RecordTableColumnHead recordField={firstScrollableRecordField} />
|
||||
) : (
|
||||
@@ -108,7 +108,7 @@ export const RecordTableHeaderFirstScrollableCell = ({
|
||||
objectMetadataId={objectMetadataItem.id}
|
||||
/>
|
||||
)}
|
||||
</RecordTableHeaderSortableHandle>
|
||||
</DragDropColumnSortableHandle>
|
||||
{isRecordTableColumnResizable && (
|
||||
<RecordTableHeaderResizeHandler recordFieldIndex={1} position="right" />
|
||||
)}
|
||||
|
||||
-28
@@ -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 (
|
||||
<StyledSortableHandle ref={sortableHandleRef}>
|
||||
{children}
|
||||
</StyledSortableHandle>
|
||||
);
|
||||
};
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
type RecordTableHeaderDndContextValue = {
|
||||
activeDropTargetIndex: number | null;
|
||||
};
|
||||
|
||||
export const RecordTableHeaderDndContext =
|
||||
createContext<RecordTableHeaderDndContextValue>({
|
||||
activeDropTargetIndex: null,
|
||||
});
|
||||
+43
-63
@@ -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<RecordTableHeaderDndData>
|
||||
>['onDragStart']
|
||||
ComponentProps<typeof DragDropProvider<DragDropColumnData>>['onDragStart']
|
||||
>
|
||||
>[0];
|
||||
type DragMovePayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<
|
||||
typeof DragDropProvider<RecordTableHeaderDndData>
|
||||
>['onDragMove']
|
||||
ComponentProps<typeof DragDropProvider<DragDropColumnData>>['onDragMove']
|
||||
>
|
||||
>[0];
|
||||
type DragEndPayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<
|
||||
typeof DragDropProvider<RecordTableHeaderDndData>
|
||||
>['onDragEnd']
|
||||
ComponentProps<typeof DragDropProvider<DragDropColumnData>>['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;
|
||||
|
||||
+5
-5
@@ -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 (
|
||||
<RecordTableHeaderDndContext.Provider value={contextValues}>
|
||||
<DragDropProvider<RecordTableHeaderDndData>
|
||||
<DragDropColumnDndContext.Provider value={contextValues}>
|
||||
<DragDropProvider<DragDropColumnData>
|
||||
sensors={DND_KIT_SENSORS}
|
||||
onDragStart={handlers.onDragStart}
|
||||
onDragMove={handlers.onDragMove}
|
||||
@@ -25,6 +25,6 @@ export const RecordTableHeaderDndKitProvider = ({
|
||||
>
|
||||
{children}
|
||||
</DragDropProvider>
|
||||
</RecordTableHeaderDndContext.Provider>
|
||||
</DragDropColumnDndContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
-23
@@ -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,
|
||||
};
|
||||
};
|
||||
-9
@@ -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<boolean>({
|
||||
key: 'isRecordTableHeaderDropProcessingComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
});
|
||||
+13
-12
@@ -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 (
|
||||
<StyledDropTarget
|
||||
$compact={compact}
|
||||
$overlay={overlay}
|
||||
data-drag-over={isDragOver ? 'true' : undefined}
|
||||
>
|
||||
{children}
|
||||
+13
-11
@@ -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,
|
||||
+23
-16
@@ -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 (
|
||||
<RecordTableHeaderSortableHandleRefContext.Provider value={handleRef}>
|
||||
<StyledSortableRoot ref={ref}>{children}</StyledSortableRoot>
|
||||
</RecordTableHeaderSortableHandleRefContext.Provider>
|
||||
<DragDropColumnSortableHandleRefContext.Provider value={handleRef}>
|
||||
<StyledSortableRoot ref={ref} $fill={fill}>
|
||||
{children}
|
||||
</StyledSortableRoot>
|
||||
</DragDropColumnSortableHandleRefContext.Provider>
|
||||
);
|
||||
};
|
||||
+36
@@ -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 (
|
||||
<StyledSortableHandle ref={sortableHandleRef} $fill={fill}>
|
||||
{children}
|
||||
</StyledSortableHandle>
|
||||
);
|
||||
};
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
type DragDropColumnDndContextValue = {
|
||||
activeDropTargetIndex: number | null;
|
||||
};
|
||||
|
||||
export const DragDropColumnDndContext =
|
||||
createContext<DragDropColumnDndContextValue>({
|
||||
activeDropTargetIndex: null,
|
||||
});
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const RecordTableHeaderSortableHandleRefContext = createContext<
|
||||
export const DragDropColumnSortableHandleRefContext = createContext<
|
||||
((element: Element | null) => void) | undefined
|
||||
>(undefined);
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
export type RecordTableHeaderDndData = {
|
||||
index: number;
|
||||
export type DragDropColumnData = {
|
||||
droppableId: string;
|
||||
index: number;
|
||||
};
|
||||
+114
@@ -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);
|
||||
});
|
||||
});
|
||||
+13
-16
@@ -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,
|
||||
+14
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user