pmx-canvas 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/dist/types/json-render/server.d.ts +2 -1
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +4 -2
- package/skills/pmx-canvas-testing/SKILL.md +15 -1
- package/src/cli/agent.ts +5 -5
- package/src/json-render/server.ts +31 -3
- package/src/mcp/server.ts +20 -9
- package/src/server/canvas-operations.ts +2 -1
- package/src/server/canvas-schema.ts +3 -3
- package/src/server/server.ts +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,56 @@
|
|
|
3
3
|
All notable changes to `pmx-canvas` are documented here. This project follows
|
|
4
4
|
[Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.1.8] - 2026-04-25
|
|
7
|
+
|
|
8
|
+
Retest-driven follow-up to 0.1.7. This next release restores compatibility
|
|
9
|
+
for image and json-render node creation, fixes the counter MCP fixture's
|
|
10
|
+
accidental iframe overflow, and syncs packaged testing guidance with the
|
|
11
|
+
canonical repo skill.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **Image `path` is accepted as a compatibility alias for `content`.**
|
|
16
|
+
`pmx-canvas node add --type image --path <file>`, HTTP
|
|
17
|
+
`/api/canvas/node` payloads with `{ type: "image", path }`, and MCP
|
|
18
|
+
`canvas_add_node({ type: "image", path })` now all validate the file
|
|
19
|
+
and populate `data.src` instead of creating an empty/broken image node.
|
|
20
|
+
- **json-render creation keeps 0.1.7 compatibility.**
|
|
21
|
+
`canvas_add_json_render_node`, `POST /api/canvas/json-render`, and
|
|
22
|
+
CLI `node add --type json-render` accept omitted titles and infer a
|
|
23
|
+
node title from the root element. Bare legacy component specs like
|
|
24
|
+
`{ type: "Badge", props: {...} }` are wrapped into a one-element
|
|
25
|
+
document before validation, while complete `{ root, elements }` specs
|
|
26
|
+
remain the canonical shape.
|
|
27
|
+
- **Counter MCP fixture no longer creates a scrollable iframe by
|
|
28
|
+
accident.** The fixture now uses border-box sizing and hides root
|
|
29
|
+
overflow so `100vh` layouts with padding do not become `100vh +
|
|
30
|
+
padding`. This removes the observed repeated downward scroll in the
|
|
31
|
+
counter app and adds browser coverage for zero iframe body overflow.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **Packaged PMX Canvas testing skill is back in sync with canonical
|
|
36
|
+
guidance.** It now documents that `test:coverage` covers only the Bun
|
|
37
|
+
unit suite, names the coverage artifact, and calls out the WebView
|
|
38
|
+
automation timeout caveat used to distinguish environment limits from
|
|
39
|
+
product regressions.
|
|
40
|
+
- **Schema metadata for the `path` alias and relaxed json-render contract
|
|
41
|
+
is version-stable.** `canvas_describe_schema` and `canvas://schema`
|
|
42
|
+
surface the image `path` alias and `title.required: false` on
|
|
43
|
+
json-render so agents discover the new shapes without reading the
|
|
44
|
+
CHANGELOG.
|
|
45
|
+
- **Agent-facing `skills/pmx-canvas/SKILL.md` documents the same
|
|
46
|
+
contract.** The skill now describes the `path` alias for image nodes
|
|
47
|
+
and the relaxed json-render contract (omitted titles, bare component
|
|
48
|
+
specs) so agents do not need to retry across shapes.
|
|
49
|
+
|
|
50
|
+
### Internal
|
|
51
|
+
|
|
52
|
+
- Regression coverage for image `path` alias handling across CLI/HTTP/MCP,
|
|
53
|
+
json-render compatibility for omitted titles plus bare component specs,
|
|
54
|
+
and hosted counter MCP app iframe overflow.
|
|
55
|
+
|
|
6
56
|
## [0.1.7] - 2026-04-26
|
|
7
57
|
|
|
8
58
|
Small retest-driven follow-up to 0.1.6. Three agent-facing ergonomics:
|
|
@@ -50,6 +100,7 @@ otherwise have to discover by trial and error.
|
|
|
50
100
|
- Regression coverage for snapshot flat-`id` aliases on both MCP and
|
|
51
101
|
HTTP surfaces, plus async / top-level-`await` WebView script bodies.
|
|
52
102
|
|
|
103
|
+
[0.1.8]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.1.8
|
|
53
104
|
[0.1.7]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.1.7
|
|
54
105
|
|
|
55
106
|
## [0.1.6] - 2026-04-26
|
|
@@ -4,7 +4,7 @@ export interface JsonRenderSpec {
|
|
|
4
4
|
state?: Record<string, unknown>;
|
|
5
5
|
}
|
|
6
6
|
export interface JsonRenderNodeInput {
|
|
7
|
-
title
|
|
7
|
+
title?: string;
|
|
8
8
|
spec: unknown;
|
|
9
9
|
x?: number;
|
|
10
10
|
y?: number;
|
|
@@ -44,6 +44,7 @@ export declare const GRAPH_NODE_SIZE: {
|
|
|
44
44
|
height: number;
|
|
45
45
|
};
|
|
46
46
|
export type GraphChartType = 'LineChart' | 'BarChart' | 'PieChart' | 'AreaChart' | 'ScatterChart' | 'RadarChart' | 'StackedBarChart' | 'ComposedChart';
|
|
47
|
+
export declare function inferJsonRenderNodeTitle(spec: JsonRenderSpec, fallback?: string): string;
|
|
47
48
|
export declare function normalizeAndValidateJsonRenderSpec(spec: unknown): JsonRenderSpec;
|
|
48
49
|
export declare function normalizeGraphType(value: string): GraphChartType;
|
|
49
50
|
export declare function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmx-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Spatial canvas workbench for coding agents — infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server/index.ts",
|
|
@@ -270,6 +270,7 @@ If a node type is rejected by `canvas_add_node`, call `canvas_describe_schema` a
|
|
|
270
270
|
- `content`: for most types, this is markdown text. For `file` type, pass the **file path**
|
|
271
271
|
(e.g., `"src/auth/login.ts"`) — the server auto-loads the file content and watches for changes.
|
|
272
272
|
For `image` type, pass a file path, URL, or data URI.
|
|
273
|
+
- `path`: compatibility alias for image paths only; prefer `content` for new image calls
|
|
273
274
|
- `x`, `y`: position (auto-placed if omitted — prefer omitting for auto-layout)
|
|
274
275
|
- `width`, `height`: dimensions (sensible defaults provided)
|
|
275
276
|
- `color`: semantic color
|
|
@@ -306,8 +307,9 @@ If a node type is rejected by `canvas_add_node`, call `canvas_describe_schema` a
|
|
|
306
307
|
```
|
|
307
308
|
|
|
308
309
|
**`canvas_add_json_render_node`** — Add a native json-render node
|
|
309
|
-
- Required: `
|
|
310
|
-
-
|
|
310
|
+
- Required: `spec`; `title` is optional and inferred from the root element when omitted
|
|
311
|
+
- Prefer a complete json-render object with `root`, `elements`, and optional `state`
|
|
312
|
+
- Legacy bare component specs like `{ type: "Badge", props: {...} }` are accepted and wrapped into a one-element document for compatibility
|
|
311
313
|
- Use this when you want a structured UI panel rendered directly inside PMX Canvas
|
|
312
314
|
- For shadcn `Badge`, prefer `props.text` with variants `default`, `secondary`, `destructive`, or
|
|
313
315
|
`outline`. Legacy `props.label` and status variants (`success`, `info`, `warning`, `error`,
|
|
@@ -40,11 +40,25 @@ bun run test:all # Bun suite + browser smoke
|
|
|
40
40
|
Manual browser validation also requires a fresh client bundle. `bun run test:web-canvas`
|
|
41
41
|
already does this for you.
|
|
42
42
|
|
|
43
|
+
## Coverage Notes
|
|
44
|
+
|
|
45
|
+
- `bun run test:coverage` covers the Bun unit suite under `tests/unit/`
|
|
46
|
+
- Coverage output is written to `coverage/lcov.info` and also printed as a text summary
|
|
47
|
+
- CI currently uses that same unit-test coverage command, then runs browser smoke separately
|
|
48
|
+
- Do not describe `test:coverage` as full-stack coverage; Playwright coverage is not wired in here
|
|
49
|
+
|
|
43
50
|
## Current Project Test Surface
|
|
44
51
|
|
|
45
52
|
- Bun tests live under `tests/unit/`
|
|
46
53
|
- Playwright browser smoke lives under `tests/e2e/`
|
|
47
|
-
- CI runs coverage plus the browser smoke flow
|
|
54
|
+
- CI runs Bun coverage plus the browser smoke flow
|
|
55
|
+
|
|
56
|
+
## WebView Automation Caveat
|
|
57
|
+
|
|
58
|
+
- Some Linux/CI environments expose `Bun.WebView` but still cannot start a usable automation
|
|
59
|
+
session within the timeout window
|
|
60
|
+
- When testing WebView automation, treat a cleanly reported unsupported/timeout runtime boundary
|
|
61
|
+
as distinct from a product regression
|
|
48
62
|
|
|
49
63
|
Prefer extending the existing suites before inventing a one-off script.
|
|
50
64
|
|
package/src/cli/agent.ts
CHANGED
|
@@ -522,11 +522,8 @@ async function buildJsonRenderRequestBody(
|
|
|
522
522
|
flags: Record<string, string | true>,
|
|
523
523
|
): Promise<Record<string, unknown>> {
|
|
524
524
|
const hint =
|
|
525
|
-
'Use: pmx-canvas node add --type json-render --
|
|
525
|
+
'Use: pmx-canvas node add --type json-render --spec-file ./dashboard.json --title "Ops Dashboard"';
|
|
526
526
|
const title = typeof flags.title === 'string' ? flags.title.trim() : '';
|
|
527
|
-
if (!title) {
|
|
528
|
-
die('json-render nodes require --title.', hint);
|
|
529
|
-
}
|
|
530
527
|
|
|
531
528
|
const rawSpec = await readTextInput(flags, {
|
|
532
529
|
fileFlags: ['spec-file'],
|
|
@@ -538,7 +535,7 @@ async function buildJsonRenderRequestBody(
|
|
|
538
535
|
});
|
|
539
536
|
|
|
540
537
|
const spec = parseJsonValue(rawSpec, 'JSON spec', hint);
|
|
541
|
-
const body: Record<string, unknown> = { title, spec };
|
|
538
|
+
const body: Record<string, unknown> = { ...(title ? { title } : {}), spec };
|
|
542
539
|
applyCommonGeometryFlags(body, flags, {
|
|
543
540
|
x: 'Use a finite number, e.g. --x 500',
|
|
544
541
|
y: 'Use a finite number, e.g. --y 300',
|
|
@@ -985,8 +982,11 @@ cmd('node add', 'Add a node to the canvas', [
|
|
|
985
982
|
const body: Record<string, unknown> = { type };
|
|
986
983
|
if (flags.title) body.title = flags.title;
|
|
987
984
|
const webpageUrl = getStringFlag(flags, 'url');
|
|
985
|
+
const imagePath = getStringFlag(flags, 'path');
|
|
988
986
|
if (type === 'webpage' && webpageUrl) {
|
|
989
987
|
body.url = webpageUrl;
|
|
988
|
+
} else if (type === 'image' && imagePath && !flags.content) {
|
|
989
|
+
body.content = imagePath;
|
|
990
990
|
} else if (flags.content) {
|
|
991
991
|
body.content = flags.content;
|
|
992
992
|
}
|
|
@@ -11,7 +11,7 @@ export interface JsonRenderSpec {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface JsonRenderNodeInput {
|
|
14
|
-
title
|
|
14
|
+
title?: string;
|
|
15
15
|
spec: unknown;
|
|
16
16
|
x?: number;
|
|
17
17
|
y?: number;
|
|
@@ -447,10 +447,38 @@ function normalizeSpec(spec: Record<string, unknown>): Record<string, unknown> {
|
|
|
447
447
|
return changed ? { ...spec, elements: normalizedElements } : spec;
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
|
|
450
|
+
function isBareJsonRenderElement(spec: Record<string, unknown>): boolean {
|
|
451
|
+
return typeof spec.type === 'string' && !('root' in spec) && !('elements' in spec);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function normalizeJsonRenderInput(spec: unknown): unknown {
|
|
451
455
|
const specRecord = asRecord(spec);
|
|
456
|
+
if (!specRecord || !isBareJsonRenderElement(specRecord)) return spec;
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
root: 'root',
|
|
460
|
+
elements: {
|
|
461
|
+
root: {
|
|
462
|
+
...specRecord,
|
|
463
|
+
children: Array.isArray(specRecord.children)
|
|
464
|
+
? specRecord.children.filter((child: unknown) => typeof child === 'string')
|
|
465
|
+
: [],
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function inferJsonRenderNodeTitle(spec: JsonRenderSpec, fallback = 'json-render'): string {
|
|
472
|
+
const rootElement = asRecord(spec.elements[spec.root]);
|
|
473
|
+
const rootProps = asRecord(rootElement?.props);
|
|
474
|
+
const title = rootProps?.title ?? rootProps?.text ?? rootElement?.type;
|
|
475
|
+
return typeof title === 'string' && title.trim().length > 0 ? title.trim() : fallback;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function normalizeAndValidateJsonRenderSpec(spec: unknown): JsonRenderSpec {
|
|
479
|
+
const specRecord = asRecord(normalizeJsonRenderInput(spec));
|
|
452
480
|
if (!specRecord || typeof specRecord.root !== 'string' || !asRecord(specRecord.elements)) {
|
|
453
|
-
throw new Error('Missing root and elements in spec.');
|
|
481
|
+
throw new Error('Missing root and elements in spec. Pass a complete {root, elements} document, or a single bare component object with a type field.');
|
|
454
482
|
}
|
|
455
483
|
|
|
456
484
|
const normalizedSpec = normalizeSpec(specRecord);
|
package/src/mcp/server.ts
CHANGED
|
@@ -41,11 +41,18 @@ import { listBundledSkills, readBundledSkill } from '../server/bundled-skills.js
|
|
|
41
41
|
|
|
42
42
|
let canvas: PmxCanvas | null = null;
|
|
43
43
|
|
|
44
|
-
const jsonRenderSpecSchema = z.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
const jsonRenderSpecSchema = z.union([
|
|
45
|
+
z.object({
|
|
46
|
+
root: z.string(),
|
|
47
|
+
elements: z.record(z.string(), z.unknown()),
|
|
48
|
+
state: z.record(z.string(), z.unknown()).optional(),
|
|
49
|
+
}).passthrough(),
|
|
50
|
+
z.object({
|
|
51
|
+
type: z.string(),
|
|
52
|
+
props: z.record(z.string(), z.unknown()).optional(),
|
|
53
|
+
children: z.array(z.string()).optional(),
|
|
54
|
+
}).passthrough(),
|
|
55
|
+
]);
|
|
49
56
|
|
|
50
57
|
function structuredSchemaDescription(): string {
|
|
51
58
|
const routing = describeCanvasSchema().mcp.nodeTypeRouting;
|
|
@@ -140,6 +147,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
140
147
|
.describe('Node type (prefer canvas_create_group for groups)'),
|
|
141
148
|
title: z.string().optional().describe('Node title'),
|
|
142
149
|
content: z.string().optional().describe('Node content (markdown for markdown nodes, file path for file nodes, image path/URL/data-URI for image nodes, URL for webpage nodes)'),
|
|
150
|
+
path: z.string().optional().describe('Compatibility alias for image node content. Prefer content for image paths.'),
|
|
143
151
|
url: z.string().optional().describe('Canonical webpage URL field for webpage nodes. Overrides content when both are provided.'),
|
|
144
152
|
x: z.number().optional().describe('X position (auto-placed if omitted)'),
|
|
145
153
|
y: z.number().optional().describe('Y position (auto-placed if omitted)'),
|
|
@@ -169,7 +177,10 @@ export async function startMcpServer(): Promise<void> {
|
|
|
169
177
|
...(result.ok ? {} : { isError: true }),
|
|
170
178
|
};
|
|
171
179
|
}
|
|
172
|
-
const
|
|
180
|
+
const nodeInput = input.type === 'image' && input.path && !input.content
|
|
181
|
+
? { ...input, content: input.path }
|
|
182
|
+
: input;
|
|
183
|
+
const id = c.addNode(nodeInput);
|
|
173
184
|
return {
|
|
174
185
|
content: [{ type: 'text', text: JSON.stringify(createdNodePayload(c, id), null, 2) }],
|
|
175
186
|
};
|
|
@@ -442,8 +453,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
442
453
|
'canvas_add_json_render_node',
|
|
443
454
|
'Create a native json-render canvas node from a complete spec. Use this for structured dashboards, forms, tables, and other interactive UI panels that should render directly inside PMX Canvas.',
|
|
444
455
|
{
|
|
445
|
-
title: z.string().describe('
|
|
446
|
-
spec:
|
|
456
|
+
title: z.string().optional().describe('Optional node title. If omitted, PMX Canvas infers one from the root element.'),
|
|
457
|
+
spec: z.unknown().describe('json-render spec. Prefer a complete {root, elements, state?} document; a single bare component object is accepted for legacy callers.'),
|
|
447
458
|
x: z.number().optional().describe('Optional X position'),
|
|
448
459
|
y: z.number().optional().describe('Optional Y position'),
|
|
449
460
|
width: z.number().optional().describe('Optional node width'),
|
|
@@ -453,7 +464,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
453
464
|
const c = await ensureCanvas();
|
|
454
465
|
try {
|
|
455
466
|
const result = c.addJsonRenderNode({
|
|
456
|
-
title: input.title,
|
|
467
|
+
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
457
468
|
spec: input.spec,
|
|
458
469
|
...(typeof input.x === 'number' ? { x: input.x } : {}),
|
|
459
470
|
...(typeof input.y === 'number' ? { y: input.y } : {}),
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
buildGraphSpec,
|
|
25
25
|
createJsonRenderNodeData,
|
|
26
26
|
GRAPH_NODE_SIZE,
|
|
27
|
+
inferJsonRenderNodeTitle,
|
|
27
28
|
JSON_RENDER_NODE_SIZE,
|
|
28
29
|
normalizeAndValidateJsonRenderSpec,
|
|
29
30
|
type GraphNodeInput,
|
|
@@ -1000,7 +1001,7 @@ export function createCanvasJsonRenderNode(
|
|
|
1000
1001
|
collapsed: false,
|
|
1001
1002
|
pinned: false,
|
|
1002
1003
|
dockPosition: null,
|
|
1003
|
-
data: createJsonRenderNodeData(id, input.title, spec, {
|
|
1004
|
+
data: createJsonRenderNodeData(id, input.title?.trim() || inferJsonRenderNodeTitle(spec), spec, {
|
|
1004
1005
|
viewerType: 'json-render',
|
|
1005
1006
|
}),
|
|
1006
1007
|
};
|
|
@@ -171,7 +171,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
171
171
|
endpoint: '/api/canvas/node',
|
|
172
172
|
mcpTool: 'canvas_add_node',
|
|
173
173
|
fields: [
|
|
174
|
-
{ name: 'content', type: 'string', required: true, description: 'Image path, URL, or data URI.' },
|
|
174
|
+
{ name: 'content', type: 'string', required: true, description: 'Image path, URL, or data URI.', aliases: ['path'] },
|
|
175
175
|
{ name: 'title', type: 'string', required: false, description: 'Optional title override.' },
|
|
176
176
|
{ name: 'data.warning', type: 'string | { title?: string; detail: string }', required: false, description: 'Optional agent-supplied warning shown above the image.' },
|
|
177
177
|
{ name: 'data.warnings', type: 'Array<string | { title?: string; detail: string }>', required: false, description: 'Optional list of agent-supplied image warnings.' },
|
|
@@ -280,8 +280,8 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
280
280
|
endpoint: '/api/canvas/json-render',
|
|
281
281
|
mcpTool: 'canvas_add_json_render_node',
|
|
282
282
|
fields: [
|
|
283
|
-
{ name: 'title', type: 'string', required:
|
|
284
|
-
{ name: 'spec', type: 'JsonRenderSpec', required: true, description: 'Complete json-render spec.' },
|
|
283
|
+
{ name: 'title', type: 'string', required: false, description: 'Optional rendered node title; inferred from the root element when omitted.' },
|
|
284
|
+
{ name: 'spec', type: 'JsonRenderSpec | JsonRenderElement', required: true, description: 'Complete {root, elements} json-render spec, or a legacy single bare component object with a type field.' },
|
|
285
285
|
{ name: 'x', type: 'number', required: false, description: 'Optional X position.' },
|
|
286
286
|
{ name: 'y', type: 'number', required: false, description: 'Optional Y position.' },
|
|
287
287
|
{ name: 'width', type: 'number', required: false, description: 'Optional node width.' },
|
package/src/server/server.ts
CHANGED
|
@@ -1210,12 +1210,15 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1210
1210
|
const extraData = body.data && typeof body.data === 'object' && !Array.isArray(body.data)
|
|
1211
1211
|
? body.data as Record<string, unknown>
|
|
1212
1212
|
: undefined;
|
|
1213
|
+
const content = type === 'image' && typeof body.path === 'string' && typeof body.content !== 'string'
|
|
1214
|
+
? body.path
|
|
1215
|
+
: body.content;
|
|
1213
1216
|
let added: ReturnType<typeof addCanvasNode>;
|
|
1214
1217
|
try {
|
|
1215
1218
|
added = addCanvasNode({
|
|
1216
1219
|
type: type as CanvasNodeState['type'],
|
|
1217
1220
|
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1218
|
-
...(typeof
|
|
1221
|
+
...(typeof content === 'string' ? { content } : {}),
|
|
1219
1222
|
...(extraData ? { data: extraData } : {}),
|
|
1220
1223
|
...(typeof body.x === 'number' ? { x: body.x } : {}),
|
|
1221
1224
|
...(typeof body.y === 'number' ? { y: body.y } : {}),
|
|
@@ -1603,13 +1606,10 @@ async function handleCanvasAddJsonRender(req: Request): Promise<Response> {
|
|
|
1603
1606
|
const title = typeof body.title === 'string' ? body.title.trim() : '';
|
|
1604
1607
|
const rawSpec =
|
|
1605
1608
|
body.spec && typeof body.spec === 'object' && !Array.isArray(body.spec) ? body.spec : body;
|
|
1606
|
-
if (!title) {
|
|
1607
|
-
return responseJson({ ok: false, error: 'Missing required field: title.' }, 400);
|
|
1608
|
-
}
|
|
1609
1609
|
|
|
1610
1610
|
try {
|
|
1611
1611
|
const result = createCanvasJsonRenderNode({
|
|
1612
|
-
title,
|
|
1612
|
+
...(title ? { title } : {}),
|
|
1613
1613
|
spec: rawSpec,
|
|
1614
1614
|
...(typeof body.x === 'number' ? { x: body.x } : {}),
|
|
1615
1615
|
...(typeof body.y === 'number' ? { y: body.y } : {}),
|