diff --git a/packages/payload-plugin-json-form-builder/src/components/JsonBuilder.tsx b/packages/payload-plugin-json-form-builder/src/components/JsonBuilder.tsx
index f23b75cd..d3851a58 100644
--- a/packages/payload-plugin-json-form-builder/src/components/JsonBuilder.tsx
+++ b/packages/payload-plugin-json-form-builder/src/components/JsonBuilder.tsx
@@ -25,6 +25,7 @@ import { labelOf } from "./JsonNode.js";
import { schemaErrors } from "../field/checks.js";
import {
addAt,
+ addManyAt,
addSection,
editAt,
keyFault,
@@ -65,7 +66,7 @@ export const JsonBuilder = ({
const [key, setKey] = useState("");
const [hovered, setHovered] = useState("");
const [making, setMaking] = useState<"field" | "section" | null>(null);
- const { anchor: richTextAnchor, library: held, uploads } = useJsonFormConfig();
+ const { anchor: richTextAnchor, library: held, shapes, uploads } = useJsonFormConfig();
const missing: NodeType[] = [
...(uploads ? [] : ["upload" as NodeType]),
...(richTextAnchor ? [] : ["richText" as NodeType]),
@@ -159,6 +160,15 @@ export const JsonBuilder = ({
openModal(formSlug);
};
+ const addShape = (shape: string, fields: TypedNode[]) =>
+ setDraft(
+ addManyAt(
+ draft,
+ here,
+ fields.map((field) => ({ ...field, shape }))
+ )
+ );
+
const edit = (spot: Spot) => {
const node = nodeAt(draft, spot);
setPicked(spot);
@@ -354,6 +364,12 @@ export const JsonBuilder = ({
onOrder: reorder,
onOpen: (index) => setAt([...at, index]),
onRemove: (index) => remove({ section: current, at: [...at, index] }),
+ onRemoveShape: (from, upto) =>
+ setDraft(
+ editAt(draft, here, (node) =>
+ withShape(node, shapeOf(node).toSpliced(from, upto - from))
+ )
+ ),
}}
/>
>
@@ -373,6 +389,22 @@ export const JsonBuilder = ({
? "A strip of tabs holds tabs only."
: "It lands where you are standing."}
+ {section && Object.keys(shapes).length > 0 && holder?.type !== "tabs" && (
+
+
Custom
+ {Object.entries(shapes).map(([name, fields]) => (
+
+ ))}
+
+ )}
{section &&
GROUPS.map((group) => {
const inGroup = offered.filter((type) => KINDS[type].group === group);
diff --git a/packages/payload-plugin-json-form-builder/src/components/JsonBuilderLevel.tsx b/packages/payload-plugin-json-form-builder/src/components/JsonBuilderLevel.tsx
index 75a4b109..71876ef7 100644
--- a/packages/payload-plugin-json-form-builder/src/components/JsonBuilderLevel.tsx
+++ b/packages/payload-plugin-json-form-builder/src/components/JsonBuilderLevel.tsx
@@ -19,6 +19,7 @@ import type { NodeType, TypedNode } from "../field/typedJson.js";
const marks = (node: TypedNode) =>
[
+ node.type,
node.required && "required",
node.readOnly && "read only",
node.hidden && "hidden",
@@ -30,11 +31,101 @@ type Hands = {
onOpen: (index: number) => void;
onEdit: (index: number) => void;
onRemove: (index: number) => void;
+ onRemoveShape: (from: number, upto: number) => void;
onOrder: (fields: TypedNode[]) => void;
};
+type Row = { from: number; upto: number; shape?: string };
+
+const rowsOf = (fields: TypedNode[]): Row[] => {
+ const rows: Row[] = [];
+ for (let at = 0; at < fields.length; ) {
+ const shape = fields[at].shape;
+ if (!shape) {
+ rows.push({ from: at, upto: at + 1 });
+ at += 1;
+ continue;
+ }
+ let upto = at + 1;
+ while (upto < fields.length && fields[upto].shape === shape) upto += 1;
+ rows.push({ from: at, shape, upto });
+ at = upto;
+ }
+ return rows;
+};
+
+const Card = ({
+ grip,
+ hands,
+ index,
+ node,
+}: {
+ grip: React.ReactNode;
+ hands: Hands;
+ index: number;
+ node: TypedNode;
+}) => {
+ const kind = KINDS[node.type as NodeType];
+ const holds = holdsFields(node);
+ const fixed = Boolean(node.shape);
+ const Body = fixed ? "span" : "button";
+
+ return (
+ <>
+ {grip}
+ (holds ? hands.onOpen(index) : hands.onEdit(index))}
+ type={fixed ? undefined : "button"}
+ >
+
+ {kind?.glyph ?? "?"}
+
+
+
+ {labelOf(node) || no name}
+ {node.name ? #{node.name} : null}
+
+
+ {marks(node).map((mark) => (
+
+ {mark}
+
+ ))}
+
+
+
+ {fixed ? null : (
+
+ hands.onEdit(index)}>
+
+
+ hands.onRemove(index)}
+ >
+
+
+ {holds ? (
+
+
+
+ ) : null}
+
+ )}
+ >
+ );
+};
+
export const JsonBuilderLevel = ({ fields, hands }: { fields: TypedNode[]; hands: Hands }) => {
- const ids = fields.map((node, index) => node.name || `#${index + 1}`);
+ const rows = rowsOf(fields);
+ const ids = rows.map((row) => fields[row.from].name || `#${row.from + 1}`);
return (
hands.onOrder(
- fields.toSpliced(moveFromIndex, 1).toSpliced(moveToIndex, 0, fields[moveFromIndex])
+ rows
+ .toSpliced(moveFromIndex, 1)
+ .toSpliced(moveToIndex, 0, rows[moveFromIndex])
+ .flatMap((row) => fields.slice(row.from, row.upto))
)
}
>
- {fields.map((node, index) => {
- const kind = KINDS[node.type as NodeType];
- const holds = holdsFields(node);
- return (
-
+ {rows.map((row, at) =>
+ row.shape ? (
+
{({ attributes, isDragging, listeners, setNodeRef, transform, transition }) => (
-
-
-
-