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 { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router';
|
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 { useClient } from 'hooks/useClient';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -38,6 +38,8 @@ export const GalleriesPanel = () => {
|
|||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
/** Paths handed over by the context menu, waiting for a name. */
|
/** Paths handed over by the context menu, waiting for a name. */
|
||||||
const [pending, setPending] = useState<string[]>([]);
|
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 [busy, setBusy] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [params, setParams] = useSearchParams();
|
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 = () =>
|
const create = () =>
|
||||||
send('create', async () => {
|
send('create', async () => {
|
||||||
// `urls` in the same request, so creating from a selection is one action — splitting it would
|
// `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'
|
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex min-w-0 flex-1 items-center gap-2 text-left"
|
className="flex min-w-0 flex-1 items-center gap-2 text-left"
|
||||||
@@ -200,6 +262,20 @@ export const GalleriesPanel = () => {
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
Reference in New Issue
Block a user