selecting a band opens its settings
Name and Title, in the editor column — the band row in the nav had only scrolled the preview until now. Both, not the title alone: a form showing one of two names invites somebody to change the wrong one, and `name` could only be set when the band was created. Last in the editor's order, so a block or a spacing editor wins — those were opened deliberately, and the band is merely still selected underneath them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,8 @@ export type CanvasComponent = {
|
|||||||
export type CanvasSection = {
|
export type CanvasSection = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
/** The heading the page draws above the band, or null. Not `name` — see the site's schema. */
|
||||||
|
title?: string | null;
|
||||||
layout: 'one' | 'two';
|
layout: 'one' | 'two';
|
||||||
padding?: Record<string, { x?: number; y?: number }>;
|
padding?: Record<string, { x?: number; y?: number }>;
|
||||||
components: CanvasComponent[];
|
components: CanvasComponent[];
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { useClient } from 'hooks/useClient';
|
||||||
|
import { readableError } from '../errors';
|
||||||
|
import { useAutosave } from '../useAutosave';
|
||||||
|
import { useFlash } from '../useFlash';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What a band is: what the editor calls it, and what the page shows.
|
||||||
|
*
|
||||||
|
* ── Two names, and they are different things ──
|
||||||
|
*
|
||||||
|
* The NAME is for whoever is editing — it is the row in the nav, and it is what makes a page of eight
|
||||||
|
* bands navigable. It never reaches a reader.
|
||||||
|
*
|
||||||
|
* The TITLE is the heading the page draws above the band, and most bands do not want one: a band that
|
||||||
|
* opens with its own words is the common case. Empty takes it off.
|
||||||
|
*
|
||||||
|
* Both are here rather than the title alone, because a form that shows one of two names invites
|
||||||
|
* somebody to change the wrong one — and until now `name` could only be set when the band was created.
|
||||||
|
*/
|
||||||
|
export const BandSettings = ({
|
||||||
|
pageId,
|
||||||
|
band,
|
||||||
|
onSaved,
|
||||||
|
}: {
|
||||||
|
pageId: number;
|
||||||
|
band: { id: number; name: string; title?: string | null };
|
||||||
|
/** The builder re-reads its sections: the nav's row is this name, and the page draws this title. */
|
||||||
|
onSaved: () => void;
|
||||||
|
}) => {
|
||||||
|
const client = useClient();
|
||||||
|
const clientRef = useRef(client);
|
||||||
|
clientRef.current = client;
|
||||||
|
|
||||||
|
const [draft, setDraft] = useState({ name: band.name, title: band.title ?? '' });
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [saved, flashSaved] = useFlash();
|
||||||
|
|
||||||
|
// A different band selected is a different form. Keyed on the id so re-reading the same band after a
|
||||||
|
// save does not throw away what is being typed.
|
||||||
|
useEffect(() => setDraft({ name: band.name, title: band.title ?? '' }), [band.id]);
|
||||||
|
|
||||||
|
const stored = JSON.stringify({ name: band.name, title: band.title ?? '' });
|
||||||
|
const pending = JSON.stringify(draft);
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
setBusy(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
// An empty title is sent as an empty string, which the API reads as "take the heading off" —
|
||||||
|
// presence, not truthiness. See the note in its PATCH handler.
|
||||||
|
await clientRef.current.patch(`/fofos-admin/pages/${pageId}/sections/${band.id}`, draft);
|
||||||
|
flashSaved();
|
||||||
|
onSaved();
|
||||||
|
} catch (failure) {
|
||||||
|
setError(readableError(failure));
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useAutosave(pending !== stored ? pending : null, busy, save);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-xs">Name</label>
|
||||||
|
<Input
|
||||||
|
value={draft.name}
|
||||||
|
onChange={(event) => setDraft((was) => ({ ...was, name: event.currentTarget.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-xs">Title</label>
|
||||||
|
<Input
|
||||||
|
value={draft.title}
|
||||||
|
placeholder="No heading"
|
||||||
|
onChange={(event) => setDraft((was) => ({ ...was, title: event.currentTarget.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{saved && <p className="text-xs text-success">Saved</p>}
|
||||||
|
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -32,6 +32,7 @@ const PLACES_ONLY: string[] = ['contact-form', 'partners'];
|
|||||||
import { AddSectionDialog, type SectionLayout } from './AddSectionDialog';
|
import { AddSectionDialog, type SectionLayout } from './AddSectionDialog';
|
||||||
import { FreeTextEditor } from './FreeTextEditor';
|
import { FreeTextEditor } from './FreeTextEditor';
|
||||||
import { SpacingEditor, type Padding } from './SpacingEditor';
|
import { SpacingEditor, type Padding } from './SpacingEditor';
|
||||||
|
import { BandSettings } from './BandSettings';
|
||||||
import { useEditorSlot } from '../useEditorSlot';
|
import { useEditorSlot } from '../useEditorSlot';
|
||||||
import { AddComponentDialog, type ComponentKind } from './AddComponentDialog';
|
import { AddComponentDialog, type ComponentKind } from './AddComponentDialog';
|
||||||
import { MediaPicker } from '../MediaPicker';
|
import { MediaPicker } from '../MediaPicker';
|
||||||
@@ -597,8 +598,22 @@ export const PageBuilderPanel = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
A band selected and nothing inside it: its own settings.
|
||||||
|
|
||||||
|
Last, so a block or a spacing editor wins — those are things you opened deliberately, and the
|
||||||
|
band is merely still selected underneath them.
|
||||||
|
*/
|
||||||
|
const band = !piece && part ? sections.find((section) => String(section.id) === part) : null;
|
||||||
|
if (band && page) {
|
||||||
|
setEditorRef.current(
|
||||||
|
<BandSettings pageId={page.id} band={band} onSaved={() => setRendered((n) => n + 1)} />,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setEditorRef.current(null);
|
setEditorRef.current(null);
|
||||||
}, [spacing, editing, creating, device, steps]);
|
}, [spacing, editing, creating, device, steps, part, piece, sections, page]);
|
||||||
|
|
||||||
// Cleared on unmount, or the panel goes on offering an editor for a page nobody is looking at.
|
// Cleared on unmount, or the panel goes on offering an editor for a page nobody is looking at.
|
||||||
useEffect(() => () => setEditorRef.current(null), []);
|
useEffect(() => () => setEditorRef.current(null), []);
|
||||||
|
|||||||
Reference in New Issue
Block a user