pmx-canvas 0.1.7 → 0.1.9
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 +116 -0
- package/Readme.md +52 -54
- package/dist/canvas/index.js +61 -61
- package/dist/types/client/canvas/auto-fit.d.ts +5 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +2 -0
- package/dist/types/json-render/server.d.ts +3 -1
- package/dist/types/server/canvas-operations.d.ts +49 -0
- package/dist/types/server/index.d.ts +9 -1
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +17 -3
- package/skills/pmx-canvas-testing/SKILL.md +15 -1
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +1 -1
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +11 -0
- package/src/cli/agent.ts +114 -19
- package/src/client/canvas/CanvasNode.tsx +5 -7
- package/src/client/canvas/auto-fit.ts +21 -0
- package/src/client/nodes/ExtAppFrame.tsx +4 -2
- package/src/json-render/server.ts +58 -3
- package/src/mcp/server.ts +66 -23
- package/src/server/canvas-operations.ts +311 -21
- package/src/server/canvas-schema.ts +5 -3
- package/src/server/index.ts +45 -6
- package/src/server/server.ts +154 -40
package/src/server/index.ts
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
createCanvasGraphNode,
|
|
17
17
|
createCanvasGroup,
|
|
18
18
|
createCanvasJsonRenderNode,
|
|
19
|
+
buildStructuredNodeUpdate,
|
|
20
|
+
fitCanvasView,
|
|
19
21
|
deleteCanvasSnapshot,
|
|
20
22
|
executeCanvasBatch,
|
|
21
23
|
groupCanvasNodes,
|
|
@@ -30,6 +32,7 @@ import {
|
|
|
30
32
|
setCanvasContextPins,
|
|
31
33
|
ungroupCanvasNodes,
|
|
32
34
|
validateCanvasNodePatch,
|
|
35
|
+
hasStructuredNodeUpdateFields,
|
|
33
36
|
} from './canvas-operations.js';
|
|
34
37
|
import { validateCanvasLayout } from './canvas-validation.js';
|
|
35
38
|
import { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
|
|
@@ -51,8 +54,6 @@ import {
|
|
|
51
54
|
} from './diagram-presets.js';
|
|
52
55
|
import {
|
|
53
56
|
buildGraphSpec,
|
|
54
|
-
buildJsonRenderViewerHtml,
|
|
55
|
-
createJsonRenderNodeData,
|
|
56
57
|
GRAPH_NODE_SIZE,
|
|
57
58
|
JSON_RENDER_NODE_SIZE,
|
|
58
59
|
normalizeAndValidateJsonRenderSpec,
|
|
@@ -221,15 +222,41 @@ export class PmxCanvas extends EventEmitter {
|
|
|
221
222
|
return result;
|
|
222
223
|
}
|
|
223
224
|
|
|
224
|
-
updateNode(id: string, patch: Partial<CanvasNodeState>): void {
|
|
225
|
+
updateNode(id: string, patch: Partial<CanvasNodeState> & Record<string, unknown>): void {
|
|
226
|
+
const existing = canvasState.getNode(id);
|
|
227
|
+
if (!existing) return;
|
|
228
|
+
const resolvedPatch: Partial<CanvasNodeState> = {};
|
|
229
|
+
if (patch.position) resolvedPatch.position = patch.position;
|
|
230
|
+
if (patch.size) resolvedPatch.size = patch.size;
|
|
231
|
+
if (patch.collapsed !== undefined) resolvedPatch.collapsed = patch.collapsed;
|
|
232
|
+
if (patch.pinned !== undefined) resolvedPatch.pinned = patch.pinned;
|
|
233
|
+
if (patch.dockPosition !== undefined) resolvedPatch.dockPosition = patch.dockPosition;
|
|
234
|
+
|
|
235
|
+
if (hasStructuredNodeUpdateFields(patch)) {
|
|
236
|
+
resolvedPatch.data = buildStructuredNodeUpdate(existing, patch).data;
|
|
237
|
+
} else if (
|
|
238
|
+
patch.data !== undefined ||
|
|
239
|
+
patch.title !== undefined ||
|
|
240
|
+
patch.content !== undefined ||
|
|
241
|
+
typeof patch.arrangeLocked === 'boolean'
|
|
242
|
+
) {
|
|
243
|
+
resolvedPatch.data = {
|
|
244
|
+
...existing.data,
|
|
245
|
+
...(patch.data && typeof patch.data === 'object' && !Array.isArray(patch.data) ? patch.data : {}),
|
|
246
|
+
...(typeof patch.title === 'string' ? { title: patch.title } : {}),
|
|
247
|
+
...(typeof patch.content === 'string' ? { content: patch.content } : {}),
|
|
248
|
+
...(typeof patch.arrangeLocked === 'boolean' ? { arrangeLocked: patch.arrangeLocked } : {}),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
225
252
|
const error = validateCanvasNodePatch({
|
|
226
|
-
...(
|
|
227
|
-
...(
|
|
253
|
+
...(resolvedPatch.position ? { position: resolvedPatch.position } : {}),
|
|
254
|
+
...(resolvedPatch.size ? { size: resolvedPatch.size } : {}),
|
|
228
255
|
});
|
|
229
256
|
if (error) {
|
|
230
257
|
throw new Error(error);
|
|
231
258
|
}
|
|
232
|
-
canvasState.updateNode(id,
|
|
259
|
+
canvasState.updateNode(id, resolvedPatch);
|
|
233
260
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
234
261
|
}
|
|
235
262
|
|
|
@@ -344,6 +371,18 @@ export class PmxCanvas extends EventEmitter {
|
|
|
344
371
|
return { focused: id, panned: !noPan };
|
|
345
372
|
}
|
|
346
373
|
|
|
374
|
+
fitView(options?: {
|
|
375
|
+
width?: number;
|
|
376
|
+
height?: number;
|
|
377
|
+
padding?: number;
|
|
378
|
+
maxScale?: number;
|
|
379
|
+
nodeIds?: string[];
|
|
380
|
+
}): ReturnType<typeof fitCanvasView> {
|
|
381
|
+
const result = fitCanvasView(options);
|
|
382
|
+
emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: result.viewport });
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
|
|
347
386
|
getLayout(): CanvasLayout {
|
|
348
387
|
return canvasState.getLayout();
|
|
349
388
|
}
|
package/src/server/server.ts
CHANGED
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
addCanvasNode,
|
|
74
74
|
addCanvasEdge,
|
|
75
75
|
applyCanvasNodeUpdates,
|
|
76
|
+
buildStructuredNodeUpdate,
|
|
76
77
|
arrangeCanvasNodes,
|
|
77
78
|
clearCanvas,
|
|
78
79
|
createCanvasGraphNode,
|
|
@@ -80,6 +81,7 @@ import {
|
|
|
80
81
|
createCanvasJsonRenderNode,
|
|
81
82
|
deleteCanvasSnapshot,
|
|
82
83
|
executeCanvasBatch,
|
|
84
|
+
fitCanvasView,
|
|
83
85
|
groupCanvasNodes,
|
|
84
86
|
listCanvasSnapshots,
|
|
85
87
|
refreshCanvasWebpageNode,
|
|
@@ -93,6 +95,7 @@ import {
|
|
|
93
95
|
setCanvasContextPins,
|
|
94
96
|
ungroupCanvasNodes,
|
|
95
97
|
validateCanvasNodePatch,
|
|
98
|
+
hasStructuredNodeUpdateFields,
|
|
96
99
|
} from './canvas-operations.js';
|
|
97
100
|
import { validateCanvasLayout } from './canvas-validation.js';
|
|
98
101
|
import { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
|
|
@@ -109,12 +112,7 @@ import {
|
|
|
109
112
|
import { traceManager } from './trace-manager.js';
|
|
110
113
|
import { buildWebArtifactOnCanvas, resolveWorkspacePath } from './web-artifacts.js';
|
|
111
114
|
import {
|
|
112
|
-
buildGraphSpec,
|
|
113
115
|
buildJsonRenderViewerHtml,
|
|
114
|
-
createJsonRenderNodeData,
|
|
115
|
-
GRAPH_NODE_SIZE,
|
|
116
|
-
JSON_RENDER_NODE_SIZE,
|
|
117
|
-
normalizeAndValidateJsonRenderSpec,
|
|
118
116
|
} from '../json-render/server.js';
|
|
119
117
|
import {
|
|
120
118
|
WEBPAGE_NODE_DEFAULT_SIZE,
|
|
@@ -651,6 +649,97 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
651
649
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
652
650
|
}
|
|
653
651
|
|
|
652
|
+
function pickFiniteNumber(record: Record<string, unknown>, key: string): number | undefined {
|
|
653
|
+
const value = record[key];
|
|
654
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function getRecord(value: unknown): Record<string, unknown> | undefined {
|
|
658
|
+
return isRecord(value) ? value : undefined;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function pickPositiveNumber(record: Record<string, unknown>, key: string): number | undefined {
|
|
662
|
+
const value = pickFiniteNumber(record, key);
|
|
663
|
+
return value !== undefined && value > 0 ? value : undefined;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function normalizeGeometryInput(body: Record<string, unknown>): {
|
|
667
|
+
x?: number;
|
|
668
|
+
y?: number;
|
|
669
|
+
width?: number;
|
|
670
|
+
height?: number;
|
|
671
|
+
position?: { x?: number; y?: number };
|
|
672
|
+
size?: { width?: number; height?: number };
|
|
673
|
+
} {
|
|
674
|
+
const position = getRecord(body.position);
|
|
675
|
+
const size = getRecord(body.size);
|
|
676
|
+
return {
|
|
677
|
+
...(pickFiniteNumber(body, 'x') !== undefined ? { x: pickFiniteNumber(body, 'x') } : {}),
|
|
678
|
+
...(pickFiniteNumber(body, 'y') !== undefined ? { y: pickFiniteNumber(body, 'y') } : {}),
|
|
679
|
+
...(pickFiniteNumber(body, 'width') !== undefined ? { width: pickFiniteNumber(body, 'width') } : {}),
|
|
680
|
+
...(pickFiniteNumber(body, 'height') !== undefined ? { height: pickFiniteNumber(body, 'height') } : {}),
|
|
681
|
+
...(position ? {
|
|
682
|
+
position: {
|
|
683
|
+
...(pickFiniteNumber(position, 'x') !== undefined ? { x: pickFiniteNumber(position, 'x') } : {}),
|
|
684
|
+
...(pickFiniteNumber(position, 'y') !== undefined ? { y: pickFiniteNumber(position, 'y') } : {}),
|
|
685
|
+
},
|
|
686
|
+
} : {}),
|
|
687
|
+
...(size ? {
|
|
688
|
+
size: {
|
|
689
|
+
...(pickFiniteNumber(size, 'width') !== undefined ? { width: pickFiniteNumber(size, 'width') } : {}),
|
|
690
|
+
...(pickFiniteNumber(size, 'height') !== undefined ? { height: pickFiniteNumber(size, 'height') } : {}),
|
|
691
|
+
},
|
|
692
|
+
} : {}),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function resolveCreateGeometry(body: Record<string, unknown>): {
|
|
697
|
+
x?: number;
|
|
698
|
+
y?: number;
|
|
699
|
+
width?: number;
|
|
700
|
+
height?: number;
|
|
701
|
+
} {
|
|
702
|
+
const geometry = normalizeGeometryInput(body);
|
|
703
|
+
const x = geometry.x ?? geometry.position?.x;
|
|
704
|
+
const y = geometry.y ?? geometry.position?.y;
|
|
705
|
+
const width = geometry.width ?? geometry.size?.width;
|
|
706
|
+
const height = geometry.height ?? geometry.size?.height;
|
|
707
|
+
return {
|
|
708
|
+
...(x !== undefined ? { x } : {}),
|
|
709
|
+
...(y !== undefined ? { y } : {}),
|
|
710
|
+
...(width !== undefined ? { width } : {}),
|
|
711
|
+
...(height !== undefined ? { height } : {}),
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function resolvePatchGeometry(
|
|
716
|
+
body: Record<string, unknown>,
|
|
717
|
+
existing: CanvasNodeState,
|
|
718
|
+
): {
|
|
719
|
+
position?: { x: number; y: number };
|
|
720
|
+
size?: { width: number; height: number };
|
|
721
|
+
} {
|
|
722
|
+
const geometry = normalizeGeometryInput(body);
|
|
723
|
+
const x = geometry.x ?? geometry.position?.x;
|
|
724
|
+
const y = geometry.y ?? geometry.position?.y;
|
|
725
|
+
const width = geometry.width ?? geometry.size?.width;
|
|
726
|
+
const height = geometry.height ?? geometry.size?.height;
|
|
727
|
+
return {
|
|
728
|
+
...(x !== undefined || y !== undefined
|
|
729
|
+
? { position: { x: x ?? existing.position.x, y: y ?? existing.position.y } }
|
|
730
|
+
: {}),
|
|
731
|
+
...(width !== undefined || height !== undefined
|
|
732
|
+
? { size: { width: width ?? existing.size.width, height: height ?? existing.size.height } }
|
|
733
|
+
: {}),
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function parseGraphPayloadData(value: unknown): Array<Record<string, unknown>> | null {
|
|
738
|
+
if (!Array.isArray(value)) return null;
|
|
739
|
+
if (value.some((item) => !isRecord(item))) return null;
|
|
740
|
+
return value as Array<Record<string, unknown>>;
|
|
741
|
+
}
|
|
742
|
+
|
|
654
743
|
function getExtAppNodeCheckpointId(node: CanvasNodeState): string {
|
|
655
744
|
const appCheckpoint = isRecord(node.data.appCheckpoint) ? node.data.appCheckpoint : null;
|
|
656
745
|
const storedCheckpointId = appCheckpoint?.id;
|
|
@@ -1130,9 +1219,11 @@ async function handleCanvasImage(pathname: string): Promise<Response> {
|
|
|
1130
1219
|
const VALID_NODE_TYPES = new Set(['markdown', 'status', 'context', 'ledger', 'trace', 'file', 'image', 'mcp-app', 'webpage', 'group']);
|
|
1131
1220
|
|
|
1132
1221
|
function buildNodeResponse(node: CanvasNodeState): Record<string, unknown> {
|
|
1222
|
+
const serialized = serializeCanvasNode(node);
|
|
1133
1223
|
return {
|
|
1134
1224
|
ok: true,
|
|
1135
|
-
|
|
1225
|
+
node: serialized,
|
|
1226
|
+
...serialized,
|
|
1136
1227
|
};
|
|
1137
1228
|
}
|
|
1138
1229
|
|
|
@@ -1153,15 +1244,15 @@ async function createCanvasWebpageNode(body: Record<string, unknown>): Promise<R
|
|
|
1153
1244
|
const extraData = body.data && typeof body.data === 'object' && !Array.isArray(body.data)
|
|
1154
1245
|
? body.data as Record<string, unknown>
|
|
1155
1246
|
: undefined;
|
|
1247
|
+
const geometry = resolveCreateGeometry(body);
|
|
1156
1248
|
const { id, node } = addCanvasNode({
|
|
1157
1249
|
type: 'webpage',
|
|
1158
1250
|
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1159
1251
|
content: normalizedUrl,
|
|
1160
1252
|
...(extraData ? { data: extraData } : {}),
|
|
1161
|
-
...
|
|
1162
|
-
...(
|
|
1163
|
-
...(
|
|
1164
|
-
...(typeof body.height === 'number' ? { height: body.height } : { height: WEBPAGE_NODE_DEFAULT_SIZE.height }),
|
|
1253
|
+
...geometry,
|
|
1254
|
+
...(geometry.width === undefined ? { width: WEBPAGE_NODE_DEFAULT_SIZE.width } : {}),
|
|
1255
|
+
...(geometry.height === undefined ? { height: WEBPAGE_NODE_DEFAULT_SIZE.height } : {}),
|
|
1165
1256
|
});
|
|
1166
1257
|
|
|
1167
1258
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
@@ -1210,17 +1301,18 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1210
1301
|
const extraData = body.data && typeof body.data === 'object' && !Array.isArray(body.data)
|
|
1211
1302
|
? body.data as Record<string, unknown>
|
|
1212
1303
|
: undefined;
|
|
1304
|
+
const content = type === 'image' && typeof body.path === 'string' && typeof body.content !== 'string'
|
|
1305
|
+
? body.path
|
|
1306
|
+
: body.content;
|
|
1213
1307
|
let added: ReturnType<typeof addCanvasNode>;
|
|
1308
|
+
const geometry = resolveCreateGeometry(body);
|
|
1214
1309
|
try {
|
|
1215
1310
|
added = addCanvasNode({
|
|
1216
1311
|
type: type as CanvasNodeState['type'],
|
|
1217
1312
|
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1218
|
-
...(typeof
|
|
1313
|
+
...(typeof content === 'string' ? { content } : {}),
|
|
1219
1314
|
...(extraData ? { data: extraData } : {}),
|
|
1220
|
-
...
|
|
1221
|
-
...(typeof body.y === 'number' ? { y: body.y } : {}),
|
|
1222
|
-
...(typeof body.width === 'number' ? { width: body.width } : {}),
|
|
1223
|
-
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
1315
|
+
...geometry,
|
|
1224
1316
|
defaultWidth: 360,
|
|
1225
1317
|
defaultHeight: 200,
|
|
1226
1318
|
fileMode: 'auto',
|
|
@@ -1371,15 +1463,19 @@ async function handleCanvasUpdateNode(nodeId: string, req: Request): Promise<Res
|
|
|
1371
1463
|
if (existing.type === 'webpage' && body.refresh === true) {
|
|
1372
1464
|
return handleCanvasRefreshWebpageNode(nodeId, req);
|
|
1373
1465
|
}
|
|
1374
|
-
const patch: Record<string, unknown> =
|
|
1375
|
-
if (body.position) patch.position = body.position;
|
|
1376
|
-
if (body.size) patch.size = body.size;
|
|
1466
|
+
const patch: Record<string, unknown> = resolvePatchGeometry(body, existing);
|
|
1377
1467
|
if (body.collapsed !== undefined) patch.collapsed = body.collapsed;
|
|
1378
1468
|
if (body.pinned !== undefined) patch.pinned = Boolean(body.pinned);
|
|
1379
1469
|
if (body.dockPosition === null || body.dockPosition === 'left' || body.dockPosition === 'right') {
|
|
1380
1470
|
patch.dockPosition = body.dockPosition;
|
|
1381
1471
|
}
|
|
1382
|
-
if (body
|
|
1472
|
+
if (hasStructuredNodeUpdateFields(body)) {
|
|
1473
|
+
try {
|
|
1474
|
+
patch.data = buildStructuredNodeUpdate(existing, body).data;
|
|
1475
|
+
} catch (error) {
|
|
1476
|
+
return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
1477
|
+
}
|
|
1478
|
+
} else if (body.title !== undefined || body.content !== undefined || body.data || typeof body.arrangeLocked === 'boolean') {
|
|
1383
1479
|
const data = { ...existing.data };
|
|
1384
1480
|
if (body.title !== undefined) {
|
|
1385
1481
|
data.title = String(body.title);
|
|
@@ -1416,7 +1512,8 @@ async function handleCanvasUpdateNode(nodeId: string, req: Request): Promise<Res
|
|
|
1416
1512
|
if (error) return responseJson({ ok: false, error }, 400);
|
|
1417
1513
|
canvasState.updateNode(nodeId, patch as Partial<CanvasNodeState>);
|
|
1418
1514
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1419
|
-
|
|
1515
|
+
const updated = canvasState.getNode(nodeId);
|
|
1516
|
+
return responseJson(updated ? buildNodeResponse(updated) : { ok: true, id: nodeId });
|
|
1420
1517
|
}
|
|
1421
1518
|
|
|
1422
1519
|
// ── Arrange nodes ───────────────────────────────────────────
|
|
@@ -1457,6 +1554,22 @@ async function handleCanvasFocus(req: Request): Promise<Response> {
|
|
|
1457
1554
|
return responseJson({ ok: true, focused: nodeId, panned: !noPan });
|
|
1458
1555
|
}
|
|
1459
1556
|
|
|
1557
|
+
async function handleCanvasFit(req: Request): Promise<Response> {
|
|
1558
|
+
const body = await readJson(req);
|
|
1559
|
+
const nodeIds = Array.isArray(body.nodeIds)
|
|
1560
|
+
? body.nodeIds.filter((id): id is string => typeof id === 'string')
|
|
1561
|
+
: undefined;
|
|
1562
|
+
const result = fitCanvasView({
|
|
1563
|
+
...(typeof body.width === 'number' ? { width: body.width } : {}),
|
|
1564
|
+
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
1565
|
+
...(typeof body.padding === 'number' ? { padding: body.padding } : {}),
|
|
1566
|
+
...(typeof body.maxScale === 'number' ? { maxScale: body.maxScale } : {}),
|
|
1567
|
+
...(nodeIds ? { nodeIds } : {}),
|
|
1568
|
+
});
|
|
1569
|
+
emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: result.viewport });
|
|
1570
|
+
return responseJson(result);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1460
1573
|
async function handleCanvasBuildWebArtifact(req: Request): Promise<Response> {
|
|
1461
1574
|
const body = await readJson(req);
|
|
1462
1575
|
const title = typeof body.title === 'string' ? body.title.trim() : '';
|
|
@@ -1549,9 +1662,7 @@ async function handleCanvasValidateSpec(req: Request): Promise<Response> {
|
|
|
1549
1662
|
}));
|
|
1550
1663
|
}
|
|
1551
1664
|
|
|
1552
|
-
const data =
|
|
1553
|
-
? body.data.filter((item: unknown) => item && typeof item === 'object') as Array<Record<string, unknown>>
|
|
1554
|
-
: null;
|
|
1665
|
+
const data = parseGraphPayloadData(body.data);
|
|
1555
1666
|
if (!data) {
|
|
1556
1667
|
return responseJson({ ok: false, error: 'Graph validation requires a data array.' }, 400);
|
|
1557
1668
|
}
|
|
@@ -1603,21 +1714,16 @@ async function handleCanvasAddJsonRender(req: Request): Promise<Response> {
|
|
|
1603
1714
|
const title = typeof body.title === 'string' ? body.title.trim() : '';
|
|
1604
1715
|
const rawSpec =
|
|
1605
1716
|
body.spec && typeof body.spec === 'object' && !Array.isArray(body.spec) ? body.spec : body;
|
|
1606
|
-
|
|
1607
|
-
return responseJson({ ok: false, error: 'Missing required field: title.' }, 400);
|
|
1608
|
-
}
|
|
1717
|
+
const geometry = resolveCreateGeometry(body);
|
|
1609
1718
|
|
|
1610
1719
|
try {
|
|
1611
1720
|
const result = createCanvasJsonRenderNode({
|
|
1612
|
-
title,
|
|
1721
|
+
...(title ? { title } : {}),
|
|
1613
1722
|
spec: rawSpec,
|
|
1614
|
-
...
|
|
1615
|
-
...(typeof body.y === 'number' ? { y: body.y } : {}),
|
|
1616
|
-
...(typeof body.width === 'number' ? { width: body.width } : {}),
|
|
1617
|
-
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
1723
|
+
...geometry,
|
|
1618
1724
|
});
|
|
1619
1725
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1620
|
-
return responseJson({
|
|
1726
|
+
return responseJson({ ...buildNodeResponse(result.node), url: result.url, spec: result.spec });
|
|
1621
1727
|
} catch (error) {
|
|
1622
1728
|
const message = error instanceof Error ? error.message : String(error);
|
|
1623
1729
|
return responseJson({ ok: false, error: message }, 400);
|
|
@@ -1628,9 +1734,7 @@ async function handleCanvasAddGraph(req: Request): Promise<Response> {
|
|
|
1628
1734
|
const body = await readJson(req);
|
|
1629
1735
|
const title = typeof body.title === 'string' && body.title.trim() ? body.title.trim() : 'Graph';
|
|
1630
1736
|
const graphType = typeof body.graphType === 'string' ? body.graphType : typeof body.type === 'string' ? body.type : 'line';
|
|
1631
|
-
const data =
|
|
1632
|
-
? body.data.filter((item: unknown) => item && typeof item === 'object') as Array<Record<string, unknown>>
|
|
1633
|
-
: null;
|
|
1737
|
+
const data = parseGraphPayloadData(body.data);
|
|
1634
1738
|
if (!data) {
|
|
1635
1739
|
return responseJson({ ok: false, error: 'Missing required field: data.' }, 400);
|
|
1636
1740
|
}
|
|
@@ -1646,6 +1750,12 @@ async function handleCanvasAddGraph(req: Request): Promise<Response> {
|
|
|
1646
1750
|
const series = Array.isArray(body.series)
|
|
1647
1751
|
? body.series.filter((s: unknown): s is string => typeof s === 'string')
|
|
1648
1752
|
: null;
|
|
1753
|
+
const position = getRecord(body.position);
|
|
1754
|
+
const size = getRecord(body.size);
|
|
1755
|
+
const x = pickFiniteNumber(body, 'x') ?? (position ? pickFiniteNumber(position, 'x') : undefined);
|
|
1756
|
+
const y = pickFiniteNumber(body, 'y') ?? (position ? pickFiniteNumber(position, 'y') : undefined);
|
|
1757
|
+
const width = pickPositiveNumber(body, 'width') ?? (size ? pickPositiveNumber(size, 'width') : undefined);
|
|
1758
|
+
const nodeHeight = pickPositiveNumber(body, 'nodeHeight') ?? (size ? pickPositiveNumber(size, 'height') : undefined);
|
|
1649
1759
|
const result = createCanvasGraphNode({
|
|
1650
1760
|
title,
|
|
1651
1761
|
graphType,
|
|
@@ -1665,13 +1775,13 @@ async function handleCanvasAddGraph(req: Request): Promise<Response> {
|
|
|
1665
1775
|
...(typeof body.barColor === 'string' ? { barColor: body.barColor } : {}),
|
|
1666
1776
|
...(typeof body.lineColor === 'string' ? { lineColor: body.lineColor } : {}),
|
|
1667
1777
|
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
1668
|
-
...(
|
|
1669
|
-
...(
|
|
1670
|
-
...(
|
|
1671
|
-
...(
|
|
1778
|
+
...(x !== undefined ? { x } : {}),
|
|
1779
|
+
...(y !== undefined ? { y } : {}),
|
|
1780
|
+
...(width !== undefined ? { width } : {}),
|
|
1781
|
+
...(nodeHeight !== undefined ? { heightPx: nodeHeight } : {}),
|
|
1672
1782
|
});
|
|
1673
1783
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1674
|
-
return responseJson({
|
|
1784
|
+
return responseJson({ ...buildNodeResponse(result.node), url: result.url, spec: result.spec });
|
|
1675
1785
|
} catch (error) {
|
|
1676
1786
|
const message = error instanceof Error ? error.message : String(error);
|
|
1677
1787
|
return responseJson({ ok: false, error: message }, 400);
|
|
@@ -3880,6 +3990,10 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3880
3990
|
return handleCanvasFocus(req);
|
|
3881
3991
|
}
|
|
3882
3992
|
|
|
3993
|
+
if (url.pathname === '/api/canvas/fit' && req.method === 'POST') {
|
|
3994
|
+
return handleCanvasFit(req);
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3883
3997
|
if (url.pathname === '/api/canvas/clear' && req.method === 'POST') {
|
|
3884
3998
|
for (const node of canvasState.getLayout().nodes) {
|
|
3885
3999
|
closeNodeAppSession(node);
|