TemplateModal's addTemplateComponents() built each template component via
React.createElement(Component, comp.props) without ever passing
comp.children, silently dropping every nested children array authored in
templates/definitions.ts (header/footer Container > Logo/Menu/TextBlock,
page Section > Heading/TextBlock/ButtonLink). The resulting Craft.js node had
nodes: [], so the header/footer zone preview (ZonePreview -> exportBodyHtml)
rendered as an empty strip, and published output was affected the same way.
Fix converts each TemplateComponent to a SerializedTreeNode and reuses
craft-tree.ts's buildNodeTree (sanitize -> flatten -> materialize) -- the
same tested tree pipeline already used for AI-generated content -- instead
of hand-rolling a React-element tree, since a naive nested-children fix via
parseReactElement crashes any component with an internal SHELL_INNER linked
canvas (Section/BackgroundSection/FormContainer) or linked columns
(ColumnLayout). Also fixes two latent bugs in buildNodeTree itself, only
surfaced by exercising it against a real Craft.js editor for the first time:
data.type must be the actual resolved component reference (not a string or
{resolvedName} object) for correct rendering, and the synthesized SHELL_INNER
node needs data.name set for actions.addNodeTree's own validation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
import { describe, test, expect } from 'vitest';
|
|
import { templateComponentToTreeNode } from './apply-template';
|
|
|
|
describe('templateComponentToTreeNode', () => {
|
|
test('converts a childless TemplateComponent to a SerializedTreeNode with an empty `nodes` array', () => {
|
|
const node = templateComponentToTreeNode({ type: 'Heading', props: { text: 'Hi' } });
|
|
expect(node).toEqual({
|
|
type: { resolvedName: 'Heading' },
|
|
props: { text: 'Hi' },
|
|
nodes: [],
|
|
});
|
|
});
|
|
|
|
test('regression: recursively converts nested `children` into `nodes` (previously dropped entirely)', () => {
|
|
const node = templateComponentToTreeNode({
|
|
type: 'Container',
|
|
props: { tag: 'header' },
|
|
children: [
|
|
{ type: 'Logo', props: { text: 'FlowStack' } },
|
|
{ type: 'Menu', props: { links: [] } },
|
|
],
|
|
});
|
|
expect(node.nodes).toHaveLength(2);
|
|
expect(node.nodes![0]).toEqual({ type: { resolvedName: 'Logo' }, props: { text: 'FlowStack' }, nodes: [] });
|
|
expect(node.nodes![1]).toEqual({ type: { resolvedName: 'Menu' }, props: { links: [] }, nodes: [] });
|
|
});
|
|
|
|
test('recurses more than one level deep', () => {
|
|
const node = templateComponentToTreeNode({
|
|
type: 'Section',
|
|
props: {},
|
|
children: [
|
|
{
|
|
type: 'ColumnLayout',
|
|
props: { columns: 1 },
|
|
children: [{ type: 'TextBlock', props: { text: 'deep' } }],
|
|
},
|
|
],
|
|
});
|
|
const col = node.nodes![0];
|
|
expect(col.type.resolvedName).toBe('ColumnLayout');
|
|
expect(col.nodes![0]).toEqual({ type: { resolvedName: 'TextBlock' }, props: { text: 'deep' }, nodes: [] });
|
|
});
|
|
|
|
test('does not mutate the input TemplateComponent', () => {
|
|
const comp = { type: 'Heading', props: { text: 'Hi' } };
|
|
const frozen = JSON.parse(JSON.stringify(comp));
|
|
templateComponentToTreeNode(comp);
|
|
expect(comp).toEqual(frozen);
|
|
});
|
|
});
|