From 172896b85f2ddecfc133664b4760b0f6ccaf2ff8 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 22 Mar 2026 07:55:35 -0500 Subject: [PATCH] Fix module drag-and-drop: custom collision detection to hit slots over racks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: SortableContext registers each rack column as a droppable. Each column is ~1800px tall (42U x 44px). The default closestCenter algorithm compared center-to-center distances, so the rack column's center consistently beat the 44px RackSlot's center — meaning over.data resolved to { dragType: 'rack' } and handleDragEnd's check for dropType === 'slot' never matched. Drops silently did nothing. Fix: replace closestCenter with a two-phase collision detection: 1. pointerWithin — returns droppables whose bounding rect contains the actual pointer position. Slots are exactly hit-tested. 2. closestCenter fallback — used when the pointer is not within any registered droppable (e.g. dragging a rack header between columns for sortable reorder where the pointer may be in the gap). Co-Authored-By: Claude Sonnet 4.6 --- client/src/components/rack/RackPlanner.tsx | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/client/src/components/rack/RackPlanner.tsx b/client/src/components/rack/RackPlanner.tsx index 56e1b3c..0232b45 100644 --- a/client/src/components/rack/RackPlanner.tsx +++ b/client/src/components/rack/RackPlanner.tsx @@ -3,10 +3,13 @@ import { DndContext, DragOverlay, PointerSensor, + pointerWithin, + closestCenter, useSensor, useSensors, type DragStartEvent, type DragEndEvent, + type CollisionDetection, } from '@dnd-kit/core'; import { SortableContext, horizontalListSortingStrategy, arrayMove } from '@dnd-kit/sortable'; import { toast } from 'sonner'; @@ -50,6 +53,23 @@ function ModuleDragOverlay({ label }: { label: string }) { ); } +/** + * Collision detection strategy: + * - pointerWithin first: finds droppables whose rect contains the pointer. + * This correctly hits the 44px-tall RackSlots since the pointer is inside them. + * - closestCenter fallback: used when the pointer is not within any droppable, + * which is when dragging a rack header between columns (sortable reorder). + * + * Without this, the default closestCenter alone would favour the large rack-column + * sortable elements (~1800px tall) over the tiny slot droppables, so over.data + * would be { dragType: 'rack' } instead of { dropType: 'slot' }. + */ +const slotFirstCollision: CollisionDetection = (args) => { + const within = pointerWithin(args); + if (within.length > 0) return within; + return closestCenter(args); +}; + export function RackPlanner() { const { racks, loading, fetchRacks, moveModule, updateRack } = useRackStore(); const canvasRef = useRef(null); @@ -143,7 +163,7 @@ export function RackPlanner() { const rackIds = racks.map((r) => r.id); return ( - +