[Twenty-front]: Record table Header drag and drop functionality (#21304)
Closes #21303 and https://github.com/twentyhq/core-team-issues/issues/151 https://github.com/user-attachments/assets/45cee1be-464f-467e-a1c0-cf5354ff87db --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
committed by
GitHub
parent
46642c81c9
commit
6e7d8ef96c
+3
-16
@@ -1,9 +1,4 @@
|
|||||||
import { PointerActivationConstraints } from '@dnd-kit/dom';
|
import { DragDropProvider } from '@dnd-kit/react';
|
||||||
import {
|
|
||||||
DragDropProvider,
|
|
||||||
KeyboardSensor,
|
|
||||||
PointerSensor,
|
|
||||||
} from '@dnd-kit/react';
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
import type { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants';
|
import type { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants';
|
||||||
@@ -12,15 +7,7 @@ import { NavigationDropTargetContext } from '@/navigation-menu-item/common/conte
|
|||||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext';
|
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext';
|
||||||
import type { DraggableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDraggableData';
|
import type { DraggableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDraggableData';
|
||||||
import { useNavigationMenuItemDndKit } from '@/navigation-menu-item/display/dnd/hooks/useNavigationMenuItemDndKit';
|
import { useNavigationMenuItemDndKit } from '@/navigation-menu-item/display/dnd/hooks/useNavigationMenuItemDndKit';
|
||||||
|
import { DND_KIT_SENSORS } from '@/ui/utilities/drag-and-drop/constants/DndKitSensors';
|
||||||
const NAVIGATION_MENU_ITEM_DND_SENSORS = [
|
|
||||||
PointerSensor.configure({
|
|
||||||
activationConstraints: [
|
|
||||||
new PointerActivationConstraints.Distance({ value: 8 }),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
KeyboardSensor,
|
|
||||||
];
|
|
||||||
|
|
||||||
type NavigationMenuItemDndKitProviderProps = {
|
type NavigationMenuItemDndKitProviderProps = {
|
||||||
section: NavigationSections;
|
section: NavigationSections;
|
||||||
@@ -38,7 +25,7 @@ export const NavigationMenuItemDndKitProvider = ({
|
|||||||
<NavigationMenuItemDragContext.Provider value={contextValues.drag}>
|
<NavigationMenuItemDragContext.Provider value={contextValues.drag}>
|
||||||
<NavigationDropTargetContext.Provider value={contextValues.dropTarget}>
|
<NavigationDropTargetContext.Provider value={contextValues.dropTarget}>
|
||||||
<DragDropProvider<DraggableData>
|
<DragDropProvider<DraggableData>
|
||||||
sensors={NAVIGATION_MENU_ITEM_DND_SENSORS}
|
sensors={DND_KIT_SENSORS}
|
||||||
onDragStart={handlers.onDragStart}
|
onDragStart={handlers.onDragStart}
|
||||||
onDragOver={handlers.onDragOver}
|
onDragOver={handlers.onDragOver}
|
||||||
onDragEnd={handlers.onDragEnd}
|
onDragEnd={handlers.onDragEnd}
|
||||||
|
|||||||
+2
-29
@@ -1,16 +1,10 @@
|
|||||||
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
|
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
|
||||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
|
||||||
import { RecordTableHeaderAddColumnButton } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderAddColumnButton';
|
|
||||||
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 { RecordTableHeaderCheckboxColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn';
|
||||||
|
import { RecordTableHeaderDnd } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDnd';
|
||||||
import { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn';
|
import { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn';
|
||||||
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 { RecordTableHeaderFirstCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderFirstCell';
|
||||||
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 { useResizeTableHeader } from '@/object-record/record-table/record-table-header/hooks/useResizeTableHeader';
|
import { useResizeTableHeader } from '@/object-record/record-table/record-table-header/hooks/useResizeTableHeader';
|
||||||
import { isRecordTableCheckboxColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableCheckboxColumnHiddenComponentState';
|
import { isRecordTableCheckboxColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableCheckboxColumnHiddenComponentState';
|
||||||
import { isRecordTableColumnHeadersReadOnlyComponentState } from '@/object-record/record-table/states/isRecordTableColumnHeadersReadOnlyComponentState';
|
|
||||||
import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState';
|
import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState';
|
||||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||||
import { styled } from '@linaria/react';
|
import { styled } from '@linaria/react';
|
||||||
@@ -24,12 +18,6 @@ const StyledHeaderContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const RecordTableHeader = () => {
|
export const RecordTableHeader = () => {
|
||||||
const { visibleRecordFields } = useRecordTableContextOrThrow();
|
|
||||||
|
|
||||||
const isRecordTableColumnHeadersReadOnly = useAtomComponentStateValue(
|
|
||||||
isRecordTableColumnHeadersReadOnlyComponentState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isRecordTableDragColumnHidden = useAtomComponentStateValue(
|
const isRecordTableDragColumnHidden = useAtomComponentStateValue(
|
||||||
isRecordTableDragColumnHiddenComponentState,
|
isRecordTableDragColumnHiddenComponentState,
|
||||||
);
|
);
|
||||||
@@ -38,8 +26,6 @@ export const RecordTableHeader = () => {
|
|||||||
isRecordTableCheckboxColumnHiddenComponentState,
|
isRecordTableCheckboxColumnHiddenComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const recordFieldsWithoutFirstTwo = visibleRecordFields.slice(2);
|
|
||||||
|
|
||||||
useResizeTableHeader();
|
useResizeTableHeader();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -49,20 +35,7 @@ export const RecordTableHeader = () => {
|
|||||||
<RecordTableHeaderCheckboxColumn />
|
<RecordTableHeaderCheckboxColumn />
|
||||||
)}
|
)}
|
||||||
<RecordTableHeaderFirstCell />
|
<RecordTableHeaderFirstCell />
|
||||||
<RecordTableHeaderFirstScrollableCell />
|
<RecordTableHeaderDnd />
|
||||||
{recordFieldsWithoutFirstTwo.map((recordField, index) => (
|
|
||||||
<RecordTableHeaderCell
|
|
||||||
key={recordField.fieldMetadataItemId}
|
|
||||||
recordField={recordField}
|
|
||||||
recordFieldIndex={index + 2}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{isRecordTableColumnHeadersReadOnly ? (
|
|
||||||
<RecordTableHeaderEmptyLastColumn />
|
|
||||||
) : (
|
|
||||||
<RecordTableHeaderAddColumnButton />
|
|
||||||
)}
|
|
||||||
<RecordTableHeaderLastEmptyColumn />
|
|
||||||
</StyledHeaderContainer>
|
</StyledHeaderContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+21
-8
@@ -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 { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
|
||||||
import { RecordTableHeaderCellContainer } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer';
|
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 { 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 { isRecordTableColumnHeadersReadOnlyComponentState } from '@/object-record/record-table/states/isRecordTableColumnHeadersReadOnlyComponentState';
|
import { isRecordTableColumnHeadersReadOnlyComponentState } from '@/object-record/record-table/states/isRecordTableColumnHeadersReadOnlyComponentState';
|
||||||
import { isRecordTableColumnResizableComponentState } from '@/object-record/record-table/states/isRecordTableColumnResizableComponentState';
|
import { isRecordTableColumnResizableComponentState } from '@/object-record/record-table/states/isRecordTableColumnResizableComponentState';
|
||||||
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
|
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
|
||||||
@@ -14,6 +15,7 @@ import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/rec
|
|||||||
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
|
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
|
||||||
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
|
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
|
||||||
import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthClassName';
|
import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthClassName';
|
||||||
|
import { useDisableDragSelectOnPointerDown } from '@/object-record/record-table/record-table-header/hooks/useDisableDragSelectOnPointerDown';
|
||||||
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
|
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
|
||||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||||
@@ -31,6 +33,12 @@ export const RecordTableHeaderCell = ({
|
|||||||
}: RecordTableHeaderCellProps) => {
|
}: RecordTableHeaderCellProps) => {
|
||||||
const { objectMetadataItem } = useRecordTableContextOrThrow();
|
const { objectMetadataItem } = useRecordTableContextOrThrow();
|
||||||
|
|
||||||
|
const {
|
||||||
|
onPointerCancel: handlePointerCancel,
|
||||||
|
onPointerDown: handlePointerDown,
|
||||||
|
onPointerUp: handlePointerUp,
|
||||||
|
} = useDisableDragSelectOnPointerDown();
|
||||||
|
|
||||||
const isRecordTableColumnHeadersReadOnly = useAtomComponentStateValue(
|
const isRecordTableColumnHeadersReadOnly = useAtomComponentStateValue(
|
||||||
isRecordTableColumnHeadersReadOnlyComponentState,
|
isRecordTableColumnHeadersReadOnlyComponentState,
|
||||||
);
|
);
|
||||||
@@ -83,6 +91,9 @@ export const RecordTableHeaderCell = ({
|
|||||||
getRecordTableColumnFieldWidthClassName(recordFieldIndex),
|
getRecordTableColumnFieldWidthClassName(recordFieldIndex),
|
||||||
)}
|
)}
|
||||||
key={recordField.fieldMetadataItemId}
|
key={recordField.fieldMetadataItemId}
|
||||||
|
onPointerCancel={handlePointerCancel}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
shouldDisplayBorderBottom={shouldDisplayBorderBottom}
|
shouldDisplayBorderBottom={shouldDisplayBorderBottom}
|
||||||
isResizing={isResizingAnyColumn}
|
isResizing={isResizingAnyColumn}
|
||||||
isReadOnly={isRecordTableColumnHeadersReadOnly}
|
isReadOnly={isRecordTableColumnHeadersReadOnly}
|
||||||
@@ -93,14 +104,16 @@ export const RecordTableHeaderCell = ({
|
|||||||
position="left"
|
position="left"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isRecordTableColumnHeadersReadOnly ? (
|
<RecordTableHeaderSortableHandle>
|
||||||
<RecordTableColumnHead recordField={recordField} />
|
{isRecordTableColumnHeadersReadOnly ? (
|
||||||
) : (
|
<RecordTableColumnHead recordField={recordField} />
|
||||||
<RecordTableColumnHeadWithDropdown
|
) : (
|
||||||
recordField={recordField}
|
<RecordTableColumnHeadWithDropdown
|
||||||
objectMetadataId={objectMetadataItem.id}
|
recordField={recordField}
|
||||||
/>
|
objectMetadataId={objectMetadataItem.id}
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
|
</RecordTableHeaderSortableHandle>
|
||||||
{isRecordTableColumnResizable && (
|
{isRecordTableColumnResizable && (
|
||||||
<RecordTableHeaderResizeHandler
|
<RecordTableHeaderResizeHandler
|
||||||
recordFieldIndex={recordFieldIndex}
|
recordFieldIndex={recordFieldIndex}
|
||||||
|
|||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
|
import { RecordTableHeaderAddColumnButton } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderAddColumnButton';
|
||||||
|
import { RecordTableHeaderCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCell';
|
||||||
|
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 { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
export const RecordTableHeaderDnd = () => {
|
||||||
|
const { visibleRecordFields } = useRecordTableContextOrThrow();
|
||||||
|
|
||||||
|
const isRecordTableColumnHeadersReadOnly = useAtomComponentStateValue(
|
||||||
|
isRecordTableColumnHeadersReadOnlyComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstScrollableRecordField = visibleRecordFields[1];
|
||||||
|
|
||||||
|
const recordFieldsWithoutFirstTwo = visibleRecordFields.slice(2);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RecordTableHeaderDndKitProvider>
|
||||||
|
<RecordTableHeaderDroppableSlot
|
||||||
|
droppableId={RECORD_TABLE_HEADER_DROPPABLE_ID}
|
||||||
|
index={0}
|
||||||
|
disabled={isRecordTableColumnHeadersReadOnly}
|
||||||
|
>
|
||||||
|
<RecordTableHeaderDropTarget index={0} compact />
|
||||||
|
</RecordTableHeaderDroppableSlot>
|
||||||
|
|
||||||
|
{isDefined(firstScrollableRecordField) && (
|
||||||
|
<RecordTableHeaderSortableCell
|
||||||
|
key={firstScrollableRecordField.fieldMetadataItemId}
|
||||||
|
id={firstScrollableRecordField.fieldMetadataItemId}
|
||||||
|
index={0}
|
||||||
|
group={RECORD_TABLE_HEADER_DROPPABLE_ID}
|
||||||
|
disabled={isRecordTableColumnHeadersReadOnly}
|
||||||
|
>
|
||||||
|
<RecordTableHeaderFirstScrollableCell
|
||||||
|
firstScrollableRecordField={firstScrollableRecordField}
|
||||||
|
/>
|
||||||
|
</RecordTableHeaderSortableCell>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{recordFieldsWithoutFirstTwo.map((recordField, index) => (
|
||||||
|
<React.Fragment key={recordField.fieldMetadataItemId}>
|
||||||
|
<RecordTableHeaderDroppableSlot
|
||||||
|
droppableId={RECORD_TABLE_HEADER_DROPPABLE_ID}
|
||||||
|
index={index + 1}
|
||||||
|
disabled={isRecordTableColumnHeadersReadOnly}
|
||||||
|
>
|
||||||
|
<RecordTableHeaderDropTarget index={index + 1} compact />
|
||||||
|
</RecordTableHeaderDroppableSlot>
|
||||||
|
<RecordTableHeaderSortableCell
|
||||||
|
id={recordField.fieldMetadataItemId}
|
||||||
|
index={index + 1}
|
||||||
|
group={RECORD_TABLE_HEADER_DROPPABLE_ID}
|
||||||
|
disabled={isRecordTableColumnHeadersReadOnly}
|
||||||
|
>
|
||||||
|
<RecordTableHeaderCell
|
||||||
|
key={recordField.fieldMetadataItemId}
|
||||||
|
recordField={recordField}
|
||||||
|
recordFieldIndex={index + 2}
|
||||||
|
/>
|
||||||
|
</RecordTableHeaderSortableCell>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
<RecordTableHeaderDroppableSlot
|
||||||
|
droppableId={RECORD_TABLE_HEADER_DROPPABLE_ID}
|
||||||
|
index={visibleRecordFields.length}
|
||||||
|
disabled={isRecordTableColumnHeadersReadOnly}
|
||||||
|
>
|
||||||
|
<RecordTableHeaderDropTarget
|
||||||
|
index={visibleRecordFields.length}
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
</RecordTableHeaderDroppableSlot>
|
||||||
|
{isRecordTableColumnHeadersReadOnly ? (
|
||||||
|
<RecordTableHeaderEmptyLastColumn />
|
||||||
|
) : (
|
||||||
|
<RecordTableHeaderAddColumnButton />
|
||||||
|
)}
|
||||||
|
<RecordTableHeaderLastEmptyColumn />
|
||||||
|
</RecordTableHeaderDndKitProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
+30
-18
@@ -8,6 +8,7 @@ import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/rec
|
|||||||
import { RecordTableHeaderCellContainer } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer';
|
import { RecordTableHeaderCellContainer } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer';
|
||||||
|
|
||||||
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
|
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
|
||||||
|
import { RecordTableHeaderSortableHandle } from '@/object-record/record-table/record-table-header/dnd/components/RecordTableHeaderSortableHandle';
|
||||||
import { isRecordTableColumnHeadersReadOnlyComponentState } from '@/object-record/record-table/states/isRecordTableColumnHeadersReadOnlyComponentState';
|
import { isRecordTableColumnHeadersReadOnlyComponentState } from '@/object-record/record-table/states/isRecordTableColumnHeadersReadOnlyComponentState';
|
||||||
import { isRecordTableColumnResizableComponentState } from '@/object-record/record-table/states/isRecordTableColumnResizableComponentState';
|
import { isRecordTableColumnResizableComponentState } from '@/object-record/record-table/states/isRecordTableColumnResizableComponentState';
|
||||||
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
|
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
|
||||||
@@ -21,10 +22,22 @@ import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/
|
|||||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||||
import { cx } from '@linaria/core';
|
import { cx } from '@linaria/core';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { useDisableDragSelectOnPointerDown } from '@/object-record/record-table/record-table-header/hooks/useDisableDragSelectOnPointerDown';
|
||||||
|
|
||||||
export const RecordTableHeaderFirstScrollableCell = () => {
|
type RecordTableHeaderFirstScrollableCellProps = {
|
||||||
const { objectMetadataItem, visibleRecordFields } =
|
firstScrollableRecordField: RecordField;
|
||||||
useRecordTableContextOrThrow();
|
};
|
||||||
|
|
||||||
|
export const RecordTableHeaderFirstScrollableCell = ({
|
||||||
|
firstScrollableRecordField,
|
||||||
|
}: RecordTableHeaderFirstScrollableCellProps) => {
|
||||||
|
const { objectMetadataItem } = useRecordTableContextOrThrow();
|
||||||
|
|
||||||
|
const {
|
||||||
|
onPointerCancel: handlePointerCancel,
|
||||||
|
onPointerDown: handlePointerDown,
|
||||||
|
onPointerUp: handlePointerUp,
|
||||||
|
} = useDisableDragSelectOnPointerDown();
|
||||||
|
|
||||||
const isRecordTableColumnHeadersReadOnly = useAtomComponentStateValue(
|
const isRecordTableColumnHeadersReadOnly = useAtomComponentStateValue(
|
||||||
isRecordTableColumnHeadersReadOnlyComponentState,
|
isRecordTableColumnHeadersReadOnlyComponentState,
|
||||||
@@ -44,8 +57,6 @@ export const RecordTableHeaderFirstScrollableCell = () => {
|
|||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
const recordField = visibleRecordFields[1] as RecordField | undefined;
|
|
||||||
|
|
||||||
const isRecordTableRowFocusActive = useAtomComponentStateValue(
|
const isRecordTableRowFocusActive = useAtomComponentStateValue(
|
||||||
isRecordTableRowFocusActiveComponentState,
|
isRecordTableRowFocusActiveComponentState,
|
||||||
);
|
);
|
||||||
@@ -73,14 +84,13 @@ export const RecordTableHeaderFirstScrollableCell = () => {
|
|||||||
|
|
||||||
const isResizingAnyColumn = isDefined(resizedFieldMetadataId);
|
const isResizingAnyColumn = isDefined(resizedFieldMetadataId);
|
||||||
|
|
||||||
if (!recordField) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordTableHeaderCellContainer
|
<RecordTableHeaderCellContainer
|
||||||
className={cx('header-cell', getRecordTableColumnFieldWidthClassName(1))}
|
className={cx('header-cell', getRecordTableColumnFieldWidthClassName(1))}
|
||||||
key={recordField.fieldMetadataItemId}
|
key={firstScrollableRecordField.fieldMetadataItemId}
|
||||||
|
onPointerCancel={handlePointerCancel}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
shouldDisplayBorderBottom={shouldDisplayBorderBottom}
|
shouldDisplayBorderBottom={shouldDisplayBorderBottom}
|
||||||
zIndex={TABLE_Z_INDEX.headerColumns.headerColumnsNormal}
|
zIndex={TABLE_Z_INDEX.headerColumns.headerColumnsNormal}
|
||||||
isResizing={isResizingAnyColumn}
|
isResizing={isResizingAnyColumn}
|
||||||
@@ -89,14 +99,16 @@ export const RecordTableHeaderFirstScrollableCell = () => {
|
|||||||
{isRecordTableColumnResizable && (
|
{isRecordTableColumnResizable && (
|
||||||
<RecordTableHeaderResizeHandler recordFieldIndex={1} position="left" />
|
<RecordTableHeaderResizeHandler recordFieldIndex={1} position="left" />
|
||||||
)}
|
)}
|
||||||
{isRecordTableColumnHeadersReadOnly ? (
|
<RecordTableHeaderSortableHandle>
|
||||||
<RecordTableColumnHead recordField={recordField} />
|
{isRecordTableColumnHeadersReadOnly ? (
|
||||||
) : (
|
<RecordTableColumnHead recordField={firstScrollableRecordField} />
|
||||||
<RecordTableColumnHeadWithDropdown
|
) : (
|
||||||
recordField={recordField}
|
<RecordTableColumnHeadWithDropdown
|
||||||
objectMetadataId={objectMetadataItem.id}
|
recordField={firstScrollableRecordField}
|
||||||
/>
|
objectMetadataId={objectMetadataItem.id}
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
|
</RecordTableHeaderSortableHandle>
|
||||||
{isRecordTableColumnResizable && (
|
{isRecordTableColumnResizable && (
|
||||||
<RecordTableHeaderResizeHandler recordFieldIndex={1} position="right" />
|
<RecordTableHeaderResizeHandler recordFieldIndex={1} position="right" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const StyledDropTarget = styled.div<{
|
||||||
|
$compact?: boolean;
|
||||||
|
}>`
|
||||||
|
height: 100%;
|
||||||
|
min-height: ${({ $compact }) =>
|
||||||
|
$compact ? '100%' : themeCssVariables.spacing[2]};
|
||||||
|
position: relative;
|
||||||
|
transition: background-color 120ms ease-out;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: ${themeCssVariables.color.blue};
|
||||||
|
border-radius: 0 ${themeCssVariables.border.radius.sm}
|
||||||
|
${themeCssVariables.border.radius.sm} 0;
|
||||||
|
content: '';
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
transform: scaleY(0.7);
|
||||||
|
transform-origin: center;
|
||||||
|
transition:
|
||||||
|
opacity 120ms ease-out,
|
||||||
|
transform 120ms ease-out;
|
||||||
|
width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-drag-over='true'] {
|
||||||
|
background-color: ${themeCssVariables.background.transparent.blue};
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-drag-over='true']::before {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scaleY(1);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type RecordTableHeaderDropTargetProps = {
|
||||||
|
index: number;
|
||||||
|
children?: ReactNode;
|
||||||
|
compact?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RecordTableHeaderDropTarget = ({
|
||||||
|
index,
|
||||||
|
children,
|
||||||
|
compact = false,
|
||||||
|
}: RecordTableHeaderDropTargetProps) => {
|
||||||
|
const { activeDropTargetIndex } = useContext(RecordTableHeaderDndContext);
|
||||||
|
|
||||||
|
const isDragOver = isDefined(activeDropTargetIndex)
|
||||||
|
? activeDropTargetIndex === index
|
||||||
|
: false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledDropTarget
|
||||||
|
$compact={compact}
|
||||||
|
data-drag-over={isDragOver ? 'true' : undefined}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</StyledDropTarget>
|
||||||
|
);
|
||||||
|
};
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const StyledSlotWrapper = styled.div`
|
||||||
|
align-self: stretch;
|
||||||
|
flex: 0 0 2px;
|
||||||
|
margin-left: -1px;
|
||||||
|
margin-right: -1px;
|
||||||
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SLOT_COLLISION_PRIORITY = 1;
|
||||||
|
|
||||||
|
type RecordTableHeaderDroppableSlotProps = {
|
||||||
|
droppableId: string;
|
||||||
|
index: number;
|
||||||
|
children?: ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
collisionPriority?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RecordTableHeaderDroppableSlot = ({
|
||||||
|
droppableId,
|
||||||
|
index,
|
||||||
|
children,
|
||||||
|
disabled = false,
|
||||||
|
collisionPriority = SLOT_COLLISION_PRIORITY,
|
||||||
|
}: RecordTableHeaderDroppableSlotProps) => {
|
||||||
|
const id = `${droppableId}::${index}`;
|
||||||
|
const data: RecordTableHeaderDndData = { droppableId, index };
|
||||||
|
const { ref } = useDroppable({
|
||||||
|
id,
|
||||||
|
disabled,
|
||||||
|
collisionPriority,
|
||||||
|
collisionDetector: pointerIntersection,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <StyledSlotWrapper ref={ref}>{children}</StyledSlotWrapper>;
|
||||||
|
};
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
import { RestrictToHorizontalAxis } from '@dnd-kit/abstract/modifiers';
|
||||||
|
import { SortableKeyboardPlugin } from '@dnd-kit/dom/sortable';
|
||||||
|
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';
|
||||||
|
|
||||||
|
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 RECORD_TABLE_HEADER_SORTABLE_TRANSITION = {
|
||||||
|
duration: 180,
|
||||||
|
easing: 'cubic-bezier(0.2, 0, 0, 1)',
|
||||||
|
idle: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledSortableRoot = styled.div`
|
||||||
|
min-height: 0;
|
||||||
|
outline: none;
|
||||||
|
position: relative;
|
||||||
|
will-change: transform;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type RecordTableHeaderSortableCellProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
group: string;
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RecordTableHeaderSortableCell = ({
|
||||||
|
id,
|
||||||
|
index,
|
||||||
|
group,
|
||||||
|
disabled = false,
|
||||||
|
children,
|
||||||
|
}: RecordTableHeaderSortableCellProps) => {
|
||||||
|
const { handleRef, ref } = useSortable({
|
||||||
|
id,
|
||||||
|
index,
|
||||||
|
group,
|
||||||
|
collisionPriority: SORTABLE_COLLISION_PRIORITY,
|
||||||
|
data: {
|
||||||
|
droppableId: group,
|
||||||
|
index: index,
|
||||||
|
},
|
||||||
|
disabled,
|
||||||
|
transition: RECORD_TABLE_HEADER_SORTABLE_TRANSITION,
|
||||||
|
plugins: PLUGINS_WITHOUT_OPTIMISTIC,
|
||||||
|
modifiers: RECORD_TABLE_HEADER_SORTABLE_MODIFIERS,
|
||||||
|
feedback: 'clone',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RecordTableHeaderSortableHandleRefContext.Provider value={handleRef}>
|
||||||
|
<StyledSortableRoot ref={ref}>{children}</StyledSortableRoot>
|
||||||
|
</RecordTableHeaderSortableHandleRefContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export const RECORD_TABLE_HEADER_DROPPABLE_ID = 'record-table-header-droppable';
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
type RecordTableHeaderDndContextValue = {
|
||||||
|
activeDropTargetIndex: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RecordTableHeaderDndContext =
|
||||||
|
createContext<RecordTableHeaderDndContextValue>({
|
||||||
|
activeDropTargetIndex: null,
|
||||||
|
});
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export const RecordTableHeaderSortableHandleRefContext = createContext<
|
||||||
|
((element: Element | null) => void) | undefined
|
||||||
|
>(undefined);
|
||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
import { type DragDropProvider } from '@dnd-kit/react';
|
||||||
|
import { useStore } from 'jotai';
|
||||||
|
import { type ComponentProps, useCallback, 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 { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||||
|
|
||||||
|
type DragStartPayload = Parameters<
|
||||||
|
NonNullable<
|
||||||
|
ComponentProps<
|
||||||
|
typeof DragDropProvider<RecordTableHeaderDndData>
|
||||||
|
>['onDragStart']
|
||||||
|
>
|
||||||
|
>[0];
|
||||||
|
type DragMovePayload = Parameters<
|
||||||
|
NonNullable<
|
||||||
|
ComponentProps<
|
||||||
|
typeof DragDropProvider<RecordTableHeaderDndData>
|
||||||
|
>['onDragMove']
|
||||||
|
>
|
||||||
|
>[0];
|
||||||
|
type DragEndPayload = Parameters<
|
||||||
|
NonNullable<
|
||||||
|
ComponentProps<
|
||||||
|
typeof DragDropProvider<RecordTableHeaderDndData>
|
||||||
|
>['onDragEnd']
|
||||||
|
>
|
||||||
|
>[0];
|
||||||
|
|
||||||
|
export type RecordTableHeaderDndKitContextValues = {
|
||||||
|
activeDropTargetIndex: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRecordTableHeaderDndKit = (): {
|
||||||
|
contextValues: RecordTableHeaderDndKitContextValues;
|
||||||
|
handlers: {
|
||||||
|
onDragStart: (event: DragStartPayload) => void;
|
||||||
|
onDragMove: (event: DragMovePayload) => void;
|
||||||
|
onDragEnd: (event: DragEndPayload) => void;
|
||||||
|
};
|
||||||
|
} => {
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const { recordTableId, visibleRecordFields } = useRecordTableContextOrThrow();
|
||||||
|
const { labelIdentifierFieldMetadataItem } = useRecordIndexContextOrThrow();
|
||||||
|
const { reorderVisibleRecordFields } =
|
||||||
|
useReorderVisibleRecordFields(recordTableId);
|
||||||
|
const { saveViewFields } = useSaveCurrentViewFields();
|
||||||
|
const { setDragSelectionStartEnabled } = useDragSelect();
|
||||||
|
const { getScrollWrapperElement } = useScrollWrapperHTMLElement();
|
||||||
|
|
||||||
|
const isRecordTableHeaderDropProcessingCallbackState =
|
||||||
|
useAtomComponentStateCallbackState(
|
||||||
|
isRecordTableHeaderDropProcessingComponentState,
|
||||||
|
);
|
||||||
|
const isRecordTableDragColumnHidden = useAtomComponentStateValue(
|
||||||
|
isRecordTableDragColumnHiddenComponentState,
|
||||||
|
);
|
||||||
|
const isRecordTableCheckboxColumnHidden = useAtomComponentStateValue(
|
||||||
|
isRecordTableCheckboxColumnHiddenComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [activeDropTargetIndex, setActiveDropTargetIndex] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
const recordFieldsWithoutLabelIdentifier = visibleRecordFields.filter(
|
||||||
|
filterOutByProperty(
|
||||||
|
'fieldMetadataItemId',
|
||||||
|
labelIdentifierFieldMetadataItem?.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const labelIdentifierRecordField = visibleRecordFields.find(
|
||||||
|
(recordField) =>
|
||||||
|
recordField.fieldMetadataItemId === labelIdentifierFieldMetadataItem?.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const nonSortableColumnsWidth =
|
||||||
|
(isRecordTableDragColumnHidden
|
||||||
|
? 0
|
||||||
|
: RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH) +
|
||||||
|
(isRecordTableCheckboxColumnHidden
|
||||||
|
? 0
|
||||||
|
: 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;
|
||||||
|
|
||||||
|
return resolveRecordTableHeaderDrop({
|
||||||
|
pointerX,
|
||||||
|
sourceIndex,
|
||||||
|
scrollWrapperElement,
|
||||||
|
nonSortableColumnsWidth,
|
||||||
|
recordFields: recordFieldsWithoutLabelIdentifier,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[
|
||||||
|
getScrollWrapperElement,
|
||||||
|
nonSortableColumnsWidth,
|
||||||
|
recordFieldsWithoutLabelIdentifier,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDragStart = (_event: DragStartPayload) => {
|
||||||
|
store.set(isRecordTableHeaderDropProcessingCallbackState, true);
|
||||||
|
|
||||||
|
setActiveDropTargetIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragMove = useCallback(
|
||||||
|
(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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[resolveDropFromPointerX, setActiveDropTargetIndex],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDragEnd = (event: DragEndPayload) => {
|
||||||
|
const { operation } = event;
|
||||||
|
const source = operation.source;
|
||||||
|
|
||||||
|
setActiveDropTargetIndex(null);
|
||||||
|
setDragSelectionStartEnabled(true);
|
||||||
|
store.set(isRecordTableHeaderDropProcessingCallbackState, false);
|
||||||
|
|
||||||
|
if (event.canceled) return;
|
||||||
|
if (!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;
|
||||||
|
|
||||||
|
// Sortable indices exclude the pinned label-identifier column at visibleRecordFields[0],
|
||||||
|
// so shift by one to address the full visible field list.
|
||||||
|
const updatedRecordField = reorderVisibleRecordFields({
|
||||||
|
fromIndex: resolvedDrop.sourceIndex + 1,
|
||||||
|
toIndex: resolvedDrop.destinationIndex + 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
saveViewFields([mapRecordFieldToViewField(updatedRecordField)]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextValues: RecordTableHeaderDndKitContextValues = {
|
||||||
|
activeDropTargetIndex,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
contextValues,
|
||||||
|
handlers: {
|
||||||
|
onDragStart: handleDragStart,
|
||||||
|
onDragMove: handleDragMove,
|
||||||
|
onDragEnd: handleDragEnd,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
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 { DND_KIT_SENSORS } from '@/ui/utilities/drag-and-drop/constants/DndKitSensors';
|
||||||
|
|
||||||
|
type RecordTableHeaderDndKitProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RecordTableHeaderDndKitProvider = ({
|
||||||
|
children,
|
||||||
|
}: RecordTableHeaderDndKitProviderProps) => {
|
||||||
|
const { contextValues, handlers } = useRecordTableHeaderDndKit();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RecordTableHeaderDndContext.Provider value={contextValues}>
|
||||||
|
<DragDropProvider<RecordTableHeaderDndData>
|
||||||
|
sensors={DND_KIT_SENSORS}
|
||||||
|
onDragStart={handlers.onDragStart}
|
||||||
|
onDragMove={handlers.onDragMove}
|
||||||
|
onDragEnd={handlers.onDragEnd}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DragDropProvider>
|
||||||
|
</RecordTableHeaderDndContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
export type RecordTableHeaderDndData = {
|
||||||
|
index: number;
|
||||||
|
droppableId: string;
|
||||||
|
};
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||||
|
|
||||||
|
type ResolveRecordTableHeaderDropArgs = {
|
||||||
|
pointerX: number;
|
||||||
|
sourceIndex: number;
|
||||||
|
scrollWrapperElement: HTMLElement;
|
||||||
|
nonSortableColumnsWidth: number;
|
||||||
|
recordFields: RecordField[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResolvedRecordTableHeaderDrop = {
|
||||||
|
sourceIndex: number;
|
||||||
|
dropTargetIndex: number;
|
||||||
|
destinationIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDestinationIndexFromDropTargetIndex = ({
|
||||||
|
sourceIndex,
|
||||||
|
dropTargetIndex,
|
||||||
|
}: {
|
||||||
|
sourceIndex: number;
|
||||||
|
dropTargetIndex: number;
|
||||||
|
}) => (dropTargetIndex > sourceIndex ? dropTargetIndex - 1 : dropTargetIndex);
|
||||||
|
|
||||||
|
export const resolveRecordTableHeaderDrop = ({
|
||||||
|
pointerX,
|
||||||
|
sourceIndex,
|
||||||
|
scrollWrapperElement,
|
||||||
|
nonSortableColumnsWidth,
|
||||||
|
recordFields,
|
||||||
|
}: ResolveRecordTableHeaderDropArgs): ResolvedRecordTableHeaderDrop => {
|
||||||
|
const scrollContainerRect = scrollWrapperElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
const contentX =
|
||||||
|
pointerX -
|
||||||
|
scrollContainerRect.left +
|
||||||
|
scrollWrapperElement.scrollLeft -
|
||||||
|
nonSortableColumnsWidth;
|
||||||
|
|
||||||
|
let left = 0;
|
||||||
|
|
||||||
|
for (const [index, field] of recordFields.entries()) {
|
||||||
|
const width = field.size;
|
||||||
|
const midpoint = left + width / 2;
|
||||||
|
|
||||||
|
if (contentX < midpoint) {
|
||||||
|
return {
|
||||||
|
sourceIndex,
|
||||||
|
dropTargetIndex: index,
|
||||||
|
destinationIndex: getDestinationIndexFromDropTargetIndex({
|
||||||
|
sourceIndex,
|
||||||
|
dropTargetIndex: index,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
left += width;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropTargetIndex = recordFields.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sourceIndex,
|
||||||
|
dropTargetIndex,
|
||||||
|
destinationIndex: getDestinationIndexFromDropTargetIndex({
|
||||||
|
sourceIndex,
|
||||||
|
dropTargetIndex,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
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
@@ -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 isRecordTableHeaderDropProcessingComponentState =
|
||||||
|
createAtomComponentState<boolean>({
|
||||||
|
key: 'isRecordTableHeaderDropProcessingComponentState',
|
||||||
|
defaultValue: false,
|
||||||
|
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||||
|
});
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
import { PointerActivationConstraints } from '@dnd-kit/dom';
|
||||||
|
import { KeyboardSensor, PointerSensor } from '@dnd-kit/react';
|
||||||
|
|
||||||
|
// Pointer drags only start past 8px so clicks on draggable items still register.
|
||||||
|
export const DND_KIT_SENSORS = [
|
||||||
|
PointerSensor.configure({
|
||||||
|
activationConstraints: [
|
||||||
|
new PointerActivationConstraints.Distance({ value: 8 }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
KeyboardSensor,
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user