finish the carousel rename in the panels
Two of these were live: PageBuilderPanel still compared kind === 'gallery' in two places, so a carousel block's editor and its Choose button were both dead branches. The type checker could not see them — both compare against string. The rest is copy somebody reads: "Change gallery", "Name a new gallery", "Reading the gallery", "Add to this gallery", Home's Gallery heading, and the "Create Gallery" file action, now "Carousel from folder". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ import { CarouselItems } from './CarouselItems';
|
||||
/**
|
||||
* Carousels — named, ordered sets of media, reusable anywhere on the site.
|
||||
*
|
||||
* ── Why a gallery is not a list on a page ──
|
||||
* ── Why a carousel is not a list on a page ──
|
||||
*
|
||||
* The home page had one, as rows hanging off its own content. That works for exactly one. The moment a
|
||||
* second page wants the same photographs in the same order, a list-per-page means choosing them again
|
||||
@@ -19,26 +19,26 @@ import { CarouselItems } from './CarouselItems';
|
||||
*
|
||||
* ── The server owns the shape ──
|
||||
*
|
||||
* Every write answers with the whole gallery and this never patches its own state from what it sent.
|
||||
* Every write answers with the whole carousel and this never patches its own state from what it sent.
|
||||
* That matters most here: reordering renumbers everything, adding appends after whatever was there,
|
||||
* and deleting closes a gap. A client keeping its own copy drifts on the first of those with no way to
|
||||
* notice.
|
||||
*/
|
||||
|
||||
type Item = { id: number; url: string; alt: string | null; sort_order: number };
|
||||
type Gallery = { id: number; name: string; slug: string; items: Item[] };
|
||||
type Carousel = { id: number; name: string; slug: string; items: Item[] };
|
||||
|
||||
export const CarouselsPanel = () => {
|
||||
const client = useClient();
|
||||
const clientRef = useRef(client);
|
||||
clientRef.current = client;
|
||||
|
||||
const [carousels, setCarousels] = useState<Gallery[] | null>(null);
|
||||
const [carousels, setCarousels] = useState<Carousel[] | null>(null);
|
||||
const [openId, setOpenId] = useState<number | null>(null);
|
||||
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. */
|
||||
/** The carousel 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);
|
||||
@@ -46,7 +46,7 @@ export const CarouselsPanel = () => {
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const response = await clientRef.current.get<{ data: Gallery[] }>('/fofos-admin/carousels');
|
||||
const response = await clientRef.current.get<{ data: Carousel[] }>('/fofos-admin/carousels');
|
||||
setCarousels(response.data);
|
||||
} catch (failure) {
|
||||
setError(readableError(failure));
|
||||
@@ -59,7 +59,7 @@ export const CarouselsPanel = () => {
|
||||
}, [load]);
|
||||
|
||||
/*
|
||||
The selected gallery's contents, handed to the editor column.
|
||||
The selected carousel's contents, handed to the editor column.
|
||||
|
||||
Announced rather than rendered, because that panel is a SIBLING — see useEditorSlot. `onChanged`
|
||||
reloads the list here, so the item count beside the name is never one behind what is in it.
|
||||
@@ -77,16 +77,16 @@ export const CarouselsPanel = () => {
|
||||
);
|
||||
}, [openId, load]);
|
||||
|
||||
// Cleared on unmount, or the panel goes on offering a gallery over somebody else's screen.
|
||||
// Cleared on unmount, or the panel goes on offering a carousel over somebody else's screen.
|
||||
useEffect(() => () => setEditorRef.current(null), []);
|
||||
|
||||
/*
|
||||
Arriving from "Create Gallery" in the file browser.
|
||||
Arriving from "Carousel from folder" in the file browser.
|
||||
|
||||
The action puts the chosen paths in the URL and sends you here rather than opening its own dialog:
|
||||
naming a gallery, seeing the slug it gets and watching the items land belongs on the screen that
|
||||
naming a carousel, seeing the slug it gets and watching the items land belongs on the screen that
|
||||
owns carousels. Consumed once and removed from the URL — a `?from=` left behind would recreate the
|
||||
gallery on every refresh, which somebody discovers as four copies.
|
||||
carousel on every refresh, which somebody discovers as four copies.
|
||||
*/
|
||||
const arriving = params.get('from');
|
||||
useEffect(() => {
|
||||
@@ -124,13 +124,13 @@ export const CarouselsPanel = () => {
|
||||
/*
|
||||
Renaming changes the NAME and leaves the slug exactly where it is.
|
||||
|
||||
Everything that points at a gallery points at its id — `collections.carousel_id`, and a gallery
|
||||
Everything that points at a carousel points at its id — `collections.carousel_id`, and a carousel
|
||||
block's `content.carouselId`. 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
|
||||
So the two are separable and this only writes one of them. A carousel whose slug is genuinely wrong
|
||||
is a different act with different consequences, and it should look like one.
|
||||
*/
|
||||
const rename = () =>
|
||||
@@ -143,7 +143,7 @@ export const CarouselsPanel = () => {
|
||||
const create = () =>
|
||||
send('create', async () => {
|
||||
// `urls` in the same request, so creating from a selection is one action — splitting it would
|
||||
// leave an empty gallery behind whenever the second call failed.
|
||||
// leave an empty carousel behind whenever the second call failed.
|
||||
await clientRef.current.post('/fofos-admin/carousels', { name: name.trim(), urls: pending });
|
||||
setName('');
|
||||
setPending([]);
|
||||
@@ -170,7 +170,7 @@ export const CarouselsPanel = () => {
|
||||
autoFocus={pending.length > 0}
|
||||
value={name}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
placeholder="Name a new gallery"
|
||||
placeholder="Name a new carousel"
|
||||
className="min-w-0 flex-1 text-xs"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && name.trim()) void create();
|
||||
@@ -192,23 +192,23 @@ export const CarouselsPanel = () => {
|
||||
{/*
|
||||
The list, and only the list.
|
||||
|
||||
The contents of a gallery — its pictures, their order, their captions — are announced to the
|
||||
editor column and drawn by `CarouselItems`, which is the same component a gallery BLOCK on a
|
||||
The contents of a carousel — its pictures, their order, their captions — are announced to the
|
||||
editor column and drawn by `CarouselItems`, which is the same component a carousel BLOCK on a
|
||||
page uses. Two copies of that list existed for a day and this is the one that went.
|
||||
|
||||
So this panel is what a master list should be: which carousels there are, making one, deleting
|
||||
one. What is inside the selected one is a different question, answered beside it.
|
||||
*/}
|
||||
{carousels.map((gallery) => {
|
||||
const open = openId === gallery.id;
|
||||
{carousels.map((carousel) => {
|
||||
const open = openId === carousel.id;
|
||||
return (
|
||||
<section
|
||||
key={gallery.id}
|
||||
key={carousel.id}
|
||||
className={`flex items-center gap-3 rounded-lg border p-4 ${
|
||||
open ? 'border-primary/60 bg-muted/40' : 'border-border'
|
||||
}`}
|
||||
>
|
||||
{renaming?.id === gallery.id ? (
|
||||
{renaming?.id === carousel.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">
|
||||
@@ -216,7 +216,7 @@ export const CarouselsPanel = () => {
|
||||
autoFocus
|
||||
value={renaming.name}
|
||||
className="h-7 text-sm"
|
||||
onChange={(event) => setRenaming({ id: gallery.id, name: event.target.value })}
|
||||
onChange={(event) => setRenaming({ id: carousel.id, name: event.target.value })}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && renaming.name.trim()) void rename();
|
||||
if (event.key === 'Escape') setRenaming(null);
|
||||
@@ -225,7 +225,7 @@ export const CarouselsPanel = () => {
|
||||
{/* 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
|
||||
{carousel.slug} · the address does not change
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
@@ -252,24 +252,24 @@ export const CarouselsPanel = () => {
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-w-0 flex-1 items-center gap-2 text-left"
|
||||
onClick={() => setOpenId(open ? null : gallery.id)}
|
||||
onClick={() => setOpenId(open ? null : carousel.id)}
|
||||
>
|
||||
<Images className="size-4 shrink-0" />
|
||||
<span className="min-w-0 space-y-1">
|
||||
<span className="block truncate text-sm font-medium">{gallery.name}</span>
|
||||
<span className="block truncate text-sm font-medium">{carousel.name}</span>
|
||||
<span className="block truncate font-mono text-xs text-muted-foreground/70">
|
||||
{gallery.slug} · {gallery.items.length} item{gallery.items.length === 1 ? '' : 's'}
|
||||
{carousel.slug} · {carousel.items.length} item{carousel.items.length === 1 ? '' : 's'}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{renaming?.id !== gallery.id && (
|
||||
{renaming?.id !== carousel.id && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={() => setRenaming({ id: gallery.id, name: gallery.name })}
|
||||
onClick={() => setRenaming({ id: carousel.id, name: carousel.name })}
|
||||
disabled={busy !== null}
|
||||
title="Rename"
|
||||
>
|
||||
@@ -282,8 +282,8 @@ export const CarouselsPanel = () => {
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={() =>
|
||||
void send(`gallery:${gallery.id}`, () =>
|
||||
clientRef.current.delete(`/fofos-admin/carousels/${gallery.id}`),
|
||||
void send(`carousel:${carousel.id}`, () =>
|
||||
clientRef.current.delete(`/fofos-admin/carousels/${carousel.id}`),
|
||||
)
|
||||
}
|
||||
disabled={busy !== null}
|
||||
|
||||
Reference in New Issue
Block a user