@principal-ai/principal-view-react 0.6.16 → 0.6.17
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/dist/components/GraphRenderer.d.ts +10 -0
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +120 -21
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/SelectionSidebar.d.ts +18 -0
- package/dist/components/SelectionSidebar.d.ts.map +1 -0
- package/dist/components/SelectionSidebar.js +183 -0
- package/dist/components/SelectionSidebar.js.map +1 -0
- package/dist/nodes/CustomNode.d.ts.map +1 -1
- package/dist/nodes/CustomNode.js +28 -11
- package/dist/nodes/CustomNode.js.map +1 -1
- package/package.json +1 -1
- package/src/components/GraphRenderer.tsx +167 -23
- package/src/components/PendingChanges.test.tsx +433 -0
- package/src/components/SelectionSidebar.tsx +341 -0
- package/src/nodes/CustomNode.tsx +43 -11
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
} from '../utils/graphConverter';
|
|
46
46
|
import { EdgeInfoPanel } from './EdgeInfoPanel';
|
|
47
47
|
import { NodeInfoPanel } from './NodeInfoPanel';
|
|
48
|
+
import { SelectionSidebar } from './SelectionSidebar';
|
|
48
49
|
|
|
49
50
|
/** Position change event for tracking node movements */
|
|
50
51
|
export interface NodePositionChange {
|
|
@@ -52,10 +53,18 @@ export interface NodePositionChange {
|
|
|
52
53
|
position: { x: number; y: number };
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
/** Dimension change event for tracking node resizing */
|
|
57
|
+
export interface NodeDimensionChange {
|
|
58
|
+
nodeId: string;
|
|
59
|
+
dimensions: { width: number; height: number };
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
/** All pending changes that can be saved */
|
|
56
63
|
export interface PendingChanges {
|
|
57
64
|
/** Node position changes */
|
|
58
65
|
positionChanges: NodePositionChange[];
|
|
66
|
+
/** Node dimension changes (from resizing) */
|
|
67
|
+
dimensionChanges: NodeDimensionChange[];
|
|
59
68
|
/** Node updates (type, data changes) */
|
|
60
69
|
nodeUpdates: Array<{
|
|
61
70
|
nodeId: string;
|
|
@@ -184,6 +193,7 @@ interface AnimationState {
|
|
|
184
193
|
// Internal edit state tracking
|
|
185
194
|
interface EditState {
|
|
186
195
|
positionChanges: Map<string, { x: number; y: number }>;
|
|
196
|
+
dimensionChanges: Map<string, { width: number; height: number }>;
|
|
187
197
|
nodeUpdates: Map<string, { type?: string; data?: Record<string, unknown> }>;
|
|
188
198
|
deletedNodeIds: Set<string>;
|
|
189
199
|
createdEdges: Array<{
|
|
@@ -199,6 +209,7 @@ interface EditState {
|
|
|
199
209
|
|
|
200
210
|
const createEmptyEditState = (): EditState => ({
|
|
201
211
|
positionChanges: new Map(),
|
|
212
|
+
dimensionChanges: new Map(),
|
|
202
213
|
nodeUpdates: new Map(),
|
|
203
214
|
deletedNodeIds: new Set(),
|
|
204
215
|
createdEdges: [],
|
|
@@ -253,11 +264,11 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
253
264
|
edgeAnimations: {},
|
|
254
265
|
});
|
|
255
266
|
|
|
256
|
-
// Track selected
|
|
257
|
-
const [
|
|
267
|
+
// Track selected edges for info panel (supports multi-select)
|
|
268
|
+
const [selectedEdgeIds, setSelectedEdgeIds] = useState<Set<string>>(new Set());
|
|
258
269
|
|
|
259
|
-
// Track selected
|
|
260
|
-
const [
|
|
270
|
+
// Track selected nodes for info panel (supports multi-select)
|
|
271
|
+
const [selectedNodeIds, setSelectedNodeIds] = useState<Set<string>>(new Set());
|
|
261
272
|
|
|
262
273
|
// Track pending connection for edge type picker
|
|
263
274
|
const [pendingConnection, setPendingConnection] = useState<{
|
|
@@ -323,6 +334,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
323
334
|
const checkHasChanges = useCallback((state: EditState): boolean => {
|
|
324
335
|
return (
|
|
325
336
|
state.positionChanges.size > 0 ||
|
|
337
|
+
state.dimensionChanges.size > 0 ||
|
|
326
338
|
state.nodeUpdates.size > 0 ||
|
|
327
339
|
state.deletedNodeIds.size > 0 ||
|
|
328
340
|
state.createdEdges.length > 0 ||
|
|
@@ -345,21 +357,65 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
345
357
|
// EVENT HANDLERS
|
|
346
358
|
// ============================================
|
|
347
359
|
|
|
348
|
-
// Handle edge click (toggle selection)
|
|
349
|
-
const onEdgeClick = useCallback(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
360
|
+
// Handle edge click (toggle selection, supports Shift for multi-select)
|
|
361
|
+
const onEdgeClick = useCallback(
|
|
362
|
+
(event: React.MouseEvent, edge: Edge) => {
|
|
363
|
+
if (event.shiftKey && editable) {
|
|
364
|
+
// Shift+click: toggle edge in selection
|
|
365
|
+
setSelectedEdgeIds((prev) => {
|
|
366
|
+
const next = new Set(prev);
|
|
367
|
+
if (next.has(edge.id)) {
|
|
368
|
+
next.delete(edge.id);
|
|
369
|
+
} else {
|
|
370
|
+
next.add(edge.id);
|
|
371
|
+
}
|
|
372
|
+
return next;
|
|
373
|
+
});
|
|
374
|
+
} else {
|
|
375
|
+
// Regular click: single select (replace selection)
|
|
376
|
+
setSelectedEdgeIds((prev) => {
|
|
377
|
+
if (prev.size === 1 && prev.has(edge.id)) {
|
|
378
|
+
return new Set(); // Deselect if clicking same edge
|
|
379
|
+
}
|
|
380
|
+
return new Set([edge.id]);
|
|
381
|
+
});
|
|
382
|
+
setSelectedNodeIds(new Set());
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
[editable]
|
|
386
|
+
);
|
|
353
387
|
|
|
354
|
-
// Handle node click (toggle selection)
|
|
355
|
-
const onNodeClick = useCallback(
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
388
|
+
// Handle node click (toggle selection, supports Shift for multi-select)
|
|
389
|
+
const onNodeClick = useCallback(
|
|
390
|
+
(event: React.MouseEvent, node: Node) => {
|
|
391
|
+
if (event.shiftKey && editable) {
|
|
392
|
+
// Shift+click: toggle node in selection
|
|
393
|
+
setSelectedNodeIds((prev) => {
|
|
394
|
+
const next = new Set(prev);
|
|
395
|
+
if (next.has(node.id)) {
|
|
396
|
+
next.delete(node.id);
|
|
397
|
+
} else {
|
|
398
|
+
next.add(node.id);
|
|
399
|
+
}
|
|
400
|
+
return next;
|
|
401
|
+
});
|
|
402
|
+
} else {
|
|
403
|
+
// Regular click: single select (replace selection)
|
|
404
|
+
setSelectedNodeIds((prev) => {
|
|
405
|
+
if (prev.size === 1 && prev.has(node.id)) {
|
|
406
|
+
return new Set(); // Deselect if clicking same node
|
|
407
|
+
}
|
|
408
|
+
return new Set([node.id]);
|
|
409
|
+
});
|
|
410
|
+
setSelectedEdgeIds(new Set());
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
[editable]
|
|
414
|
+
);
|
|
359
415
|
|
|
360
416
|
// Handle close edge info panel
|
|
361
417
|
const onCloseEdgeInfoPanel = useCallback(() => {
|
|
362
|
-
|
|
418
|
+
setSelectedEdgeIds(new Set());
|
|
363
419
|
}, []);
|
|
364
420
|
|
|
365
421
|
// Handle edge side updates from EdgeInfoPanel
|
|
@@ -385,9 +441,26 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
385
441
|
|
|
386
442
|
// Handle close node info panel
|
|
387
443
|
const onCloseNodeInfoPanel = useCallback(() => {
|
|
388
|
-
|
|
444
|
+
setSelectedNodeIds(new Set());
|
|
445
|
+
}, []);
|
|
446
|
+
|
|
447
|
+
// Handle pane click (clear selection when clicking empty space)
|
|
448
|
+
const onPaneClick = useCallback(() => {
|
|
449
|
+
setSelectedNodeIds(new Set());
|
|
450
|
+
setSelectedEdgeIds(new Set());
|
|
389
451
|
}, []);
|
|
390
452
|
|
|
453
|
+
// Handle selection change from ReactFlow (box selection)
|
|
454
|
+
const handleSelectionChange = useCallback(
|
|
455
|
+
({ nodes: selectedNodes, edges: selectedEdges }: { nodes: Node[]; edges: Edge[] }) => {
|
|
456
|
+
if (editable) {
|
|
457
|
+
setSelectedNodeIds(new Set(selectedNodes.map((n) => n.id)));
|
|
458
|
+
setSelectedEdgeIds(new Set(selectedEdges.map((e) => e.id)));
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
[editable]
|
|
462
|
+
);
|
|
463
|
+
|
|
391
464
|
// Handle node update (internal - updates local state only)
|
|
392
465
|
const handleNodeUpdate = useCallback(
|
|
393
466
|
(nodeId: string, updates: { type?: string; data?: Record<string, unknown> }) => {
|
|
@@ -440,6 +513,9 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
440
513
|
// Remove any position changes for this node
|
|
441
514
|
const newPositions = new Map(prev.positionChanges);
|
|
442
515
|
newPositions.delete(nodeId);
|
|
516
|
+
// Remove any dimension changes for this node
|
|
517
|
+
const newDimensions = new Map(prev.dimensionChanges);
|
|
518
|
+
newDimensions.delete(nodeId);
|
|
443
519
|
// Remove created edges that involve this node
|
|
444
520
|
const newCreatedEdges = prev.createdEdges.filter(
|
|
445
521
|
(e) => e.from !== nodeId && e.to !== nodeId
|
|
@@ -449,11 +525,12 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
449
525
|
deletedNodeIds: newDeletedNodes,
|
|
450
526
|
nodeUpdates: newUpdates,
|
|
451
527
|
positionChanges: newPositions,
|
|
528
|
+
dimensionChanges: newDimensions,
|
|
452
529
|
createdEdges: newCreatedEdges,
|
|
453
530
|
};
|
|
454
531
|
});
|
|
455
532
|
|
|
456
|
-
|
|
533
|
+
setSelectedNodeIds(new Set());
|
|
457
534
|
},
|
|
458
535
|
[editable, updateEditState]
|
|
459
536
|
);
|
|
@@ -495,7 +572,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
495
572
|
return prev;
|
|
496
573
|
});
|
|
497
574
|
|
|
498
|
-
|
|
575
|
+
setSelectedEdgeIds(new Set());
|
|
499
576
|
},
|
|
500
577
|
[editable, updateEditState, localEdges]
|
|
501
578
|
);
|
|
@@ -713,6 +790,12 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
713
790
|
// SELECTED ITEMS
|
|
714
791
|
// ============================================
|
|
715
792
|
|
|
793
|
+
// Get first selected edge (for single-selection info panel)
|
|
794
|
+
const selectedEdgeId = useMemo(() => {
|
|
795
|
+
if (selectedEdgeIds.size === 0) return null;
|
|
796
|
+
return selectedEdgeIds.values().next().value;
|
|
797
|
+
}, [selectedEdgeIds]);
|
|
798
|
+
|
|
716
799
|
const selectedEdge = useMemo(() => {
|
|
717
800
|
if (!selectedEdgeId) return null;
|
|
718
801
|
return edges.find((e) => e.id === selectedEdgeId);
|
|
@@ -723,6 +806,12 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
723
806
|
return configuration.edgeTypes[selectedEdge.type];
|
|
724
807
|
}, [selectedEdge, configuration.edgeTypes]);
|
|
725
808
|
|
|
809
|
+
// Get first selected node (for single-selection info panel)
|
|
810
|
+
const selectedNodeId = useMemo(() => {
|
|
811
|
+
if (selectedNodeIds.size === 0) return null;
|
|
812
|
+
return selectedNodeIds.values().next().value;
|
|
813
|
+
}, [selectedNodeIds]);
|
|
814
|
+
|
|
726
815
|
const selectedNode = useMemo(() => {
|
|
727
816
|
if (!selectedNodeId) return null;
|
|
728
817
|
return nodes.find((n) => n.id === selectedNodeId);
|
|
@@ -907,7 +996,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
907
996
|
|
|
908
997
|
const xyflowNodes = editable ? xyflowLocalNodes : xyflowNodesBase;
|
|
909
998
|
|
|
910
|
-
// Handle node changes (drag events)
|
|
999
|
+
// Handle node changes (drag and resize events)
|
|
911
1000
|
const handleNodesChange = useCallback(
|
|
912
1001
|
(changes: NodeChange[]) => {
|
|
913
1002
|
if (!editable) return;
|
|
@@ -930,6 +1019,37 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
930
1019
|
change.dragging === false
|
|
931
1020
|
);
|
|
932
1021
|
|
|
1022
|
+
// Track dimension changes (from NodeResizer)
|
|
1023
|
+
const dimensionChanges = changes.filter(
|
|
1024
|
+
(
|
|
1025
|
+
change
|
|
1026
|
+
): change is NodeChange & {
|
|
1027
|
+
type: 'dimensions';
|
|
1028
|
+
dimensions: { width: number; height: number };
|
|
1029
|
+
resizing: boolean;
|
|
1030
|
+
} =>
|
|
1031
|
+
change.type === 'dimensions' &&
|
|
1032
|
+
'dimensions' in change &&
|
|
1033
|
+
change.dimensions !== undefined &&
|
|
1034
|
+
'resizing' in change &&
|
|
1035
|
+
change.resizing === false
|
|
1036
|
+
);
|
|
1037
|
+
|
|
1038
|
+
if (dimensionChanges.length > 0) {
|
|
1039
|
+
updateEditState((prev) => {
|
|
1040
|
+
const newDimensions = new Map(prev.dimensionChanges);
|
|
1041
|
+
for (const change of dimensionChanges) {
|
|
1042
|
+
if (change.dimensions) {
|
|
1043
|
+
newDimensions.set(change.id, {
|
|
1044
|
+
width: Math.round(change.dimensions.width),
|
|
1045
|
+
height: Math.round(change.dimensions.height),
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return { ...prev, dimensionChanges: newDimensions };
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
|
|
933
1053
|
if (positionChanges.length > 0) {
|
|
934
1054
|
updateEditState((prev) => {
|
|
935
1055
|
const newPositions = new Map(prev.positionChanges);
|
|
@@ -1063,8 +1183,12 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
1063
1183
|
onReconnectStart={handleReconnectStart}
|
|
1064
1184
|
onReconnect={handleReconnect}
|
|
1065
1185
|
onReconnectEnd={handleReconnectEnd}
|
|
1066
|
-
|
|
1067
|
-
|
|
1186
|
+
onPaneClick={onPaneClick}
|
|
1187
|
+
onSelectionChange={handleSelectionChange}
|
|
1188
|
+
panOnDrag={!editable}
|
|
1189
|
+
selectionOnDrag={editable}
|
|
1190
|
+
selectionKeyCode={null}
|
|
1191
|
+
multiSelectionKeyCode="Shift"
|
|
1068
1192
|
>
|
|
1069
1193
|
{showBackground && <Background color={theme.colors.border} gap={16} size={1} />}
|
|
1070
1194
|
{showControls && <Controls showZoom showFitView showInteractive />}
|
|
@@ -1081,7 +1205,18 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
1081
1205
|
)}
|
|
1082
1206
|
</ReactFlow>
|
|
1083
1207
|
|
|
1084
|
-
{
|
|
1208
|
+
{/* Multi-selection sidebar - shown when 2+ nodes are selected */}
|
|
1209
|
+
{selectedNodeIds.size >= 2 && (
|
|
1210
|
+
<SelectionSidebar
|
|
1211
|
+
selectedNodeIds={selectedNodeIds}
|
|
1212
|
+
nodes={nodes}
|
|
1213
|
+
nodeTypeDefinitions={configuration.nodeTypes}
|
|
1214
|
+
onClose={onCloseNodeInfoPanel}
|
|
1215
|
+
/>
|
|
1216
|
+
)}
|
|
1217
|
+
|
|
1218
|
+
{/* Single edge info panel - hidden when multiple edges selected */}
|
|
1219
|
+
{selectedEdgeIds.size === 1 && selectedEdge && selectedEdgeTypeDefinition && (
|
|
1085
1220
|
<EdgeInfoPanel
|
|
1086
1221
|
edge={selectedEdge}
|
|
1087
1222
|
typeDefinition={selectedEdgeTypeDefinition}
|
|
@@ -1093,7 +1228,8 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
1093
1228
|
/>
|
|
1094
1229
|
)}
|
|
1095
1230
|
|
|
1096
|
-
{
|
|
1231
|
+
{/* Single node info panel - hidden when multiple nodes selected */}
|
|
1232
|
+
{selectedNodeIds.size === 1 && selectedNode && selectedNodeTypeDefinition && (
|
|
1097
1233
|
<NodeInfoPanel
|
|
1098
1234
|
node={selectedNode}
|
|
1099
1235
|
typeDefinition={selectedNodeTypeDefinition}
|
|
@@ -1382,6 +1518,12 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
|
|
|
1382
1518
|
position,
|
|
1383
1519
|
})
|
|
1384
1520
|
),
|
|
1521
|
+
dimensionChanges: Array.from(state.dimensionChanges.entries()).map(
|
|
1522
|
+
([nodeId, dimensions]) => ({
|
|
1523
|
+
nodeId,
|
|
1524
|
+
dimensions,
|
|
1525
|
+
})
|
|
1526
|
+
),
|
|
1385
1527
|
nodeUpdates: Array.from(state.nodeUpdates.entries()).map(([nodeId, updates]) => ({
|
|
1386
1528
|
nodeId,
|
|
1387
1529
|
updates,
|
|
@@ -1397,6 +1539,7 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
|
|
|
1397
1539
|
deletedEdges: state.deletedEdges.map((e) => ({ from: e.from, to: e.to, type: e.type })),
|
|
1398
1540
|
hasChanges:
|
|
1399
1541
|
state.positionChanges.size > 0 ||
|
|
1542
|
+
state.dimensionChanges.size > 0 ||
|
|
1400
1543
|
state.nodeUpdates.size > 0 ||
|
|
1401
1544
|
state.deletedNodeIds.size > 0 ||
|
|
1402
1545
|
state.createdEdges.length > 0 ||
|
|
@@ -1410,6 +1553,7 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
|
|
|
1410
1553
|
const state = editStateRef.current;
|
|
1411
1554
|
return (
|
|
1412
1555
|
state.positionChanges.size > 0 ||
|
|
1556
|
+
state.dimensionChanges.size > 0 ||
|
|
1413
1557
|
state.nodeUpdates.size > 0 ||
|
|
1414
1558
|
state.deletedNodeIds.size > 0 ||
|
|
1415
1559
|
state.createdEdges.length > 0 ||
|