gallery items reorder by dragging
Nine items meant eighteen arrows and four presses to move something from the bottom to the top. A grip on the left, the row as the drop target, and the list rearranging as the pointer crosses it — the same gesture the page rows use. One write at the end, carrying every id in order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { ArrowDown, ArrowUp, Plus, Trash2 } from 'lucide-react';
|
import { GripVertical, Plus, Trash2 } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
@@ -69,15 +69,44 @@ export const GalleryItems = ({ galleryId, onChanged }: { galleryId: number; onCh
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Every id, in order — which is what makes two items claiming a position impossible. */
|
/*
|
||||||
const move = (index: number, by: -1 | 1) => {
|
Reordering by dragging, the same way the page rows do it.
|
||||||
if (!gallery) return;
|
|
||||||
|
The list rearranges as the pointer crosses a row rather than on drop, so what you see while
|
||||||
|
dragging is what you will get. The write happens once, at the end, and it sends EVERY id in order —
|
||||||
|
which is what makes two items claiming a position impossible.
|
||||||
|
|
||||||
|
It replaced a pair of arrows per row. Nine items meant eighteen buttons and four presses to move
|
||||||
|
something from the bottom to the top.
|
||||||
|
*/
|
||||||
|
const [dragged, setDragged] = useState<number | null>(null);
|
||||||
|
const orderedRef = useRef<Item[] | null>(null);
|
||||||
|
|
||||||
|
const moveOver = (event: React.DragEvent, overId: number) => {
|
||||||
|
if (dragged === null || !gallery) return;
|
||||||
|
// Without this the drop is refused and the browser animates the row back to where it started.
|
||||||
|
event.preventDefault();
|
||||||
|
if (dragged === overId) return;
|
||||||
|
|
||||||
const next = [...gallery.items];
|
const next = [...gallery.items];
|
||||||
const target = index + by;
|
const from = next.findIndex((one) => one.id === dragged);
|
||||||
if (target < 0 || target >= next.length) return;
|
const to = next.findIndex((one) => one.id === overId);
|
||||||
[next[index], next[target]] = [next[target]!, next[index]!];
|
if (from < 0 || to < 0) return;
|
||||||
|
|
||||||
|
next.splice(to, 0, next.splice(from, 1)[0]!);
|
||||||
|
orderedRef.current = next;
|
||||||
|
setGallery({ ...gallery, items: next });
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitOrder = () => {
|
||||||
|
const ordered = orderedRef.current;
|
||||||
|
setDragged(null);
|
||||||
|
orderedRef.current = null;
|
||||||
|
// Dropped where it started, or outside the list. Nothing moved, so nothing is written.
|
||||||
|
if (!ordered) return;
|
||||||
|
|
||||||
void send(() =>
|
void send(() =>
|
||||||
clientRef.current.put(`/fofos-admin/galleries/${galleryId}/items/order`, { ids: next.map((one) => one.id) }),
|
clientRef.current.put(`/fofos-admin/galleries/${galleryId}/items/order`, { ids: ordered.map((one) => one.id) }),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,8 +122,30 @@ export const GalleryItems = ({ galleryId, onChanged }: { galleryId: number; onCh
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="flex flex-col gap-1">
|
<ul className="flex flex-col gap-1">
|
||||||
{gallery.items.map((item, index) => (
|
{gallery.items.map((item) => (
|
||||||
<li key={item.id} className="flex items-center gap-2 rounded border border-border/60 p-1.5">
|
<li
|
||||||
|
key={item.id}
|
||||||
|
/* The row is the drop target; the grip is what drags. See the note on the handler. */
|
||||||
|
onDragOver={(event) => moveOver(event, item.id)}
|
||||||
|
onDrop={commitOrder}
|
||||||
|
className={`flex items-center gap-2 rounded border border-border/60 p-1.5 ${
|
||||||
|
dragged === item.id ? 'opacity-40' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
draggable
|
||||||
|
onDragStart={(event) => {
|
||||||
|
setDragged(item.id);
|
||||||
|
// Firefox starts no drag at all without data on the transfer. The value is unused.
|
||||||
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
|
event.dataTransfer.setData('text/plain', String(item.id));
|
||||||
|
}}
|
||||||
|
onDragEnd={commitOrder}
|
||||||
|
className="shrink-0 cursor-grab text-muted-foreground/60 active:cursor-grabbing"
|
||||||
|
aria-label="Reorder"
|
||||||
|
>
|
||||||
|
<GripVertical className="size-4" />
|
||||||
|
</span>
|
||||||
{/*
|
{/*
|
||||||
From the DERIVED ladder, which is the whole point of the pipeline: most of these are HEIC
|
From the DERIVED ladder, which is the whole point of the pipeline: most of these are HEIC
|
||||||
and a browser cannot show the original at all.
|
and a browser cannot show the original at all.
|
||||||
@@ -133,18 +184,6 @@ export const GalleryItems = ({ galleryId, onChanged }: { galleryId: number; onCh
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="flex shrink-0">
|
<span className="flex shrink-0">
|
||||||
<Button variant="ghost" size="sm" className="h-7 px-1.5" disabled={busy || index === 0} onClick={() => move(index, -1)}>
|
|
||||||
<ArrowUp className="size-3.5" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 px-1.5"
|
|
||||||
disabled={busy || index === gallery.items.length - 1}
|
|
||||||
onClick={() => move(index, 1)}
|
|
||||||
>
|
|
||||||
<ArrowDown className="size-3.5" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
Reference in New Issue
Block a user