chore: migrate row sort in SupabaseGrid to DndKit (#44628)

## Problem

We currently have 3 different libraries for drag & drop, two of which
are not actively maintained anymore.

## Solution

Migrate all usage of the two unmaintained libraries to DndKit.
This PR focuses on using DndKit instead of `react-dnd` for the row
sorting popover in the table editor

## Screencast


https://github.com/user-attachments/assets/9d5cf43c-fbbd-4e5e-8b5a-e9e803b71421



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Enhanced sorting functionality with drag-and-drop support to reorder
sort rules.

* **Improvements**
  * Updated drag handle styling for better visual clarity and usability.
  * Simplified sorting interface architecture.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Gildas Garcia authored and GitHub committed 2026-04-07 15:33:36 +02:00
1 parent 8923d52511
commit acb0929abe
3 files changed
+73 -103

No files matched your search

@@ -1,3 +1,17 @@
import {
closestCenter,
DndContext,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core'
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import { THRESHOLD_COUNT } from '@supabase/pg-meta'
import { keepPreviousData } from '@tanstack/react-query'
import { useParams } from 'common'
@@ -214,6 +228,13 @@ export const SortPopoverPrimitive = ({
}
}, [sorts])
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
)
return (
<>
<Popover_Shadcn_ modal={false} open={open} onOpenChange={setOpen}>
@@ -224,17 +245,35 @@ export const SortPopoverPrimitive = ({
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="p-0 w-96" side="bottom" align="start">
<div className="space-y-2 py-2">
{localSorts.map((sort, index) => (
<SortRow
key={getSortRowKey(sort, index)}
index={index}
columnName={sort.column}
sort={sort}
onDelete={onDeleteSort}
onToggle={onToggleSort}
onDrag={onDragSort}
/>
))}
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={(result) => {
if (result.over == null) return
const activeIndex = localSorts.findIndex((sort) => sort.column === result.active.id)
const overIndex = localSorts.findIndex((sort) => sort.column === result.over!.id)
if (activeIndex === -1 || overIndex === -1) return
setLocalSorts(arrayMove(localSorts, activeIndex, overIndex))
}}
>
<SortableContext
items={localSorts.map((sort) => sort.column)}
strategy={verticalListSortingStrategy}
>
{localSorts.map((sort, index) => (
<SortRow
key={getSortRowKey(sort, index)}
index={index}
columnName={sort.column}
sort={sort}
onDelete={onDeleteSort}
onToggle={onToggleSort}
onDrag={onDragSort}
/>
))}
</SortableContext>
</DndContext>
{localSorts.length === 0 && (
<div className="space-y-1 px-3">
<h5 className="text-xs text-foreground-light">No sorts applied to this view</h5>
@@ -1,10 +1,10 @@
import type { XYCoord } from 'dnd-core'
import { Menu, X } from 'lucide-react'
import { memo, useRef } from 'react'
import { useDrag, useDrop } from 'react-dnd'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { GripVertical, X } from 'lucide-react'
import { memo } from 'react'
import { Button, Switch } from 'ui'
import type { DragItem, Sort } from '@/components/grid/types'
import type { Sort } from '@/components/grid/types'
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
export interface SortRowProps {
@@ -20,94 +20,29 @@ const SortRow = ({ index, columnName, sort, onDelete, onToggle, onDrag }: SortRo
const snap = useTableEditorTableStateSnapshot()
const column = snap.table.columns.find((x) => x.name === columnName)
const ref = useRef<HTMLDivElement>(null)
const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition } =
useSortable({
id: columnName,
})
const [{ isDragging }, drag] = useDrag({
type: 'sort-row',
item: () => {
return { key: columnName, index }
},
collect: (monitor: any) => ({
isDragging: monitor.isDragging(),
}),
})
const [{ handlerId }, drop] = useDrop({
accept: 'sort-row',
collect(monitor) {
return {
handlerId: monitor.getHandlerId(),
}
},
hover(item, monitor) {
if (!ref.current) {
return
}
const dragIndex = (item as DragItem).index
const hoverIndex = index
// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return
}
// Determine rectangle on screen
const hoverBoundingRect = ref.current?.getBoundingClientRect()
// Get vertical middle
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2
// Determine mouse position
const clientOffset = monitor.getClientOffset()
// Get pixels to the top
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return
}
// Time to actually perform the action
moveSort(dragIndex, hoverIndex)
// Note: we're mutating the monitor item here!
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
;(item as DragItem).index = hoverIndex
},
})
const moveSort = (dragIndex: number, hoverIndex: number) => {
if (dragIndex == hoverIndex) return
onDrag(dragIndex, hoverIndex)
const style = {
transform: CSS.Transform.toString(transform),
transition,
}
const opacity = isDragging ? 0 : 1
drag(drop(ref))
if (!column) return null
return (
<div
className="flex items-center gap-3 px-3"
ref={ref}
style={{ opacity }}
data-handler-id={handlerId}
>
<span className="transition-color text-foreground-lighter hover:text-foreground-light">
<Menu strokeWidth={2} size={16} />
</span>
<div className="flex items-center gap-3 px-3" ref={setNodeRef} style={style}>
<button
ref={setActivatorNodeRef}
{...attributes}
{...listeners}
className="opacity-50 hover:opacity-100 transition cursor-grab text-foreground"
type="button"
>
<GripVertical size={16} strokeWidth={1.5} />
</button>
<div className="grow">
<span className="flex grow items-center gap-1 truncate text-sm text-foreground">
<span className="text-xs text-foreground-lighter">
@@ -2,8 +2,6 @@ import { keepPreviousData } from '@tanstack/react-query'
import { useParams } from 'common'
import { Loader2, X } from 'lucide-react'
import { useEffect, useState } from 'react'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { Button, SidePanel } from 'ui'
import { ForeignKey } from '../../ForeignKeySelector/ForeignKeySelector.types'
@@ -216,9 +214,7 @@ export const ForeignRowSelector = ({
<div className="flex items-center">
<RefreshButton tableId={table?.id} isRefetching={isRefetching} />
<FilterPopoverPrimitive filters={filters} onApplyFilters={onApplyFilters} />
<DndProvider backend={HTML5Backend} context={window}>
<SortPopoverPrimitive sorts={sorts} onApplySorts={onApplySorts} />
</DndProvider>
<SortPopoverPrimitive sorts={sorts} onApplySorts={onApplySorts} />
</div>
<div className="flex items-center gap-x-3 divide-x">