a control that saved and changed nothing, and saves that failed silently
Two defects found by reviewing the panel rather than the site.
── The lead photograph had a cropper that did nothing ──
It wrote `content.crop` on a subject-cover block. The site stopped reading that
when the crop moved to the row - one rectangle framed against one photograph and
applied to every row a template draws goes through the face of the next one - and
the control stayed behind. It saved, the API allowlisted it, and nothing rendered
it, under help text promising it applied to every row.
Gone, with the lead fetch that fed it. Shape stays: one decision for the whole
section, and it is read. The per-row cropper in each section's settings is the
one that works.
── A failed save said nothing at all ──
SubjectFields drives Collections, Pieces and Collabs. Neither of its save paths
had a failure branch, so a rejected PATCH was an unhandled rejection and a person
watching their own change had no way to know it was not written. There is no Save
button here by design, which makes reporting the failure the only signal there
is.
It matters most for the cover, which does not go through the draft: typing
survives, because the draft is only cleared on success and the next keystroke
retries, but a crop that fails is simply gone while the picture goes on showing
the rectangle that was dragged.
Also: the shape is computed once instead of three times from a fabricated
`{ url: 'x.png' }`, and the SHAPES import moved to the top of the file with the
others rather than sitting after a top-level const.
This commit is contained in:
@@ -68,8 +68,6 @@ import { BlockMedia } from './BlockMedia';
|
||||
import { PageStructure } from './PageStructure';
|
||||
import { BandBlocks } from './BandBlocks';
|
||||
|
||||
|
||||
|
||||
/** Stored as a float, matched with a tolerance: 21/9 does not survive a round trip exactly. */
|
||||
const closeTo = (a: number, b: unknown): boolean => typeof b === 'number' && Math.abs(a - b) < 0.01;
|
||||
|
||||
@@ -187,18 +185,6 @@ export const PageBuilderPanel = () => {
|
||||
*/
|
||||
const [rendered, setRendered] = useState(0);
|
||||
|
||||
/*
|
||||
The lead photograph the TEMPLATE is previewing, so its cover block can be framed.
|
||||
|
||||
Asked of the site rather than worked out here: which template draws which section, which row is
|
||||
the example, and which of its pictures leads are four facts that live in `templates.ts`, and a
|
||||
second copy in this panel is how the two drift when a fifth template arrives.
|
||||
|
||||
Null on an ordinary page, on a template nothing routes, and on a section with no rows — all
|
||||
ordinary states, and all of them mean the same thing here: nothing to frame.
|
||||
*/
|
||||
const [subjectLead, setSubjectLead] = useState<string | null>(null);
|
||||
|
||||
/*
|
||||
The builder's chrome, painted into the rendered page.
|
||||
|
||||
@@ -397,21 +383,6 @@ export const PageBuilderPanel = () => {
|
||||
|
||||
const page = pageFromParam(pages, slug);
|
||||
|
||||
useEffect(() => {
|
||||
if (!page?.is_template) {
|
||||
setSubjectLead(null);
|
||||
return;
|
||||
}
|
||||
let alive = true;
|
||||
void clientRef.current
|
||||
.get<{ data: { path: string | null } }>(`/fofos-admin/pages/${page.id}/subject-lead`)
|
||||
.then((body) => alive && setSubjectLead(body.data.path))
|
||||
// Left null, which the block reads as "nothing to frame" — the same as a template with no rows.
|
||||
.catch(() => alive && setSubjectLead(null));
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [page?.id, page?.is_template]);
|
||||
|
||||
/*
|
||||
The page's sections, which the canvas needs to name and outline what it finds in the document.
|
||||
@@ -776,6 +747,15 @@ export const PageBuilderPanel = () => {
|
||||
const owner = piece ? sections.find((one) => one.components.some((c) => String(c.id) === piece)) : null;
|
||||
const block = owner?.components.find((c) => String(c.id) === piece);
|
||||
|
||||
/*
|
||||
The shape a `subject-cover` is framed into, as Tailwind spells it.
|
||||
|
||||
`shapeOf` takes a whole block because a media block's shape depends on it having a picture at
|
||||
all; a subject cover has no `url` of its own - the picture is the row's - so it was being asked
|
||||
three times with a fabricated one. Asked once, from the only field that decides it.
|
||||
*/
|
||||
const coverShape = SHAPES.find((one) => closeTo(one.ratio ?? Number.NaN, block?.content?.ratio))?.value ?? null;
|
||||
|
||||
if (block && owner && page) {
|
||||
setEditorRef.current(
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -838,19 +818,25 @@ export const PageBuilderPanel = () => {
|
||||
)}
|
||||
|
||||
{/*
|
||||
The lead photograph gets Shape, and only Shape.
|
||||
The lead photograph gets Shape, and ONLY Shape.
|
||||
|
||||
The same framing the media block has had since it existed — a template wanting a letterbox
|
||||
lead had no way to ask for one. Not the rest of that block's controls: there is no file to
|
||||
choose, because the picture is the carousel's first item, and no crop to set, because the
|
||||
crop belongs to the carousel item and two crops for one placement is two answers able to
|
||||
disagree.
|
||||
The same framing the media block has had since it existed - a template wanting a letterbox
|
||||
lead had no way to ask for one. Not the rest of that block's controls, and not a crop:
|
||||
there is no file to choose, because the picture is the row's, and no rectangle to drag,
|
||||
because a template draws EVERY row in its section and one rectangle framed against one
|
||||
photograph goes through somebody's head on the next one.
|
||||
|
||||
That is not a guess about what would happen - it is what did. This block briefly offered
|
||||
zoom and pan, writing `content.crop`; the site stopped reading it when the crop moved to
|
||||
the row, where there is one per picture, and the control stayed behind. It saved, it
|
||||
persisted, and it changed nothing, under help text promising it applied to every row. The
|
||||
per-row cropper is in each section's own settings - see SubjectFields.
|
||||
*/}
|
||||
{block.kind === 'subject-cover' && (
|
||||
<div className="mt-3 flex flex-col gap-1">
|
||||
<label className="text-xs">Shape</label>
|
||||
<select
|
||||
value={shapeOf({ content: { url: 'x.png', ratio: block.content?.ratio } }) ?? ''}
|
||||
value={coverShape ?? ''}
|
||||
onChange={(event) => {
|
||||
const chosen = SHAPES.find((one) => one.value === event.currentTarget.value);
|
||||
void handlersRef.current.setContent(block, {
|
||||
@@ -866,42 +852,10 @@ export const PageBuilderPanel = () => {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/*
|
||||
Zoom and pan, once there is a shape to frame INTO.
|
||||
|
||||
No shape means the picture draws at its own proportions and there is no rectangle to
|
||||
choose — the same rule the media block follows, where the cropper appears only after a
|
||||
shape is picked.
|
||||
|
||||
`fixed`, so there is no Change or Remove: the picture is the subject's first carousel
|
||||
item and this block cannot store a different one. Framing is its decision; which
|
||||
photograph is not.
|
||||
*/}
|
||||
{shapeOf({ content: { url: 'x.png', ratio: block.content?.ratio } }) && subjectLead && (
|
||||
<>
|
||||
<BlockMedia
|
||||
url={subjectLead}
|
||||
crop={block.content?.crop ?? null}
|
||||
aspect={shapeOf({ content: { url: 'x.png', ratio: block.content?.ratio } })!}
|
||||
onUrl={() => {}}
|
||||
onCrop={(crop) =>
|
||||
void handlersRef.current.setContent(block, { ...block.content, crop: crop || undefined })
|
||||
}
|
||||
fixed
|
||||
/>
|
||||
{/*
|
||||
Said plainly, because it is the one surprising thing about framing a template.
|
||||
|
||||
A template has no subject of its own; this is the row it previews against. The
|
||||
rectangle chosen here is used for EVERY row the template draws, so it is framed
|
||||
against one photograph and applied to all of them.
|
||||
*/}
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Framed against the preview. This crop applies to every one of them.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
One shape for every row this template draws. Which part of each photograph survives it
|
||||
is set on the row itself.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user