Skip to content

Commit 08657c9

Browse files
atrakhConvex, Inc.
authored andcommitted
dashboard: migrate dnd behavior to dnd-kit (#42543)
Migrates from react-dnd to dnd-kit for better previews and smoother drag and drop experiences. Also spruces up the previews when dragging columns + the preview when resizing (not dnd behavior, but related visual experience) GitOrigin-RevId: a710d6618a15faae00f2b1a3fea3b1ea706feb69
1 parent ecdb676 commit 08657c9

File tree

9 files changed

+389
-183
lines changed

9 files changed

+389
-183
lines changed

npm-packages/common/config/rush/pnpm-lock.yaml

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

npm-packages/dashboard-common/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
},
2222
"dependencies": {
2323
"@convex-dev/design-system": "workspace:*",
24+
"@dnd-kit/core": "~6.3.1",
25+
"@dnd-kit/sortable": "~10.0.0",
2426
"@floating-ui/react": "^0.27.0",
2527
"@headlessui/react": "1.7.19",
2628
"@heroicons/react": "2.2.0",

npm-packages/dashboard-common/src/features/data/components/Table/ColumnHeader.tsx

Lines changed: 41 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
import classNames from "classnames";
77
import { GenericDocument } from "convex/server";
88
import { HeaderGroup } from "react-table";
9-
import { useDrop, useDrag } from "react-dnd";
10-
import { useRef, useState } from "react";
9+
import { useSortable } from "@dnd-kit/sortable";
10+
import { useRef, useState, RefObject } from "react";
1111
import omit from "lodash/omit";
1212
import { useContextMenuTrigger } from "@common/features/data/lib/useContextMenuTrigger";
1313
import { useTableDensity } from "@common/features/data/lib/useTableDensity";
@@ -29,17 +29,16 @@ type ColumnHeaderProps = {
2929
hasFilters: boolean;
3030
isSelectionExhaustive: boolean;
3131
toggleAll: () => void;
32-
reorder(item: { index: number }, newIndex: number): void;
3332
isResizingColumn?: string;
3433
isLastColumn: boolean;
3534
openContextMenu: DataCellProps["onOpenContextMenu"];
3635
sort?: "asc" | "desc";
3736
activeSchema: any | null;
3837
tableName: string;
38+
tableContainerRef: RefObject<HTMLDivElement>;
3939
};
4040

4141
export function ColumnHeader({
42-
reorder,
4342
column,
4443
columnIndex,
4544
allRowsSelected = false,
@@ -52,14 +51,27 @@ export function ColumnHeader({
5251
sort,
5352
activeSchema,
5453
tableName,
54+
tableContainerRef,
5555
}: ColumnHeaderProps) {
5656
const canDragOrDrop = columnIndex !== 0 && !isResizingColumn;
5757

5858
const headerNode = useRef<HTMLDivElement | null>(null);
5959

60-
const { isDragging, isHovering, direction, drop, drag, dragPreview } =
61-
useColumnDragAndDrop(column, columnIndex, reorder, canDragOrDrop);
6260
const columnName = column.Header as string;
61+
const columnId = column.id;
62+
63+
const { attributes, listeners, setNodeRef, isDragging, isOver, active } =
64+
useSortable({
65+
id: columnId,
66+
disabled: !canDragOrDrop,
67+
});
68+
69+
// Always drop to the right of the hovered column
70+
const direction =
71+
isOver && !isDragging && active && active.id !== columnId
72+
? "right"
73+
: undefined;
74+
const isHovering = isOver && !isDragging && active?.id !== columnId;
6375
useContextMenuTrigger(
6476
headerNode,
6577
(pos) =>
@@ -87,43 +99,43 @@ export function ColumnHeader({
8799
<div
88100
key={column.getHeaderProps().key}
89101
{...omit(column.getHeaderProps({ style: { width } }), "key")}
102+
ref={setNodeRef}
90103
className={classNames(
91-
isDragging && "cursor-grabbing",
104+
isDragging && "opacity-50",
92105
"font-semibold text-left text-xs bg-background-secondary text-content-secondary tracking-wider",
93106
"select-none duration-300 transition-colors",
94-
!isLastColumn && "border-r",
95-
isResizingColumn === columnName && "border-r-util-accent",
107+
"border-r",
96108
"relative",
97109
)}
98110
onMouseEnter={() => setIsHovered(true)}
99111
onMouseLeave={() => setIsHovered(false)}
100112
>
101-
{/* Show a border on the side the column will be dropped */}
102-
{!isDragging && isHovering && direction && (
113+
{/* Show a vertical line on the right side where the column will be dropped */}
114+
{isHovering && direction && (
103115
<div
104-
className={classNames(
105-
"absolute top-px h-full w-px bg-util-accent",
106-
direction === "left" ? "left-0" : "right-0",
107-
)}
116+
className="absolute top-0 right-0 z-10 w-0.5 bg-util-accent"
117+
style={{
118+
height: tableContainerRef.current?.offsetHeight || "100%",
119+
}}
120+
/>
121+
)}
122+
{/* Show a vertical line when resizing this column */}
123+
{isResizingColumn === columnName && (
124+
<div
125+
className="absolute top-0 right-0 z-10 w-0.5 bg-util-accent"
126+
style={{
127+
height: tableContainerRef.current?.offsetHeight || "100%",
128+
}}
108129
/>
109130
)}
110131
<ValidatorTooltip
111132
fieldSchema={fieldSchema}
112133
columnName={columnName}
113-
disableTooltip={!!isResizingColumn}
134+
disableTooltip={!!isResizingColumn || isDragging}
114135
>
115136
<div
116-
ref={(node) => {
117-
headerNode.current = node;
118-
if (node) {
119-
drop(node);
120-
dragPreview(node);
121-
}
122-
}}
123-
className={cn(
124-
"flex w-full items-center justify-between space-x-2",
125-
isDragging && "cursor-grabbing",
126-
)}
137+
ref={headerNode}
138+
className="flex w-full items-center space-x-2"
127139
style={{
128140
padding: `${densityValues.paddingY}px ${columnIndex === 0 ? "12" : densityValues.paddingX}px`,
129141
width,
@@ -173,9 +185,8 @@ export function ColumnHeader({
173185
</div>
174186
{canDragOrDrop && isHovered && (
175187
<Button
176-
ref={(node) => {
177-
node && drag(node);
178-
}}
188+
{...attributes}
189+
{...listeners}
179190
className={cn(
180191
"absolute right-1.5 animate-fadeInFromLoading cursor-grab items-center bg-background-secondary/50 text-content-secondary backdrop-blur-[2px]",
181192
isDragging && "cursor-grabbing",
@@ -184,12 +195,6 @@ export function ColumnHeader({
184195
variant="neutral"
185196
inline
186197
size="xs"
187-
onKeyDown={(e) => {
188-
if (e.key === " " || e.key === "Enter") {
189-
e.preventDefault();
190-
// Optionally, trigger drag start here if needed for keyboard users
191-
}
192-
}}
193198
icon={<DragHandleDots2Icon />}
194199
/>
195200
)}
@@ -210,39 +215,3 @@ export function ColumnHeader({
210215
</div>
211216
);
212217
}
213-
214-
export function useColumnDragAndDrop(
215-
column: HeaderGroup<GenericDocument>,
216-
columnIndex: number,
217-
reorder: (item: { index: number }, newIndex: number) => void,
218-
canDragOrDrop: boolean,
219-
) {
220-
const { id } = column;
221-
const [{ isHovering, offset }, drop] = useDrop({
222-
accept: "column",
223-
canDrop: () => canDragOrDrop,
224-
drop: (item: { index: number }) => {
225-
reorder(item, columnIndex);
226-
},
227-
collect: (monitor) => ({
228-
isHovering: canDragOrDrop && monitor.isOver({ shallow: true }),
229-
offset: monitor.getDifferenceFromInitialOffset(),
230-
}),
231-
});
232-
233-
const direction = offset?.x ? (offset.x > 0 ? "right" : "left") : undefined;
234-
235-
const [{ isDragging }, drag, dragPreview] = useDrag({
236-
type: "column",
237-
canDrag: canDragOrDrop,
238-
item: () => ({
239-
id,
240-
index: columnIndex,
241-
}),
242-
collect: (monitor) => ({
243-
isDragging: monitor.isDragging(),
244-
}),
245-
});
246-
247-
return { isDragging, isHovering, direction, drop, drag, dragPreview };
248-
}

npm-packages/dashboard-common/src/features/data/components/Table/DataCell/DataCell.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { GenericId, Value } from "convex/values";
22
import { GenericDocument } from "convex/server";
33
import classNames from "classnames";
44
import React, { memo, useRef, useState } from "react";
5-
import { useClickAway, useHoverDirty } from "react-use";
5+
import { useClickAway } from "react-use";
66
import { areEqual } from "react-window";
77
import { usePopper } from "react-popper";
88
import { ColumnInstance } from "react-table";
@@ -87,7 +87,7 @@ function DataCellImpl({
8787
// Derive all the information needed to render the cell
8888
const columnName = column.Header as string;
8989
const stringValue = typeof value === "string" ? value : stringifyValue(value);
90-
const isHoveringCell = useHoverDirty(cellRef);
90+
const [isHoveringCell, setIsHoveringCell] = useState(false);
9191
const [isFocused, setIsFocused] = useState(false);
9292
const isSystemField = columnName?.startsWith("_");
9393
const isEditable = !isSystemField && canManageTable;
@@ -202,6 +202,8 @@ function DataCellImpl({
202202
}}
203203
className="relative flex h-full w-full items-center hover:bg-background-tertiary/75"
204204
style={{ width }}
205+
onMouseEnter={() => setIsHoveringCell(true)}
206+
onMouseLeave={() => setIsHoveringCell(false)}
205207
>
206208
{/* We do not use Button here because it's expensive and this table needs to be fast */}
207209
{/* eslint-disable-next-line react/forbid-elements */}

npm-packages/dashboard-common/src/features/data/components/Table/DataRow.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { Row } from "react-table";
1313
import classNames from "classnames";
1414
import { useFirstMountState, usePrevious } from "react-use";
1515
import { areEqual } from "react-window";
16-
import { cn } from "@ui/cn";
1716
import omit from "lodash/omit";
1817
import { useContextMenuTrigger } from "@common/features/data/lib/useContextMenuTrigger";
1918
import { Target } from "@common/features/data/components/ContextMenu";
@@ -120,7 +119,7 @@ function DataRowLoaded({ index, style, data }: DataRowProps) {
120119
onCloseContextMenu,
121120
canManageTable,
122121
activeSchema,
123-
resizingColumn,
122+
resizingColumn: _resizingColumn,
124123
onEditDocument,
125124
contextMenuColumn,
126125
contextMenuRow,
@@ -183,13 +182,7 @@ function DataRowLoaded({ index, style, data }: DataRowProps) {
183182
<div
184183
{...cell.getCellProps({ style: { width } })}
185184
key={cell.getCellProps().key}
186-
className={cn(
187-
columnIndex < row.cells.length - 1
188-
? "border-r transition-colors duration-300"
189-
: "transition-colors duration-300",
190-
resizingColumn === (cell.column.Header as string) &&
191-
"border-r-util-accent",
192-
)}
185+
className="border-r transition-colors duration-300"
193186
>
194187
{columnIndex === 0 ? (
195188
<TableCheckbox

0 commit comments

Comments
 (0)