rename a gallery, and leave its address alone
A gallery could be created and deleted and not renamed, so fixing "Collections - 70´S" meant deleting it and choosing its photographs again — which would also have detached the collection pointing at it. Renaming writes the NAME and nothing else. Everything that references a gallery references its id — `collections.gallery_id`, and a gallery block's `content.galleryId` — and nothing renders through the slug, so a rename cannot break a reference today. The slug is left alone anyway, because it is an ADDRESS: it can end up in a link somebody sent, and on the day something does resolve by it, a label having quietly moved it would be a very hard bug to find. The row says so while you are typing rather than afterwards, because somebody about to change a name is exactly who wonders whether the address moves with it. A gallery whose slug is genuinely wrong is a different act with different consequences, and it should look like one. The API already takes them separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router';
|
||||
import { Images, Loader2, Plus, Trash2 } from 'lucide-react';
|
||||
import { Check, Images, Loader2, Pencil, Plus, Trash2, X } from 'lucide-react';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -38,6 +38,8 @@ export const GalleriesPanel = () => {
|
||||
const [name, setName] = useState('');
|
||||
/** Paths handed over by the context menu, waiting for a name. */
|
||||
const [pending, setPending] = useState<string[]>([]);
|
||||
/** The gallery being renamed, and the name being typed into it. Null when nobody is renaming. */
|
||||
const [renaming, setRenaming] = useState<{ id: number; name: string } | null>(null);
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [params, setParams] = useSearchParams();
|
||||
@@ -119,6 +121,25 @@ export const GalleriesPanel = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Renaming changes the NAME and leaves the slug exactly where it is.
|
||||
|
||||
Everything that points at a gallery points at its id — `collections.gallery_id`, and a gallery
|
||||
block's `content.galleryId`. Nothing renders through the slug, so a rename cannot break a reference
|
||||
today. It is left alone anyway, because a slug is an ADDRESS: it can end up in a link somebody
|
||||
sent, and the day something does resolve by it, a label having quietly moved it would be a very
|
||||
hard bug to find.
|
||||
|
||||
So the two are separable and this only writes one of them. A gallery whose slug is genuinely wrong
|
||||
is a different act with different consequences, and it should look like one.
|
||||
*/
|
||||
const rename = () =>
|
||||
send('rename', async () => {
|
||||
if (!renaming?.name.trim()) return;
|
||||
await clientRef.current.patch(`/fofos-admin/galleries/${renaming.id}`, { name: renaming.name.trim() });
|
||||
setRenaming(null);
|
||||
});
|
||||
|
||||
const create = () =>
|
||||
send('create', async () => {
|
||||
// `urls` in the same request, so creating from a selection is one action — splitting it would
|
||||
@@ -187,6 +208,47 @@ export const GalleriesPanel = () => {
|
||||
open ? 'border-primary/60 bg-muted/40' : 'border-border'
|
||||
}`}
|
||||
>
|
||||
{renaming?.id === gallery.id ? (
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<Images className="size-4 shrink-0" />
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<Input
|
||||
autoFocus
|
||||
value={renaming.name}
|
||||
className="h-7 text-sm"
|
||||
onChange={(event) => setRenaming({ id: gallery.id, name: event.target.value })}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && renaming.name.trim()) void rename();
|
||||
if (event.key === 'Escape') setRenaming(null);
|
||||
}}
|
||||
/>
|
||||
{/* Said while renaming, not afterwards: somebody about to change a name is exactly
|
||||
who wonders whether the address moves with it. */}
|
||||
<span className="block truncate font-mono text-xs text-muted-foreground/70">
|
||||
{gallery.slug} · the address does not change
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="size-7 shrink-0 p-0"
|
||||
disabled={busy !== null || !renaming.name.trim()}
|
||||
onClick={() => void rename()}
|
||||
title="Save"
|
||||
>
|
||||
{busy === 'rename' ? <Loader2 className="size-3.5 animate-spin" /> : <Check className="size-3.5" />}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="size-7 shrink-0 p-0"
|
||||
onClick={() => setRenaming(null)}
|
||||
title="Cancel"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-w-0 flex-1 items-center gap-2 text-left"
|
||||
@@ -200,6 +262,20 @@ export const GalleriesPanel = () => {
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{renaming?.id !== gallery.id && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={() => setRenaming({ id: gallery.id, name: gallery.name })}
|
||||
disabled={busy !== null}
|
||||
title="Rename"
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
Reference in New Issue
Block a user