diff --git a/packages/payload-plugin-json-form-builder/src/field/edits.ts b/packages/payload-plugin-json-form-builder/src/field/edits.ts index dffa54ff..4286b3d9 100644 --- a/packages/payload-plugin-json-form-builder/src/field/edits.ts +++ b/packages/payload-plugin-json-form-builder/src/field/edits.ts @@ -1,5 +1,5 @@ -import { holdsFields } from "./typedJson.js"; -import type { ArrayNode, Container, TypedNode, TypedRoot } from "./typedJson.js"; +import { HIDDEN, holdsFields } from "./typedJson.js"; +import type { ArrayNode, Container, Leaf, TypedNode, TypedRoot } from "./typedJson.js"; export type Spot = { section: string; at: number[] }; @@ -8,6 +8,49 @@ export const shapeOf = (node: TypedNode): TypedNode[] => ? ((node as ArrayNode).rows?.[0] ?? (node as ArrayNode).fields ?? []) : ((node as Container).fields ?? []); +// A hidden row is marked by a field the schema does not have, so it belongs to the row rather than +// to the shape: taken from the row being dressed, never from the one the shape was read off. +export const dressed = (shape: TypedNode[], row: TypedNode[]): TypedNode[] => { + const worn = shape + .filter((field) => field.name !== HIDDEN) + .map((field) => + valued(field, field.name ? row.find((entry) => entry.name === field.name) : undefined) + ); + const mark = row.find((field) => field.name === HIDDEN); + return mark ? [...worn, mark] : worn; +}; + +// A shape worn by values that are someone else's: the shape says which fields there are and how +// they are drawn, the value says what they hold, and every row keeps what it holds. `withShape` is +// the other half of the pair — there row one *is* the shape, because it is the edit it came from. +export const valued = (shape: TypedNode, own: TypedNode | undefined): TypedNode => { + if (!own) return shape; + const head = { ...shape, hidden: own.hidden }; + + // A list carries rows, a container carries fields and a leaf carries a value, so a kind that + // turned into another kind has nothing to hand over — except between the containers, which hold + // the same thing and are told apart only by how the admin draws them. + if (shape.type === "array") { + if (own.type !== "array") return shape; + const { fields: _shape, ...rest } = head as ArrayNode; + return { + ...rest, + rows: ((own as ArrayNode).rows ?? []).map((row) => dressed(shapeOf(shape), row)), + } as TypedNode; + } + + if (holdsFields(shape)) { + if (!holdsFields(own)) return shape; + return { + ...head, + fields: dressed(shapeOf(shape), (own as Container).fields ?? []), + } as TypedNode; + } + + if (own.type !== shape.type) return shape; + return { ...head, value: (own as Leaf).value } as TypedNode; +}; + // Row one is the shape itself — it is where the edit came from — and the rest are re-dressed in it. export const withShape = (node: TypedNode, shape: TypedNode[]): TypedNode => { if (node.type !== "array") return { ...node, fields: shape } as TypedNode; @@ -20,17 +63,6 @@ export const withShape = (node: TypedNode, shape: TypedNode[]): TypedNode => { } as TypedNode; }; -export const dressed = (shape: TypedNode[], row: TypedNode[]): TypedNode[] => - shape.map((field) => { - const had = field.name ? row.find((entry) => entry.name === field.name) : undefined; - if (!had || had.type !== field.type) return field; - const kids = childrenOf(field); - if (!kids) return had; - return field.type === "array" - ? withShape(had, shapeOf(field)) - : ({ ...field, fields: dressed(kids, childrenOf(had) ?? []) } as TypedNode); - }); - const childrenOf = (node: TypedNode) => (holdsFields(node) ? shapeOf(node) : undefined); const seatOf = (root: TypedRoot, name: string) => root.findIndex((node) => node.name === name); diff --git a/packages/payload-plugin-json-form-builder/src/library/reconcileSection.ts b/packages/payload-plugin-json-form-builder/src/library/reconcileSection.ts index cdbcb482..9675a7ff 100644 --- a/packages/payload-plugin-json-form-builder/src/library/reconcileSection.ts +++ b/packages/payload-plugin-json-form-builder/src/library/reconcileSection.ts @@ -1,29 +1,14 @@ -import { dressed, shapeOf } from "../field/edits.js"; +import { valued } from "../field/edits.js"; import { isTyped } from "../field/typedJson.js"; -import type { ArrayNode, Container, TypedNode, TypedRoot } from "../field/typedJson.js"; +import type { TypedNode, TypedRoot } from "../field/typedJson.js"; const sourceOf = (node: TypedNode) => node.source ?? ""; -const reshape = (own: TypedNode, source: TypedNode): TypedNode => { - const head = { ...source, name: own.name, hidden: own.hidden, source: own.source }; - - if (source.type === "array") { - const { fields: _shape, ...rest } = head as ArrayNode; - return { - ...rest, - rows: ((own as ArrayNode).rows ?? []).map((row) => dressed(shapeOf(source), row)), - } as TypedNode; - } - - if ("fields" in source) { - return { - ...head, - fields: dressed(shapeOf(source), (own as Container).fields ?? []), - } as TypedNode; - } - - return { ...head, value: (own as { value?: unknown }).value } as TypedNode; -}; +// The library owns the shape and the edition owns what is written into it — which is `valued`, the +// same pairing every row of every list below here goes through. Only the three that say *which* +// section this is are the edition's: what it is called, whether it runs, and what it follows. +const reshape = (own: TypedNode, source: TypedNode): TypedNode => + ({ ...valued(source, own), name: own.name, hidden: own.hidden, source: own.source }) as TypedNode; export const reconcile = (value: unknown, library: unknown): unknown => { if (!isTyped(value)) return value;