Gallery is a component you can add
The picker listed it as `soon` beside Image. It is not soon any more — the set, its ordering and the carousel that draws it were already built on the site, so the block is a reference to one and the admin's whole job is choosing which. `GalleryPicker` chooses a SET and nothing else. Editing what is IN the set stays under Galleries: a block that could also rearrange its own pictures would be a second place to do one job, and the two would disagree. No thumbnails — every stored path names an original a browser cannot display, and the plugin has no way to address a rendition, so the name and the count identify it instead. Saved on choose, like a video and for the same reason: picking one thing is one act with one outcome, so there is nothing to draft and nothing to cancel out of. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import { FreeTextEditor } from './FreeTextEditor';
|
||||
import { SpacingEditor, type Padding } from './SpacingEditor';
|
||||
import { AddComponentDialog, type ComponentKind } from './AddComponentDialog';
|
||||
import { MediaPicker } from '../MediaPicker';
|
||||
import { GalleryPicker } from '../GalleryPicker';
|
||||
import { pageFromParam, usePages, type Page } from '../usePages';
|
||||
|
||||
/**
|
||||
@@ -76,6 +77,20 @@ export const PageBuilderPanel = () => {
|
||||
/** Where a component is about to be added, while the kind is being chosen. */
|
||||
const [choosing, setChoosing] = useState<{ sectionId: number; column: number } | null>(null);
|
||||
/** The component being written, and where a new one would go. Null when the dialog is shut. */
|
||||
/*
|
||||
Which gallery a block shows, while it is being chosen.
|
||||
|
||||
The same shape as `picking` and for the same reason: choosing a set is one act with one outcome, so
|
||||
there is nothing to draft and nothing to cancel out of afterwards. `galleryId` rides along so
|
||||
reopening the picker can show where the block already stands.
|
||||
*/
|
||||
const [choosingGallery, setChoosingGallery] = useState<{
|
||||
id: number | null;
|
||||
sectionId: number;
|
||||
column: number;
|
||||
galleryId: number | null;
|
||||
} | null>(null);
|
||||
|
||||
const [editing, setEditing] = useState<{ id: number | null; sectionId: number; column: number; html: string } | null>(
|
||||
null,
|
||||
);
|
||||
@@ -231,6 +246,7 @@ export const PageBuilderPanel = () => {
|
||||
// Each kind opens its own editor. A kind with no case here cannot be created, which is the point.
|
||||
if (kind === 'free-text') setEditing({ id: null, ...choosing, html: '' });
|
||||
if (kind === 'video') setPicking({ id: null, ...choosing });
|
||||
if (kind === 'gallery') setChoosingGallery({ id: null, ...choosing, galleryId: null });
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -256,6 +272,31 @@ export const PageBuilderPanel = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
A gallery block is a REFERENCE, saved as soon as the set is chosen — see GalleryPicker.
|
||||
|
||||
Identical in shape to saveVideo because it is the same interaction: pick one thing, store one
|
||||
value. The two are left as two functions rather than one generic saver, because what they store
|
||||
happens to be one field each today and a shared helper would be an abstraction over a coincidence.
|
||||
*/
|
||||
const saveGallery = async (galleryId: number) => {
|
||||
if (!page || !choosingGallery) return;
|
||||
const base = `/fofos-admin/pages/${page.id}/sections/${choosingGallery.sectionId}/components`;
|
||||
const target = choosingGallery;
|
||||
setChoosingGallery(null);
|
||||
setError(null);
|
||||
try {
|
||||
if (target.id === null) {
|
||||
await clientRef.current.post(base, { kind: 'gallery', column: target.column, content: { galleryId } });
|
||||
} else {
|
||||
await clientRef.current.patch(`${base}/${target.id}`, { content: { galleryId } });
|
||||
}
|
||||
setRendered((n) => n + 1);
|
||||
} catch (failure) {
|
||||
setError(readableError(failure));
|
||||
}
|
||||
};
|
||||
|
||||
const editComponent = (component: CanvasComponent) => {
|
||||
const owner = sections.find((section) => section.components.some((one) => one.id === component.id));
|
||||
if (!owner) return;
|
||||
@@ -267,6 +308,17 @@ export const PageBuilderPanel = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (component.kind === 'gallery') {
|
||||
const held = owner.components.find((one) => one.id === component.id)?.content?.galleryId;
|
||||
setChoosingGallery({
|
||||
id: component.id,
|
||||
sectionId: owner.id,
|
||||
column: component.column,
|
||||
galleryId: typeof held === 'number' ? held : null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const stored = owner.components.find((one) => one.id === component.id);
|
||||
const html = typeof stored?.content?.html === 'string' ? stored.content.html : '';
|
||||
setEditing({ id: component.id, sectionId: owner.id, column: component.column, html });
|
||||
@@ -366,7 +418,7 @@ export const PageBuilderPanel = () => {
|
||||
kind: 'component',
|
||||
id: component.id,
|
||||
sectionId: owner.id,
|
||||
label: component.kind === 'video' ? 'Video' : 'Text',
|
||||
label: component.kind === 'video' ? 'Video' : component.kind === 'gallery' ? 'Gallery' : 'Text',
|
||||
padding: (stored?.padding ?? {}) as Padding,
|
||||
});
|
||||
};
|
||||
@@ -419,6 +471,13 @@ export const PageBuilderPanel = () => {
|
||||
title="Choose a video"
|
||||
/>
|
||||
|
||||
<GalleryPicker
|
||||
open={choosingGallery !== null}
|
||||
onClose={() => setChoosingGallery(null)}
|
||||
onPick={(galleryId) => void saveGallery(galleryId)}
|
||||
selected={choosingGallery?.galleryId ?? null}
|
||||
/>
|
||||
|
||||
{spacing && (
|
||||
<SpacingEditor
|
||||
label={spacing.label}
|
||||
|
||||
Reference in New Issue
Block a user