Contact form and Partners, placed in one click
Both hold nothing, so adding one opens no editor: a dialog with no field on it is a step that exists only to be got past. The picker itself answers the opposite way for the kinds that DO have a choice to make, which is the distinction worth keeping. The canvas draws no Edit button for them either — an Edit that opens nothing is worse than no Edit. It asks the panel rather than holding a list of kinds, because which kinds are editable is the same knowledge that decides which editor opens, and that already lives there. Absent means editable, so a caller that has never thought about it gets what it had. The Spacing dialog's label became a map. It was two kinds and readable as a ternary, then three and not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+20
-3
@@ -80,6 +80,17 @@ export type Canvas = {
|
||||
onAddComponent: (section: CanvasSection, column: number) => void;
|
||||
/** Something already in a column was pressed. */
|
||||
onEditComponent: (component: CanvasComponent) => void;
|
||||
/**
|
||||
* Whether this component has anything to edit — no button if not.
|
||||
*
|
||||
* A predicate from the panel rather than a list of kinds here, because which kinds are editable is
|
||||
* the same knowledge that decides WHICH editor opens, and that already lives there. A contact form
|
||||
* and a partner row are code the site owns: the block places them and there is nothing to type, so
|
||||
* an Edit button would be a door onto an empty room.
|
||||
*
|
||||
* Absent means editable, so a caller that has never thought about it gets what it had before.
|
||||
*/
|
||||
canEditComponent?: (component: CanvasComponent) => boolean;
|
||||
onDeleteComponent: (component: CanvasComponent) => void;
|
||||
};
|
||||
|
||||
@@ -324,11 +335,17 @@ export function paintCanvas(document: Document, canvas: Canvas): () => void {
|
||||
'z-index:5',
|
||||
]);
|
||||
|
||||
for (const [label, run] of [
|
||||
['Edit', () => canvas.onEditComponent(component)],
|
||||
const actions: [string, () => void][] = [
|
||||
['Spacing', () => canvas.onSpaceComponent(component)],
|
||||
['Delete', () => canvas.onDeleteComponent(component)],
|
||||
] as const) {
|
||||
];
|
||||
// First, so the tools read Edit / Spacing / Delete — the order they were in when Edit was
|
||||
// unconditional, rather than the order the array happens to be built in.
|
||||
if (canvas.canEditComponent?.(component) ?? true) {
|
||||
actions.unshift(['Edit', () => canvas.onEditComponent(component)]);
|
||||
}
|
||||
|
||||
for (const [label, run] of actions) {
|
||||
const action = document.createElement('button');
|
||||
action.type = 'button';
|
||||
action.textContent = label;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Images, Type, Video, type LucideIcon } from 'lucide-react';
|
||||
import { Handshake, Images, Mail, Type, Video, type LucideIcon } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
|
||||
/**
|
||||
@@ -23,7 +23,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
|
||||
* A kind the server would accept and this has never heard of is not creatable anyway.
|
||||
*/
|
||||
|
||||
export type ComponentKind = 'free-text' | 'video' | 'gallery';
|
||||
export type ComponentKind = 'free-text' | 'video' | 'gallery' | 'contact-form' | 'partners';
|
||||
|
||||
type Choice = {
|
||||
kind: ComponentKind | null;
|
||||
@@ -45,6 +45,15 @@ const CHOICES: Choice[] = [
|
||||
{ kind: null, label: 'Image', hint: 'One picture from the media store', Icon: Images },
|
||||
{ kind: 'video', label: 'Video', hint: 'No controls, a poster, autoplay and loop', Icon: Video },
|
||||
{ kind: 'gallery', label: 'Gallery', hint: 'A named set of media', Icon: Images },
|
||||
/*
|
||||
Two that place something the site already draws.
|
||||
|
||||
Nothing to choose and nothing to type, so adding one is a single click and there is no editor
|
||||
behind it. They are here rather than hidden because a page is built by picking blocks, and a block
|
||||
somebody cannot find is a block that does not exist.
|
||||
*/
|
||||
{ kind: 'contact-form', label: 'Contact form', hint: 'The site\'s form, as it is', Icon: Mail },
|
||||
{ kind: 'partners', label: 'Partners', hint: 'The partner logos, from Site', Icon: Handshake },
|
||||
];
|
||||
|
||||
export const AddComponentDialog = ({
|
||||
|
||||
@@ -5,6 +5,29 @@ import { readableError } from '../errors';
|
||||
import { SitePreview, useDevPort } from '../SitePreview';
|
||||
import { usePreviewable, useViewMode } from '../ViewMode';
|
||||
import { paintCanvas, type CanvasComponent, type CanvasSection } from '../canvas';
|
||||
|
||||
/*
|
||||
What the Spacing dialog calls a block, by kind.
|
||||
|
||||
A map rather than a chain of ternaries: it was two kinds and readable, then three and not. Unknown
|
||||
falls back to 'Text' — a label is a heading on a dialog, and the worst a wrong one does is read oddly
|
||||
for a kind nobody has finished adding.
|
||||
*/
|
||||
const LABELS: Record<string, string> = {
|
||||
'free-text': 'Text',
|
||||
video: 'Video',
|
||||
gallery: 'Gallery',
|
||||
'contact-form': 'Contact form',
|
||||
partners: 'Partners',
|
||||
};
|
||||
|
||||
/*
|
||||
The kinds that hold nothing, named once.
|
||||
|
||||
Both the picker and the canvas ask the same question — one to decide whether to open an editor, the
|
||||
other whether to draw an Edit button — and two lists would drift into a button that opens nothing.
|
||||
*/
|
||||
const PLACES_ONLY: string[] = ['contact-form', 'partners'];
|
||||
import { AddSectionDialog, type SectionLayout } from './AddSectionDialog';
|
||||
import { FreeTextEditor } from './FreeTextEditor';
|
||||
import { SpacingEditor, type Padding } from './SpacingEditor';
|
||||
@@ -147,6 +170,8 @@ export const PageBuilderPanel = () => {
|
||||
onDeleteSection: (section) => handlersRef.current.remove(section.id),
|
||||
onAddComponent: (section, column) => handlersRef.current.addComponent(section, column),
|
||||
onEditComponent: (component) => handlersRef.current.editComponent(component),
|
||||
// Nothing to edit, so no button. See PLACES_ONLY.
|
||||
canEditComponent: (component) => !PLACES_ONLY.includes(component.kind),
|
||||
onDeleteComponent: (component) => handlersRef.current.removeComponent(component),
|
||||
onSpaceSection: (section) => handlersRef.current.spaceSection(section),
|
||||
onSpaceComponent: (component) => handlersRef.current.spaceComponent(component),
|
||||
@@ -240,13 +265,38 @@ export const PageBuilderPanel = () => {
|
||||
const addComponent = (section: CanvasSection, column: number) =>
|
||||
setChoosing({ sectionId: section.id, column });
|
||||
|
||||
/*
|
||||
A block that holds nothing is CREATED on the spot.
|
||||
|
||||
No dialog, because there is nothing to fill in: the contact form is the site's form and the partner
|
||||
row is the partner table. Asking for a confirmation with no field on it would be a step that exists
|
||||
only to be got past — the same argument the picker itself answers the other way, where there IS a
|
||||
choice to make.
|
||||
*/
|
||||
const createEmpty = async (kind: ComponentKind, at: { sectionId: number; column: number }) => {
|
||||
if (!page) return;
|
||||
setError(null);
|
||||
try {
|
||||
await clientRef.current.post(`/fofos-admin/pages/${page.id}/sections/${at.sectionId}/components`, {
|
||||
kind,
|
||||
column: at.column,
|
||||
content: {},
|
||||
});
|
||||
setRendered((n) => n + 1);
|
||||
} catch (failure) {
|
||||
setError(readableError(failure));
|
||||
}
|
||||
};
|
||||
|
||||
const chooseKind = (kind: ComponentKind) => {
|
||||
if (!choosing) return;
|
||||
const at = choosing;
|
||||
setChoosing(null);
|
||||
// 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 });
|
||||
if (kind === 'free-text') setEditing({ id: null, ...at, html: '' });
|
||||
if (kind === 'video') setPicking({ id: null, ...at });
|
||||
if (kind === 'gallery') setChoosingGallery({ id: null, ...at, galleryId: null });
|
||||
if (PLACES_ONLY.includes(kind)) void createEmpty(kind, at);
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -418,7 +468,7 @@ export const PageBuilderPanel = () => {
|
||||
kind: 'component',
|
||||
id: component.id,
|
||||
sectionId: owner.id,
|
||||
label: component.kind === 'video' ? 'Video' : component.kind === 'gallery' ? 'Gallery' : 'Text',
|
||||
label: LABELS[component.kind] ?? 'Text',
|
||||
padding: (stored?.padding ?? {}) as Padding,
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user