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;
|
onAddComponent: (section: CanvasSection, column: number) => void;
|
||||||
/** Something already in a column was pressed. */
|
/** Something already in a column was pressed. */
|
||||||
onEditComponent: (component: CanvasComponent) => void;
|
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;
|
onDeleteComponent: (component: CanvasComponent) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -324,11 +335,17 @@ export function paintCanvas(document: Document, canvas: Canvas): () => void {
|
|||||||
'z-index:5',
|
'z-index:5',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
for (const [label, run] of [
|
const actions: [string, () => void][] = [
|
||||||
['Edit', () => canvas.onEditComponent(component)],
|
|
||||||
['Spacing', () => canvas.onSpaceComponent(component)],
|
['Spacing', () => canvas.onSpaceComponent(component)],
|
||||||
['Delete', () => canvas.onDeleteComponent(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');
|
const action = document.createElement('button');
|
||||||
action.type = 'button';
|
action.type = 'button';
|
||||||
action.textContent = label;
|
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';
|
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.
|
* 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 = {
|
type Choice = {
|
||||||
kind: ComponentKind | null;
|
kind: ComponentKind | null;
|
||||||
@@ -45,6 +45,15 @@ const CHOICES: Choice[] = [
|
|||||||
{ kind: null, label: 'Image', hint: 'One picture from the media store', Icon: Images },
|
{ 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: '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 },
|
{ 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 = ({
|
export const AddComponentDialog = ({
|
||||||
|
|||||||
@@ -5,6 +5,29 @@ import { readableError } from '../errors';
|
|||||||
import { SitePreview, useDevPort } from '../SitePreview';
|
import { SitePreview, useDevPort } from '../SitePreview';
|
||||||
import { usePreviewable, useViewMode } from '../ViewMode';
|
import { usePreviewable, useViewMode } from '../ViewMode';
|
||||||
import { paintCanvas, type CanvasComponent, type CanvasSection } from '../canvas';
|
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 { 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';
|
||||||
@@ -147,6 +170,8 @@ export const PageBuilderPanel = () => {
|
|||||||
onDeleteSection: (section) => handlersRef.current.remove(section.id),
|
onDeleteSection: (section) => handlersRef.current.remove(section.id),
|
||||||
onAddComponent: (section, column) => handlersRef.current.addComponent(section, column),
|
onAddComponent: (section, column) => handlersRef.current.addComponent(section, column),
|
||||||
onEditComponent: (component) => handlersRef.current.editComponent(component),
|
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),
|
onDeleteComponent: (component) => handlersRef.current.removeComponent(component),
|
||||||
onSpaceSection: (section) => handlersRef.current.spaceSection(section),
|
onSpaceSection: (section) => handlersRef.current.spaceSection(section),
|
||||||
onSpaceComponent: (component) => handlersRef.current.spaceComponent(component),
|
onSpaceComponent: (component) => handlersRef.current.spaceComponent(component),
|
||||||
@@ -240,13 +265,38 @@ export const PageBuilderPanel = () => {
|
|||||||
const addComponent = (section: CanvasSection, column: number) =>
|
const addComponent = (section: CanvasSection, column: number) =>
|
||||||
setChoosing({ sectionId: section.id, column });
|
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) => {
|
const chooseKind = (kind: ComponentKind) => {
|
||||||
if (!choosing) return;
|
if (!choosing) return;
|
||||||
|
const at = choosing;
|
||||||
setChoosing(null);
|
setChoosing(null);
|
||||||
// Each kind opens its own editor. A kind with no case here cannot be created, which is the point.
|
// 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 === 'free-text') setEditing({ id: null, ...at, html: '' });
|
||||||
if (kind === 'video') setPicking({ id: null, ...choosing });
|
if (kind === 'video') setPicking({ id: null, ...at });
|
||||||
if (kind === 'gallery') setChoosingGallery({ id: null, ...choosing, galleryId: null });
|
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',
|
kind: 'component',
|
||||||
id: component.id,
|
id: component.id,
|
||||||
sectionId: owner.id,
|
sectionId: owner.id,
|
||||||
label: component.kind === 'video' ? 'Video' : component.kind === 'gallery' ? 'Gallery' : 'Text',
|
label: LABELS[component.kind] ?? 'Text',
|
||||||
padding: (stored?.padding ?? {}) as Padding,
|
padding: (stored?.padding ?? {}) as Padding,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user