@petrarca/sonnet-graph 0.4.2 → 0.5.0
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/index.d.ts +1070 -8
- package/dist/index.js +3317 -278
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -78,6 +78,7 @@ function extractDisplayLabel(item, displayConfig) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// src/graph/enrichment.ts
|
|
81
|
+
var SELECTION_COLOR = "#2d64d2";
|
|
81
82
|
var BASE_COLORS = [
|
|
82
83
|
"#f94144",
|
|
83
84
|
"#118ab2",
|
|
@@ -399,6 +400,8 @@ function createGraphFilterStore() {
|
|
|
399
400
|
const now = !get().allLabelsDisabled;
|
|
400
401
|
set({ allLabelsDisabled: now, nodeLabelFilters: [] });
|
|
401
402
|
},
|
|
403
|
+
setAllLabelsDisabled: (disabled) => set({ allLabelsDisabled: disabled }),
|
|
404
|
+
setNodeFilterState: (disabled, filters) => set({ allLabelsDisabled: disabled, nodeLabelFilters: filters }),
|
|
402
405
|
toggleEdgeLabelFilter: (label, allAvailableLabels, edgeToNodeLabels) => {
|
|
403
406
|
const getNodeLabelUpdate = (edgeLabel) => {
|
|
404
407
|
if (!edgeToNodeLabels) return null;
|
|
@@ -449,12 +452,12 @@ function collectReachable(startId, direction, allEdges, includeStart) {
|
|
|
449
452
|
for (const edge of allEdges) {
|
|
450
453
|
const srcId = String(edge.start_id_);
|
|
451
454
|
const tgtId = String(edge.end_id_);
|
|
452
|
-
const
|
|
455
|
+
const edgeId2 = String(edge.id_);
|
|
453
456
|
if (direction === "in" && tgtId === currentId) {
|
|
454
|
-
edges.add(
|
|
457
|
+
edges.add(edgeId2);
|
|
455
458
|
traverse(srcId, false);
|
|
456
459
|
} else if (direction === "out" && srcId === currentId) {
|
|
457
|
-
edges.add(
|
|
460
|
+
edges.add(edgeId2);
|
|
458
461
|
traverse(tgtId, false);
|
|
459
462
|
}
|
|
460
463
|
}
|
|
@@ -494,24 +497,24 @@ function buildFocusSet(referenceNodeId, direction, edgeLabel, allEdges) {
|
|
|
494
497
|
}
|
|
495
498
|
return focusNodeIds;
|
|
496
499
|
}
|
|
497
|
-
function handleExistingNode(
|
|
498
|
-
const existingNode = existingNodes.find((n) => n.id_ ===
|
|
500
|
+
function handleExistingNode(nodeId2, nodeIdStr, existingNodes, autoFocus, graphFocusStore) {
|
|
501
|
+
const existingNode = existingNodes.find((n) => n.id_ === nodeId2);
|
|
499
502
|
if (!existingNode) return false;
|
|
500
503
|
if (autoFocus) {
|
|
501
|
-
devLog3("[GraphExploration] Node already in graph, focusing", { nodeId });
|
|
504
|
+
devLog3("[GraphExploration] Node already in graph, focusing", { nodeId: nodeId2 });
|
|
502
505
|
graphFocusStore.getState().requestFocus(nodeIdStr, "node", false);
|
|
503
506
|
} else {
|
|
504
507
|
devLog3("[GraphExploration] Node already in graph, skipping focus", {
|
|
505
|
-
nodeId
|
|
508
|
+
nodeId: nodeId2
|
|
506
509
|
});
|
|
507
510
|
}
|
|
508
511
|
return true;
|
|
509
512
|
}
|
|
510
|
-
async function fetchAndEnrichNode(
|
|
511
|
-
devLog3("[GraphExploration] Fetching node from backend", { nodeId });
|
|
512
|
-
const result = await stores.getNodes([
|
|
513
|
+
async function fetchAndEnrichNode(nodeId2, existingNodes, stores) {
|
|
514
|
+
devLog3("[GraphExploration] Fetching node from backend", { nodeId: nodeId2 });
|
|
515
|
+
const result = await stores.getNodes([nodeId2]);
|
|
513
516
|
if (result.length === 0) {
|
|
514
|
-
throw new Error(`Node ${
|
|
517
|
+
throw new Error(`Node ${nodeId2} not found`);
|
|
515
518
|
}
|
|
516
519
|
const colorPalette = buildColorPaletteFromNodes([
|
|
517
520
|
...existingNodes,
|
|
@@ -523,27 +526,27 @@ async function fetchAndEnrichNode(nodeId, existingNodes, stores) {
|
|
|
523
526
|
stores.getLabelDisplayMap?.()
|
|
524
527
|
);
|
|
525
528
|
devLog3("[GraphExploration] Enriched nodes", {
|
|
526
|
-
nodeId,
|
|
529
|
+
nodeId: nodeId2,
|
|
527
530
|
enrichedCount: enrichedNodes.length
|
|
528
531
|
});
|
|
529
532
|
stores.graphData.getState().mergeExpandedData(enrichedNodes, []);
|
|
530
533
|
return enrichedNodes;
|
|
531
534
|
}
|
|
532
|
-
function ensureNodeLabelInFilters(enrichedNodes,
|
|
535
|
+
function ensureNodeLabelInFilters(enrichedNodes, nodeId2, graphFilterStore) {
|
|
533
536
|
const addedNode = enrichedNodes[0];
|
|
534
537
|
if (!addedNode) return;
|
|
535
538
|
const { nodeLabelFilters: currentFilters } = graphFilterStore.getState();
|
|
536
539
|
const nodeLabel = addedNode.label_;
|
|
537
540
|
if (currentFilters.length > 0 && !currentFilters.includes(nodeLabel)) {
|
|
538
541
|
devLog3("[GraphExploration] Adding node label to active filters", {
|
|
539
|
-
nodeId,
|
|
542
|
+
nodeId: nodeId2,
|
|
540
543
|
nodeLabel,
|
|
541
544
|
currentFilters
|
|
542
545
|
});
|
|
543
546
|
graphFilterStore.getState().setNodeLabelFilters([...currentFilters, nodeLabel]);
|
|
544
547
|
}
|
|
545
548
|
}
|
|
546
|
-
function mergeExpandedLabelsIntoFilters(stores, enrichedNodes,
|
|
549
|
+
function mergeExpandedLabelsIntoFilters(stores, enrichedNodes, nodeId2) {
|
|
547
550
|
const graphFilterState = stores.graphFilter.getState();
|
|
548
551
|
const currentFilters = graphFilterState.nodeLabelFilters;
|
|
549
552
|
if (currentFilters.length === 0 || enrichedNodes.length === 0) return;
|
|
@@ -553,7 +556,7 @@ function mergeExpandedLabelsIntoFilters(stores, enrichedNodes, nodeId) {
|
|
|
553
556
|
);
|
|
554
557
|
if (labelsToAdd.length > 0) {
|
|
555
558
|
devLog3("[GraphExploration] Adding expanded node labels to active filters", {
|
|
556
|
-
nodeId,
|
|
559
|
+
nodeId: nodeId2,
|
|
557
560
|
labelsToAdd,
|
|
558
561
|
currentFilters
|
|
559
562
|
});
|
|
@@ -577,10 +580,10 @@ function createGraphExplorationStore(deps) {
|
|
|
577
580
|
isFocusMode: true
|
|
578
581
|
});
|
|
579
582
|
},
|
|
580
|
-
setFocusModeWithNeighborhood: (
|
|
583
|
+
setFocusModeWithNeighborhood: (nodeId2) => {
|
|
581
584
|
const edges = stores.graphData.getState().edges;
|
|
582
585
|
const connectedNodeIds = /* @__PURE__ */ new Set();
|
|
583
|
-
const nodeIdStr = String(
|
|
586
|
+
const nodeIdStr = String(nodeId2);
|
|
584
587
|
for (const edge of edges) {
|
|
585
588
|
const sourceId = String(edge.start_id_);
|
|
586
589
|
const targetId = String(edge.end_id_);
|
|
@@ -628,19 +631,19 @@ function createGraphExplorationStore(deps) {
|
|
|
628
631
|
});
|
|
629
632
|
set({ hiddenElementIds: updated });
|
|
630
633
|
},
|
|
631
|
-
hideNodeIncoming: (
|
|
634
|
+
hideNodeIncoming: (nodeId2) => {
|
|
632
635
|
devLog3("[GraphExploration] hideNodeIncoming - hiding predecessors", {
|
|
633
|
-
nodeId
|
|
636
|
+
nodeId: nodeId2
|
|
634
637
|
});
|
|
635
638
|
const { edges } = stores.graphData.getState();
|
|
636
639
|
const { nodes: nodesToHide, edges: edgesToHide } = collectReachable(
|
|
637
|
-
|
|
640
|
+
nodeId2,
|
|
638
641
|
"in",
|
|
639
642
|
edges,
|
|
640
643
|
false
|
|
641
644
|
);
|
|
642
645
|
devLog3("[GraphExploration] hideNodeIncoming - traversal complete", {
|
|
643
|
-
nodeId,
|
|
646
|
+
nodeId: nodeId2,
|
|
644
647
|
nodesToHide: nodesToHide.size,
|
|
645
648
|
edgesToHide: edgesToHide.size
|
|
646
649
|
});
|
|
@@ -651,19 +654,19 @@ function createGraphExplorationStore(deps) {
|
|
|
651
654
|
devLog3("[GraphExploration] hideNodeIncoming - no predecessors found");
|
|
652
655
|
}
|
|
653
656
|
},
|
|
654
|
-
hideNodeOutgoing: (
|
|
657
|
+
hideNodeOutgoing: (nodeId2) => {
|
|
655
658
|
devLog3("[GraphExploration] hideNodeOutgoing - hiding successors", {
|
|
656
|
-
nodeId
|
|
659
|
+
nodeId: nodeId2
|
|
657
660
|
});
|
|
658
661
|
const { edges } = stores.graphData.getState();
|
|
659
662
|
const { nodes: nodesToHide, edges: edgesToHide } = collectReachable(
|
|
660
|
-
|
|
663
|
+
nodeId2,
|
|
661
664
|
"out",
|
|
662
665
|
edges,
|
|
663
666
|
false
|
|
664
667
|
);
|
|
665
668
|
devLog3("[GraphExploration] hideNodeOutgoing - traversal complete", {
|
|
666
|
-
nodeId,
|
|
669
|
+
nodeId: nodeId2,
|
|
667
670
|
nodesToHide: nodesToHide.size,
|
|
668
671
|
edgesToHide: edgesToHide.size
|
|
669
672
|
});
|
|
@@ -674,21 +677,21 @@ function createGraphExplorationStore(deps) {
|
|
|
674
677
|
devLog3("[GraphExploration] hideNodeOutgoing - no successors found");
|
|
675
678
|
}
|
|
676
679
|
},
|
|
677
|
-
hideEdgeForwardPath: (
|
|
680
|
+
hideEdgeForwardPath: (edgeId2) => {
|
|
678
681
|
devLog3(
|
|
679
682
|
"[GraphExploration] hideEdgeForwardPath - hiding edge + target + downstream",
|
|
680
|
-
{ edgeId }
|
|
683
|
+
{ edgeId: edgeId2 }
|
|
681
684
|
);
|
|
682
685
|
const { edges } = stores.graphData.getState();
|
|
683
|
-
const edge = edges.find((e) => String(e.id_) ===
|
|
686
|
+
const edge = edges.find((e) => String(e.id_) === edgeId2);
|
|
684
687
|
if (!edge) {
|
|
685
688
|
devLog3("[GraphExploration] hideEdgeForwardPath - edge not found", {
|
|
686
|
-
edgeId
|
|
689
|
+
edgeId: edgeId2
|
|
687
690
|
});
|
|
688
691
|
return;
|
|
689
692
|
}
|
|
690
693
|
const targetNodeId = String(edge.end_id_);
|
|
691
|
-
get().hideElements([
|
|
694
|
+
get().hideElements([edgeId2]);
|
|
692
695
|
const { nodes: nodesToHide, edges: edgesToHide } = collectReachable(
|
|
693
696
|
targetNodeId,
|
|
694
697
|
"out",
|
|
@@ -696,28 +699,28 @@ function createGraphExplorationStore(deps) {
|
|
|
696
699
|
true
|
|
697
700
|
);
|
|
698
701
|
devLog3("[GraphExploration] hideEdgeForwardPath - traversal complete", {
|
|
699
|
-
edgeId,
|
|
702
|
+
edgeId: edgeId2,
|
|
700
703
|
nodesToHide: nodesToHide.size,
|
|
701
704
|
edgesToHide: edgesToHide.size
|
|
702
705
|
});
|
|
703
706
|
const allElementsToHide = [...nodesToHide, ...edgesToHide];
|
|
704
707
|
if (allElementsToHide.length > 0) get().hideElements(allElementsToHide);
|
|
705
708
|
},
|
|
706
|
-
hideEdgeReversePath: (
|
|
709
|
+
hideEdgeReversePath: (edgeId2) => {
|
|
707
710
|
devLog3(
|
|
708
711
|
"[GraphExploration] hideEdgeReversePath - hiding edge + source + upstream",
|
|
709
|
-
{ edgeId }
|
|
712
|
+
{ edgeId: edgeId2 }
|
|
710
713
|
);
|
|
711
714
|
const { edges } = stores.graphData.getState();
|
|
712
|
-
const edge = edges.find((e) => String(e.id_) ===
|
|
715
|
+
const edge = edges.find((e) => String(e.id_) === edgeId2);
|
|
713
716
|
if (!edge) {
|
|
714
717
|
devLog3("[GraphExploration] hideEdgeReversePath - edge not found", {
|
|
715
|
-
edgeId
|
|
718
|
+
edgeId: edgeId2
|
|
716
719
|
});
|
|
717
720
|
return;
|
|
718
721
|
}
|
|
719
722
|
const sourceNodeId = String(edge.start_id_);
|
|
720
|
-
get().hideElements([
|
|
723
|
+
get().hideElements([edgeId2]);
|
|
721
724
|
const { nodes: nodesToHide, edges: edgesToHide } = collectReachable(
|
|
722
725
|
sourceNodeId,
|
|
723
726
|
"in",
|
|
@@ -725,29 +728,29 @@ function createGraphExplorationStore(deps) {
|
|
|
725
728
|
true
|
|
726
729
|
);
|
|
727
730
|
devLog3("[GraphExploration] hideEdgeReversePath - traversal complete", {
|
|
728
|
-
edgeId,
|
|
731
|
+
edgeId: edgeId2,
|
|
729
732
|
nodesToHide: nodesToHide.size,
|
|
730
733
|
edgesToHide: edgesToHide.size
|
|
731
734
|
});
|
|
732
735
|
const allElementsToHide = [...nodesToHide, ...edgesToHide];
|
|
733
736
|
if (allElementsToHide.length > 0) get().hideElements(allElementsToHide);
|
|
734
737
|
},
|
|
735
|
-
focusOnEdgeType: (
|
|
738
|
+
focusOnEdgeType: (edgeId2) => {
|
|
736
739
|
devLog3(
|
|
737
740
|
"[GraphExploration] focusOnEdgeType - show only edges of this type and connected nodes",
|
|
738
|
-
{ edgeId }
|
|
741
|
+
{ edgeId: edgeId2 }
|
|
739
742
|
);
|
|
740
743
|
const graphData = stores.graphData.getState();
|
|
741
|
-
const edge = graphData.edges.find((e) => String(e.id_) ===
|
|
744
|
+
const edge = graphData.edges.find((e) => String(e.id_) === edgeId2);
|
|
742
745
|
if (!edge) {
|
|
743
746
|
devLog3("[GraphExploration] focusOnEdgeType - edge not found", {
|
|
744
|
-
edgeId
|
|
747
|
+
edgeId: edgeId2
|
|
745
748
|
});
|
|
746
749
|
return;
|
|
747
750
|
}
|
|
748
751
|
const edgeLabel = edge.label_;
|
|
749
752
|
devLog3("[GraphExploration] focusOnEdgeType - filtering by label", {
|
|
750
|
-
edgeId,
|
|
753
|
+
edgeId: edgeId2,
|
|
751
754
|
edgeLabel
|
|
752
755
|
});
|
|
753
756
|
const edgesOfType = graphData.edges.filter((e) => e.label_ === edgeLabel);
|
|
@@ -761,9 +764,9 @@ function createGraphExplorationStore(deps) {
|
|
|
761
764
|
const nodesToHide = [];
|
|
762
765
|
const edgesToHide = [];
|
|
763
766
|
for (const node of graphData.nodes) {
|
|
764
|
-
const
|
|
765
|
-
if (!connectedNodeIds.has(
|
|
766
|
-
nodesToHide.push(
|
|
767
|
+
const nodeId2 = String(node.id_);
|
|
768
|
+
if (!connectedNodeIds.has(nodeId2)) {
|
|
769
|
+
nodesToHide.push(nodeId2);
|
|
767
770
|
}
|
|
768
771
|
}
|
|
769
772
|
for (const e of graphData.edges) {
|
|
@@ -784,16 +787,16 @@ function createGraphExplorationStore(deps) {
|
|
|
784
787
|
get().hideElements(allElementsToHide);
|
|
785
788
|
}
|
|
786
789
|
},
|
|
787
|
-
focusEdge: (
|
|
790
|
+
focusEdge: (edgeId2) => {
|
|
788
791
|
devLog3(
|
|
789
792
|
"[GraphExploration] focusEdge - unified focus with auto-direction detection",
|
|
790
|
-
{ edgeId }
|
|
793
|
+
{ edgeId: edgeId2 }
|
|
791
794
|
);
|
|
792
795
|
const graphData = stores.graphData.getState();
|
|
793
796
|
const selection = stores.selection.getState();
|
|
794
|
-
const edge = graphData.edges.find((e) => String(e.id_) ===
|
|
797
|
+
const edge = graphData.edges.find((e) => String(e.id_) === edgeId2);
|
|
795
798
|
if (!edge) {
|
|
796
|
-
devLog3("[GraphExploration] focusEdge - edge not found", { edgeId });
|
|
799
|
+
devLog3("[GraphExploration] focusEdge - edge not found", { edgeId: edgeId2 });
|
|
797
800
|
return;
|
|
798
801
|
}
|
|
799
802
|
const endpoints = {
|
|
@@ -806,7 +809,7 @@ function createGraphExplorationStore(deps) {
|
|
|
806
809
|
selection.selectedNodeIds
|
|
807
810
|
);
|
|
808
811
|
devLog3("[GraphExploration] focusEdge - determined direction", {
|
|
809
|
-
edgeId,
|
|
812
|
+
edgeId: edgeId2,
|
|
810
813
|
edgeLabel: endpoints.edgeLabel,
|
|
811
814
|
direction,
|
|
812
815
|
referenceNodeId,
|
|
@@ -849,27 +852,27 @@ function createGraphExplorationStore(deps) {
|
|
|
849
852
|
expandedNodeIds: /* @__PURE__ */ new Set()
|
|
850
853
|
});
|
|
851
854
|
},
|
|
852
|
-
markNodeExpanded: (
|
|
855
|
+
markNodeExpanded: (nodeId2) => {
|
|
853
856
|
const current = get().expandedNodeIds;
|
|
854
857
|
const updated = new Set(current);
|
|
855
|
-
updated.add(
|
|
856
|
-
devLog3("[GraphExploration] markNodeExpanded", { nodeId });
|
|
858
|
+
updated.add(nodeId2);
|
|
859
|
+
devLog3("[GraphExploration] markNodeExpanded", { nodeId: nodeId2 });
|
|
857
860
|
set({ expandedNodeIds: updated });
|
|
858
861
|
},
|
|
859
|
-
isNodeExpanded: (
|
|
860
|
-
return get().expandedNodeIds.has(
|
|
862
|
+
isNodeExpanded: (nodeId2) => {
|
|
863
|
+
return get().expandedNodeIds.has(nodeId2);
|
|
861
864
|
},
|
|
862
|
-
expandNode: async (
|
|
863
|
-
devLog3("[GraphExploration] expandNode", { nodeId, options });
|
|
865
|
+
expandNode: async (nodeId2, options) => {
|
|
866
|
+
devLog3("[GraphExploration] expandNode", { nodeId: nodeId2, options });
|
|
864
867
|
try {
|
|
865
|
-
const result = await stores.expandNode(Number(
|
|
868
|
+
const result = await stores.expandNode(Number(nodeId2), options);
|
|
866
869
|
devLog3("[GraphExploration] Expansion API result", {
|
|
867
|
-
nodeId,
|
|
870
|
+
nodeId: nodeId2,
|
|
868
871
|
newNodes: result.nodes.length,
|
|
869
872
|
newEdges: result.edges.length
|
|
870
873
|
});
|
|
871
874
|
if (result.nodes.length === 0 && result.edges.length === 0) {
|
|
872
|
-
devLog3("[GraphExploration] No connected nodes found", { nodeId });
|
|
875
|
+
devLog3("[GraphExploration] No connected nodes found", { nodeId: nodeId2 });
|
|
873
876
|
return;
|
|
874
877
|
}
|
|
875
878
|
const existingNodes = stores.graphData.getState().nodes;
|
|
@@ -884,8 +887,8 @@ function createGraphExplorationStore(deps) {
|
|
|
884
887
|
);
|
|
885
888
|
const enrichedEdges = enrichEdges(result.edges);
|
|
886
889
|
stores.graphData.getState().mergeExpandedData(enrichedNodes, enrichedEdges);
|
|
887
|
-
mergeExpandedLabelsIntoFilters(stores, enrichedNodes,
|
|
888
|
-
get().markNodeExpanded(
|
|
890
|
+
mergeExpandedLabelsIntoFilters(stores, enrichedNodes, nodeId2);
|
|
891
|
+
get().markNodeExpanded(nodeId2);
|
|
889
892
|
const currentState = get();
|
|
890
893
|
if (currentState.isFocusMode) {
|
|
891
894
|
const newFocusedIds = new Set(currentState.focusedNodeIds);
|
|
@@ -893,7 +896,7 @@ function createGraphExplorationStore(deps) {
|
|
|
893
896
|
newFocusedIds.add(String(node.id_));
|
|
894
897
|
}
|
|
895
898
|
devLog3("[GraphExploration] Adding expanded nodes to focus set", {
|
|
896
|
-
nodeId,
|
|
899
|
+
nodeId: nodeId2,
|
|
897
900
|
previousFocusCount: currentState.focusedNodeIds.size,
|
|
898
901
|
addedToFocus: enrichedNodes.length,
|
|
899
902
|
newFocusCount: newFocusedIds.size
|
|
@@ -906,23 +909,23 @@ function createGraphExplorationStore(deps) {
|
|
|
906
909
|
);
|
|
907
910
|
}
|
|
908
911
|
devLog3("[GraphExploration] Expansion complete", {
|
|
909
|
-
nodeId,
|
|
912
|
+
nodeId: nodeId2,
|
|
910
913
|
addedNodes: enrichedNodes.length,
|
|
911
914
|
addedEdges: enrichedEdges.length,
|
|
912
915
|
hasMore: result.has_more
|
|
913
916
|
});
|
|
914
917
|
} catch (error) {
|
|
915
|
-
errorLog("[GraphExploration] Expansion failed", { nodeId, error });
|
|
918
|
+
errorLog("[GraphExploration] Expansion failed", { nodeId: nodeId2, error });
|
|
916
919
|
throw error;
|
|
917
920
|
}
|
|
918
921
|
},
|
|
919
|
-
addNodeById: async (
|
|
920
|
-
const nodeIdStr = String(
|
|
921
|
-
devLog3("[GraphExploration] addNodeById", { nodeId, autoFocus });
|
|
922
|
+
addNodeById: async (nodeId2, autoFocus = false) => {
|
|
923
|
+
const nodeIdStr = String(nodeId2);
|
|
924
|
+
devLog3("[GraphExploration] addNodeById", { nodeId: nodeId2, autoFocus });
|
|
922
925
|
try {
|
|
923
926
|
const existingNodes = stores.graphData.getState().nodes;
|
|
924
927
|
if (handleExistingNode(
|
|
925
|
-
|
|
928
|
+
nodeId2,
|
|
926
929
|
nodeIdStr,
|
|
927
930
|
existingNodes,
|
|
928
931
|
autoFocus,
|
|
@@ -931,19 +934,19 @@ function createGraphExplorationStore(deps) {
|
|
|
931
934
|
return;
|
|
932
935
|
}
|
|
933
936
|
const enrichedNodes = await fetchAndEnrichNode(
|
|
934
|
-
|
|
937
|
+
nodeId2,
|
|
935
938
|
existingNodes,
|
|
936
939
|
stores
|
|
937
940
|
);
|
|
938
|
-
ensureNodeLabelInFilters(enrichedNodes,
|
|
941
|
+
ensureNodeLabelInFilters(enrichedNodes, nodeId2, stores.graphFilter);
|
|
939
942
|
if (autoFocus) {
|
|
940
943
|
devLog3("[GraphExploration] Requesting focus for newly added node", {
|
|
941
|
-
nodeId
|
|
944
|
+
nodeId: nodeId2
|
|
942
945
|
});
|
|
943
946
|
stores.graphFocus.getState().requestFocus(nodeIdStr, "node", false);
|
|
944
947
|
} else {
|
|
945
948
|
devLog3("[GraphExploration] Skipping focus for newly added node", {
|
|
946
|
-
nodeId
|
|
949
|
+
nodeId: nodeId2
|
|
947
950
|
});
|
|
948
951
|
}
|
|
949
952
|
const currentState = get();
|
|
@@ -951,19 +954,19 @@ function createGraphExplorationStore(deps) {
|
|
|
951
954
|
const newFocusedIds = new Set(currentState.focusedNodeIds);
|
|
952
955
|
newFocusedIds.add(nodeIdStr);
|
|
953
956
|
devLog3("[GraphExploration] Adding node to focus set", {
|
|
954
|
-
nodeId,
|
|
957
|
+
nodeId: nodeId2,
|
|
955
958
|
previousFocusCount: currentState.focusedNodeIds.size,
|
|
956
959
|
newFocusCount: newFocusedIds.size
|
|
957
960
|
});
|
|
958
961
|
set({ focusedNodeIds: newFocusedIds });
|
|
959
962
|
}
|
|
960
963
|
devLog3("[GraphExploration] addNodeById complete", {
|
|
961
|
-
nodeId,
|
|
964
|
+
nodeId: nodeId2,
|
|
962
965
|
addedNodes: enrichedNodes.length,
|
|
963
966
|
autoFocus
|
|
964
967
|
});
|
|
965
968
|
} catch (error) {
|
|
966
|
-
devLog3("[GraphExploration] addNodeById failed", { nodeId, error });
|
|
969
|
+
devLog3("[GraphExploration] addNodeById failed", { nodeId: nodeId2, error });
|
|
967
970
|
throw error;
|
|
968
971
|
}
|
|
969
972
|
}
|
|
@@ -1061,6 +1064,17 @@ function useWorkspaceSelection() {
|
|
|
1061
1064
|
return useGraphStores().selection;
|
|
1062
1065
|
}
|
|
1063
1066
|
|
|
1067
|
+
// src/hooks/useStableRef.ts
|
|
1068
|
+
import { useCallback, useRef } from "react";
|
|
1069
|
+
function useStableRef(fn) {
|
|
1070
|
+
const ref = useRef(fn);
|
|
1071
|
+
ref.current = fn;
|
|
1072
|
+
return useCallback(
|
|
1073
|
+
((...args) => ref.current?.(...args)),
|
|
1074
|
+
[]
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1064
1078
|
// src/hooks/useLiveGraphData.ts
|
|
1065
1079
|
import { useMemo, useEffect } from "react";
|
|
1066
1080
|
import { devLog as devLog6 } from "@petrarca/sonnet-core";
|
|
@@ -1425,7 +1439,7 @@ function useGraphRenderer() {
|
|
|
1425
1439
|
}
|
|
1426
1440
|
|
|
1427
1441
|
// src/hooks/graphRendererProvider.tsx
|
|
1428
|
-
import { useState, useCallback } from "react";
|
|
1442
|
+
import { useState, useCallback as useCallback2 } from "react";
|
|
1429
1443
|
import { jsx } from "react/jsx-runtime";
|
|
1430
1444
|
function GraphRendererProvider({
|
|
1431
1445
|
children,
|
|
@@ -1433,7 +1447,7 @@ function GraphRendererProvider({
|
|
|
1433
1447
|
onRendererChange
|
|
1434
1448
|
}) {
|
|
1435
1449
|
const [renderer, setRendererState] = useState(defaultRenderer);
|
|
1436
|
-
const setRenderer =
|
|
1450
|
+
const setRenderer = useCallback2(
|
|
1437
1451
|
(newRenderer) => {
|
|
1438
1452
|
setRendererState(newRenderer);
|
|
1439
1453
|
onRendererChange?.(newRenderer);
|
|
@@ -1446,9 +1460,9 @@ function GraphRendererProvider({
|
|
|
1446
1460
|
// src/components/rea/ReaGraph.tsx
|
|
1447
1461
|
import {
|
|
1448
1462
|
useEffect as useEffect3,
|
|
1449
|
-
useRef as
|
|
1463
|
+
useRef as useRef3,
|
|
1450
1464
|
useState as useState5,
|
|
1451
|
-
useCallback as
|
|
1465
|
+
useCallback as useCallback4
|
|
1452
1466
|
} from "react";
|
|
1453
1467
|
import { devLog as devLog8 } from "@petrarca/sonnet-core";
|
|
1454
1468
|
import {
|
|
@@ -1458,8 +1472,8 @@ import {
|
|
|
1458
1472
|
|
|
1459
1473
|
// src/components/rea/theme.ts
|
|
1460
1474
|
import { lightTheme } from "reagraph";
|
|
1461
|
-
var highlightColor =
|
|
1462
|
-
var highlightColorText = "#
|
|
1475
|
+
var highlightColor = SELECTION_COLOR;
|
|
1476
|
+
var highlightColorText = "#1d4ed8";
|
|
1463
1477
|
var theme = {
|
|
1464
1478
|
...lightTheme,
|
|
1465
1479
|
node: {
|
|
@@ -1672,7 +1686,7 @@ import React3, { useState as useState4 } from "react";
|
|
|
1672
1686
|
import { devLog as devLog7 } from "@petrarca/sonnet-core";
|
|
1673
1687
|
|
|
1674
1688
|
// src/components/rea/ExpandOptionsPanel.tsx
|
|
1675
|
-
import { useCallback as
|
|
1689
|
+
import { useCallback as useCallback3, useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
|
|
1676
1690
|
import { Checkbox, Input, Label } from "@petrarca/sonnet-ui";
|
|
1677
1691
|
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1678
1692
|
function ExpandOptionsPanel({
|
|
@@ -1687,9 +1701,9 @@ function ExpandOptionsPanel({
|
|
|
1687
1701
|
const [selectedTypes, setSelectedTypes] = useState3([]);
|
|
1688
1702
|
const [loading, setLoading] = useState3(!!getEdgeTypes);
|
|
1689
1703
|
const [offset, setOffset] = useState3({ x: 0, y: 0 });
|
|
1690
|
-
const expandBtnRef =
|
|
1691
|
-
const dragRef =
|
|
1692
|
-
const onPointerDown =
|
|
1704
|
+
const expandBtnRef = useRef2(null);
|
|
1705
|
+
const dragRef = useRef2(null);
|
|
1706
|
+
const onPointerDown = useCallback3(
|
|
1693
1707
|
(e) => {
|
|
1694
1708
|
if (e.target.closest("button")) return;
|
|
1695
1709
|
e.preventDefault();
|
|
@@ -1703,13 +1717,13 @@ function ExpandOptionsPanel({
|
|
|
1703
1717
|
},
|
|
1704
1718
|
[offset]
|
|
1705
1719
|
);
|
|
1706
|
-
const onPointerMove =
|
|
1720
|
+
const onPointerMove = useCallback3((e) => {
|
|
1707
1721
|
if (!dragRef.current) return;
|
|
1708
1722
|
const dx = e.clientX - dragRef.current.startX;
|
|
1709
1723
|
const dy = e.clientY - dragRef.current.startY;
|
|
1710
1724
|
setOffset({ x: dragRef.current.origX + dx, y: dragRef.current.origY + dy });
|
|
1711
1725
|
}, []);
|
|
1712
|
-
const onPointerUp =
|
|
1726
|
+
const onPointerUp = useCallback3(() => {
|
|
1713
1727
|
dragRef.current = null;
|
|
1714
1728
|
}, []);
|
|
1715
1729
|
useEffect2(() => {
|
|
@@ -1865,9 +1879,9 @@ function ReaContextMenu({
|
|
|
1865
1879
|
}, [onClose]);
|
|
1866
1880
|
const handleFocus = () => {
|
|
1867
1881
|
if (elementType === "node") {
|
|
1868
|
-
const
|
|
1869
|
-
devLog7("[ReaContextMenu] Focus on node and neighborhood:",
|
|
1870
|
-
useGraphExploration.getState().setFocusModeWithNeighborhood(
|
|
1882
|
+
const nodeId2 = data.id;
|
|
1883
|
+
devLog7("[ReaContextMenu] Focus on node and neighborhood:", nodeId2);
|
|
1884
|
+
useGraphExploration.getState().setFocusModeWithNeighborhood(nodeId2);
|
|
1871
1885
|
} else {
|
|
1872
1886
|
const startNodeId = data.data.start_id_;
|
|
1873
1887
|
devLog7("[ReaContextMenu] Focus on edge start node:", startNodeId);
|
|
@@ -1893,58 +1907,58 @@ function ReaContextMenu({
|
|
|
1893
1907
|
};
|
|
1894
1908
|
const handleHideNode = () => {
|
|
1895
1909
|
if (elementType === "node") {
|
|
1896
|
-
const
|
|
1897
|
-
devLog7("[ReaContextMenu] Hide node:",
|
|
1898
|
-
useGraphExploration.getState().hideElements([
|
|
1910
|
+
const nodeId2 = data.id;
|
|
1911
|
+
devLog7("[ReaContextMenu] Hide node:", nodeId2);
|
|
1912
|
+
useGraphExploration.getState().hideElements([nodeId2]);
|
|
1899
1913
|
}
|
|
1900
1914
|
onClose();
|
|
1901
1915
|
};
|
|
1902
1916
|
const handleHideIncoming = () => {
|
|
1903
1917
|
if (elementType === "node") {
|
|
1904
|
-
const
|
|
1905
|
-
devLog7("[ReaContextMenu] Hide incoming edges:",
|
|
1906
|
-
useGraphExploration.getState().hideNodeIncoming(
|
|
1918
|
+
const nodeId2 = data.id;
|
|
1919
|
+
devLog7("[ReaContextMenu] Hide incoming edges:", nodeId2);
|
|
1920
|
+
useGraphExploration.getState().hideNodeIncoming(nodeId2);
|
|
1907
1921
|
}
|
|
1908
1922
|
onClose();
|
|
1909
1923
|
};
|
|
1910
1924
|
const handleHideOutgoing = () => {
|
|
1911
1925
|
if (elementType === "node") {
|
|
1912
|
-
const
|
|
1913
|
-
devLog7("[ReaContextMenu] Hide outgoing edges:",
|
|
1914
|
-
useGraphExploration.getState().hideNodeOutgoing(
|
|
1926
|
+
const nodeId2 = data.id;
|
|
1927
|
+
devLog7("[ReaContextMenu] Hide outgoing edges:", nodeId2);
|
|
1928
|
+
useGraphExploration.getState().hideNodeOutgoing(nodeId2);
|
|
1915
1929
|
}
|
|
1916
1930
|
onClose();
|
|
1917
1931
|
};
|
|
1918
1932
|
const handleExpandWithOptions = (options) => {
|
|
1919
1933
|
if (elementType === "node") {
|
|
1920
|
-
const
|
|
1921
|
-
devLog7("[ReaContextMenu] Expand node with options:",
|
|
1922
|
-
useGraphExploration.getState().expandNode(
|
|
1934
|
+
const nodeId2 = data.id;
|
|
1935
|
+
devLog7("[ReaContextMenu] Expand node with options:", nodeId2, options);
|
|
1936
|
+
useGraphExploration.getState().expandNode(nodeId2, options);
|
|
1923
1937
|
}
|
|
1924
1938
|
onClose();
|
|
1925
1939
|
};
|
|
1926
1940
|
const handleHideForwardPath = () => {
|
|
1927
1941
|
if (elementType === "edge") {
|
|
1928
|
-
const
|
|
1929
|
-
devLog7("[ReaContextMenu] Hide forward path from edge:",
|
|
1930
|
-
useGraphExploration.getState().hideEdgeForwardPath(
|
|
1942
|
+
const edgeId2 = data.id;
|
|
1943
|
+
devLog7("[ReaContextMenu] Hide forward path from edge:", edgeId2);
|
|
1944
|
+
useGraphExploration.getState().hideEdgeForwardPath(edgeId2);
|
|
1931
1945
|
}
|
|
1932
1946
|
onClose();
|
|
1933
1947
|
};
|
|
1934
1948
|
const handleHideReversePath = () => {
|
|
1935
1949
|
if (elementType === "edge") {
|
|
1936
|
-
const
|
|
1937
|
-
devLog7("[ReaContextMenu] Hide reverse path from edge:",
|
|
1938
|
-
useGraphExploration.getState().hideEdgeReversePath(
|
|
1950
|
+
const edgeId2 = data.id;
|
|
1951
|
+
devLog7("[ReaContextMenu] Hide reverse path from edge:", edgeId2);
|
|
1952
|
+
useGraphExploration.getState().hideEdgeReversePath(edgeId2);
|
|
1939
1953
|
}
|
|
1940
1954
|
onClose();
|
|
1941
1955
|
};
|
|
1942
1956
|
const handleFocusOnEdgeType = () => {
|
|
1943
1957
|
if (elementType === "edge") {
|
|
1944
|
-
const
|
|
1958
|
+
const edgeId2 = data.id;
|
|
1945
1959
|
const edgeLabel = data.data?.label_ || "edge";
|
|
1946
|
-
devLog7("[ReaContextMenu] Focus on edge type:",
|
|
1947
|
-
useGraphExploration.getState().focusOnEdgeType(
|
|
1960
|
+
devLog7("[ReaContextMenu] Focus on edge type:", edgeId2, edgeLabel);
|
|
1961
|
+
useGraphExploration.getState().focusOnEdgeType(edgeId2);
|
|
1948
1962
|
}
|
|
1949
1963
|
onClose();
|
|
1950
1964
|
};
|
|
@@ -2070,7 +2084,7 @@ function ReaGraph({
|
|
|
2070
2084
|
extraToolbarContent,
|
|
2071
2085
|
getNodeEdgeTypes
|
|
2072
2086
|
}) {
|
|
2073
|
-
const isMountedRef =
|
|
2087
|
+
const isMountedRef = useRef3(false);
|
|
2074
2088
|
const { nodes, edges } = useReagraphData();
|
|
2075
2089
|
const { normalized } = useLiveGraphData();
|
|
2076
2090
|
useEffect3(() => {
|
|
@@ -2078,7 +2092,7 @@ function ReaGraph({
|
|
|
2078
2092
|
if (nodes.length > 0) devLog8("[ReGraph] sample node", nodes[0]);
|
|
2079
2093
|
if (edges.length > 0) devLog8("[ReGraph] sample edge", edges[0]);
|
|
2080
2094
|
}, [nodes, edges]);
|
|
2081
|
-
const canvas =
|
|
2095
|
+
const canvas = useRef3(null);
|
|
2082
2096
|
const [hoveredNode, setHoveredNode] = useState5(
|
|
2083
2097
|
void 0
|
|
2084
2098
|
);
|
|
@@ -2139,7 +2153,7 @@ function ReaGraph({
|
|
|
2139
2153
|
clearSelections();
|
|
2140
2154
|
clearSelectionStore();
|
|
2141
2155
|
}, [lastDataChangeTime, clearSelections, clearSelectionStore]);
|
|
2142
|
-
const fitToScreen =
|
|
2156
|
+
const fitToScreen = useCallback4(
|
|
2143
2157
|
(reason) => {
|
|
2144
2158
|
if (!isMountedRef.current) return;
|
|
2145
2159
|
if (nodes.length === 0) return;
|
|
@@ -2185,6 +2199,20 @@ function ReaGraph({
|
|
|
2185
2199
|
isMountedRef.current = false;
|
|
2186
2200
|
};
|
|
2187
2201
|
}, [fitToScreen, setSelections, setSelectedEdge, useGraphSelection]);
|
|
2202
|
+
const selectionsRef = useRef3(selections);
|
|
2203
|
+
selectionsRef.current = selections;
|
|
2204
|
+
useEffect3(() => {
|
|
2205
|
+
const inSync = (nodeIds) => {
|
|
2206
|
+
const cur = selectionsRef.current;
|
|
2207
|
+
return cur.length === nodeIds.length && nodeIds.every((id) => cur.includes(id));
|
|
2208
|
+
};
|
|
2209
|
+
const unsubscribe = useGraphSelection.subscribe((state) => {
|
|
2210
|
+
if (!inSync(state.selectedNodeIds)) {
|
|
2211
|
+
setSelections(state.selectedNodeIds);
|
|
2212
|
+
}
|
|
2213
|
+
});
|
|
2214
|
+
return unsubscribe;
|
|
2215
|
+
}, [useGraphSelection, setSelections]);
|
|
2188
2216
|
useEffect3(() => {
|
|
2189
2217
|
if (isMountedRef.current) fitToScreen("counts-changed");
|
|
2190
2218
|
}, [nodes.length, edges.length, fitToScreen]);
|
|
@@ -2300,7 +2328,7 @@ function ReaGraph({
|
|
|
2300
2328
|
window.removeEventListener("resize", handleResize);
|
|
2301
2329
|
};
|
|
2302
2330
|
}, [fitToScreen]);
|
|
2303
|
-
const firstDataRef =
|
|
2331
|
+
const firstDataRef = useRef3(true);
|
|
2304
2332
|
useEffect3(() => {
|
|
2305
2333
|
if (!isMountedRef.current) return;
|
|
2306
2334
|
if (nodes.length > 0) {
|
|
@@ -2401,9 +2429,9 @@ var ReaGraph_default = ReaGraph;
|
|
|
2401
2429
|
// src/components/cyto/CytoscapeGraph.tsx
|
|
2402
2430
|
import {
|
|
2403
2431
|
useEffect as useEffect4,
|
|
2404
|
-
useRef as
|
|
2432
|
+
useRef as useRef4,
|
|
2405
2433
|
useState as useState6,
|
|
2406
|
-
useCallback as
|
|
2434
|
+
useCallback as useCallback5
|
|
2407
2435
|
} from "react";
|
|
2408
2436
|
import { devLog as devLog10, errorLog as errorLog3 } from "@petrarca/sonnet-core";
|
|
2409
2437
|
import cytoscape from "cytoscape";
|
|
@@ -2824,9 +2852,9 @@ var createNodeContextMenu = ({
|
|
|
2824
2852
|
{
|
|
2825
2853
|
content: "Focus",
|
|
2826
2854
|
select: (ele) => {
|
|
2827
|
-
const
|
|
2828
|
-
devLog9("[CxtMenu] Focus on node and neighborhood:",
|
|
2829
|
-
useGraphExploration.getState().setFocusModeWithNeighborhood(
|
|
2855
|
+
const nodeId2 = ele.id();
|
|
2856
|
+
devLog9("[CxtMenu] Focus on node and neighborhood:", nodeId2);
|
|
2857
|
+
useGraphExploration.getState().setFocusModeWithNeighborhood(nodeId2);
|
|
2830
2858
|
}
|
|
2831
2859
|
},
|
|
2832
2860
|
{
|
|
@@ -2844,38 +2872,38 @@ var createNodeContextMenu = ({
|
|
|
2844
2872
|
{
|
|
2845
2873
|
content: "Hide Node",
|
|
2846
2874
|
select: (ele) => {
|
|
2847
|
-
const
|
|
2848
|
-
devLog9("[CxtMenu] Hide node:",
|
|
2849
|
-
useGraphExploration.getState().hideElements([
|
|
2875
|
+
const nodeId2 = ele.id();
|
|
2876
|
+
devLog9("[CxtMenu] Hide node:", nodeId2);
|
|
2877
|
+
useGraphExploration.getState().hideElements([nodeId2]);
|
|
2850
2878
|
}
|
|
2851
2879
|
},
|
|
2852
2880
|
{
|
|
2853
2881
|
content: "Hide Incoming",
|
|
2854
2882
|
select: (ele) => {
|
|
2855
|
-
const
|
|
2856
|
-
devLog9("[CxtMenu] Hide incoming (predecessors) of node:",
|
|
2857
|
-
useGraphExploration.getState().hideNodeIncoming(
|
|
2883
|
+
const nodeId2 = ele.id();
|
|
2884
|
+
devLog9("[CxtMenu] Hide incoming (predecessors) of node:", nodeId2);
|
|
2885
|
+
useGraphExploration.getState().hideNodeIncoming(nodeId2);
|
|
2858
2886
|
}
|
|
2859
2887
|
},
|
|
2860
2888
|
{
|
|
2861
2889
|
content: "Hide Outgoing",
|
|
2862
2890
|
select: (ele) => {
|
|
2863
|
-
const
|
|
2864
|
-
devLog9("[CxtMenu] Hide outgoing (successors) of node:",
|
|
2865
|
-
useGraphExploration.getState().hideNodeOutgoing(
|
|
2891
|
+
const nodeId2 = ele.id();
|
|
2892
|
+
devLog9("[CxtMenu] Hide outgoing (successors) of node:", nodeId2);
|
|
2893
|
+
useGraphExploration.getState().hideNodeOutgoing(nodeId2);
|
|
2866
2894
|
}
|
|
2867
2895
|
},
|
|
2868
2896
|
{
|
|
2869
2897
|
content: "Expand",
|
|
2870
2898
|
select: (ele) => {
|
|
2871
|
-
const
|
|
2872
|
-
devLog9("[CxtMenu] Expand request for node:",
|
|
2899
|
+
const nodeId2 = ele.id();
|
|
2900
|
+
devLog9("[CxtMenu] Expand request for node:", nodeId2);
|
|
2873
2901
|
if (onExpandRequest) {
|
|
2874
2902
|
const pos = ele.renderedPosition();
|
|
2875
|
-
const label = ele.data("label") ||
|
|
2876
|
-
onExpandRequest({ nodeId, nodeLabel: label, x: pos.x, y: pos.y });
|
|
2903
|
+
const label = ele.data("label") || nodeId2;
|
|
2904
|
+
onExpandRequest({ nodeId: nodeId2, nodeLabel: label, x: pos.x, y: pos.y });
|
|
2877
2905
|
} else {
|
|
2878
|
-
useGraphExploration.getState().expandNode(
|
|
2906
|
+
useGraphExploration.getState().expandNode(nodeId2).catch((error) => {
|
|
2879
2907
|
errorLog2("[CxtMenu] Expansion failed:", error);
|
|
2880
2908
|
toast3.error("Failed to expand node");
|
|
2881
2909
|
});
|
|
@@ -2911,42 +2939,42 @@ var createEdgeContextMenu = ({
|
|
|
2911
2939
|
{
|
|
2912
2940
|
content: "Focus",
|
|
2913
2941
|
select: (ele) => {
|
|
2914
|
-
const
|
|
2942
|
+
const edgeId2 = ele.id();
|
|
2915
2943
|
devLog9(
|
|
2916
2944
|
"[CxtMenu] Focus on edge (auto-direction based on selection):",
|
|
2917
|
-
|
|
2945
|
+
edgeId2
|
|
2918
2946
|
);
|
|
2919
|
-
useGraphExploration.getState().focusEdge(
|
|
2947
|
+
useGraphExploration.getState().focusEdge(edgeId2);
|
|
2920
2948
|
}
|
|
2921
2949
|
},
|
|
2922
2950
|
{
|
|
2923
2951
|
content: "Focus Edge Type",
|
|
2924
2952
|
select: (ele) => {
|
|
2925
|
-
const
|
|
2926
|
-
devLog9("[CxtMenu] Focus on edge type:",
|
|
2927
|
-
useGraphExploration.getState().focusOnEdgeType(
|
|
2953
|
+
const edgeId2 = ele.id();
|
|
2954
|
+
devLog9("[CxtMenu] Focus on edge type:", edgeId2);
|
|
2955
|
+
useGraphExploration.getState().focusOnEdgeType(edgeId2);
|
|
2928
2956
|
}
|
|
2929
2957
|
},
|
|
2930
2958
|
{
|
|
2931
2959
|
content: "Hide Forward Path",
|
|
2932
2960
|
select: (ele) => {
|
|
2933
|
-
const
|
|
2961
|
+
const edgeId2 = ele.id();
|
|
2934
2962
|
devLog9(
|
|
2935
2963
|
"[CxtMenu] Hide forward path (edge + target + downstream):",
|
|
2936
|
-
|
|
2964
|
+
edgeId2
|
|
2937
2965
|
);
|
|
2938
|
-
useGraphExploration.getState().hideEdgeForwardPath(
|
|
2966
|
+
useGraphExploration.getState().hideEdgeForwardPath(edgeId2);
|
|
2939
2967
|
}
|
|
2940
2968
|
},
|
|
2941
2969
|
{
|
|
2942
2970
|
content: "Hide Reverse Path",
|
|
2943
2971
|
select: (ele) => {
|
|
2944
|
-
const
|
|
2972
|
+
const edgeId2 = ele.id();
|
|
2945
2973
|
devLog9(
|
|
2946
2974
|
"[CxtMenu] Hide reverse path (edge + source + upstream):",
|
|
2947
|
-
|
|
2975
|
+
edgeId2
|
|
2948
2976
|
);
|
|
2949
|
-
useGraphExploration.getState().hideEdgeReversePath(
|
|
2977
|
+
useGraphExploration.getState().hideEdgeReversePath(edgeId2);
|
|
2950
2978
|
}
|
|
2951
2979
|
}
|
|
2952
2980
|
],
|
|
@@ -2997,9 +3025,9 @@ function CytoscapeExpandOverlay({
|
|
|
2997
3025
|
{
|
|
2998
3026
|
nodeLabel: expandRequest.nodeLabel,
|
|
2999
3027
|
onExpand: (options) => {
|
|
3000
|
-
const { nodeId } = expandRequest;
|
|
3028
|
+
const { nodeId: nodeId2 } = expandRequest;
|
|
3001
3029
|
onClose();
|
|
3002
|
-
useGraphExploration.getState().expandNode(
|
|
3030
|
+
useGraphExploration.getState().expandNode(nodeId2, options).catch((err) => {
|
|
3003
3031
|
errorLog3("[CxtMenu] Expansion failed:", err);
|
|
3004
3032
|
toast4.error("Failed to expand node");
|
|
3005
3033
|
});
|
|
@@ -3329,7 +3357,7 @@ function useFullscreen(fitToScreen, cyRef) {
|
|
|
3329
3357
|
return {
|
|
3330
3358
|
fullscreen,
|
|
3331
3359
|
setFullscreen,
|
|
3332
|
-
toggleFullscreen:
|
|
3360
|
+
toggleFullscreen: useCallback5(() => setFullscreen((f) => !f), [])
|
|
3333
3361
|
};
|
|
3334
3362
|
}
|
|
3335
3363
|
function setupCytoscapeEventHandlers({
|
|
@@ -3392,8 +3420,8 @@ function setupCytoscapeEventHandlers({
|
|
|
3392
3420
|
});
|
|
3393
3421
|
cy.on("tap", "edge", (evt) => {
|
|
3394
3422
|
const edgeEle = evt.target;
|
|
3395
|
-
const
|
|
3396
|
-
const edgeObj = edgesRef.current.find((e) => String(e.id_) ===
|
|
3423
|
+
const edgeId2 = edgeEle.id();
|
|
3424
|
+
const edgeObj = edgesRef.current.find((e) => String(e.id_) === edgeId2);
|
|
3397
3425
|
if (!edgeObj) return;
|
|
3398
3426
|
const sourceId = edgeEle.data("source");
|
|
3399
3427
|
const targetId = edgeEle.data("target");
|
|
@@ -3403,7 +3431,7 @@ function setupCytoscapeEventHandlers({
|
|
|
3403
3431
|
const selectedNodeId = currentlySelectedNodes[0].id();
|
|
3404
3432
|
if (selectedNodeId === sourceId || selectedNodeId === targetId) {
|
|
3405
3433
|
devLog10("[CytoscapeGraph] tap edge - node+edge selection", {
|
|
3406
|
-
edgeId,
|
|
3434
|
+
edgeId: edgeId2,
|
|
3407
3435
|
selectedNode: selectedNodeId,
|
|
3408
3436
|
direction: selectedNodeId === sourceId ? "outgoing" : "incoming"
|
|
3409
3437
|
});
|
|
@@ -3417,7 +3445,7 @@ function setupCytoscapeEventHandlers({
|
|
|
3417
3445
|
return;
|
|
3418
3446
|
}
|
|
3419
3447
|
}
|
|
3420
|
-
devLog10("[CytoscapeGraph] tap edge - default (both endpoints)", { edgeId });
|
|
3448
|
+
devLog10("[CytoscapeGraph] tap edge - default (both endpoints)", { edgeId: edgeId2 });
|
|
3421
3449
|
evtCy.batch(() => {
|
|
3422
3450
|
evtCy.elements().unselect();
|
|
3423
3451
|
evtCy.$id(sourceId).select();
|
|
@@ -3445,16 +3473,16 @@ function CytoscapeGraph({
|
|
|
3445
3473
|
extraToolbarContent,
|
|
3446
3474
|
getNodeEdgeTypes
|
|
3447
3475
|
}) {
|
|
3448
|
-
const containerRef =
|
|
3449
|
-
const cyRef =
|
|
3450
|
-
const isInitializedRef =
|
|
3476
|
+
const containerRef = useRef4(null);
|
|
3477
|
+
const cyRef = useRef4(null);
|
|
3478
|
+
const isInitializedRef = useRef4(false);
|
|
3451
3479
|
const [currentLayout, setCurrentLayout] = useState6(initialLayout);
|
|
3452
3480
|
const [expandRequest, setExpandRequest] = useState6(
|
|
3453
3481
|
null
|
|
3454
3482
|
);
|
|
3455
|
-
const nodesRef =
|
|
3456
|
-
const edgesRef =
|
|
3457
|
-
const pendingFocusActionRef =
|
|
3483
|
+
const nodesRef = useRef4([]);
|
|
3484
|
+
const edgesRef = useRef4([]);
|
|
3485
|
+
const pendingFocusActionRef = useRef4(null);
|
|
3458
3486
|
const cytoData = useCytoscapeData();
|
|
3459
3487
|
const live = useLiveGraphData();
|
|
3460
3488
|
const nodes = live.nodes;
|
|
@@ -3474,31 +3502,51 @@ function CytoscapeGraph({
|
|
|
3474
3502
|
setSelectedEdge: setSelectedEdgeStore,
|
|
3475
3503
|
clear: clearSelectionStore
|
|
3476
3504
|
} = useSelection3.getState();
|
|
3477
|
-
const selectedNodeIdsRef =
|
|
3505
|
+
const selectedNodeIdsRef = useRef4(
|
|
3478
3506
|
useSelection3.getState().selectedNodeIds
|
|
3479
3507
|
);
|
|
3480
|
-
const selectedEdgeRef =
|
|
3508
|
+
const selectedEdgeRef = useRef4(
|
|
3481
3509
|
useSelection3.getState().selectedEdge
|
|
3482
3510
|
);
|
|
3483
3511
|
useEffect4(() => {
|
|
3484
|
-
const
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3512
|
+
const applyStoreSelection = (nodeIds, edge) => {
|
|
3513
|
+
selectedNodeIdsRef.current = nodeIds;
|
|
3514
|
+
selectedEdgeRef.current = edge;
|
|
3515
|
+
const cy = cyRef.current;
|
|
3516
|
+
if (!cy) return;
|
|
3517
|
+
const want = new Set(nodeIds);
|
|
3518
|
+
const haveNodes = cy.$("node:selected");
|
|
3519
|
+
const have = new Set(haveNodes.map((e) => e.id()));
|
|
3520
|
+
const wantEdgeId = edge ? String(edge.id_) : null;
|
|
3521
|
+
const haveEdge = cy.$("edge:selected");
|
|
3522
|
+
const haveEdgeId = haveEdge.length ? haveEdge[0].id() : null;
|
|
3523
|
+
const nodesInSync = want.size === have.size && [...want].every((id) => have.has(id));
|
|
3524
|
+
const edgeInSync = wantEdgeId === haveEdgeId;
|
|
3525
|
+
if (nodesInSync && edgeInSync) return;
|
|
3489
3526
|
cy.batch(() => {
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3527
|
+
haveNodes.forEach((ele) => {
|
|
3528
|
+
if (!want.has(ele.id())) ele.unselect();
|
|
3529
|
+
});
|
|
3530
|
+
if (haveEdgeId && haveEdgeId !== wantEdgeId) haveEdge.unselect();
|
|
3531
|
+
nodeIds.forEach((id) => {
|
|
3532
|
+
if (!have.has(id)) cy.$id(id).select();
|
|
3533
|
+
});
|
|
3534
|
+
if (wantEdgeId && wantEdgeId !== haveEdgeId) {
|
|
3535
|
+
const edgeEle = cy.$id(wantEdgeId);
|
|
3536
|
+
if (edgeEle) {
|
|
3537
|
+
edgeEle.select();
|
|
3538
|
+
cy.$id(edgeEle.data("source")).select();
|
|
3539
|
+
cy.$id(edgeEle.data("target")).select();
|
|
3498
3540
|
}
|
|
3499
3541
|
}
|
|
3500
3542
|
});
|
|
3501
|
-
}
|
|
3543
|
+
};
|
|
3544
|
+
const initial = useSelection3.getState();
|
|
3545
|
+
applyStoreSelection(initial.selectedNodeIds, initial.selectedEdge);
|
|
3546
|
+
const unsubscribe = useSelection3.subscribe((state) => {
|
|
3547
|
+
applyStoreSelection(state.selectedNodeIds, state.selectedEdge);
|
|
3548
|
+
});
|
|
3549
|
+
return unsubscribe;
|
|
3502
3550
|
}, [useSelection3]);
|
|
3503
3551
|
useEffect4(() => {
|
|
3504
3552
|
nodesRef.current = nodes;
|
|
@@ -3512,8 +3560,10 @@ function CytoscapeGraph({
|
|
|
3512
3560
|
cyRef.current = cytoscape({
|
|
3513
3561
|
container: containerRef.current,
|
|
3514
3562
|
elements: [],
|
|
3515
|
-
|
|
3516
|
-
//
|
|
3563
|
+
// Plain click selects a single node (replaces the selection); hold
|
|
3564
|
+
// Shift/Cmd to add a second node (e.g. for the two-node edge action).
|
|
3565
|
+
// This matches the conventional canvas-selection expectation.
|
|
3566
|
+
selectionType: "single",
|
|
3517
3567
|
style: [
|
|
3518
3568
|
// Base node style with data mappers for dynamic properties
|
|
3519
3569
|
{
|
|
@@ -3607,19 +3657,20 @@ function CytoscapeGraph({
|
|
|
3607
3657
|
{
|
|
3608
3658
|
selector: "edge:selected",
|
|
3609
3659
|
style: {
|
|
3610
|
-
"line-color":
|
|
3611
|
-
"target-arrow-color":
|
|
3612
|
-
width
|
|
3613
|
-
//
|
|
3660
|
+
"line-color": SELECTION_COLOR,
|
|
3661
|
+
"target-arrow-color": SELECTION_COLOR,
|
|
3662
|
+
// Modest bump over the base width (~3); selection is conveyed mainly
|
|
3663
|
+
// by colour + overlay, not a slab. (Was 8, which looked oversized.)
|
|
3664
|
+
width: 4,
|
|
3614
3665
|
"z-index": 999,
|
|
3615
3666
|
"font-size": "11px",
|
|
3616
3667
|
"font-weight": "bold",
|
|
3617
3668
|
"text-background-opacity": 1,
|
|
3618
3669
|
"text-background-color": "#ffffff",
|
|
3619
3670
|
"text-background-padding": "3px",
|
|
3620
|
-
"arrow-scale":
|
|
3671
|
+
"arrow-scale": 0.9,
|
|
3621
3672
|
opacity: 1,
|
|
3622
|
-
"overlay-color":
|
|
3673
|
+
"overlay-color": SELECTION_COLOR,
|
|
3623
3674
|
"overlay-opacity": 0.08
|
|
3624
3675
|
}
|
|
3625
3676
|
},
|
|
@@ -3628,13 +3679,13 @@ function CytoscapeGraph({
|
|
|
3628
3679
|
style: {
|
|
3629
3680
|
"border-width": 10,
|
|
3630
3681
|
// thicker circle outline
|
|
3631
|
-
"border-color":
|
|
3682
|
+
"border-color": SELECTION_COLOR,
|
|
3632
3683
|
"border-opacity": 1,
|
|
3633
3684
|
"background-opacity": 1,
|
|
3634
3685
|
"text-background-opacity": 1,
|
|
3635
3686
|
"text-background-color": "#ffffff",
|
|
3636
3687
|
"text-background-padding": "4px",
|
|
3637
|
-
"overlay-color":
|
|
3688
|
+
"overlay-color": SELECTION_COLOR,
|
|
3638
3689
|
"overlay-opacity": 0.18,
|
|
3639
3690
|
width: 48,
|
|
3640
3691
|
// slight size bump from 40
|
|
@@ -3666,9 +3717,9 @@ function CytoscapeGraph({
|
|
|
3666
3717
|
cyRef.current = null;
|
|
3667
3718
|
};
|
|
3668
3719
|
}, []);
|
|
3669
|
-
const elementIdsRef =
|
|
3670
|
-
const lastLayoutRef =
|
|
3671
|
-
const lastFitPaddingRef =
|
|
3720
|
+
const elementIdsRef = useRef4("");
|
|
3721
|
+
const lastLayoutRef = useRef4("");
|
|
3722
|
+
const lastFitPaddingRef = useRef4(fitPadding);
|
|
3672
3723
|
useEffect4(() => {
|
|
3673
3724
|
const cy = cyRef.current;
|
|
3674
3725
|
if (!cy || !isInitializedRef.current) return;
|
|
@@ -3766,7 +3817,7 @@ function CytoscapeGraph({
|
|
|
3766
3817
|
const updateLayout = (newLayout) => {
|
|
3767
3818
|
setCurrentLayout(newLayout);
|
|
3768
3819
|
};
|
|
3769
|
-
const fitToScreen =
|
|
3820
|
+
const fitToScreen = useCallback5(() => {
|
|
3770
3821
|
if (cyRef.current) {
|
|
3771
3822
|
cyRef.current.fit(void 0, fitPadding);
|
|
3772
3823
|
}
|
|
@@ -3823,9 +3874,9 @@ function CytoscapeGraph({
|
|
|
3823
3874
|
extraToolbarContent,
|
|
3824
3875
|
zoomIn: () => {
|
|
3825
3876
|
if (cyRef.current) {
|
|
3826
|
-
const
|
|
3877
|
+
const z3 = cyRef.current.zoom();
|
|
3827
3878
|
cyRef.current.zoom({
|
|
3828
|
-
level: Math.min(
|
|
3879
|
+
level: Math.min(z3 * 1.2, 5),
|
|
3829
3880
|
renderedPosition: {
|
|
3830
3881
|
x: cyRef.current.width() / 2,
|
|
3831
3882
|
y: cyRef.current.height() / 2
|
|
@@ -3835,9 +3886,9 @@ function CytoscapeGraph({
|
|
|
3835
3886
|
},
|
|
3836
3887
|
zoomOut: () => {
|
|
3837
3888
|
if (cyRef.current) {
|
|
3838
|
-
const
|
|
3889
|
+
const z3 = cyRef.current.zoom();
|
|
3839
3890
|
cyRef.current.zoom({
|
|
3840
|
-
level: Math.max(
|
|
3891
|
+
level: Math.max(z3 / 1.2, 0.1),
|
|
3841
3892
|
renderedPosition: {
|
|
3842
3893
|
x: cyRef.current.width() / 2,
|
|
3843
3894
|
y: cyRef.current.height() / 2
|
|
@@ -4035,18 +4086,11 @@ function NodeLabelBadges({
|
|
|
4035
4086
|
labels,
|
|
4036
4087
|
labelCounts,
|
|
4037
4088
|
colorPalette,
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
toggleNodeLabelFilter
|
|
4089
|
+
shown,
|
|
4090
|
+
onToggleLabel
|
|
4041
4091
|
}) {
|
|
4042
4092
|
return labels.map((label) => {
|
|
4043
|
-
const bgColor =
|
|
4044
|
-
allLabelsDisabled,
|
|
4045
|
-
nodeLabelFilters,
|
|
4046
|
-
label,
|
|
4047
|
-
colorPalette,
|
|
4048
|
-
label
|
|
4049
|
-
);
|
|
4093
|
+
const bgColor = shown.has(label) ? colorPalette[label] : colorPalette["faded"];
|
|
4050
4094
|
return /* @__PURE__ */ jsxs12(
|
|
4051
4095
|
Badge,
|
|
4052
4096
|
{
|
|
@@ -4056,7 +4100,7 @@ function NodeLabelBadges({
|
|
|
4056
4100
|
backgroundColor: bgColor,
|
|
4057
4101
|
borderRadius: "10px"
|
|
4058
4102
|
},
|
|
4059
|
-
onClick: () =>
|
|
4103
|
+
onClick: () => onToggleLabel(label),
|
|
4060
4104
|
children: [
|
|
4061
4105
|
label,
|
|
4062
4106
|
" (",
|
|
@@ -4106,12 +4150,53 @@ function EdgeLabelBadges({
|
|
|
4106
4150
|
);
|
|
4107
4151
|
});
|
|
4108
4152
|
}
|
|
4153
|
+
function bucketCount(labels, labelCounts) {
|
|
4154
|
+
return labels.reduce((sum, l) => sum + (labelCounts[l] ?? 0), 0);
|
|
4155
|
+
}
|
|
4156
|
+
function effectiveShownNodeLabels(allLabels, allLabelsDisabled, nodeLabelFilters) {
|
|
4157
|
+
if (allLabelsDisabled) return /* @__PURE__ */ new Set();
|
|
4158
|
+
if (nodeLabelFilters.length === 0) return new Set(allLabels);
|
|
4159
|
+
return new Set(nodeLabelFilters);
|
|
4160
|
+
}
|
|
4161
|
+
function isBucketAllOff(bucketLabels, shown) {
|
|
4162
|
+
return bucketLabels.every((l) => !shown.has(l));
|
|
4163
|
+
}
|
|
4164
|
+
function commitShownNodeLabels(shown, allLabels, setNodeFilterState) {
|
|
4165
|
+
if (shown.size === 0) {
|
|
4166
|
+
setNodeFilterState(true, []);
|
|
4167
|
+
} else {
|
|
4168
|
+
setNodeFilterState(
|
|
4169
|
+
false,
|
|
4170
|
+
shown.size === allLabels.length ? [] : [...shown]
|
|
4171
|
+
);
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
function toggleBucketAll(bucketLabels, allLabels, shown, setNodeFilterState) {
|
|
4175
|
+
const next = new Set(shown);
|
|
4176
|
+
if (isBucketAllOff(bucketLabels, next)) {
|
|
4177
|
+
for (const l of bucketLabels) next.add(l);
|
|
4178
|
+
} else {
|
|
4179
|
+
for (const l of bucketLabels) next.delete(l);
|
|
4180
|
+
}
|
|
4181
|
+
commitShownNodeLabels(next, allLabels, setNodeFilterState);
|
|
4182
|
+
}
|
|
4183
|
+
function toggleNodeLabelScoped(label, allLabels, shown, setNodeFilterState) {
|
|
4184
|
+
const next = new Set(shown);
|
|
4185
|
+
if (next.has(label)) {
|
|
4186
|
+
next.delete(label);
|
|
4187
|
+
} else {
|
|
4188
|
+
next.add(label);
|
|
4189
|
+
}
|
|
4190
|
+
commitShownNodeLabels(next, allLabels, setNodeFilterState);
|
|
4191
|
+
}
|
|
4109
4192
|
function GraphOverview({
|
|
4110
4193
|
stats,
|
|
4111
4194
|
colorPalette,
|
|
4112
4195
|
labelCounts,
|
|
4113
4196
|
edgeLabelCounts,
|
|
4114
|
-
edgeToNodeLabels
|
|
4197
|
+
edgeToNodeLabels,
|
|
4198
|
+
nodeBuckets,
|
|
4199
|
+
edgeSectionTitle = "Relations"
|
|
4115
4200
|
}) {
|
|
4116
4201
|
const useGraphFilter2 = useWorkspaceGraphFilter();
|
|
4117
4202
|
const nodeLabelFilters = useGraphFilter2((state) => state.nodeLabelFilters);
|
|
@@ -4120,24 +4205,15 @@ function GraphOverview({
|
|
|
4120
4205
|
const allEdgeLabelsDisabled = useGraphFilter2(
|
|
4121
4206
|
(state) => state.allEdgeLabelsDisabled
|
|
4122
4207
|
);
|
|
4123
|
-
const toggleNodeLabelFilter = useGraphFilter2(
|
|
4124
|
-
(state) => state.toggleNodeLabelFilter
|
|
4125
|
-
);
|
|
4126
|
-
const clearNodeLabelFilters = useGraphFilter2(
|
|
4127
|
-
(state) => state.clearNodeLabelFilters
|
|
4128
|
-
);
|
|
4129
|
-
const toggleAllLabelsDisabled = useGraphFilter2(
|
|
4130
|
-
(state) => state.toggleAllLabelsDisabled
|
|
4131
|
-
);
|
|
4132
4208
|
const toggleEdgeLabelFilter = useGraphFilter2(
|
|
4133
4209
|
(state) => state.toggleEdgeLabelFilter
|
|
4134
4210
|
);
|
|
4135
|
-
const clearEdgeLabelFilters = useGraphFilter2(
|
|
4136
|
-
(state) => state.clearEdgeLabelFilters
|
|
4137
|
-
);
|
|
4138
4211
|
const toggleAllEdgeLabelsDisabled = useGraphFilter2(
|
|
4139
4212
|
(state) => state.toggleAllEdgeLabelsDisabled
|
|
4140
4213
|
);
|
|
4214
|
+
const setNodeFilterState = useGraphFilter2(
|
|
4215
|
+
(state) => state.setNodeFilterState
|
|
4216
|
+
);
|
|
4141
4217
|
const allLabels = Object.keys(labelCounts).sort();
|
|
4142
4218
|
devLog11("[Overview] Rendering labels", {
|
|
4143
4219
|
labels: allLabels,
|
|
@@ -4146,52 +4222,63 @@ function GraphOverview({
|
|
|
4146
4222
|
stats
|
|
4147
4223
|
});
|
|
4148
4224
|
const allEdgeLabels = Object.keys(edgeLabelCounts || {}).sort();
|
|
4149
|
-
const
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
toggleAllEdgeLabelsDisabled();
|
|
4160
|
-
clearEdgeLabelFilters();
|
|
4161
|
-
} else {
|
|
4162
|
-
toggleAllEdgeLabelsDisabled();
|
|
4163
|
-
}
|
|
4164
|
-
};
|
|
4225
|
+
const buckets = nodeBuckets ? nodeBuckets.map((b) => ({
|
|
4226
|
+
title: b.title,
|
|
4227
|
+
labels: b.labels.filter((l) => l in labelCounts).sort()
|
|
4228
|
+
})) : [{ title: "Node Labels", labels: allLabels }];
|
|
4229
|
+
const shownNodeLabels = effectiveShownNodeLabels(
|
|
4230
|
+
allLabels,
|
|
4231
|
+
allLabelsDisabled,
|
|
4232
|
+
nodeLabelFilters
|
|
4233
|
+
);
|
|
4234
|
+
const handleEdgeToggle = toggleAllEdgeLabelsDisabled;
|
|
4165
4235
|
return /* @__PURE__ */ jsxs12(Fragment3, { children: [
|
|
4166
|
-
/* @__PURE__ */ jsx13("p", { className: "mt-2
|
|
4167
|
-
allLabels.length > 0 && /* @__PURE__ */
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
/* @__PURE__ */ jsx13(
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4236
|
+
/* @__PURE__ */ jsx13("p", { className: "mt-2 heading-component", children: "Overview" }),
|
|
4237
|
+
allLabels.length > 0 && /* @__PURE__ */ jsx13("div", { children: buckets.map((bucket) => {
|
|
4238
|
+
if (bucket.labels.length === 0) return null;
|
|
4239
|
+
const bucketAllDisabled = isBucketAllOff(
|
|
4240
|
+
bucket.labels,
|
|
4241
|
+
shownNodeLabels
|
|
4242
|
+
);
|
|
4243
|
+
const onBucketAll = () => toggleBucketAll(
|
|
4244
|
+
bucket.labels,
|
|
4245
|
+
allLabels,
|
|
4246
|
+
shownNodeLabels,
|
|
4247
|
+
setNodeFilterState
|
|
4248
|
+
);
|
|
4249
|
+
return /* @__PURE__ */ jsxs12("div", { className: "mb-2", children: [
|
|
4250
|
+
/* @__PURE__ */ jsx13("p", { className: "heading-meta mb-2", children: bucket.title }),
|
|
4251
|
+
/* @__PURE__ */ jsxs12("div", { className: "flex flex-col items-start gap-2", children: [
|
|
4252
|
+
/* @__PURE__ */ jsx13(
|
|
4253
|
+
AllToggleBadge,
|
|
4254
|
+
{
|
|
4255
|
+
allDisabled: bucketAllDisabled,
|
|
4256
|
+
colorPalette,
|
|
4257
|
+
count: bucketCount(bucket.labels, labelCounts),
|
|
4258
|
+
onToggle: onBucketAll,
|
|
4259
|
+
badgeKey: `all-${bucket.title}`
|
|
4260
|
+
}
|
|
4261
|
+
),
|
|
4262
|
+
/* @__PURE__ */ jsx13(
|
|
4263
|
+
NodeLabelBadges,
|
|
4264
|
+
{
|
|
4265
|
+
labels: bucket.labels,
|
|
4266
|
+
labelCounts,
|
|
4267
|
+
colorPalette,
|
|
4268
|
+
shown: shownNodeLabels,
|
|
4269
|
+
onToggleLabel: (label) => toggleNodeLabelScoped(
|
|
4270
|
+
label,
|
|
4271
|
+
allLabels,
|
|
4272
|
+
shownNodeLabels,
|
|
4273
|
+
setNodeFilterState
|
|
4274
|
+
)
|
|
4275
|
+
}
|
|
4276
|
+
)
|
|
4277
|
+
] })
|
|
4278
|
+
] }, bucket.title);
|
|
4279
|
+
}) }),
|
|
4193
4280
|
allEdgeLabels.length > 0 && /* @__PURE__ */ jsxs12("div", { children: [
|
|
4194
|
-
/* @__PURE__ */ jsx13("p", { className: "
|
|
4281
|
+
/* @__PURE__ */ jsx13("p", { className: "heading-meta mb-2", children: edgeSectionTitle }),
|
|
4195
4282
|
/* @__PURE__ */ jsxs12("div", { className: "flex flex-col items-start gap-2", children: [
|
|
4196
4283
|
/* @__PURE__ */ jsx13(
|
|
4197
4284
|
AllToggleBadge,
|
|
@@ -4219,7 +4306,7 @@ function GraphOverview({
|
|
|
4219
4306
|
] })
|
|
4220
4307
|
] });
|
|
4221
4308
|
}
|
|
4222
|
-
function Overview() {
|
|
4309
|
+
function Overview({ nodeBuckets, edgeSectionTitle } = {}) {
|
|
4223
4310
|
const { stats, colorPalette, labelCounts, edgeLabelCounts, baseNormalized } = useLiveGraphData();
|
|
4224
4311
|
const edgeToNodeLabels = useMemo4(() => {
|
|
4225
4312
|
if (!baseNormalized?.nodesById || !baseNormalized?.edgesById) return {};
|
|
@@ -4230,7 +4317,7 @@ function Overview() {
|
|
|
4230
4317
|
}, [baseNormalized]);
|
|
4231
4318
|
if (stats && stats.values != null && stats.values > 0) {
|
|
4232
4319
|
return /* @__PURE__ */ jsxs12(Fragment3, { children: [
|
|
4233
|
-
/* @__PURE__ */ jsx13("p", { className: "mt-2
|
|
4320
|
+
/* @__PURE__ */ jsx13("p", { className: "mt-2 heading-component", children: "Overview" }),
|
|
4234
4321
|
/* @__PURE__ */ jsxs12("p", { className: "text-sm text-muted-foreground", children: [
|
|
4235
4322
|
formatNumber(stats.values),
|
|
4236
4323
|
" Values"
|
|
@@ -4245,7 +4332,9 @@ function Overview() {
|
|
|
4245
4332
|
colorPalette,
|
|
4246
4333
|
labelCounts,
|
|
4247
4334
|
edgeLabelCounts,
|
|
4248
|
-
edgeToNodeLabels
|
|
4335
|
+
edgeToNodeLabels,
|
|
4336
|
+
nodeBuckets,
|
|
4337
|
+
edgeSectionTitle
|
|
4249
4338
|
}
|
|
4250
4339
|
);
|
|
4251
4340
|
}
|
|
@@ -4291,11 +4380,12 @@ function ActionPanel({ actions }) {
|
|
|
4291
4380
|
var ActionPanel_default = ActionPanel;
|
|
4292
4381
|
|
|
4293
4382
|
// src/components/GraphA11yList/GraphA11yList.tsx
|
|
4294
|
-
import { useMemo as useMemo6, useRef as
|
|
4383
|
+
import { useCallback as useCallback6, useMemo as useMemo6, useRef as useRef5 } from "react";
|
|
4295
4384
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
4296
4385
|
import { cn as cn2 } from "@petrarca/sonnet-core";
|
|
4297
4386
|
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
4298
4387
|
var SR_ONLY = "absolute h-px w-px overflow-hidden whitespace-nowrap border-0 p-0 [clip:rect(0,0,0,0)] [clip-path:inset(50%)] -m-px";
|
|
4388
|
+
var MAX_NODE_SELECTION = 2;
|
|
4299
4389
|
function GraphA11yList({
|
|
4300
4390
|
visible = false,
|
|
4301
4391
|
rowHeight = 28,
|
|
@@ -4329,28 +4419,46 @@ function GraphA11yList({
|
|
|
4329
4419
|
}));
|
|
4330
4420
|
return [...nodeRows, ...edgeRows];
|
|
4331
4421
|
}, [live.nodes, live.edges]);
|
|
4332
|
-
const parentRef =
|
|
4422
|
+
const parentRef = useRef5(null);
|
|
4333
4423
|
const virtualizer = useVirtualizer({
|
|
4334
4424
|
count: rows.length,
|
|
4335
4425
|
getScrollElement: () => parentRef.current,
|
|
4336
4426
|
estimateSize: () => rowHeight,
|
|
4337
4427
|
overscan: 8
|
|
4338
4428
|
});
|
|
4339
|
-
const
|
|
4340
|
-
|
|
4429
|
+
const activateNode = useCallback6(
|
|
4430
|
+
(id, additive) => {
|
|
4341
4431
|
setSelectedEdge(null);
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4432
|
+
if (!additive) {
|
|
4433
|
+
setSelectedNodeIds([id]);
|
|
4434
|
+
return;
|
|
4435
|
+
}
|
|
4436
|
+
if (selectedNodeIds.includes(id)) {
|
|
4437
|
+
setSelectedNodeIds(selectedNodeIds.filter((x) => x !== id));
|
|
4438
|
+
return;
|
|
4439
|
+
}
|
|
4440
|
+
setSelectedNodeIds([...selectedNodeIds, id].slice(-MAX_NODE_SELECTION));
|
|
4441
|
+
},
|
|
4442
|
+
[selectedNodeIds, setSelectedNodeIds, setSelectedEdge]
|
|
4443
|
+
);
|
|
4444
|
+
const selectRow = useCallback6(
|
|
4445
|
+
(row, e) => {
|
|
4446
|
+
if (row.kind === "node") {
|
|
4447
|
+
activateNode(row.id, e.metaKey || e.ctrlKey || e.shiftKey);
|
|
4448
|
+
} else {
|
|
4449
|
+
setSelectedNodeIds([]);
|
|
4450
|
+
setSelectedEdge(row.edge);
|
|
4451
|
+
}
|
|
4452
|
+
},
|
|
4453
|
+
[activateNode, setSelectedNodeIds, setSelectedEdge]
|
|
4454
|
+
);
|
|
4348
4455
|
const isSelected = (row) => row.kind === "node" ? selectedNodeIds.includes(row.id) : selectedEdge?.id_ === row.edge.id_;
|
|
4349
4456
|
return /* @__PURE__ */ jsx15(
|
|
4350
4457
|
"div",
|
|
4351
4458
|
{
|
|
4352
4459
|
role: "listbox",
|
|
4353
4460
|
"aria-label": "Graph elements",
|
|
4461
|
+
"aria-multiselectable": true,
|
|
4354
4462
|
className: cn2(visible ? "rounded-md border" : SR_ONLY, className),
|
|
4355
4463
|
children: /* @__PURE__ */ jsx15(
|
|
4356
4464
|
"div",
|
|
@@ -4378,7 +4486,7 @@ function GraphA11yList({
|
|
|
4378
4486
|
"aria-label": `${row.kind === "node" ? "Node" : "Edge"} ${row.label}`,
|
|
4379
4487
|
"data-testid": `graph-${row.kind}-${row.id}`,
|
|
4380
4488
|
...row.kind === "node" ? { "data-graph-node-id": row.id } : { "data-graph-edge-id": row.id },
|
|
4381
|
-
onClick: () => selectRow(row),
|
|
4489
|
+
onClick: (e) => selectRow(row, e),
|
|
4382
4490
|
className: cn2(
|
|
4383
4491
|
"absolute left-0 top-0 flex w-full items-center px-2 text-left text-sm",
|
|
4384
4492
|
visible && "hover:bg-accent aria-selected:bg-state-selected aria-selected:text-foreground"
|
|
@@ -4399,46 +4507,2977 @@ function GraphA11yList({
|
|
|
4399
4507
|
}
|
|
4400
4508
|
);
|
|
4401
4509
|
}
|
|
4510
|
+
|
|
4511
|
+
// src/graph-model/types.ts
|
|
4512
|
+
function isNodeType(schema) {
|
|
4513
|
+
return schema["x-graph"].type !== "EDGE";
|
|
4514
|
+
}
|
|
4515
|
+
function isEdgeType(schema) {
|
|
4516
|
+
return schema["x-graph"].type === "EDGE";
|
|
4517
|
+
}
|
|
4518
|
+
function typeLabel(schema) {
|
|
4519
|
+
return schema["x-graph"].label || schema.title || "Unknown";
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
// src/graph-model/converter.ts
|
|
4523
|
+
function stableId(seed) {
|
|
4524
|
+
let h = 2166136261;
|
|
4525
|
+
for (let i = 0; i < seed.length; i++) {
|
|
4526
|
+
h ^= seed.charCodeAt(i);
|
|
4527
|
+
h = Math.imul(h, 16777619);
|
|
4528
|
+
}
|
|
4529
|
+
return (h & 2147483647) >>> 0;
|
|
4530
|
+
}
|
|
4531
|
+
function nodeId(typeName) {
|
|
4532
|
+
return stableId(`schema.node.${typeName}`);
|
|
4533
|
+
}
|
|
4534
|
+
function edgeId(source, target, rel) {
|
|
4535
|
+
return stableId(`schema.edge.${source}.${target}.${rel}`);
|
|
4536
|
+
}
|
|
4537
|
+
function toCamelCase(key) {
|
|
4538
|
+
const parts = key.split("-");
|
|
4539
|
+
return parts[0] + parts.slice(1).map((w) => w ? w[0].toUpperCase() + w.slice(1) : "").join("");
|
|
4540
|
+
}
|
|
4541
|
+
function buildNode(schema) {
|
|
4542
|
+
const name = typeLabel(schema);
|
|
4543
|
+
const properties = {
|
|
4544
|
+
gid_: `schema:${name}`,
|
|
4545
|
+
name,
|
|
4546
|
+
type: schema.type ?? "object",
|
|
4547
|
+
propertyDefinitions: schema.properties ?? {},
|
|
4548
|
+
required: schema.required ?? []
|
|
4549
|
+
};
|
|
4550
|
+
for (const key of Object.keys(schema)) {
|
|
4551
|
+
if (key.startsWith("x-")) {
|
|
4552
|
+
properties[toCamelCase(key)] = schema[key];
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
return {
|
|
4556
|
+
id_: nodeId(name),
|
|
4557
|
+
// Type label drives legend colour/filtering; node vs edge type is in xGraph.
|
|
4558
|
+
label_: name,
|
|
4559
|
+
properties_: properties
|
|
4560
|
+
};
|
|
4561
|
+
}
|
|
4562
|
+
function buildRelationEdges(schema, knownTypes) {
|
|
4563
|
+
const source = typeLabel(schema);
|
|
4564
|
+
const sourceId = nodeId(source);
|
|
4565
|
+
const edges = [];
|
|
4566
|
+
for (const relation of schema["x-graph"].relations ?? []) {
|
|
4567
|
+
const target = relation.to_type_name;
|
|
4568
|
+
if (!target || !knownTypes.has(target)) continue;
|
|
4569
|
+
const rel = relation.rel_type_name || "RELATED_TO";
|
|
4570
|
+
const properties = {
|
|
4571
|
+
gid_: `schema:rel:${source}\0${target}`,
|
|
4572
|
+
name: rel,
|
|
4573
|
+
cardinality: relation.cardinality ?? "ONE_TO_MANY"
|
|
4574
|
+
};
|
|
4575
|
+
if (relation.form_field !== void 0)
|
|
4576
|
+
properties.form_field = relation.form_field;
|
|
4577
|
+
if (relation.description !== void 0)
|
|
4578
|
+
properties.description = relation.description;
|
|
4579
|
+
edges.push({
|
|
4580
|
+
id_: edgeId(source, target, rel),
|
|
4581
|
+
label_: rel,
|
|
4582
|
+
start_id_: sourceId,
|
|
4583
|
+
end_id_: nodeId(target),
|
|
4584
|
+
properties_: properties
|
|
4585
|
+
});
|
|
4586
|
+
}
|
|
4587
|
+
return edges;
|
|
4588
|
+
}
|
|
4589
|
+
function buildInheritanceEdges(schema, knownTypes) {
|
|
4590
|
+
const source = typeLabel(schema);
|
|
4591
|
+
const sourceId = nodeId(source);
|
|
4592
|
+
const edges = [];
|
|
4593
|
+
for (const parent of schema["x-graph"].inherits_from ?? []) {
|
|
4594
|
+
if (!knownTypes.has(parent)) continue;
|
|
4595
|
+
edges.push({
|
|
4596
|
+
id_: edgeId(source, parent, "INHERITS_FROM_"),
|
|
4597
|
+
label_: "INHERITS_FROM_",
|
|
4598
|
+
start_id_: sourceId,
|
|
4599
|
+
end_id_: nodeId(parent),
|
|
4600
|
+
properties_: {
|
|
4601
|
+
gid_: `schema:rel:${source}\0${parent}`,
|
|
4602
|
+
name: "INHERITS_FROM_",
|
|
4603
|
+
parent
|
|
4604
|
+
}
|
|
4605
|
+
});
|
|
4606
|
+
}
|
|
4607
|
+
return edges;
|
|
4608
|
+
}
|
|
4609
|
+
function modelToGraph(model) {
|
|
4610
|
+
const nodeTypes = model.filter(isNodeType);
|
|
4611
|
+
const knownTypes = new Set(nodeTypes.map(typeLabel));
|
|
4612
|
+
const nodes = model.map(buildNode);
|
|
4613
|
+
const edges = [];
|
|
4614
|
+
for (const schema of nodeTypes) {
|
|
4615
|
+
edges.push(...buildRelationEdges(schema, knownTypes));
|
|
4616
|
+
edges.push(...buildInheritanceEdges(schema, knownTypes));
|
|
4617
|
+
}
|
|
4618
|
+
return { nodes, edges };
|
|
4619
|
+
}
|
|
4620
|
+
|
|
4621
|
+
// src/graph-model/filter.ts
|
|
4622
|
+
function reconcileFilter(current, model, baselineReset, showEdgeTypes) {
|
|
4623
|
+
const nodeTypeLabels2 = model.filter(isNodeType).map(typeLabel);
|
|
4624
|
+
if (baselineReset) {
|
|
4625
|
+
return showEdgeTypes ? [] : nodeTypeLabels2;
|
|
4626
|
+
}
|
|
4627
|
+
if (current.length === 0) return [];
|
|
4628
|
+
const allLabels = new Set(model.map(typeLabel));
|
|
4629
|
+
const kept = current.filter((l) => allLabels.has(l));
|
|
4630
|
+
const keptSet = new Set(kept);
|
|
4631
|
+
const added = nodeTypeLabels2.filter((l) => !keptSet.has(l));
|
|
4632
|
+
return added.length > 0 ? [...kept, ...added] : kept;
|
|
4633
|
+
}
|
|
4634
|
+
|
|
4635
|
+
// src/graph-model/GraphModelEditor.tsx
|
|
4636
|
+
import { useCallback as useCallback7, useEffect as useEffect5, useRef as useRef6, useState as useState13 } from "react";
|
|
4637
|
+
import { ResizeHandle } from "@petrarca/sonnet-ui";
|
|
4638
|
+
|
|
4639
|
+
// src/graph-model/ModelActionPanel.tsx
|
|
4640
|
+
import { useMemo as useMemo8, useState as useState12 } from "react";
|
|
4641
|
+
import { Button as Button6, IconButton as IconButton2 } from "@petrarca/sonnet-ui";
|
|
4642
|
+
import { GitBranchPlus, CirclePlus, Waypoints } from "lucide-react";
|
|
4643
|
+
|
|
4644
|
+
// src/graph-model/TypeDetailPanel.tsx
|
|
4645
|
+
import { useState as useState11 } from "react";
|
|
4646
|
+
import { Badge as Badge2, IconButton } from "@petrarca/sonnet-ui";
|
|
4647
|
+
import { Plus, Pencil, Trash2 } from "lucide-react";
|
|
4648
|
+
|
|
4649
|
+
// src/graph-model/view.ts
|
|
4650
|
+
function cardinalityLabel(c) {
|
|
4651
|
+
return c === "ONE_TO_ONE" ? "one-to-one" : "one-to-many";
|
|
4652
|
+
}
|
|
4653
|
+
function propTypeOf(prop) {
|
|
4654
|
+
if (Array.isArray(prop.type)) return prop.type.join(" | ");
|
|
4655
|
+
return prop.type ?? "object";
|
|
4656
|
+
}
|
|
4657
|
+
function toPropertyViews(schema) {
|
|
4658
|
+
if (!schema?.properties) return [];
|
|
4659
|
+
const required = new Set(schema.required ?? []);
|
|
4660
|
+
return Object.entries(schema.properties).map(([name, prop]) => ({
|
|
4661
|
+
name,
|
|
4662
|
+
type: propTypeOf(prop),
|
|
4663
|
+
required: required.has(name),
|
|
4664
|
+
title: typeof prop.title === "string" ? prop.title : void 0,
|
|
4665
|
+
description: typeof prop.description === "string" ? prop.description : void 0,
|
|
4666
|
+
widget: typeof prop["x-ui-widget"] === "string" ? prop["x-ui-widget"] : void 0,
|
|
4667
|
+
enum: prop.enum
|
|
4668
|
+
}));
|
|
4669
|
+
}
|
|
4670
|
+
function toRelationViews(schema) {
|
|
4671
|
+
const relations = schema["x-graph"].relations ?? [];
|
|
4672
|
+
return relations.map((r) => ({
|
|
4673
|
+
relType: r.rel_type_name,
|
|
4674
|
+
target: r.to_type_name,
|
|
4675
|
+
cardinality: r.cardinality ?? "ONE_TO_MANY",
|
|
4676
|
+
description: r.description
|
|
4677
|
+
}));
|
|
4678
|
+
}
|
|
4679
|
+
function findType(model, label) {
|
|
4680
|
+
return model.find((s) => typeLabel(s) === label);
|
|
4681
|
+
}
|
|
4682
|
+
function findEdgeType(model, relType) {
|
|
4683
|
+
return model.find(
|
|
4684
|
+
(s) => s["x-graph"].type === "EDGE" && typeLabel(s) === relType
|
|
4685
|
+
);
|
|
4686
|
+
}
|
|
4687
|
+
function resolveRelationEdge(edge) {
|
|
4688
|
+
if (edge.label_ === "INHERITS_FROM_") return null;
|
|
4689
|
+
const gid = edge.properties_?.gid_;
|
|
4690
|
+
if (typeof gid !== "string" || !gid.startsWith("schema:rel:")) return null;
|
|
4691
|
+
const rest = gid.slice("schema:rel:".length);
|
|
4692
|
+
const sep = rest.indexOf("\0");
|
|
4693
|
+
if (sep < 0) return null;
|
|
4694
|
+
return {
|
|
4695
|
+
source: rest.slice(0, sep),
|
|
4696
|
+
relType: edge.label_,
|
|
4697
|
+
target: rest.slice(sep + 1)
|
|
4698
|
+
};
|
|
4699
|
+
}
|
|
4700
|
+
function nodeTypeView(model, label) {
|
|
4701
|
+
const schema = findType(model, label);
|
|
4702
|
+
if (!schema || !isNodeType(schema)) return null;
|
|
4703
|
+
return {
|
|
4704
|
+
kind: "node",
|
|
4705
|
+
label: typeLabel(schema),
|
|
4706
|
+
description: typeof schema.description === "string" ? schema.description : void 0,
|
|
4707
|
+
properties: toPropertyViews(schema),
|
|
4708
|
+
relations: toRelationViews(schema)
|
|
4709
|
+
};
|
|
4710
|
+
}
|
|
4711
|
+
function edgeTypeView(model, label) {
|
|
4712
|
+
const schema = findType(model, label);
|
|
4713
|
+
if (!schema || isNodeType(schema)) return null;
|
|
4714
|
+
return {
|
|
4715
|
+
kind: "edge",
|
|
4716
|
+
label: typeLabel(schema),
|
|
4717
|
+
description: typeof schema.description === "string" ? schema.description : void 0,
|
|
4718
|
+
properties: toPropertyViews(schema)
|
|
4719
|
+
};
|
|
4720
|
+
}
|
|
4721
|
+
function relationView(model, source, relType, target) {
|
|
4722
|
+
const sourceSchema = findType(model, source);
|
|
4723
|
+
if (!sourceSchema || !isNodeType(sourceSchema)) return null;
|
|
4724
|
+
const relation = (sourceSchema["x-graph"].relations ?? []).find(
|
|
4725
|
+
(r) => r.rel_type_name === relType && r.to_type_name === target
|
|
4726
|
+
);
|
|
4727
|
+
if (!relation) return null;
|
|
4728
|
+
const edgeType = findEdgeType(model, relType);
|
|
4729
|
+
return {
|
|
4730
|
+
kind: "relation",
|
|
4731
|
+
relType,
|
|
4732
|
+
source,
|
|
4733
|
+
target,
|
|
4734
|
+
cardinality: relation.cardinality ?? "ONE_TO_MANY",
|
|
4735
|
+
description: relation.description,
|
|
4736
|
+
hasEdgeType: !!edgeType,
|
|
4737
|
+
properties: toPropertyViews(edgeType)
|
|
4738
|
+
};
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4741
|
+
// src/graph-model/mutate.ts
|
|
4742
|
+
function toSchemaProperty(input) {
|
|
4743
|
+
const prop = { type: input.type };
|
|
4744
|
+
if (input.title) prop.title = input.title;
|
|
4745
|
+
if (input.description) prop.description = input.description;
|
|
4746
|
+
if (input.widget && input.widget !== "default")
|
|
4747
|
+
prop["x-ui-widget"] = input.widget;
|
|
4748
|
+
if (input.enum && input.enum.length) prop.enum = input.enum;
|
|
4749
|
+
return prop;
|
|
4750
|
+
}
|
|
4751
|
+
function replaceSchema(model, label, next) {
|
|
4752
|
+
return model.map((s) => typeLabel(s) === label ? next : s);
|
|
4753
|
+
}
|
|
4754
|
+
function withProperty(schema, input, prevName) {
|
|
4755
|
+
const properties = {
|
|
4756
|
+
...schema.properties ?? {}
|
|
4757
|
+
};
|
|
4758
|
+
const required = new Set(schema.required ?? []);
|
|
4759
|
+
if (prevName) {
|
|
4760
|
+
delete properties[prevName];
|
|
4761
|
+
required.delete(prevName);
|
|
4762
|
+
}
|
|
4763
|
+
if (input) {
|
|
4764
|
+
properties[input.name] = toSchemaProperty(input);
|
|
4765
|
+
if (input.required) required.add(input.name);
|
|
4766
|
+
else required.delete(input.name);
|
|
4767
|
+
}
|
|
4768
|
+
const next = { ...schema, properties };
|
|
4769
|
+
if (required.size) next.required = [...required];
|
|
4770
|
+
else delete next.required;
|
|
4771
|
+
return next;
|
|
4772
|
+
}
|
|
4773
|
+
function setNodeProperty(model, typeLabel_, input, prevName) {
|
|
4774
|
+
const schema = findType(model, typeLabel_);
|
|
4775
|
+
if (!schema) return model;
|
|
4776
|
+
return replaceSchema(
|
|
4777
|
+
model,
|
|
4778
|
+
typeLabel_,
|
|
4779
|
+
withProperty(schema, input, prevName)
|
|
4780
|
+
);
|
|
4781
|
+
}
|
|
4782
|
+
function deleteNodeProperty(model, typeLabel_, name) {
|
|
4783
|
+
const schema = findType(model, typeLabel_);
|
|
4784
|
+
if (!schema) return model;
|
|
4785
|
+
return replaceSchema(model, typeLabel_, withProperty(schema, null, name));
|
|
4786
|
+
}
|
|
4787
|
+
function newEdgeType(relType) {
|
|
4788
|
+
return {
|
|
4789
|
+
title: relType,
|
|
4790
|
+
type: "object",
|
|
4791
|
+
properties: {},
|
|
4792
|
+
"x-graph": { type: "EDGE", label: relType }
|
|
4793
|
+
};
|
|
4794
|
+
}
|
|
4795
|
+
function setRelationProperty(model, relType, input, prevName) {
|
|
4796
|
+
const existing = findEdgeType(model, relType);
|
|
4797
|
+
const base = existing ?? newEdgeType(relType);
|
|
4798
|
+
const next = withProperty(base, input, prevName);
|
|
4799
|
+
return existing ? replaceSchema(model, relType, next) : [...model, next];
|
|
4800
|
+
}
|
|
4801
|
+
function deleteRelationProperty(model, relType, name) {
|
|
4802
|
+
const existing = findEdgeType(model, relType);
|
|
4803
|
+
if (!existing) return model;
|
|
4804
|
+
const next = withProperty(existing, null, name);
|
|
4805
|
+
const empty = Object.keys(next.properties ?? {}).length === 0;
|
|
4806
|
+
if (empty) return model.filter((s) => s !== existing);
|
|
4807
|
+
return replaceSchema(model, relType, next);
|
|
4808
|
+
}
|
|
4809
|
+
function setEdgeTypeProperty(model, edgeLabel, input, prevName) {
|
|
4810
|
+
const existing = findEdgeType(model, edgeLabel);
|
|
4811
|
+
if (!existing) return model;
|
|
4812
|
+
return replaceSchema(
|
|
4813
|
+
model,
|
|
4814
|
+
edgeLabel,
|
|
4815
|
+
withProperty(existing, input, prevName)
|
|
4816
|
+
);
|
|
4817
|
+
}
|
|
4818
|
+
function deleteEdgeTypeProperty(model, edgeLabel, name) {
|
|
4819
|
+
const existing = findEdgeType(model, edgeLabel);
|
|
4820
|
+
if (!existing) return model;
|
|
4821
|
+
return replaceSchema(model, edgeLabel, withProperty(existing, null, name));
|
|
4822
|
+
}
|
|
4823
|
+
function addEdgeType(model, label) {
|
|
4824
|
+
const name = label.trim();
|
|
4825
|
+
if (!name || model.some((s) => typeLabel(s) === name)) return model;
|
|
4826
|
+
return [...model, newEdgeType(name)];
|
|
4827
|
+
}
|
|
4828
|
+
function addRelation(model, source, input) {
|
|
4829
|
+
const schema = findType(model, source);
|
|
4830
|
+
if (!schema) return model;
|
|
4831
|
+
const relations = schema["x-graph"].relations ?? [];
|
|
4832
|
+
if (relations.some(
|
|
4833
|
+
(r) => r.rel_type_name === input.relType && r.to_type_name === input.target
|
|
4834
|
+
)) {
|
|
4835
|
+
return model;
|
|
4836
|
+
}
|
|
4837
|
+
const rel = {
|
|
4838
|
+
rel_type_name: input.relType,
|
|
4839
|
+
to_type_name: input.target,
|
|
4840
|
+
cardinality: input.cardinality
|
|
4841
|
+
};
|
|
4842
|
+
if (input.description) rel.description = input.description;
|
|
4843
|
+
const next = {
|
|
4844
|
+
...schema,
|
|
4845
|
+
"x-graph": { ...schema["x-graph"], relations: [...relations, rel] }
|
|
4846
|
+
};
|
|
4847
|
+
return replaceSchema(model, source, next);
|
|
4848
|
+
}
|
|
4849
|
+
function updateRelation(model, source, prevRelType, prevTarget, input) {
|
|
4850
|
+
const schema = findType(model, source);
|
|
4851
|
+
if (!schema) return model;
|
|
4852
|
+
const relations = (schema["x-graph"].relations ?? []).map(
|
|
4853
|
+
(r) => r.rel_type_name === prevRelType && r.to_type_name === prevTarget ? {
|
|
4854
|
+
rel_type_name: input.relType,
|
|
4855
|
+
to_type_name: input.target,
|
|
4856
|
+
cardinality: input.cardinality,
|
|
4857
|
+
...input.description ? { description: input.description } : {}
|
|
4858
|
+
} : r
|
|
4859
|
+
);
|
|
4860
|
+
const next = {
|
|
4861
|
+
...schema,
|
|
4862
|
+
"x-graph": { ...schema["x-graph"], relations }
|
|
4863
|
+
};
|
|
4864
|
+
return replaceSchema(model, source, next);
|
|
4865
|
+
}
|
|
4866
|
+
function addNodeType(model, label) {
|
|
4867
|
+
const name = label.trim();
|
|
4868
|
+
if (!name || model.some((s) => typeLabel(s) === name)) return model;
|
|
4869
|
+
const schema = {
|
|
4870
|
+
title: name,
|
|
4871
|
+
type: "object",
|
|
4872
|
+
properties: {},
|
|
4873
|
+
"x-graph": { type: "NODE", label: name }
|
|
4874
|
+
};
|
|
4875
|
+
return [...model, schema];
|
|
4876
|
+
}
|
|
4877
|
+
function renameNodeType(model, oldLabel, newLabel) {
|
|
4878
|
+
const name = newLabel.trim();
|
|
4879
|
+
if (!name || name === oldLabel) return model;
|
|
4880
|
+
if (model.some((s) => typeLabel(s) === name)) return model;
|
|
4881
|
+
return model.map((s) => {
|
|
4882
|
+
const xg = s["x-graph"];
|
|
4883
|
+
const isTarget = typeLabel(s) === oldLabel;
|
|
4884
|
+
const relations = xg.relations?.map(
|
|
4885
|
+
(r) => r.to_type_name === oldLabel ? { ...r, to_type_name: name } : r
|
|
4886
|
+
);
|
|
4887
|
+
const inherits = xg.inherits_from?.map((p) => p === oldLabel ? name : p);
|
|
4888
|
+
return {
|
|
4889
|
+
...s,
|
|
4890
|
+
...isTarget && s.title ? { title: name } : {},
|
|
4891
|
+
"x-graph": {
|
|
4892
|
+
...xg,
|
|
4893
|
+
...isTarget ? { label: name } : {},
|
|
4894
|
+
...relations ? { relations } : {},
|
|
4895
|
+
...inherits ? { inherits_from: inherits } : {}
|
|
4896
|
+
}
|
|
4897
|
+
};
|
|
4898
|
+
});
|
|
4899
|
+
}
|
|
4900
|
+
function deleteNodeType(model, label) {
|
|
4901
|
+
return model.filter((s) => typeLabel(s) !== label).map((s) => {
|
|
4902
|
+
const xg = s["x-graph"];
|
|
4903
|
+
const relations = xg.relations?.filter((r) => r.to_type_name !== label);
|
|
4904
|
+
const inherits = xg.inherits_from?.filter((p) => p !== label);
|
|
4905
|
+
return {
|
|
4906
|
+
...s,
|
|
4907
|
+
"x-graph": {
|
|
4908
|
+
...xg,
|
|
4909
|
+
...relations ? { relations } : {},
|
|
4910
|
+
...inherits ? { inherits_from: inherits } : {}
|
|
4911
|
+
}
|
|
4912
|
+
};
|
|
4913
|
+
});
|
|
4914
|
+
}
|
|
4915
|
+
function deleteRelation(model, source, relType, target) {
|
|
4916
|
+
const schema = findType(model, source);
|
|
4917
|
+
if (!schema) return model;
|
|
4918
|
+
const relations = (schema["x-graph"].relations ?? []).filter(
|
|
4919
|
+
(r) => !(r.rel_type_name === relType && r.to_type_name === target)
|
|
4920
|
+
);
|
|
4921
|
+
const next = {
|
|
4922
|
+
...schema,
|
|
4923
|
+
"x-graph": { ...schema["x-graph"], relations }
|
|
4924
|
+
};
|
|
4925
|
+
return replaceSchema(model, source, next);
|
|
4926
|
+
}
|
|
4927
|
+
function renameEdgeType(model, oldLabel, newLabel) {
|
|
4928
|
+
const name = newLabel.trim();
|
|
4929
|
+
if (!name || name === oldLabel) return model;
|
|
4930
|
+
if (model.some((s) => typeLabel(s) === name)) return model;
|
|
4931
|
+
const edgeType = findEdgeType(model, oldLabel);
|
|
4932
|
+
if (!edgeType) return model;
|
|
4933
|
+
return model.map((s) => {
|
|
4934
|
+
const xg = s["x-graph"];
|
|
4935
|
+
const isTarget = s === edgeType;
|
|
4936
|
+
const relations = xg.relations?.map(
|
|
4937
|
+
(r) => r.rel_type_name === oldLabel ? { ...r, rel_type_name: name } : r
|
|
4938
|
+
);
|
|
4939
|
+
return {
|
|
4940
|
+
...s,
|
|
4941
|
+
...isTarget && s.title ? { title: name } : {},
|
|
4942
|
+
"x-graph": {
|
|
4943
|
+
...xg,
|
|
4944
|
+
...isTarget ? { label: name } : {},
|
|
4945
|
+
...relations ? { relations } : {}
|
|
4946
|
+
}
|
|
4947
|
+
};
|
|
4948
|
+
});
|
|
4949
|
+
}
|
|
4950
|
+
function deleteEdgeType(model, label) {
|
|
4951
|
+
return model.filter((s) => !(isEdgeType(s) && typeLabel(s) === label));
|
|
4952
|
+
}
|
|
4953
|
+
|
|
4954
|
+
// src/graph-model/PropertyFormSheet.tsx
|
|
4955
|
+
import { useMemo as useMemo7 } from "react";
|
|
4956
|
+
import {
|
|
4957
|
+
Sheet,
|
|
4958
|
+
SheetContent,
|
|
4959
|
+
SheetHeader,
|
|
4960
|
+
SheetBody,
|
|
4961
|
+
SheetTitle,
|
|
4962
|
+
SheetDescription
|
|
4963
|
+
} from "@petrarca/sonnet-ui";
|
|
4964
|
+
import { JsonSchemaFormRenderer } from "@petrarca/sonnet-forms";
|
|
4965
|
+
import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
4966
|
+
var TYPE_OPTIONS = [
|
|
4967
|
+
"string",
|
|
4968
|
+
"integer",
|
|
4969
|
+
"number",
|
|
4970
|
+
"boolean",
|
|
4971
|
+
"array",
|
|
4972
|
+
"object"
|
|
4973
|
+
];
|
|
4974
|
+
var DEFAULT_WIDGETS = ["default"];
|
|
4975
|
+
function buildSchema(widgetOptions) {
|
|
4976
|
+
return {
|
|
4977
|
+
type: "object",
|
|
4978
|
+
properties: {
|
|
4979
|
+
name: {
|
|
4980
|
+
type: "string",
|
|
4981
|
+
title: "Property name",
|
|
4982
|
+
description: "The JSON Schema property key."
|
|
4983
|
+
},
|
|
4984
|
+
type: { type: "string", title: "Type", enum: TYPE_OPTIONS },
|
|
4985
|
+
required: { type: "boolean", title: "Required" },
|
|
4986
|
+
title: { type: "string", title: "Title" },
|
|
4987
|
+
description: {
|
|
4988
|
+
type: "string",
|
|
4989
|
+
title: "Description",
|
|
4990
|
+
"x-ui-widget": "textarea"
|
|
4991
|
+
},
|
|
4992
|
+
widget: {
|
|
4993
|
+
type: "string",
|
|
4994
|
+
title: "Widget",
|
|
4995
|
+
description: "x-ui-widget -- render this property with a specific widget.",
|
|
4996
|
+
enum: widgetOptions
|
|
4997
|
+
},
|
|
4998
|
+
enumValues: {
|
|
4999
|
+
type: "array",
|
|
5000
|
+
title: "Allowed values",
|
|
5001
|
+
description: "Restrict the property to a fixed set of values (leave empty for free input).",
|
|
5002
|
+
items: { type: "string" },
|
|
5003
|
+
"x-ui-widget": "tags"
|
|
5004
|
+
}
|
|
5005
|
+
},
|
|
5006
|
+
required: ["name", "type"]
|
|
5007
|
+
};
|
|
5008
|
+
}
|
|
5009
|
+
function toFormData(prop) {
|
|
5010
|
+
if (!prop)
|
|
5011
|
+
return {
|
|
5012
|
+
type: "string",
|
|
5013
|
+
required: false,
|
|
5014
|
+
widget: "default",
|
|
5015
|
+
enumValues: []
|
|
5016
|
+
};
|
|
5017
|
+
return {
|
|
5018
|
+
name: prop.name,
|
|
5019
|
+
type: prop.type,
|
|
5020
|
+
required: prop.required,
|
|
5021
|
+
title: prop.title ?? "",
|
|
5022
|
+
description: prop.description ?? "",
|
|
5023
|
+
widget: prop.widget ?? "default",
|
|
5024
|
+
enumValues: prop.enum ? prop.enum.map(String) : []
|
|
5025
|
+
};
|
|
5026
|
+
}
|
|
5027
|
+
function toInput(data) {
|
|
5028
|
+
const raw = data.enumValues;
|
|
5029
|
+
const enumValues = Array.isArray(raw) ? raw.map(String).filter((v) => v.length > 0) : void 0;
|
|
5030
|
+
return {
|
|
5031
|
+
name: String(data.name ?? "").trim(),
|
|
5032
|
+
type: String(data.type ?? "string"),
|
|
5033
|
+
required: Boolean(data.required),
|
|
5034
|
+
title: data.title ? String(data.title) : void 0,
|
|
5035
|
+
description: data.description ? String(data.description) : void 0,
|
|
5036
|
+
widget: data.widget ? String(data.widget) : void 0,
|
|
5037
|
+
enum: enumValues && enumValues.length ? enumValues : void 0
|
|
5038
|
+
};
|
|
5039
|
+
}
|
|
5040
|
+
function PropertyFormSheet({
|
|
5041
|
+
open,
|
|
5042
|
+
onOpenChange,
|
|
5043
|
+
property,
|
|
5044
|
+
ownerLabel,
|
|
5045
|
+
widgetOptions,
|
|
5046
|
+
onSave
|
|
5047
|
+
}) {
|
|
5048
|
+
const widgets = useMemo7(
|
|
5049
|
+
() => [...DEFAULT_WIDGETS, ...widgetOptions ?? []],
|
|
5050
|
+
[widgetOptions]
|
|
5051
|
+
);
|
|
5052
|
+
const schema = useMemo7(() => buildSchema(widgets), [widgets]);
|
|
5053
|
+
const data = useMemo7(() => toFormData(property), [property]);
|
|
5054
|
+
const isEdit = !!property;
|
|
5055
|
+
return /* @__PURE__ */ jsx16(Sheet, { open, onOpenChange, children: /* @__PURE__ */ jsxs14(SheetContent, { children: [
|
|
5056
|
+
/* @__PURE__ */ jsxs14(SheetHeader, { children: [
|
|
5057
|
+
/* @__PURE__ */ jsxs14(SheetTitle, { children: [
|
|
5058
|
+
isEdit ? "Edit property" : "Add property",
|
|
5059
|
+
" ",
|
|
5060
|
+
"\u2014",
|
|
5061
|
+
" ",
|
|
5062
|
+
ownerLabel
|
|
5063
|
+
] }),
|
|
5064
|
+
/* @__PURE__ */ jsx16(SheetDescription, { children: "Define the property and how it is rendered." })
|
|
5065
|
+
] }),
|
|
5066
|
+
/* @__PURE__ */ jsx16(SheetBody, { children: /* @__PURE__ */ jsx16(
|
|
5067
|
+
JsonSchemaFormRenderer,
|
|
5068
|
+
{
|
|
5069
|
+
schema,
|
|
5070
|
+
data,
|
|
5071
|
+
showActions: true,
|
|
5072
|
+
saveLabel: isEdit ? "Save" : "Add",
|
|
5073
|
+
showExtraProperties: false,
|
|
5074
|
+
onUpdate: (d) => {
|
|
5075
|
+
onSave(toInput(d), property?.name);
|
|
5076
|
+
onOpenChange(false);
|
|
5077
|
+
},
|
|
5078
|
+
onCancel: () => onOpenChange(false)
|
|
5079
|
+
}
|
|
5080
|
+
) })
|
|
5081
|
+
] }) });
|
|
5082
|
+
}
|
|
5083
|
+
|
|
5084
|
+
// src/graph-model/RelationFormSheet.tsx
|
|
5085
|
+
import { useState as useState9 } from "react";
|
|
5086
|
+
import {
|
|
5087
|
+
Sheet as Sheet2,
|
|
5088
|
+
SheetContent as SheetContent2,
|
|
5089
|
+
SheetHeader as SheetHeader2,
|
|
5090
|
+
SheetBody as SheetBody2,
|
|
5091
|
+
SheetTitle as SheetTitle2,
|
|
5092
|
+
SheetDescription as SheetDescription2,
|
|
5093
|
+
Button as Button4
|
|
5094
|
+
} from "@petrarca/sonnet-ui";
|
|
5095
|
+
import { FormSelect } from "@petrarca/sonnet-forms";
|
|
5096
|
+
import { ArrowRight } from "lucide-react";
|
|
5097
|
+
import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
5098
|
+
var CARDINALITIES = ["ONE_TO_MANY", "ONE_TO_ONE"];
|
|
5099
|
+
function RelationTypeField({
|
|
5100
|
+
relType,
|
|
5101
|
+
setRelType,
|
|
5102
|
+
edgeTypeOptions,
|
|
5103
|
+
edgeTypeMode
|
|
5104
|
+
}) {
|
|
5105
|
+
const labels = Array.from(
|
|
5106
|
+
/* @__PURE__ */ new Set([...edgeTypeOptions, ...relType ? [relType] : []])
|
|
5107
|
+
);
|
|
5108
|
+
const options = labels.map((l) => ({ value: l, label: l }));
|
|
5109
|
+
const canCreate = edgeTypeMode === "edit";
|
|
5110
|
+
return /* @__PURE__ */ jsx17(
|
|
5111
|
+
FormSelect,
|
|
5112
|
+
{
|
|
5113
|
+
label: "Relation type",
|
|
5114
|
+
options,
|
|
5115
|
+
value: relType || null,
|
|
5116
|
+
onChange: (v) => setRelType(v ?? ""),
|
|
5117
|
+
allowCreate: canCreate,
|
|
5118
|
+
onCreate: setRelType,
|
|
5119
|
+
clearable: false,
|
|
5120
|
+
placeholder: "Select an edge type",
|
|
5121
|
+
searchPlaceholder: canCreate ? "Search or type to create..." : "Search edge types...",
|
|
5122
|
+
emptyMessage: canCreate ? "Type a name to create a new edge type." : "No matching edge type."
|
|
5123
|
+
}
|
|
5124
|
+
);
|
|
5125
|
+
}
|
|
5126
|
+
function DirectionField({
|
|
5127
|
+
source,
|
|
5128
|
+
target,
|
|
5129
|
+
canSwap,
|
|
5130
|
+
onSwap
|
|
5131
|
+
}) {
|
|
5132
|
+
return /* @__PURE__ */ jsxs15("div", { children: [
|
|
5133
|
+
/* @__PURE__ */ jsx17("label", { className: "mb-1 block text-xs font-medium", children: "Direction" }),
|
|
5134
|
+
/* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2 text-sm", children: [
|
|
5135
|
+
/* @__PURE__ */ jsx17("span", { className: "font-medium", children: source }),
|
|
5136
|
+
/* @__PURE__ */ jsx17(ArrowRight, { className: "h-4 w-4 text-muted-foreground" }),
|
|
5137
|
+
/* @__PURE__ */ jsx17("span", { className: "font-medium", children: target }),
|
|
5138
|
+
canSwap && /* @__PURE__ */ jsx17(
|
|
5139
|
+
Button4,
|
|
5140
|
+
{
|
|
5141
|
+
variant: "ghost",
|
|
5142
|
+
size: "sm",
|
|
5143
|
+
className: "ml-auto",
|
|
5144
|
+
onClick: onSwap,
|
|
5145
|
+
children: "Swap"
|
|
5146
|
+
}
|
|
5147
|
+
)
|
|
5148
|
+
] })
|
|
5149
|
+
] });
|
|
5150
|
+
}
|
|
5151
|
+
function CardinalityField({
|
|
5152
|
+
cardinality,
|
|
5153
|
+
onChange
|
|
5154
|
+
}) {
|
|
5155
|
+
return /* @__PURE__ */ jsxs15("div", { children: [
|
|
5156
|
+
/* @__PURE__ */ jsx17("label", { className: "mb-1 block text-xs font-medium", children: "Cardinality" }),
|
|
5157
|
+
/* @__PURE__ */ jsx17("div", { className: "flex gap-2", children: CARDINALITIES.map((c) => /* @__PURE__ */ jsx17(
|
|
5158
|
+
Button4,
|
|
5159
|
+
{
|
|
5160
|
+
variant: c === cardinality ? "default" : "outline",
|
|
5161
|
+
size: "sm",
|
|
5162
|
+
onClick: () => onChange(c),
|
|
5163
|
+
children: cardinalityLabel(c)
|
|
5164
|
+
},
|
|
5165
|
+
c
|
|
5166
|
+
)) })
|
|
5167
|
+
] });
|
|
5168
|
+
}
|
|
5169
|
+
function RelationFormSheet({
|
|
5170
|
+
open,
|
|
5171
|
+
onOpenChange,
|
|
5172
|
+
nodeA,
|
|
5173
|
+
nodeB,
|
|
5174
|
+
initial,
|
|
5175
|
+
edgeTypeOptions,
|
|
5176
|
+
edgeTypeMode = "edit",
|
|
5177
|
+
onSave
|
|
5178
|
+
}) {
|
|
5179
|
+
const [relType, setRelType] = useState9(initial?.relType ?? "");
|
|
5180
|
+
const [swapped, setSwapped] = useState9(false);
|
|
5181
|
+
const [cardinality, setCardinality] = useState9(
|
|
5182
|
+
initial?.cardinality ?? "ONE_TO_MANY"
|
|
5183
|
+
);
|
|
5184
|
+
const source = swapped ? nodeB : nodeA;
|
|
5185
|
+
const target = swapped ? nodeA : nodeB;
|
|
5186
|
+
const isEdit = !!initial;
|
|
5187
|
+
const canSave = relType.trim().length > 0;
|
|
5188
|
+
const handleSave = () => {
|
|
5189
|
+
if (!canSave) return;
|
|
5190
|
+
onSave(source, target, {
|
|
5191
|
+
relType: relType.trim(),
|
|
5192
|
+
target,
|
|
5193
|
+
cardinality
|
|
5194
|
+
});
|
|
5195
|
+
onOpenChange(false);
|
|
5196
|
+
};
|
|
5197
|
+
return /* @__PURE__ */ jsx17(Sheet2, { open, onOpenChange, children: /* @__PURE__ */ jsxs15(SheetContent2, { children: [
|
|
5198
|
+
/* @__PURE__ */ jsxs15(SheetHeader2, { children: [
|
|
5199
|
+
/* @__PURE__ */ jsx17(SheetTitle2, { children: isEdit ? "Edit relation" : "Create relation" }),
|
|
5200
|
+
/* @__PURE__ */ jsx17(SheetDescription2, { children: "A relation is stored on the source node type." })
|
|
5201
|
+
] }),
|
|
5202
|
+
/* @__PURE__ */ jsxs15(SheetBody2, { className: "flex flex-col gap-4", children: [
|
|
5203
|
+
/* @__PURE__ */ jsx17(
|
|
5204
|
+
RelationTypeField,
|
|
5205
|
+
{
|
|
5206
|
+
relType,
|
|
5207
|
+
setRelType,
|
|
5208
|
+
edgeTypeOptions,
|
|
5209
|
+
edgeTypeMode
|
|
5210
|
+
}
|
|
5211
|
+
),
|
|
5212
|
+
/* @__PURE__ */ jsx17(
|
|
5213
|
+
DirectionField,
|
|
5214
|
+
{
|
|
5215
|
+
source,
|
|
5216
|
+
target,
|
|
5217
|
+
canSwap: !isEdit,
|
|
5218
|
+
onSwap: () => setSwapped((s) => !s)
|
|
5219
|
+
}
|
|
5220
|
+
),
|
|
5221
|
+
/* @__PURE__ */ jsx17(
|
|
5222
|
+
CardinalityField,
|
|
5223
|
+
{
|
|
5224
|
+
cardinality,
|
|
5225
|
+
onChange: setCardinality
|
|
5226
|
+
}
|
|
5227
|
+
),
|
|
5228
|
+
/* @__PURE__ */ jsxs15("div", { className: "flex justify-end gap-2 pt-2", children: [
|
|
5229
|
+
/* @__PURE__ */ jsx17(Button4, { variant: "outline", onClick: () => onOpenChange(false), children: "Cancel" }),
|
|
5230
|
+
/* @__PURE__ */ jsx17(Button4, { disabled: !canSave, onClick: handleSave, children: isEdit ? "Save" : "Create relation" })
|
|
5231
|
+
] })
|
|
5232
|
+
] })
|
|
5233
|
+
] }) });
|
|
5234
|
+
}
|
|
5235
|
+
|
|
5236
|
+
// src/graph-model/NodeTypeNameSheet.tsx
|
|
5237
|
+
import { useState as useState10 } from "react";
|
|
5238
|
+
import {
|
|
5239
|
+
Sheet as Sheet3,
|
|
5240
|
+
SheetContent as SheetContent3,
|
|
5241
|
+
SheetHeader as SheetHeader3,
|
|
5242
|
+
SheetBody as SheetBody3,
|
|
5243
|
+
SheetTitle as SheetTitle3,
|
|
5244
|
+
SheetDescription as SheetDescription3,
|
|
5245
|
+
Button as Button5,
|
|
5246
|
+
Input as Input2
|
|
5247
|
+
} from "@petrarca/sonnet-ui";
|
|
5248
|
+
import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
5249
|
+
function NodeTypeNameSheet({
|
|
5250
|
+
open,
|
|
5251
|
+
onOpenChange,
|
|
5252
|
+
initial,
|
|
5253
|
+
existing,
|
|
5254
|
+
noun = "node",
|
|
5255
|
+
onSave
|
|
5256
|
+
}) {
|
|
5257
|
+
const [name, setName] = useState10(initial ?? "");
|
|
5258
|
+
const isEdit = initial !== void 0;
|
|
5259
|
+
const trimmed = name.trim();
|
|
5260
|
+
const duplicate = trimmed !== initial && existing.includes(trimmed);
|
|
5261
|
+
const canSave = trimmed.length > 0 && !duplicate;
|
|
5262
|
+
const typeNoun = noun === "edge" ? "edge type" : "node type";
|
|
5263
|
+
return /* @__PURE__ */ jsx18(Sheet3, { open, onOpenChange, children: /* @__PURE__ */ jsxs16(SheetContent3, { children: [
|
|
5264
|
+
/* @__PURE__ */ jsxs16(SheetHeader3, { children: [
|
|
5265
|
+
/* @__PURE__ */ jsx18(SheetTitle3, { children: isEdit ? `Rename ${typeNoun}` : `Add ${typeNoun}` }),
|
|
5266
|
+
/* @__PURE__ */ jsx18(SheetDescription3, { children: "The label identifies this type and its instances." })
|
|
5267
|
+
] }),
|
|
5268
|
+
/* @__PURE__ */ jsxs16(SheetBody3, { className: "flex flex-col gap-4", children: [
|
|
5269
|
+
/* @__PURE__ */ jsxs16("div", { children: [
|
|
5270
|
+
/* @__PURE__ */ jsx18("label", { className: "mb-1 block text-xs font-medium", children: "Label" }),
|
|
5271
|
+
/* @__PURE__ */ jsx18(
|
|
5272
|
+
Input2,
|
|
5273
|
+
{
|
|
5274
|
+
value: name,
|
|
5275
|
+
onChange: (e) => setName(e.target.value),
|
|
5276
|
+
placeholder: "e.g. Hospital",
|
|
5277
|
+
autoFocus: true
|
|
5278
|
+
}
|
|
5279
|
+
),
|
|
5280
|
+
duplicate && /* @__PURE__ */ jsx18("p", { className: "mt-1 text-xs text-destructive", children: "A type with this label already exists." })
|
|
5281
|
+
] }),
|
|
5282
|
+
/* @__PURE__ */ jsxs16("div", { className: "flex justify-end gap-2", children: [
|
|
5283
|
+
/* @__PURE__ */ jsx18(Button5, { variant: "outline", onClick: () => onOpenChange(false), children: "Cancel" }),
|
|
5284
|
+
/* @__PURE__ */ jsx18(
|
|
5285
|
+
Button5,
|
|
5286
|
+
{
|
|
5287
|
+
disabled: !canSave,
|
|
5288
|
+
onClick: () => {
|
|
5289
|
+
onSave(trimmed);
|
|
5290
|
+
onOpenChange(false);
|
|
5291
|
+
},
|
|
5292
|
+
children: isEdit ? "Rename" : "Add"
|
|
5293
|
+
}
|
|
5294
|
+
)
|
|
5295
|
+
] })
|
|
5296
|
+
] })
|
|
5297
|
+
] }) });
|
|
5298
|
+
}
|
|
5299
|
+
|
|
5300
|
+
// src/graph-model/TypeDetailPanel.tsx
|
|
5301
|
+
import { Fragment as Fragment4, jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
5302
|
+
function relationViewOf(view) {
|
|
5303
|
+
return {
|
|
5304
|
+
relType: view.relType,
|
|
5305
|
+
target: view.target,
|
|
5306
|
+
cardinality: view.cardinality,
|
|
5307
|
+
description: view.description
|
|
5308
|
+
};
|
|
5309
|
+
}
|
|
5310
|
+
function SectionTitle({
|
|
5311
|
+
children,
|
|
5312
|
+
action
|
|
5313
|
+
}) {
|
|
5314
|
+
return /* @__PURE__ */ jsxs17("div", { className: "mt-4 mb-2 flex items-center gap-2", children: [
|
|
5315
|
+
/* @__PURE__ */ jsx19("span", { className: "heading-meta", children }),
|
|
5316
|
+
action && /* @__PURE__ */ jsx19("span", { className: "ml-auto", children: action })
|
|
5317
|
+
] });
|
|
5318
|
+
}
|
|
5319
|
+
function PropertyRows({
|
|
5320
|
+
properties,
|
|
5321
|
+
emptyText,
|
|
5322
|
+
editable,
|
|
5323
|
+
onEdit,
|
|
5324
|
+
onDelete
|
|
5325
|
+
}) {
|
|
5326
|
+
if (properties.length === 0) {
|
|
5327
|
+
return /* @__PURE__ */ jsx19("p", { className: "text-xs text-muted-foreground", children: emptyText });
|
|
5328
|
+
}
|
|
5329
|
+
return /* @__PURE__ */ jsx19("div", { className: "rounded-lg border divide-y", children: properties.map((p) => /* @__PURE__ */ jsxs17(
|
|
5330
|
+
"div",
|
|
5331
|
+
{
|
|
5332
|
+
className: "group/row flex items-center gap-2 px-3 py-2",
|
|
5333
|
+
children: [
|
|
5334
|
+
/* @__PURE__ */ jsx19("span", { className: "text-sm font-medium min-w-24", children: p.name }),
|
|
5335
|
+
/* @__PURE__ */ jsxs17("span", { className: "flex-1 font-mono text-xs text-muted-foreground", children: [
|
|
5336
|
+
p.type,
|
|
5337
|
+
p.widget ? ` \xB7 ${p.widget}` : ""
|
|
5338
|
+
] }),
|
|
5339
|
+
p.required && /* @__PURE__ */ jsx19(Badge2, { badgeColor: "orange", children: "required" }),
|
|
5340
|
+
editable && /* @__PURE__ */ jsxs17("span", { className: "flex gap-1", children: [
|
|
5341
|
+
/* @__PURE__ */ jsx19(
|
|
5342
|
+
IconButton,
|
|
5343
|
+
{
|
|
5344
|
+
label: "Edit property",
|
|
5345
|
+
revealOnHover: true,
|
|
5346
|
+
onClick: () => onEdit(p),
|
|
5347
|
+
children: /* @__PURE__ */ jsx19(Pencil, {})
|
|
5348
|
+
}
|
|
5349
|
+
),
|
|
5350
|
+
/* @__PURE__ */ jsx19(
|
|
5351
|
+
IconButton,
|
|
5352
|
+
{
|
|
5353
|
+
label: "Delete property",
|
|
5354
|
+
tone: "destructive",
|
|
5355
|
+
revealOnHover: true,
|
|
5356
|
+
onClick: () => onDelete(p),
|
|
5357
|
+
children: /* @__PURE__ */ jsx19(Trash2, {})
|
|
5358
|
+
}
|
|
5359
|
+
)
|
|
5360
|
+
] })
|
|
5361
|
+
]
|
|
5362
|
+
},
|
|
5363
|
+
p.name
|
|
5364
|
+
)) });
|
|
5365
|
+
}
|
|
5366
|
+
function areaEditable(editable, mode) {
|
|
5367
|
+
return editable && (mode ?? "edit") === "edit";
|
|
5368
|
+
}
|
|
5369
|
+
function resolveGating(kind, editable, c) {
|
|
5370
|
+
const propsMode = (kind === "node" ? c?.nodeProperties : c?.edgeProperties) ?? "edit";
|
|
5371
|
+
const edgeTypeEditable = editable && (c?.edgeTypes ?? "edit") === "edit";
|
|
5372
|
+
return {
|
|
5373
|
+
propsHidden: propsMode === "hidden",
|
|
5374
|
+
propsEditable: areaEditable(editable, propsMode),
|
|
5375
|
+
nodeTypeEditable: areaEditable(editable, c?.nodeTypes),
|
|
5376
|
+
edgeTypeEditable,
|
|
5377
|
+
relationsEditable: areaEditable(editable, c?.relations)
|
|
5378
|
+
};
|
|
5379
|
+
}
|
|
5380
|
+
function resolveView(model, selection) {
|
|
5381
|
+
if (selection.kind === "node") return nodeTypeView(model, selection.label);
|
|
5382
|
+
if (selection.kind === "edge") return edgeTypeView(model, selection.label);
|
|
5383
|
+
return relationView(
|
|
5384
|
+
model,
|
|
5385
|
+
selection.source,
|
|
5386
|
+
selection.relType,
|
|
5387
|
+
selection.target
|
|
5388
|
+
);
|
|
5389
|
+
}
|
|
5390
|
+
function applySaveProperty(model, view, input, prevName) {
|
|
5391
|
+
if (view.kind === "node")
|
|
5392
|
+
return setNodeProperty(model, view.label, input, prevName);
|
|
5393
|
+
if (view.kind === "edge")
|
|
5394
|
+
return setEdgeTypeProperty(model, view.label, input, prevName);
|
|
5395
|
+
return setRelationProperty(model, view.relType, input, prevName);
|
|
5396
|
+
}
|
|
5397
|
+
function applyRemoveProperty(model, view, name) {
|
|
5398
|
+
if (view.kind === "node") return deleteNodeProperty(model, view.label, name);
|
|
5399
|
+
if (view.kind === "edge")
|
|
5400
|
+
return deleteEdgeTypeProperty(model, view.label, name);
|
|
5401
|
+
return deleteRelationProperty(model, view.relType, name);
|
|
5402
|
+
}
|
|
5403
|
+
function TypeDetailPanel({
|
|
5404
|
+
model,
|
|
5405
|
+
selection,
|
|
5406
|
+
onChange,
|
|
5407
|
+
readOnly,
|
|
5408
|
+
capabilities,
|
|
5409
|
+
widgetOptions
|
|
5410
|
+
}) {
|
|
5411
|
+
const [sheet, setSheet] = useState11({ open: false });
|
|
5412
|
+
const [relSheet, setRelSheet] = useState11(null);
|
|
5413
|
+
const [renameOpen, setRenameOpen] = useState11(false);
|
|
5414
|
+
const view = resolveView(model, selection);
|
|
5415
|
+
if (!view) return null;
|
|
5416
|
+
const editable = !readOnly && !!onChange;
|
|
5417
|
+
const ownerLabel = view.kind === "relation" ? view.relType : view.label;
|
|
5418
|
+
const {
|
|
5419
|
+
propsHidden,
|
|
5420
|
+
propsEditable,
|
|
5421
|
+
nodeTypeEditable,
|
|
5422
|
+
edgeTypeEditable,
|
|
5423
|
+
relationsEditable
|
|
5424
|
+
} = resolveGating(view.kind, editable, capabilities);
|
|
5425
|
+
const saveProperty = (input, prevName) => {
|
|
5426
|
+
if (onChange) onChange(applySaveProperty(model, view, input, prevName));
|
|
5427
|
+
};
|
|
5428
|
+
const removeProperty = (p) => {
|
|
5429
|
+
if (onChange) onChange(applyRemoveProperty(model, view, p.name));
|
|
5430
|
+
};
|
|
5431
|
+
const removeRelation = (r) => {
|
|
5432
|
+
if (!onChange || view.kind === "edge") return;
|
|
5433
|
+
const source = view.kind === "node" ? view.label : view.source;
|
|
5434
|
+
onChange(deleteRelation(model, source, r.relType, r.target));
|
|
5435
|
+
};
|
|
5436
|
+
const saveRelation = (source, _target, input, prev) => {
|
|
5437
|
+
if (!onChange) return;
|
|
5438
|
+
onChange(updateRelation(model, source, prev.relType, prev.target, input));
|
|
5439
|
+
};
|
|
5440
|
+
const propertiesSection = propsHidden ? null : /* @__PURE__ */ jsx19(
|
|
5441
|
+
PropertiesSection,
|
|
5442
|
+
{
|
|
5443
|
+
view,
|
|
5444
|
+
editable: propsEditable,
|
|
5445
|
+
onAdd: () => setSheet({ open: true, property: void 0 }),
|
|
5446
|
+
onEdit: (p) => setSheet({ open: true, property: p }),
|
|
5447
|
+
onDelete: removeProperty
|
|
5448
|
+
}
|
|
5449
|
+
);
|
|
5450
|
+
return /* @__PURE__ */ jsxs17("div", { children: [
|
|
5451
|
+
view.kind === "node" ? /* @__PURE__ */ jsx19(
|
|
5452
|
+
NodeDetail,
|
|
5453
|
+
{
|
|
5454
|
+
view,
|
|
5455
|
+
nodeTypeEditable,
|
|
5456
|
+
relationsEditable,
|
|
5457
|
+
onRename: () => setRenameOpen(true),
|
|
5458
|
+
onDelete: () => onChange?.(deleteNodeType(model, view.label)),
|
|
5459
|
+
onEditRelation: setRelSheet,
|
|
5460
|
+
onDeleteRelation: removeRelation,
|
|
5461
|
+
children: propertiesSection
|
|
5462
|
+
}
|
|
5463
|
+
) : view.kind === "edge" ? /* @__PURE__ */ jsx19(
|
|
5464
|
+
EdgeDetail,
|
|
5465
|
+
{
|
|
5466
|
+
view,
|
|
5467
|
+
editable: edgeTypeEditable,
|
|
5468
|
+
onRename: () => setRenameOpen(true),
|
|
5469
|
+
onDelete: () => onChange?.(deleteEdgeType(model, view.label)),
|
|
5470
|
+
children: propertiesSection
|
|
5471
|
+
}
|
|
5472
|
+
) : /* @__PURE__ */ jsx19(
|
|
5473
|
+
RelationDetail,
|
|
5474
|
+
{
|
|
5475
|
+
view,
|
|
5476
|
+
editable: relationsEditable,
|
|
5477
|
+
onEdit: () => setRelSheet(relationViewOf(view)),
|
|
5478
|
+
onDelete: () => removeRelation(relationViewOf(view)),
|
|
5479
|
+
children: propertiesSection
|
|
5480
|
+
}
|
|
5481
|
+
),
|
|
5482
|
+
editable && /* @__PURE__ */ jsx19(
|
|
5483
|
+
DetailSheets,
|
|
5484
|
+
{
|
|
5485
|
+
model,
|
|
5486
|
+
view,
|
|
5487
|
+
ownerLabel,
|
|
5488
|
+
widgetOptions,
|
|
5489
|
+
edgeTypeMode: capabilities?.edgeTypes ?? "edit",
|
|
5490
|
+
sheet,
|
|
5491
|
+
setSheet,
|
|
5492
|
+
relSheet,
|
|
5493
|
+
setRelSheet,
|
|
5494
|
+
renameOpen,
|
|
5495
|
+
setRenameOpen,
|
|
5496
|
+
onSaveProperty: saveProperty,
|
|
5497
|
+
onSaveRelation: saveRelation,
|
|
5498
|
+
onRename: (label) => {
|
|
5499
|
+
if (!onChange) return;
|
|
5500
|
+
if (view.kind === "node")
|
|
5501
|
+
onChange(renameNodeType(model, view.label, label));
|
|
5502
|
+
else if (view.kind === "edge")
|
|
5503
|
+
onChange(renameEdgeType(model, view.label, label));
|
|
5504
|
+
}
|
|
5505
|
+
}
|
|
5506
|
+
)
|
|
5507
|
+
] });
|
|
5508
|
+
}
|
|
5509
|
+
function DetailSheets({
|
|
5510
|
+
model,
|
|
5511
|
+
view,
|
|
5512
|
+
ownerLabel,
|
|
5513
|
+
widgetOptions,
|
|
5514
|
+
edgeTypeMode,
|
|
5515
|
+
sheet,
|
|
5516
|
+
setSheet,
|
|
5517
|
+
relSheet,
|
|
5518
|
+
setRelSheet,
|
|
5519
|
+
renameOpen,
|
|
5520
|
+
setRenameOpen,
|
|
5521
|
+
onSaveProperty,
|
|
5522
|
+
onSaveRelation,
|
|
5523
|
+
onRename
|
|
5524
|
+
}) {
|
|
5525
|
+
const edgeTypeOptions = model.filter(isEdgeType).map(typeLabel);
|
|
5526
|
+
return /* @__PURE__ */ jsxs17(Fragment4, { children: [
|
|
5527
|
+
/* @__PURE__ */ jsx19(
|
|
5528
|
+
PropertyFormSheet,
|
|
5529
|
+
{
|
|
5530
|
+
open: sheet.open,
|
|
5531
|
+
onOpenChange: (o) => setSheet((s) => ({ ...s, open: o })),
|
|
5532
|
+
property: sheet.property,
|
|
5533
|
+
ownerLabel,
|
|
5534
|
+
widgetOptions,
|
|
5535
|
+
onSave: onSaveProperty
|
|
5536
|
+
}
|
|
5537
|
+
),
|
|
5538
|
+
relSheet && (view.kind === "node" || view.kind === "relation") && /* @__PURE__ */ jsx19(
|
|
5539
|
+
RelationFormSheet,
|
|
5540
|
+
{
|
|
5541
|
+
open: !!relSheet,
|
|
5542
|
+
onOpenChange: (o) => !o && setRelSheet(null),
|
|
5543
|
+
nodeA: view.kind === "node" ? view.label : view.source,
|
|
5544
|
+
nodeB: relSheet.target,
|
|
5545
|
+
initial: {
|
|
5546
|
+
relType: relSheet.relType,
|
|
5547
|
+
cardinality: relSheet.cardinality
|
|
5548
|
+
},
|
|
5549
|
+
edgeTypeOptions,
|
|
5550
|
+
edgeTypeMode,
|
|
5551
|
+
onSave: (source, target, input) => onSaveRelation(source, target, input, relSheet)
|
|
5552
|
+
},
|
|
5553
|
+
`${relSheet.relType}:${relSheet.target}`
|
|
5554
|
+
),
|
|
5555
|
+
(view.kind === "node" || view.kind === "edge") && renameOpen && /* @__PURE__ */ jsx19(
|
|
5556
|
+
NodeTypeNameSheet,
|
|
5557
|
+
{
|
|
5558
|
+
open: renameOpen,
|
|
5559
|
+
onOpenChange: setRenameOpen,
|
|
5560
|
+
initial: view.label,
|
|
5561
|
+
existing: model.map(typeLabel),
|
|
5562
|
+
noun: view.kind === "edge" ? "edge" : "node",
|
|
5563
|
+
onSave: onRename
|
|
5564
|
+
},
|
|
5565
|
+
view.label
|
|
5566
|
+
)
|
|
5567
|
+
] });
|
|
5568
|
+
}
|
|
5569
|
+
function PropertiesSection({
|
|
5570
|
+
view,
|
|
5571
|
+
editable,
|
|
5572
|
+
onAdd,
|
|
5573
|
+
onEdit,
|
|
5574
|
+
onDelete
|
|
5575
|
+
}) {
|
|
5576
|
+
return /* @__PURE__ */ jsxs17(Fragment4, { children: [
|
|
5577
|
+
/* @__PURE__ */ jsx19(
|
|
5578
|
+
SectionTitle,
|
|
5579
|
+
{
|
|
5580
|
+
action: editable ? /* @__PURE__ */ jsx19(IconButton, { label: "Add property", onClick: onAdd, children: /* @__PURE__ */ jsx19(Plus, {}) }) : void 0,
|
|
5581
|
+
children: "Properties"
|
|
5582
|
+
}
|
|
5583
|
+
),
|
|
5584
|
+
/* @__PURE__ */ jsx19(
|
|
5585
|
+
PropertyRows,
|
|
5586
|
+
{
|
|
5587
|
+
properties: view.properties,
|
|
5588
|
+
emptyText: "No properties yet.",
|
|
5589
|
+
editable,
|
|
5590
|
+
onEdit,
|
|
5591
|
+
onDelete
|
|
5592
|
+
}
|
|
5593
|
+
)
|
|
5594
|
+
] });
|
|
5595
|
+
}
|
|
5596
|
+
function NodeDetail({
|
|
5597
|
+
view,
|
|
5598
|
+
nodeTypeEditable,
|
|
5599
|
+
relationsEditable,
|
|
5600
|
+
onRename,
|
|
5601
|
+
onDelete,
|
|
5602
|
+
onEditRelation,
|
|
5603
|
+
onDeleteRelation,
|
|
5604
|
+
children
|
|
5605
|
+
}) {
|
|
5606
|
+
return /* @__PURE__ */ jsxs17(Fragment4, { children: [
|
|
5607
|
+
/* @__PURE__ */ jsxs17("div", { className: "flex items-center gap-2", children: [
|
|
5608
|
+
/* @__PURE__ */ jsx19("span", { className: "text-base font-semibold", children: view.label }),
|
|
5609
|
+
/* @__PURE__ */ jsx19(Badge2, { children: "NODE TYPE" }),
|
|
5610
|
+
nodeTypeEditable && /* @__PURE__ */ jsx19(
|
|
5611
|
+
RenameDeleteActions,
|
|
5612
|
+
{
|
|
5613
|
+
noun: "node type",
|
|
5614
|
+
onRename,
|
|
5615
|
+
onDelete
|
|
5616
|
+
}
|
|
5617
|
+
)
|
|
5618
|
+
] }),
|
|
5619
|
+
view.description && /* @__PURE__ */ jsx19("p", { className: "mt-1 text-sm text-muted-foreground", children: view.description }),
|
|
5620
|
+
children,
|
|
5621
|
+
/* @__PURE__ */ jsx19(SectionTitle, { children: "Relations" }),
|
|
5622
|
+
view.relations.length === 0 ? /* @__PURE__ */ jsx19("p", { className: "text-xs text-muted-foreground", children: "No relations." }) : /* @__PURE__ */ jsx19("div", { className: "rounded-lg border divide-y", children: view.relations.map((r) => /* @__PURE__ */ jsxs17(
|
|
5623
|
+
"div",
|
|
5624
|
+
{
|
|
5625
|
+
className: "group/row flex items-center gap-2 px-3 py-2",
|
|
5626
|
+
children: [
|
|
5627
|
+
/* @__PURE__ */ jsx19("span", { className: "text-sm font-medium min-w-24", children: r.relType }),
|
|
5628
|
+
/* @__PURE__ */ jsxs17("span", { className: "flex-1 font-mono text-xs text-muted-foreground", children: [
|
|
5629
|
+
"\u2192",
|
|
5630
|
+
" ",
|
|
5631
|
+
r.target,
|
|
5632
|
+
" ",
|
|
5633
|
+
"\xB7",
|
|
5634
|
+
" ",
|
|
5635
|
+
cardinalityLabel(r.cardinality)
|
|
5636
|
+
] }),
|
|
5637
|
+
relationsEditable && /* @__PURE__ */ jsxs17("span", { className: "flex gap-1", children: [
|
|
5638
|
+
/* @__PURE__ */ jsx19(
|
|
5639
|
+
IconButton,
|
|
5640
|
+
{
|
|
5641
|
+
label: "Edit relation",
|
|
5642
|
+
revealOnHover: true,
|
|
5643
|
+
onClick: () => onEditRelation(r),
|
|
5644
|
+
children: /* @__PURE__ */ jsx19(Pencil, {})
|
|
5645
|
+
}
|
|
5646
|
+
),
|
|
5647
|
+
/* @__PURE__ */ jsx19(
|
|
5648
|
+
IconButton,
|
|
5649
|
+
{
|
|
5650
|
+
label: "Delete relation",
|
|
5651
|
+
tone: "destructive",
|
|
5652
|
+
revealOnHover: true,
|
|
5653
|
+
onClick: () => onDeleteRelation(r),
|
|
5654
|
+
children: /* @__PURE__ */ jsx19(Trash2, {})
|
|
5655
|
+
}
|
|
5656
|
+
)
|
|
5657
|
+
] })
|
|
5658
|
+
]
|
|
5659
|
+
},
|
|
5660
|
+
`${r.relType}:${r.target}`
|
|
5661
|
+
)) })
|
|
5662
|
+
] });
|
|
5663
|
+
}
|
|
5664
|
+
function EdgeTypeNote({
|
|
5665
|
+
children
|
|
5666
|
+
}) {
|
|
5667
|
+
return /* @__PURE__ */ jsx19("p", { className: "mt-3 rounded-md bg-muted/50 px-3 py-2 text-xs text-muted-foreground", children });
|
|
5668
|
+
}
|
|
5669
|
+
function RenameDeleteActions({
|
|
5670
|
+
noun,
|
|
5671
|
+
editVerb = "Rename",
|
|
5672
|
+
onRename,
|
|
5673
|
+
onDelete
|
|
5674
|
+
}) {
|
|
5675
|
+
return /* @__PURE__ */ jsxs17("span", { className: "ml-auto flex gap-1", children: [
|
|
5676
|
+
/* @__PURE__ */ jsx19(IconButton, { label: `${editVerb} ${noun}`, onClick: onRename, children: /* @__PURE__ */ jsx19(Pencil, {}) }),
|
|
5677
|
+
/* @__PURE__ */ jsx19(
|
|
5678
|
+
IconButton,
|
|
5679
|
+
{
|
|
5680
|
+
label: `Delete ${noun}`,
|
|
5681
|
+
tone: "destructive",
|
|
5682
|
+
onClick: onDelete,
|
|
5683
|
+
children: /* @__PURE__ */ jsx19(Trash2, {})
|
|
5684
|
+
}
|
|
5685
|
+
)
|
|
5686
|
+
] });
|
|
5687
|
+
}
|
|
5688
|
+
function EdgeDetail({
|
|
5689
|
+
view,
|
|
5690
|
+
editable,
|
|
5691
|
+
onRename,
|
|
5692
|
+
onDelete,
|
|
5693
|
+
children
|
|
5694
|
+
}) {
|
|
5695
|
+
return /* @__PURE__ */ jsxs17(Fragment4, { children: [
|
|
5696
|
+
/* @__PURE__ */ jsxs17("div", { className: "flex items-center gap-2", children: [
|
|
5697
|
+
/* @__PURE__ */ jsx19("span", { className: "text-base font-semibold", children: view.label }),
|
|
5698
|
+
/* @__PURE__ */ jsx19(Badge2, { children: "EDGE TYPE" }),
|
|
5699
|
+
editable && /* @__PURE__ */ jsx19(
|
|
5700
|
+
RenameDeleteActions,
|
|
5701
|
+
{
|
|
5702
|
+
noun: "edge type",
|
|
5703
|
+
onRename,
|
|
5704
|
+
onDelete
|
|
5705
|
+
}
|
|
5706
|
+
)
|
|
5707
|
+
] }),
|
|
5708
|
+
view.description && /* @__PURE__ */ jsx19("p", { className: "mt-1 text-sm text-muted-foreground", children: view.description }),
|
|
5709
|
+
/* @__PURE__ */ jsxs17(EdgeTypeNote, { children: [
|
|
5710
|
+
"These properties belong to the",
|
|
5711
|
+
" ",
|
|
5712
|
+
/* @__PURE__ */ jsx19("span", { className: "font-medium", children: view.label }),
|
|
5713
|
+
" edge type and are shared by every relation of this type."
|
|
5714
|
+
] }),
|
|
5715
|
+
children
|
|
5716
|
+
] });
|
|
5717
|
+
}
|
|
5718
|
+
function RelationDetail({
|
|
5719
|
+
view,
|
|
5720
|
+
editable,
|
|
5721
|
+
onEdit,
|
|
5722
|
+
onDelete,
|
|
5723
|
+
children
|
|
5724
|
+
}) {
|
|
5725
|
+
return /* @__PURE__ */ jsxs17(Fragment4, { children: [
|
|
5726
|
+
/* @__PURE__ */ jsxs17("div", { className: "flex items-center gap-2", children: [
|
|
5727
|
+
/* @__PURE__ */ jsx19("span", { className: "text-base font-semibold", children: view.relType }),
|
|
5728
|
+
/* @__PURE__ */ jsx19(Badge2, { children: "RELATION" }),
|
|
5729
|
+
editable && /* @__PURE__ */ jsx19(
|
|
5730
|
+
RenameDeleteActions,
|
|
5731
|
+
{
|
|
5732
|
+
noun: "relation",
|
|
5733
|
+
editVerb: "Edit",
|
|
5734
|
+
onRename: onEdit,
|
|
5735
|
+
onDelete
|
|
5736
|
+
}
|
|
5737
|
+
)
|
|
5738
|
+
] }),
|
|
5739
|
+
/* @__PURE__ */ jsxs17("p", { className: "mt-1 text-sm text-muted-foreground", children: [
|
|
5740
|
+
/* @__PURE__ */ jsx19("span", { className: "font-medium text-foreground", children: view.source }),
|
|
5741
|
+
" \u2192 ",
|
|
5742
|
+
/* @__PURE__ */ jsx19("span", { className: "font-medium text-foreground", children: view.target }),
|
|
5743
|
+
" \xB7 ",
|
|
5744
|
+
cardinalityLabel(view.cardinality)
|
|
5745
|
+
] }),
|
|
5746
|
+
/* @__PURE__ */ jsxs17(EdgeTypeNote, { children: [
|
|
5747
|
+
"Properties are defined on the",
|
|
5748
|
+
" ",
|
|
5749
|
+
/* @__PURE__ */ jsx19("span", { className: "font-medium", children: view.relType }),
|
|
5750
|
+
" edge type, shared by every relation of this type -- not on this single relation."
|
|
5751
|
+
] }),
|
|
5752
|
+
children
|
|
5753
|
+
] });
|
|
5754
|
+
}
|
|
5755
|
+
|
|
5756
|
+
// src/graph-model/capabilities.ts
|
|
5757
|
+
import { z } from "zod";
|
|
5758
|
+
import { warnLog } from "@petrarca/sonnet-core";
|
|
5759
|
+
var capabilitiesSchema = z.object({
|
|
5760
|
+
nodeTypes: z.enum(["edit", "readonly"]).default("edit").meta({
|
|
5761
|
+
description: "Node types (add / rename / delete). 'edit' (default): add new types, rename and delete from the panel. 'readonly': types are shown but cannot be added, renamed, or deleted. No 'hidden': node types are the canvas nodes and are always present."
|
|
5762
|
+
}),
|
|
5763
|
+
edgeTypes: z.enum(["edit", "catalog", "readonly"]).default("edit").meta({
|
|
5764
|
+
description: "Edge-type catalog governance for relation creation. 'edit' (default): select an existing edge type or create one inline, plus 'Add edge type' and edge-type rename/delete. 'catalog': select only from existing edge types; no create/rename/delete. 'readonly': no edge-type changes."
|
|
5765
|
+
}),
|
|
5766
|
+
nodeProperties: z.enum(["edit", "readonly", "hidden"]).default("edit").meta({
|
|
5767
|
+
description: "Node-type properties. 'edit' (default): add / edit / delete. 'readonly': shown, but no add / edit / delete affordances. 'hidden': the Properties section is not rendered."
|
|
5768
|
+
}),
|
|
5769
|
+
edgeProperties: z.enum(["edit", "readonly", "hidden"]).default("edit").meta({
|
|
5770
|
+
description: "Edge-type / relation properties (defined on the edge type, shared by every relation of that type). 'edit' (default): add / edit / delete. 'readonly': shown, but not editable. 'hidden': the Properties section is not rendered."
|
|
5771
|
+
}),
|
|
5772
|
+
relations: z.enum(["edit", "readonly"]).default("edit").meta({
|
|
5773
|
+
description: "Relations between node types (create / edit / delete). 'edit' (default): create (select two nodes), edit, and delete relations. 'readonly': relations are shown but cannot be changed. How the relation type is chosen is governed by edgeTypes."
|
|
5774
|
+
}),
|
|
5775
|
+
showEdgeTypes: z.boolean().default(false).meta({
|
|
5776
|
+
description: "Whether edge-type nodes (the catalog of relationship types) are shown on the canvas initially. false (default): edge types start filtered out (re-enable from the Edge types legend). true: shown from the start."
|
|
5777
|
+
})
|
|
5778
|
+
}).meta({
|
|
5779
|
+
id: "GraphModelCapabilities",
|
|
5780
|
+
title: "GraphModelCapabilities",
|
|
5781
|
+
description: "Capability profile for GraphModelEditor. Gates what a consumer can edit. Every field is optional and defaults to the most permissive value (forward-compatible). Passing readOnly to the editor overrides this object to a fully-locked profile."
|
|
5782
|
+
});
|
|
5783
|
+
var LOCKED = {
|
|
5784
|
+
nodeTypes: "readonly",
|
|
5785
|
+
edgeTypes: "readonly",
|
|
5786
|
+
nodeProperties: "readonly",
|
|
5787
|
+
edgeProperties: "readonly",
|
|
5788
|
+
relations: "readonly",
|
|
5789
|
+
showEdgeTypes: false
|
|
5790
|
+
};
|
|
5791
|
+
function resolveCapabilities(capabilities, readOnly) {
|
|
5792
|
+
if (readOnly) return LOCKED;
|
|
5793
|
+
const parsed = capabilitiesSchema.safeParse(capabilities ?? {});
|
|
5794
|
+
if (!parsed.success) {
|
|
5795
|
+
warnLog(
|
|
5796
|
+
"[GraphModelEditor] Invalid capabilities \u2014 falling back to defaults.",
|
|
5797
|
+
parsed.error.issues
|
|
5798
|
+
);
|
|
5799
|
+
return capabilitiesSchema.parse({});
|
|
5800
|
+
}
|
|
5801
|
+
return parsed.data;
|
|
5802
|
+
}
|
|
5803
|
+
function validateCapabilities(input) {
|
|
5804
|
+
const parsed = capabilitiesSchema.safeParse(input);
|
|
5805
|
+
if (parsed.success) return { ok: true, value: parsed.data };
|
|
5806
|
+
const errors = parsed.error.issues.map((i) => {
|
|
5807
|
+
const path = i.path.length ? i.path.join(".") : "(root)";
|
|
5808
|
+
return `${path}: ${i.message}`;
|
|
5809
|
+
});
|
|
5810
|
+
return { ok: false, errors };
|
|
5811
|
+
}
|
|
5812
|
+
|
|
5813
|
+
// src/graph-model/ModelActionPanel.tsx
|
|
5814
|
+
import { Fragment as Fragment5, jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
5815
|
+
function nodeTypeLabels(model) {
|
|
5816
|
+
return model.filter(isNodeType).map(typeLabel);
|
|
5817
|
+
}
|
|
5818
|
+
function edgeTypeLabels(model) {
|
|
5819
|
+
return model.filter(isEdgeType).map(typeLabel);
|
|
5820
|
+
}
|
|
5821
|
+
function CreateRelationPrompt({
|
|
5822
|
+
nodeA,
|
|
5823
|
+
nodeB,
|
|
5824
|
+
model,
|
|
5825
|
+
edgeTypeMode,
|
|
5826
|
+
onChange
|
|
5827
|
+
}) {
|
|
5828
|
+
const [open, setOpen] = useState12(false);
|
|
5829
|
+
return /* @__PURE__ */ jsxs18("div", { children: [
|
|
5830
|
+
/* @__PURE__ */ jsxs18("div", { className: "text-sm", children: [
|
|
5831
|
+
/* @__PURE__ */ jsx20("span", { className: "font-medium", children: nodeA }),
|
|
5832
|
+
" and ",
|
|
5833
|
+
/* @__PURE__ */ jsx20("span", { className: "font-medium", children: nodeB }),
|
|
5834
|
+
" selected."
|
|
5835
|
+
] }),
|
|
5836
|
+
/* @__PURE__ */ jsxs18(Button6, { size: "sm", className: "mt-3 gap-1.5", onClick: () => setOpen(true), children: [
|
|
5837
|
+
/* @__PURE__ */ jsx20(GitBranchPlus, { className: "h-4 w-4" }),
|
|
5838
|
+
"Create relation"
|
|
5839
|
+
] }),
|
|
5840
|
+
/* @__PURE__ */ jsx20(
|
|
5841
|
+
RelationFormSheet,
|
|
5842
|
+
{
|
|
5843
|
+
open,
|
|
5844
|
+
onOpenChange: setOpen,
|
|
5845
|
+
nodeA,
|
|
5846
|
+
nodeB,
|
|
5847
|
+
edgeTypeOptions: edgeTypeLabels(model),
|
|
5848
|
+
edgeTypeMode,
|
|
5849
|
+
onSave: (source, _target, input) => onChange(addRelation(model, source, input))
|
|
5850
|
+
}
|
|
5851
|
+
)
|
|
5852
|
+
] });
|
|
5853
|
+
}
|
|
5854
|
+
function ModelActionPanel({
|
|
5855
|
+
model,
|
|
5856
|
+
onChange,
|
|
5857
|
+
readOnly,
|
|
5858
|
+
capabilities,
|
|
5859
|
+
widgetOptions
|
|
5860
|
+
}) {
|
|
5861
|
+
const caps = resolveCapabilities(capabilities, readOnly);
|
|
5862
|
+
const useSelection3 = useWorkspaceSelection();
|
|
5863
|
+
const selectedNodeIds = useSelection3(
|
|
5864
|
+
(s) => s.selectedNodeIds
|
|
5865
|
+
);
|
|
5866
|
+
const selectedEdge = useSelection3((s) => s.selectedEdge);
|
|
5867
|
+
const live = useLiveGraphData();
|
|
5868
|
+
const labelsById = (ids) => ids.map((id) => live.normalized.nodesById[id]?.label_).filter((l) => !!l);
|
|
5869
|
+
const selection = useMemo8(() => {
|
|
5870
|
+
if (selectedEdge) {
|
|
5871
|
+
const r = resolveRelationEdge(selectedEdge);
|
|
5872
|
+
if (r) {
|
|
5873
|
+
return {
|
|
5874
|
+
kind: "relation",
|
|
5875
|
+
source: r.source,
|
|
5876
|
+
relType: r.relType,
|
|
5877
|
+
target: r.target
|
|
5878
|
+
};
|
|
5879
|
+
}
|
|
5880
|
+
}
|
|
5881
|
+
const firstId = selectedNodeIds[0];
|
|
5882
|
+
if (selectedNodeIds.length === 1 && firstId != null) {
|
|
5883
|
+
const node = live.normalized.nodesById[firstId];
|
|
5884
|
+
if (node) {
|
|
5885
|
+
const schema = findType(model, node.label_);
|
|
5886
|
+
const kind = schema && isEdgeType(schema) ? "edge" : "node";
|
|
5887
|
+
return { kind, label: node.label_ };
|
|
5888
|
+
}
|
|
5889
|
+
}
|
|
5890
|
+
return null;
|
|
5891
|
+
}, [selectedEdge, selectedNodeIds, live.normalized, model]);
|
|
5892
|
+
const twoLabels = selectedNodeIds.length === 2 && !selectedEdge ? labelsById(selectedNodeIds).filter((l) => {
|
|
5893
|
+
const schema = findType(model, l);
|
|
5894
|
+
return schema ? isNodeType(schema) : false;
|
|
5895
|
+
}) : [];
|
|
5896
|
+
return /* @__PURE__ */ jsx20("div", { className: "h-full overflow-auto p-4", children: /* @__PURE__ */ jsx20(
|
|
5897
|
+
PanelBody,
|
|
5898
|
+
{
|
|
5899
|
+
model,
|
|
5900
|
+
onChange,
|
|
5901
|
+
readOnly,
|
|
5902
|
+
caps,
|
|
5903
|
+
widgetOptions,
|
|
5904
|
+
selection,
|
|
5905
|
+
twoLabels
|
|
5906
|
+
}
|
|
5907
|
+
) });
|
|
5908
|
+
}
|
|
5909
|
+
function PanelBody({
|
|
5910
|
+
model,
|
|
5911
|
+
onChange,
|
|
5912
|
+
readOnly,
|
|
5913
|
+
caps,
|
|
5914
|
+
widgetOptions,
|
|
5915
|
+
selection,
|
|
5916
|
+
twoLabels
|
|
5917
|
+
}) {
|
|
5918
|
+
if (selection) {
|
|
5919
|
+
return /* @__PURE__ */ jsx20(
|
|
5920
|
+
TypeDetailPanel,
|
|
5921
|
+
{
|
|
5922
|
+
model,
|
|
5923
|
+
selection,
|
|
5924
|
+
onChange,
|
|
5925
|
+
readOnly,
|
|
5926
|
+
capabilities: caps,
|
|
5927
|
+
widgetOptions
|
|
5928
|
+
}
|
|
5929
|
+
);
|
|
5930
|
+
}
|
|
5931
|
+
const canEdit = !readOnly && !!onChange;
|
|
5932
|
+
if (twoLabels.length === 2 && canEdit && caps.relations === "edit") {
|
|
5933
|
+
return /* @__PURE__ */ jsx20(
|
|
5934
|
+
CreateRelationPrompt,
|
|
5935
|
+
{
|
|
5936
|
+
nodeA: twoLabels[0],
|
|
5937
|
+
nodeB: twoLabels[1],
|
|
5938
|
+
model,
|
|
5939
|
+
edgeTypeMode: caps.edgeTypes,
|
|
5940
|
+
onChange
|
|
5941
|
+
}
|
|
5942
|
+
);
|
|
5943
|
+
}
|
|
5944
|
+
return /* @__PURE__ */ jsxs18(Fragment5, { children: [
|
|
5945
|
+
canEdit && /* @__PURE__ */ jsx20(
|
|
5946
|
+
AddTypeToolbar,
|
|
5947
|
+
{
|
|
5948
|
+
model,
|
|
5949
|
+
onChange,
|
|
5950
|
+
canAddNode: caps.nodeTypes === "edit",
|
|
5951
|
+
canAddEdge: caps.edgeTypes === "edit"
|
|
5952
|
+
}
|
|
5953
|
+
),
|
|
5954
|
+
/* @__PURE__ */ jsx20(
|
|
5955
|
+
Overview_default,
|
|
5956
|
+
{
|
|
5957
|
+
nodeBuckets: [
|
|
5958
|
+
{ title: "Node types", labels: nodeTypeLabels(model) },
|
|
5959
|
+
{ title: "Edge types", labels: edgeTypeLabels(model) }
|
|
5960
|
+
],
|
|
5961
|
+
edgeSectionTitle: "Relations"
|
|
5962
|
+
}
|
|
5963
|
+
)
|
|
5964
|
+
] });
|
|
5965
|
+
}
|
|
5966
|
+
function AddTypeIconButton({
|
|
5967
|
+
tooltip,
|
|
5968
|
+
icon,
|
|
5969
|
+
noun,
|
|
5970
|
+
existing,
|
|
5971
|
+
onSave
|
|
5972
|
+
}) {
|
|
5973
|
+
const [open, setOpen] = useState12(false);
|
|
5974
|
+
return /* @__PURE__ */ jsxs18(Fragment5, { children: [
|
|
5975
|
+
/* @__PURE__ */ jsx20(
|
|
5976
|
+
IconButton2,
|
|
5977
|
+
{
|
|
5978
|
+
label: tooltip,
|
|
5979
|
+
variant: "outline",
|
|
5980
|
+
onClick: () => setOpen(true),
|
|
5981
|
+
children: icon
|
|
5982
|
+
}
|
|
5983
|
+
),
|
|
5984
|
+
/* @__PURE__ */ jsx20(
|
|
5985
|
+
NodeTypeNameSheet,
|
|
5986
|
+
{
|
|
5987
|
+
open,
|
|
5988
|
+
onOpenChange: setOpen,
|
|
5989
|
+
existing,
|
|
5990
|
+
noun,
|
|
5991
|
+
onSave
|
|
5992
|
+
}
|
|
5993
|
+
)
|
|
5994
|
+
] });
|
|
5995
|
+
}
|
|
5996
|
+
function AddTypeToolbar({
|
|
5997
|
+
model,
|
|
5998
|
+
onChange,
|
|
5999
|
+
canAddNode,
|
|
6000
|
+
canAddEdge
|
|
6001
|
+
}) {
|
|
6002
|
+
if (!canAddNode && !canAddEdge) return null;
|
|
6003
|
+
const existing = model.map(typeLabel);
|
|
6004
|
+
return /* @__PURE__ */ jsxs18("div", { className: "mb-3 flex gap-1.5", children: [
|
|
6005
|
+
canAddNode && /* @__PURE__ */ jsx20(
|
|
6006
|
+
AddTypeIconButton,
|
|
6007
|
+
{
|
|
6008
|
+
tooltip: "Add node type",
|
|
6009
|
+
icon: /* @__PURE__ */ jsx20(CirclePlus, {}),
|
|
6010
|
+
noun: "node",
|
|
6011
|
+
existing,
|
|
6012
|
+
onSave: (name) => onChange(addNodeType(model, name))
|
|
6013
|
+
}
|
|
6014
|
+
),
|
|
6015
|
+
canAddEdge && /* @__PURE__ */ jsx20(
|
|
6016
|
+
AddTypeIconButton,
|
|
6017
|
+
{
|
|
6018
|
+
tooltip: "Add edge type",
|
|
6019
|
+
icon: /* @__PURE__ */ jsx20(Waypoints, {}),
|
|
6020
|
+
noun: "edge",
|
|
6021
|
+
existing,
|
|
6022
|
+
onSave: (name) => onChange(addEdgeType(model, name))
|
|
6023
|
+
}
|
|
6024
|
+
)
|
|
6025
|
+
] });
|
|
6026
|
+
}
|
|
6027
|
+
|
|
6028
|
+
// src/graph-model/GraphModelEditor.tsx
|
|
6029
|
+
import { Fragment as Fragment6, jsx as jsx21, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
6030
|
+
function createEditorStores() {
|
|
6031
|
+
const graphFilter = createGraphFilterStore();
|
|
6032
|
+
const graphData = createGraphDataStore();
|
|
6033
|
+
const selection = createSelectionStore();
|
|
6034
|
+
const graphFocus = createGraphFocusStore();
|
|
6035
|
+
const graphExploration = createGraphExplorationStore({
|
|
6036
|
+
graphData,
|
|
6037
|
+
selection,
|
|
6038
|
+
graphFocus,
|
|
6039
|
+
graphFilter,
|
|
6040
|
+
expandNode: async () => ({ nodes: [], edges: [], has_more: false }),
|
|
6041
|
+
getNodes: async () => []
|
|
6042
|
+
});
|
|
6043
|
+
return { graphFilter, graphData, graphExploration, selection };
|
|
6044
|
+
}
|
|
6045
|
+
function loadModel(stores, model) {
|
|
6046
|
+
const { nodes: rawNodes, edges: rawEdges } = modelToGraph(model);
|
|
6047
|
+
const labelCounts = {};
|
|
6048
|
+
for (const n of rawNodes) {
|
|
6049
|
+
labelCounts[n.label_] = (labelCounts[n.label_] ?? 0) + 1;
|
|
6050
|
+
}
|
|
6051
|
+
const palette = buildColorPalette(labelCounts);
|
|
6052
|
+
const nodes = enrichNodes(rawNodes, palette);
|
|
6053
|
+
const edges = enrichEdges(rawEdges, palette);
|
|
6054
|
+
stores.graphData.getState().setQueryData(nodes, edges, { nodes: nodes.length, edges: edges.length });
|
|
6055
|
+
stores.graphFilter.getState().notifyDataChange();
|
|
6056
|
+
}
|
|
6057
|
+
function GraphModelEditor({
|
|
6058
|
+
value,
|
|
6059
|
+
readOnly,
|
|
6060
|
+
capabilities,
|
|
6061
|
+
onChange,
|
|
6062
|
+
widgetOptions,
|
|
6063
|
+
defaultRenderer = "cytoscape",
|
|
6064
|
+
hideActionPanel = false
|
|
6065
|
+
}) {
|
|
6066
|
+
const storesRef = useRef6(null);
|
|
6067
|
+
if (!storesRef.current) storesRef.current = createEditorStores();
|
|
6068
|
+
const stores = storesRef.current;
|
|
6069
|
+
const caps = resolveCapabilities(capabilities, readOnly);
|
|
6070
|
+
const showEdgeTypes = caps.showEdgeTypes;
|
|
6071
|
+
const firstLoadRef = useRef6(true);
|
|
6072
|
+
const prevShowEdgeTypesRef = useRef6(showEdgeTypes);
|
|
6073
|
+
useEffect5(() => {
|
|
6074
|
+
loadModel(stores, value);
|
|
6075
|
+
const filter = stores.graphFilter.getState();
|
|
6076
|
+
const baselineReset = firstLoadRef.current || prevShowEdgeTypesRef.current !== showEdgeTypes;
|
|
6077
|
+
const next = reconcileFilter(
|
|
6078
|
+
filter.nodeLabelFilters,
|
|
6079
|
+
value,
|
|
6080
|
+
baselineReset,
|
|
6081
|
+
showEdgeTypes
|
|
6082
|
+
);
|
|
6083
|
+
filter.setNodeLabelFilters(next);
|
|
6084
|
+
firstLoadRef.current = false;
|
|
6085
|
+
prevShowEdgeTypesRef.current = showEdgeTypes;
|
|
6086
|
+
}, [stores, value, showEdgeTypes]);
|
|
6087
|
+
const rowRef = useRef6(null);
|
|
6088
|
+
const [panelWidth, setPanelWidth] = useState13(380);
|
|
6089
|
+
const startResize = useCallback7((e) => {
|
|
6090
|
+
e.preventDefault();
|
|
6091
|
+
const target = e.currentTarget;
|
|
6092
|
+
target.setPointerCapture(e.pointerId);
|
|
6093
|
+
const move = (ev) => {
|
|
6094
|
+
const right = rowRef.current?.getBoundingClientRect().right ?? 0;
|
|
6095
|
+
setPanelWidth(Math.min(640, Math.max(300, right - ev.clientX)));
|
|
6096
|
+
};
|
|
6097
|
+
const up = () => {
|
|
6098
|
+
window.removeEventListener("pointermove", move);
|
|
6099
|
+
window.removeEventListener("pointerup", up);
|
|
6100
|
+
};
|
|
6101
|
+
window.addEventListener("pointermove", move);
|
|
6102
|
+
window.addEventListener("pointerup", up);
|
|
6103
|
+
}, []);
|
|
6104
|
+
return /* @__PURE__ */ jsx21(GraphStoresContext.Provider, { value: stores, children: /* @__PURE__ */ jsx21(GraphRendererProvider, { defaultRenderer, children: /* @__PURE__ */ jsxs19("div", { ref: rowRef, className: "flex w-full h-full overflow-hidden", children: [
|
|
6105
|
+
/* @__PURE__ */ jsx21("div", { className: "flex-1 min-w-0", children: /* @__PURE__ */ jsx21(GraphVisualizer_default, {}) }),
|
|
6106
|
+
!hideActionPanel && /* @__PURE__ */ jsxs19(Fragment6, { children: [
|
|
6107
|
+
/* @__PURE__ */ jsx21(
|
|
6108
|
+
ResizeHandle,
|
|
6109
|
+
{
|
|
6110
|
+
orientation: "vertical",
|
|
6111
|
+
"aria-valuenow": panelWidth,
|
|
6112
|
+
"aria-valuemin": 300,
|
|
6113
|
+
"aria-valuemax": 640,
|
|
6114
|
+
onPointerDown: startResize,
|
|
6115
|
+
onDoubleClick: () => setPanelWidth(380)
|
|
6116
|
+
}
|
|
6117
|
+
),
|
|
6118
|
+
/* @__PURE__ */ jsx21(
|
|
6119
|
+
"div",
|
|
6120
|
+
{
|
|
6121
|
+
style: { width: panelWidth },
|
|
6122
|
+
className: "shrink-0 border-l overflow-hidden bg-card",
|
|
6123
|
+
children: /* @__PURE__ */ jsx21(
|
|
6124
|
+
ModelActionPanel,
|
|
6125
|
+
{
|
|
6126
|
+
model: value,
|
|
6127
|
+
onChange,
|
|
6128
|
+
readOnly,
|
|
6129
|
+
capabilities,
|
|
6130
|
+
widgetOptions
|
|
6131
|
+
}
|
|
6132
|
+
)
|
|
6133
|
+
}
|
|
6134
|
+
)
|
|
6135
|
+
] })
|
|
6136
|
+
] }) }) });
|
|
6137
|
+
}
|
|
6138
|
+
|
|
6139
|
+
// src/graph-edit/capabilities.ts
|
|
6140
|
+
import { z as z2 } from "zod";
|
|
6141
|
+
import { warnLog as warnLog2 } from "@petrarca/sonnet-core";
|
|
6142
|
+
var graphCapabilitiesSchema = z2.object({
|
|
6143
|
+
createNode: z2.enum(["edit", "readonly"]).default("readonly").meta({
|
|
6144
|
+
description: "Create new nodes. 'edit': the add-node affordance is shown. 'readonly' (default): hidden."
|
|
6145
|
+
}),
|
|
6146
|
+
updateNode: z2.enum(["edit", "readonly"]).default("readonly").meta({
|
|
6147
|
+
description: "Edit existing nodes (properties and relations). 'edit': the edit-node affordance is shown. 'readonly' (default): hidden."
|
|
6148
|
+
}),
|
|
6149
|
+
deleteNode: z2.enum(["edit", "readonly"]).default("readonly").meta({
|
|
6150
|
+
description: "Delete nodes. 'edit': the delete-node affordance is shown. 'readonly' (default): hidden."
|
|
6151
|
+
}),
|
|
6152
|
+
createEdge: z2.enum(["edit", "readonly"]).default("readonly").meta({
|
|
6153
|
+
description: "Create edges between two selected nodes. 'edit': the add-edge affordance is shown. 'readonly' (default): hidden."
|
|
6154
|
+
}),
|
|
6155
|
+
updateEdge: z2.enum(["edit", "readonly"]).default("readonly").meta({
|
|
6156
|
+
description: "Edit edge properties. 'edit': the edit-edge affordance is shown. 'readonly' (default): hidden."
|
|
6157
|
+
}),
|
|
6158
|
+
deleteEdge: z2.enum(["edit", "readonly"]).default("readonly").meta({
|
|
6159
|
+
description: "Delete edges. 'edit': the delete-edge affordance is shown. 'readonly' (default): hidden."
|
|
6160
|
+
})
|
|
6161
|
+
}).meta({
|
|
6162
|
+
id: "GraphCapabilities",
|
|
6163
|
+
title: "GraphCapabilities",
|
|
6164
|
+
description: "Capability profile for the graph viewer + instance editor. Gates what a consumer can do with a live data graph. Every field is optional and defaults to read-only (no editing affordances) so existing viewers are unaffected until a consumer opts in. Passing readOnly to the editor overrides this object to a fully read-only profile."
|
|
6165
|
+
});
|
|
6166
|
+
var READ_ONLY = {
|
|
6167
|
+
createNode: "readonly",
|
|
6168
|
+
updateNode: "readonly",
|
|
6169
|
+
deleteNode: "readonly",
|
|
6170
|
+
createEdge: "readonly",
|
|
6171
|
+
updateEdge: "readonly",
|
|
6172
|
+
deleteEdge: "readonly"
|
|
6173
|
+
};
|
|
6174
|
+
function resolveGraphCapabilities(capabilities, readOnly) {
|
|
6175
|
+
if (readOnly) return READ_ONLY;
|
|
6176
|
+
const parsed = graphCapabilitiesSchema.safeParse(capabilities ?? {});
|
|
6177
|
+
if (!parsed.success) {
|
|
6178
|
+
warnLog2(
|
|
6179
|
+
"[GraphEditor] Invalid capabilities \u2014 falling back to defaults.",
|
|
6180
|
+
parsed.error.issues
|
|
6181
|
+
);
|
|
6182
|
+
return graphCapabilitiesSchema.parse({});
|
|
6183
|
+
}
|
|
6184
|
+
return parsed.data;
|
|
6185
|
+
}
|
|
6186
|
+
function validateGraphCapabilities(input) {
|
|
6187
|
+
const parsed = graphCapabilitiesSchema.safeParse(input);
|
|
6188
|
+
if (parsed.success) return { ok: true, value: parsed.data };
|
|
6189
|
+
const errors = parsed.error.issues.map((i) => {
|
|
6190
|
+
const path = i.path.length ? i.path.join(".") : "(root)";
|
|
6191
|
+
return `${path}: ${i.message}`;
|
|
6192
|
+
});
|
|
6193
|
+
return { ok: false, errors };
|
|
6194
|
+
}
|
|
6195
|
+
|
|
6196
|
+
// src/graph-edit/relationFields.ts
|
|
6197
|
+
var REL_PREFIX = "__rel_";
|
|
6198
|
+
var REL_SUFFIX = "__";
|
|
6199
|
+
function isRelationField(key) {
|
|
6200
|
+
return key.startsWith(REL_PREFIX) && key.endsWith(REL_SUFFIX);
|
|
6201
|
+
}
|
|
6202
|
+
function relationFieldName(relationType) {
|
|
6203
|
+
return `${REL_PREFIX}${relationType.toLowerCase()}${REL_SUFFIX}`;
|
|
6204
|
+
}
|
|
6205
|
+
function relationTypeFromField(fieldName) {
|
|
6206
|
+
return fieldName.slice(REL_PREFIX.length, -REL_SUFFIX.length).toUpperCase();
|
|
6207
|
+
}
|
|
6208
|
+
function groupEdgesByRelation(nodeId2, edges) {
|
|
6209
|
+
const relations = {};
|
|
6210
|
+
for (const edge of edges) {
|
|
6211
|
+
const targetId = edge.start_id_ === nodeId2 ? edge.end_id_ : edge.start_id_;
|
|
6212
|
+
(relations[edge.label_] ??= []).push(targetId);
|
|
6213
|
+
}
|
|
6214
|
+
return relations;
|
|
6215
|
+
}
|
|
6216
|
+
function initializeFormData(properties, relations, editableRelationTypes) {
|
|
6217
|
+
const formData = { ...properties };
|
|
6218
|
+
for (const relationType of editableRelationTypes) {
|
|
6219
|
+
const ids = relations[relationType] ?? [];
|
|
6220
|
+
formData[relationFieldName(relationType)] = ids.map(String);
|
|
6221
|
+
}
|
|
6222
|
+
return formData;
|
|
6223
|
+
}
|
|
6224
|
+
function extractRelationFields(formData) {
|
|
6225
|
+
const properties = {};
|
|
6226
|
+
const relations = {};
|
|
6227
|
+
for (const [key, value] of Object.entries(formData)) {
|
|
6228
|
+
if (isRelationField(key)) {
|
|
6229
|
+
const relationType = relationTypeFromField(key);
|
|
6230
|
+
if (Array.isArray(value)) {
|
|
6231
|
+
relations[relationType] = value.map(Number);
|
|
6232
|
+
} else if (value != null) {
|
|
6233
|
+
relations[relationType] = [Number(value)];
|
|
6234
|
+
} else {
|
|
6235
|
+
relations[relationType] = [];
|
|
6236
|
+
}
|
|
6237
|
+
} else {
|
|
6238
|
+
properties[key] = value;
|
|
6239
|
+
}
|
|
6240
|
+
}
|
|
6241
|
+
return { properties, relations };
|
|
6242
|
+
}
|
|
6243
|
+
function hasRelationFields(formData) {
|
|
6244
|
+
return Object.keys(formData).some(isRelationField);
|
|
6245
|
+
}
|
|
6246
|
+
function relationTitle(relationType) {
|
|
6247
|
+
return relationType.toLowerCase().split("_").map((w) => w ? w.charAt(0).toUpperCase() + w.slice(1) : w).join(" ");
|
|
6248
|
+
}
|
|
6249
|
+
function editableRelations(relations) {
|
|
6250
|
+
return relations.filter((r) => r.form_field === true);
|
|
6251
|
+
}
|
|
6252
|
+
function hasProperties(schema) {
|
|
6253
|
+
return !!schema && Object.keys(schema.properties ?? {}).length > 0;
|
|
6254
|
+
}
|
|
6255
|
+
var DEFAULT_RELATION_WIDGET = "entity-select";
|
|
6256
|
+
function enrichSchemaWithRelations(baseSchema, relations) {
|
|
6257
|
+
const editable = editableRelations(relations);
|
|
6258
|
+
if (editable.length === 0) {
|
|
6259
|
+
return { schema: baseSchema, uiSchema: {} };
|
|
6260
|
+
}
|
|
6261
|
+
const properties = {
|
|
6262
|
+
...baseSchema.properties ?? {}
|
|
6263
|
+
};
|
|
6264
|
+
const uiSchema = {};
|
|
6265
|
+
for (const rel of editable) {
|
|
6266
|
+
const fieldName = relationFieldName(rel.rel_type_name);
|
|
6267
|
+
const multiple = rel.cardinality !== "ONE_TO_ONE";
|
|
6268
|
+
const title = rel.title ?? relationTitle(rel.rel_type_name);
|
|
6269
|
+
properties[fieldName] = multiple ? { type: "array", items: { type: "integer" }, title } : { type: "integer", title };
|
|
6270
|
+
uiSchema[fieldName] = {
|
|
6271
|
+
"x-ui-widget": rel.widget ?? DEFAULT_RELATION_WIDGET,
|
|
6272
|
+
"x-ui-options": {
|
|
6273
|
+
targetType: rel.to_type_name,
|
|
6274
|
+
multiple,
|
|
6275
|
+
relationType: rel.rel_type_name
|
|
6276
|
+
}
|
|
6277
|
+
};
|
|
6278
|
+
}
|
|
6279
|
+
const schema = {
|
|
6280
|
+
...baseSchema,
|
|
6281
|
+
properties
|
|
6282
|
+
};
|
|
6283
|
+
return { schema, uiSchema };
|
|
6284
|
+
}
|
|
6285
|
+
|
|
6286
|
+
// src/graph-edit/GraphEditToolbar.tsx
|
|
6287
|
+
import { useCallback as useCallback11, useMemo as useMemo11, useState as useState17 } from "react";
|
|
6288
|
+
import { IconButton as IconButton4 } from "@petrarca/sonnet-ui";
|
|
6289
|
+
import { Plus as Plus3, Edit, Link, Trash2 as Trash23 } from "lucide-react";
|
|
6290
|
+
|
|
6291
|
+
// src/graph-edit/NodeFormSheet.tsx
|
|
6292
|
+
import {
|
|
6293
|
+
useCallback as useCallback8,
|
|
6294
|
+
useEffect as useEffect6,
|
|
6295
|
+
useMemo as useMemo9,
|
|
6296
|
+
useRef as useRef7,
|
|
6297
|
+
useState as useState14
|
|
6298
|
+
} from "react";
|
|
6299
|
+
import {
|
|
6300
|
+
Sheet as Sheet4,
|
|
6301
|
+
SheetContent as SheetContent4,
|
|
6302
|
+
SheetHeader as SheetHeader4,
|
|
6303
|
+
SheetBody as SheetBody4,
|
|
6304
|
+
SheetTitle as SheetTitle4,
|
|
6305
|
+
SheetDescription as SheetDescription4,
|
|
6306
|
+
Alert
|
|
6307
|
+
} from "@petrarca/sonnet-ui";
|
|
6308
|
+
import {
|
|
6309
|
+
FormSelect as FormSelect2,
|
|
6310
|
+
FormCheckbox,
|
|
6311
|
+
JsonSchemaFormRenderer as JsonSchemaFormRenderer2
|
|
6312
|
+
} from "@petrarca/sonnet-forms";
|
|
6313
|
+
import { errorLog as errorLog4 } from "@petrarca/sonnet-core";
|
|
6314
|
+
import { Fragment as Fragment7, jsx as jsx22, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
6315
|
+
var EMPTY_RESOLVED = {
|
|
6316
|
+
schema: null,
|
|
6317
|
+
uiSchema: {},
|
|
6318
|
+
relationTypes: []
|
|
6319
|
+
};
|
|
6320
|
+
function temporarySchema(properties) {
|
|
6321
|
+
const props = {};
|
|
6322
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
6323
|
+
if (key.endsWith("_")) continue;
|
|
6324
|
+
const type = Array.isArray(value) ? "array" : typeof value;
|
|
6325
|
+
props[key] = { type, title: key };
|
|
6326
|
+
}
|
|
6327
|
+
return { type: "object", properties: props };
|
|
6328
|
+
}
|
|
6329
|
+
function resolveBaseSchema(provided, node) {
|
|
6330
|
+
if (provided) return provided;
|
|
6331
|
+
const properties = node?.properties_;
|
|
6332
|
+
if (properties && Object.keys(properties).length > 0) {
|
|
6333
|
+
return temporarySchema(properties);
|
|
6334
|
+
}
|
|
6335
|
+
return { type: "object", properties: {} };
|
|
6336
|
+
}
|
|
6337
|
+
function useNodeSchema(schemaProvider, label, node) {
|
|
6338
|
+
const [resolved, setResolved] = useState14(EMPTY_RESOLVED);
|
|
6339
|
+
const nodeRef = useRef7(node);
|
|
6340
|
+
nodeRef.current = node;
|
|
6341
|
+
useEffect6(() => {
|
|
6342
|
+
let cancelled = false;
|
|
6343
|
+
if (!label) {
|
|
6344
|
+
setResolved(EMPTY_RESOLVED);
|
|
6345
|
+
return;
|
|
6346
|
+
}
|
|
6347
|
+
void (async () => {
|
|
6348
|
+
try {
|
|
6349
|
+
const [provided, rels] = await Promise.all([
|
|
6350
|
+
schemaProvider.getNodeSchema(label),
|
|
6351
|
+
schemaProvider.relationsFor(label)
|
|
6352
|
+
]);
|
|
6353
|
+
if (cancelled) return;
|
|
6354
|
+
const base = resolveBaseSchema(provided, nodeRef.current);
|
|
6355
|
+
const editable = editableRelations(rels);
|
|
6356
|
+
const { schema, uiSchema } = enrichSchemaWithRelations(base, editable);
|
|
6357
|
+
setResolved({
|
|
6358
|
+
schema,
|
|
6359
|
+
uiSchema,
|
|
6360
|
+
relationTypes: editable.map((r) => r.rel_type_name)
|
|
6361
|
+
});
|
|
6362
|
+
} catch (e) {
|
|
6363
|
+
if (cancelled) return;
|
|
6364
|
+
errorLog4("[NodeFormSheet] failed to load node schema", e);
|
|
6365
|
+
setResolved({
|
|
6366
|
+
schema: resolveBaseSchema(null, nodeRef.current),
|
|
6367
|
+
uiSchema: {},
|
|
6368
|
+
relationTypes: []
|
|
6369
|
+
});
|
|
6370
|
+
}
|
|
6371
|
+
})();
|
|
6372
|
+
return () => {
|
|
6373
|
+
cancelled = true;
|
|
6374
|
+
};
|
|
6375
|
+
}, [schemaProvider, label]);
|
|
6376
|
+
return resolved;
|
|
6377
|
+
}
|
|
6378
|
+
function useExistingFormData(port, node, relationTypes) {
|
|
6379
|
+
const [data, setData] = useState14(null);
|
|
6380
|
+
const nodeId2 = node?.id_;
|
|
6381
|
+
const propertiesRef = useRef7(node?.properties_);
|
|
6382
|
+
propertiesRef.current = node?.properties_;
|
|
6383
|
+
useEffect6(() => {
|
|
6384
|
+
let cancelled = false;
|
|
6385
|
+
if (nodeId2 == null) {
|
|
6386
|
+
setData(null);
|
|
6387
|
+
return;
|
|
6388
|
+
}
|
|
6389
|
+
const properties = propertiesRef.current ?? {};
|
|
6390
|
+
if (relationTypes.length === 0) {
|
|
6391
|
+
setData({ ...properties });
|
|
6392
|
+
return;
|
|
6393
|
+
}
|
|
6394
|
+
void (async () => {
|
|
6395
|
+
try {
|
|
6396
|
+
const [inbound, outbound] = await Promise.all([
|
|
6397
|
+
port.getInboundEdges(nodeId2),
|
|
6398
|
+
port.getOutboundEdges(nodeId2)
|
|
6399
|
+
]);
|
|
6400
|
+
if (cancelled) return;
|
|
6401
|
+
const relations = groupEdgesByRelation(nodeId2, [
|
|
6402
|
+
...inbound,
|
|
6403
|
+
...outbound
|
|
6404
|
+
]);
|
|
6405
|
+
setData(initializeFormData(properties, relations, relationTypes));
|
|
6406
|
+
} catch (e) {
|
|
6407
|
+
if (cancelled) return;
|
|
6408
|
+
errorLog4("[NodeFormSheet] failed to load node relations", e);
|
|
6409
|
+
setData({ ...properties });
|
|
6410
|
+
}
|
|
6411
|
+
})();
|
|
6412
|
+
return () => {
|
|
6413
|
+
cancelled = true;
|
|
6414
|
+
};
|
|
6415
|
+
}, [port, nodeId2, relationTypes]);
|
|
6416
|
+
return data;
|
|
6417
|
+
}
|
|
6418
|
+
function LabelField({
|
|
6419
|
+
isEdit,
|
|
6420
|
+
selectedLabel,
|
|
6421
|
+
labels,
|
|
6422
|
+
onSelect
|
|
6423
|
+
}) {
|
|
6424
|
+
if (isEdit) {
|
|
6425
|
+
return /* @__PURE__ */ jsxs20("div", { children: [
|
|
6426
|
+
/* @__PURE__ */ jsx22("span", { className: "heading-meta block", children: "Label" }),
|
|
6427
|
+
/* @__PURE__ */ jsx22("span", { className: "text-sm text-muted-foreground", children: selectedLabel })
|
|
6428
|
+
] });
|
|
6429
|
+
}
|
|
6430
|
+
return /* @__PURE__ */ jsx22(
|
|
6431
|
+
FormSelect2,
|
|
6432
|
+
{
|
|
6433
|
+
label: "Label",
|
|
6434
|
+
options: labels.map((l) => ({ value: l, label: l })),
|
|
6435
|
+
clearable: false,
|
|
6436
|
+
value: selectedLabel,
|
|
6437
|
+
onChange: onSelect
|
|
6438
|
+
}
|
|
6439
|
+
);
|
|
6440
|
+
}
|
|
6441
|
+
function useCreateModeLabels(schemaProvider, open, isEdit) {
|
|
6442
|
+
const [labels, setLabels] = useState14([]);
|
|
6443
|
+
useEffect6(() => {
|
|
6444
|
+
let cancelled = false;
|
|
6445
|
+
if (!open || isEdit) return;
|
|
6446
|
+
void schemaProvider.listNodeLabels().then((ls) => {
|
|
6447
|
+
if (!cancelled) setLabels(ls);
|
|
6448
|
+
});
|
|
6449
|
+
return () => {
|
|
6450
|
+
cancelled = true;
|
|
6451
|
+
};
|
|
6452
|
+
}, [open, isEdit, schemaProvider]);
|
|
6453
|
+
return labels;
|
|
6454
|
+
}
|
|
6455
|
+
function NodeForm({
|
|
6456
|
+
formKey,
|
|
6457
|
+
schema,
|
|
6458
|
+
uiSchema,
|
|
6459
|
+
data,
|
|
6460
|
+
widgets,
|
|
6461
|
+
isEdit,
|
|
6462
|
+
onSave,
|
|
6463
|
+
onCancel
|
|
6464
|
+
}) {
|
|
6465
|
+
return /* @__PURE__ */ jsx22(
|
|
6466
|
+
JsonSchemaFormRenderer2,
|
|
6467
|
+
{
|
|
6468
|
+
schema,
|
|
6469
|
+
uiSchema,
|
|
6470
|
+
data,
|
|
6471
|
+
widgets,
|
|
6472
|
+
showActions: true,
|
|
6473
|
+
allowPristineSave: !isEdit,
|
|
6474
|
+
saveLabel: isEdit ? "Save" : "Create",
|
|
6475
|
+
cancelLabel: "Cancel",
|
|
6476
|
+
showExtraProperties: true,
|
|
6477
|
+
onUpdate: onSave,
|
|
6478
|
+
onCancel
|
|
6479
|
+
},
|
|
6480
|
+
formKey
|
|
6481
|
+
);
|
|
6482
|
+
}
|
|
6483
|
+
function NodeFormSheetBody({
|
|
6484
|
+
isEdit,
|
|
6485
|
+
selectedLabel,
|
|
6486
|
+
labels,
|
|
6487
|
+
onSelectLabel,
|
|
6488
|
+
schema,
|
|
6489
|
+
uiSchema,
|
|
6490
|
+
formKey,
|
|
6491
|
+
formData,
|
|
6492
|
+
widgets,
|
|
6493
|
+
error,
|
|
6494
|
+
keepOpen,
|
|
6495
|
+
onKeepOpenChange,
|
|
6496
|
+
onSave,
|
|
6497
|
+
onCancel
|
|
6498
|
+
}) {
|
|
6499
|
+
return /* @__PURE__ */ jsxs20(Fragment7, { children: [
|
|
6500
|
+
/* @__PURE__ */ jsxs20(SheetHeader4, { children: [
|
|
6501
|
+
/* @__PURE__ */ jsx22(SheetTitle4, { children: isEdit ? "Edit node" : "Add node" }),
|
|
6502
|
+
/* @__PURE__ */ jsx22(SheetDescription4, { children: isEdit ? "Edit this node's properties and relations." : "Create a node and (optionally) its relations." })
|
|
6503
|
+
] }),
|
|
6504
|
+
/* @__PURE__ */ jsxs20(SheetBody4, { className: "flex flex-col gap-4", children: [
|
|
6505
|
+
/* @__PURE__ */ jsx22(
|
|
6506
|
+
LabelField,
|
|
6507
|
+
{
|
|
6508
|
+
isEdit,
|
|
6509
|
+
selectedLabel,
|
|
6510
|
+
labels,
|
|
6511
|
+
onSelect: onSelectLabel
|
|
6512
|
+
}
|
|
6513
|
+
),
|
|
6514
|
+
schema && /* @__PURE__ */ jsx22(
|
|
6515
|
+
NodeForm,
|
|
6516
|
+
{
|
|
6517
|
+
formKey,
|
|
6518
|
+
schema,
|
|
6519
|
+
uiSchema,
|
|
6520
|
+
data: formData,
|
|
6521
|
+
widgets,
|
|
6522
|
+
isEdit,
|
|
6523
|
+
onSave,
|
|
6524
|
+
onCancel
|
|
6525
|
+
}
|
|
6526
|
+
),
|
|
6527
|
+
!isEdit && /* @__PURE__ */ jsx22(
|
|
6528
|
+
FormCheckbox,
|
|
6529
|
+
{
|
|
6530
|
+
label: "Create another node",
|
|
6531
|
+
checked: keepOpen,
|
|
6532
|
+
onChange: (c) => onKeepOpenChange(c === true)
|
|
6533
|
+
}
|
|
6534
|
+
),
|
|
6535
|
+
error && /* @__PURE__ */ jsx22(Alert, { variant: "destructive", children: error })
|
|
6536
|
+
] })
|
|
6537
|
+
] });
|
|
6538
|
+
}
|
|
6539
|
+
function NodeFormSheet({
|
|
6540
|
+
open,
|
|
6541
|
+
onOpenChange,
|
|
6542
|
+
node,
|
|
6543
|
+
port,
|
|
6544
|
+
schemaProvider,
|
|
6545
|
+
widgets,
|
|
6546
|
+
onSaved
|
|
6547
|
+
}) {
|
|
6548
|
+
const isEdit = !!node?.id_;
|
|
6549
|
+
const [selectedLabel, setSelectedLabel] = useState14(
|
|
6550
|
+
node?.label_ ?? null
|
|
6551
|
+
);
|
|
6552
|
+
const [error, setError] = useState14("");
|
|
6553
|
+
const [keepOpen, setKeepOpen] = useState14(false);
|
|
6554
|
+
const [formNonce, setFormNonce] = useState14(0);
|
|
6555
|
+
const labels = useCreateModeLabels(schemaProvider, open, isEdit);
|
|
6556
|
+
useEffect6(() => {
|
|
6557
|
+
if (open) {
|
|
6558
|
+
setSelectedLabel(node?.label_ ?? null);
|
|
6559
|
+
setError("");
|
|
6560
|
+
setKeepOpen(false);
|
|
6561
|
+
setFormNonce(0);
|
|
6562
|
+
}
|
|
6563
|
+
}, [open, node]);
|
|
6564
|
+
const { schema, uiSchema, relationTypes } = useNodeSchema(
|
|
6565
|
+
schemaProvider,
|
|
6566
|
+
selectedLabel,
|
|
6567
|
+
node
|
|
6568
|
+
);
|
|
6569
|
+
const existingData = useExistingFormData(port, node, relationTypes);
|
|
6570
|
+
const formData = useMemo9(() => existingData ?? {}, [existingData]);
|
|
6571
|
+
const stableOnSaved = useStableRef(onSaved);
|
|
6572
|
+
const stableOnOpenChange = useStableRef(onOpenChange);
|
|
6573
|
+
const handleSave = useCallback8(
|
|
6574
|
+
(data) => {
|
|
6575
|
+
if (!selectedLabel) return;
|
|
6576
|
+
void (async () => {
|
|
6577
|
+
try {
|
|
6578
|
+
const saved = await saveNode({
|
|
6579
|
+
port,
|
|
6580
|
+
label: selectedLabel,
|
|
6581
|
+
id: node?.id_,
|
|
6582
|
+
data
|
|
6583
|
+
});
|
|
6584
|
+
setError("");
|
|
6585
|
+
stableOnSaved?.(saved);
|
|
6586
|
+
if (!isEdit && keepOpen) {
|
|
6587
|
+
setFormNonce((n) => n + 1);
|
|
6588
|
+
} else {
|
|
6589
|
+
stableOnOpenChange(false);
|
|
6590
|
+
}
|
|
6591
|
+
} catch (e) {
|
|
6592
|
+
setError(saveErrorMessage(e, isEdit));
|
|
6593
|
+
}
|
|
6594
|
+
})();
|
|
6595
|
+
},
|
|
6596
|
+
[
|
|
6597
|
+
port,
|
|
6598
|
+
selectedLabel,
|
|
6599
|
+
node?.id_,
|
|
6600
|
+
isEdit,
|
|
6601
|
+
keepOpen,
|
|
6602
|
+
stableOnSaved,
|
|
6603
|
+
stableOnOpenChange
|
|
6604
|
+
]
|
|
6605
|
+
);
|
|
6606
|
+
return /* @__PURE__ */ jsx22(Sheet4, { open, onOpenChange, children: /* @__PURE__ */ jsx22(SheetContent4, { children: /* @__PURE__ */ jsx22(
|
|
6607
|
+
NodeFormSheetBody,
|
|
6608
|
+
{
|
|
6609
|
+
isEdit,
|
|
6610
|
+
selectedLabel,
|
|
6611
|
+
labels,
|
|
6612
|
+
onSelectLabel: setSelectedLabel,
|
|
6613
|
+
schema,
|
|
6614
|
+
uiSchema,
|
|
6615
|
+
formKey: `${selectedLabel}:${node?.id_ ?? "new"}:${formNonce}`,
|
|
6616
|
+
formData,
|
|
6617
|
+
widgets,
|
|
6618
|
+
error,
|
|
6619
|
+
keepOpen,
|
|
6620
|
+
onKeepOpenChange: setKeepOpen,
|
|
6621
|
+
onSave: handleSave,
|
|
6622
|
+
onCancel: () => stableOnOpenChange(false)
|
|
6623
|
+
}
|
|
6624
|
+
) }) });
|
|
6625
|
+
}
|
|
6626
|
+
function saveErrorMessage(e, isEdit) {
|
|
6627
|
+
if (e instanceof Error) return e.message;
|
|
6628
|
+
return isEdit ? "Failed to update node" : "Failed to create node";
|
|
6629
|
+
}
|
|
6630
|
+
async function saveNode({
|
|
6631
|
+
port,
|
|
6632
|
+
label,
|
|
6633
|
+
id,
|
|
6634
|
+
data
|
|
6635
|
+
}) {
|
|
6636
|
+
const isEdit = id != null;
|
|
6637
|
+
const { properties, relations } = extractRelationFields(data);
|
|
6638
|
+
const hasRelations = Object.keys(relations).length > 0;
|
|
6639
|
+
if (hasRelations) {
|
|
6640
|
+
if (isEdit) {
|
|
6641
|
+
const res2 = await port.updateNodeWithRelations({
|
|
6642
|
+
id_: id,
|
|
6643
|
+
label_: label,
|
|
6644
|
+
properties_: properties,
|
|
6645
|
+
relations
|
|
6646
|
+
});
|
|
6647
|
+
return res2.node;
|
|
6648
|
+
}
|
|
6649
|
+
const res = await port.createNodeWithRelations({
|
|
6650
|
+
label_: label,
|
|
6651
|
+
properties_: properties,
|
|
6652
|
+
relations
|
|
6653
|
+
});
|
|
6654
|
+
return res.node;
|
|
6655
|
+
}
|
|
6656
|
+
if (isEdit) {
|
|
6657
|
+
return port.updateNode({ id_: id, label_: label, properties_: properties });
|
|
6658
|
+
}
|
|
6659
|
+
return port.createNode({ label_: label, properties_: properties });
|
|
6660
|
+
}
|
|
6661
|
+
|
|
6662
|
+
// src/graph-edit/EdgeFormSheet.tsx
|
|
6663
|
+
import { useCallback as useCallback9, useEffect as useEffect7, useMemo as useMemo10, useState as useState15 } from "react";
|
|
6664
|
+
import {
|
|
6665
|
+
Sheet as Sheet5,
|
|
6666
|
+
SheetContent as SheetContent5,
|
|
6667
|
+
SheetHeader as SheetHeader5,
|
|
6668
|
+
SheetBody as SheetBody5,
|
|
6669
|
+
SheetTitle as SheetTitle5,
|
|
6670
|
+
SheetDescription as SheetDescription5,
|
|
6671
|
+
Dialog,
|
|
6672
|
+
DialogContent,
|
|
6673
|
+
DialogHeader,
|
|
6674
|
+
DialogTitle,
|
|
6675
|
+
DialogDescription,
|
|
6676
|
+
DialogFooter,
|
|
6677
|
+
Button as Button7,
|
|
6678
|
+
IconButton as IconButton3,
|
|
6679
|
+
Alert as Alert2
|
|
6680
|
+
} from "@petrarca/sonnet-ui";
|
|
6681
|
+
import { Pencil as Pencil2, Plus as Plus2, Trash2 as Trash22, ArrowRight as ArrowRight2 } from "lucide-react";
|
|
6682
|
+
import {
|
|
6683
|
+
JsonSchemaFormRenderer as JsonSchemaFormRenderer3
|
|
6684
|
+
} from "@petrarca/sonnet-forms";
|
|
6685
|
+
import { errorLog as errorLog5 } from "@petrarca/sonnet-core";
|
|
6686
|
+
import { jsx as jsx23, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
6687
|
+
function usePairState(port, schemaProvider, open, nodeAId, nodeALabel, nodeBId, nodeBLabel, reloadKey) {
|
|
6688
|
+
const [active, setActive] = useState15([]);
|
|
6689
|
+
const [available, setAvailable] = useState15([]);
|
|
6690
|
+
useEffect7(() => {
|
|
6691
|
+
let cancelled = false;
|
|
6692
|
+
if (!open || nodeAId == null || nodeBId == null) {
|
|
6693
|
+
setActive([]);
|
|
6694
|
+
setAvailable([]);
|
|
6695
|
+
return;
|
|
6696
|
+
}
|
|
6697
|
+
void (async () => {
|
|
6698
|
+
try {
|
|
6699
|
+
const [edges, relationsAB, relationsBA] = await Promise.all([
|
|
6700
|
+
port.getPairEdges(nodeAId, nodeBId),
|
|
6701
|
+
schemaProvider.relationsFor(nodeALabel),
|
|
6702
|
+
schemaProvider.relationsFor(nodeBLabel)
|
|
6703
|
+
]);
|
|
6704
|
+
if (cancelled) return;
|
|
6705
|
+
setActive(edges);
|
|
6706
|
+
const present = new Set(edges.map((e) => e.label_));
|
|
6707
|
+
const forward = relationsAB.filter((r) => r.to_type_name === nodeBLabel).map((r) => ({
|
|
6708
|
+
label: r.rel_type_name,
|
|
6709
|
+
sourceId: nodeAId,
|
|
6710
|
+
targetId: nodeBId
|
|
6711
|
+
}));
|
|
6712
|
+
const backward = relationsBA.filter((r) => r.to_type_name === nodeALabel).map((r) => ({
|
|
6713
|
+
label: r.rel_type_name,
|
|
6714
|
+
sourceId: nodeBId,
|
|
6715
|
+
targetId: nodeAId
|
|
6716
|
+
}));
|
|
6717
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6718
|
+
const creatable = [];
|
|
6719
|
+
for (const entry of [...forward, ...backward]) {
|
|
6720
|
+
const key = `${entry.label}:${entry.sourceId}:${entry.targetId}`;
|
|
6721
|
+
if (present.has(entry.label) || seen.has(key)) continue;
|
|
6722
|
+
seen.add(key);
|
|
6723
|
+
creatable.push(entry);
|
|
6724
|
+
}
|
|
6725
|
+
setAvailable(creatable);
|
|
6726
|
+
} catch (e) {
|
|
6727
|
+
if (cancelled) return;
|
|
6728
|
+
errorLog5("[EdgeFormSheet] failed to load pair edges", e);
|
|
6729
|
+
}
|
|
6730
|
+
})();
|
|
6731
|
+
return () => {
|
|
6732
|
+
cancelled = true;
|
|
6733
|
+
};
|
|
6734
|
+
}, [
|
|
6735
|
+
port,
|
|
6736
|
+
schemaProvider,
|
|
6737
|
+
open,
|
|
6738
|
+
nodeAId,
|
|
6739
|
+
nodeALabel,
|
|
6740
|
+
nodeBId,
|
|
6741
|
+
nodeBLabel,
|
|
6742
|
+
reloadKey
|
|
6743
|
+
]);
|
|
6744
|
+
return { active, available };
|
|
6745
|
+
}
|
|
6746
|
+
function draftLabel(draft) {
|
|
6747
|
+
if (!draft) return null;
|
|
6748
|
+
return draft.mode === "create" ? draft.entry.label : draft.edge.label_;
|
|
6749
|
+
}
|
|
6750
|
+
var EMPTY_EDGE_SCHEMA = {
|
|
6751
|
+
type: "object",
|
|
6752
|
+
properties: {}
|
|
6753
|
+
};
|
|
6754
|
+
function useEdgeSchema(schemaProvider, draft) {
|
|
6755
|
+
const [schema, setSchema] = useState15(null);
|
|
6756
|
+
const label = useMemo10(() => draftLabel(draft), [draft]);
|
|
6757
|
+
useEffect7(() => {
|
|
6758
|
+
let cancelled = false;
|
|
6759
|
+
if (!label) {
|
|
6760
|
+
setSchema(null);
|
|
6761
|
+
return;
|
|
6762
|
+
}
|
|
6763
|
+
void schemaProvider.getEdgeSchema(label).then((s) => {
|
|
6764
|
+
if (!cancelled) setSchema(s ?? EMPTY_EDGE_SCHEMA);
|
|
6765
|
+
}).catch((e) => {
|
|
6766
|
+
if (cancelled) return;
|
|
6767
|
+
errorLog5("[EdgeFormSheet] failed to load edge schema", e);
|
|
6768
|
+
setSchema(EMPTY_EDGE_SCHEMA);
|
|
6769
|
+
});
|
|
6770
|
+
return () => {
|
|
6771
|
+
cancelled = true;
|
|
6772
|
+
};
|
|
6773
|
+
}, [schemaProvider, label]);
|
|
6774
|
+
return schema;
|
|
6775
|
+
}
|
|
6776
|
+
function useEditableEdgeTypes(schemaProvider, active) {
|
|
6777
|
+
const [editable, setEditable] = useState15({});
|
|
6778
|
+
const labelsKey = useMemo10(
|
|
6779
|
+
() => Array.from(new Set(active.map((e) => e.label_))).sort().join("\0"),
|
|
6780
|
+
[active]
|
|
6781
|
+
);
|
|
6782
|
+
useEffect7(() => {
|
|
6783
|
+
let cancelled = false;
|
|
6784
|
+
const labels = labelsKey ? labelsKey.split("\0") : [];
|
|
6785
|
+
if (labels.length === 0) {
|
|
6786
|
+
setEditable({});
|
|
6787
|
+
return;
|
|
6788
|
+
}
|
|
6789
|
+
void (async () => {
|
|
6790
|
+
const entries = await Promise.all(
|
|
6791
|
+
labels.map(async (label) => {
|
|
6792
|
+
try {
|
|
6793
|
+
const schema = await schemaProvider.getEdgeSchema(label);
|
|
6794
|
+
return [label, hasProperties(schema)];
|
|
6795
|
+
} catch {
|
|
6796
|
+
return [label, false];
|
|
6797
|
+
}
|
|
6798
|
+
})
|
|
6799
|
+
);
|
|
6800
|
+
if (!cancelled) setEditable(Object.fromEntries(entries));
|
|
6801
|
+
})();
|
|
6802
|
+
return () => {
|
|
6803
|
+
cancelled = true;
|
|
6804
|
+
};
|
|
6805
|
+
}, [schemaProvider, labelsKey]);
|
|
6806
|
+
return editable;
|
|
6807
|
+
}
|
|
6808
|
+
function SectionTitle2({
|
|
6809
|
+
children
|
|
6810
|
+
}) {
|
|
6811
|
+
return /* @__PURE__ */ jsx23("div", { className: "mb-2", children: /* @__PURE__ */ jsx23("span", { className: "heading-meta", children }) });
|
|
6812
|
+
}
|
|
6813
|
+
function EdgeRow({
|
|
6814
|
+
label,
|
|
6815
|
+
onEdit,
|
|
6816
|
+
onDelete,
|
|
6817
|
+
onAdd
|
|
6818
|
+
}) {
|
|
6819
|
+
return /* @__PURE__ */ jsxs21("div", { className: "group/row flex items-center gap-2 px-3 py-2", children: [
|
|
6820
|
+
/* @__PURE__ */ jsx23("span", { className: "flex-1 text-sm font-medium", children: label }),
|
|
6821
|
+
/* @__PURE__ */ jsxs21("span", { className: "flex gap-1", children: [
|
|
6822
|
+
onAdd && /* @__PURE__ */ jsx23(IconButton3, { label: "Create edge", revealOnHover: true, onClick: onAdd, children: /* @__PURE__ */ jsx23(Plus2, {}) }),
|
|
6823
|
+
onEdit && /* @__PURE__ */ jsx23(IconButton3, { label: "Edit edge", revealOnHover: true, onClick: onEdit, children: /* @__PURE__ */ jsx23(Pencil2, {}) }),
|
|
6824
|
+
onDelete && /* @__PURE__ */ jsx23(
|
|
6825
|
+
IconButton3,
|
|
6826
|
+
{
|
|
6827
|
+
label: "Delete edge",
|
|
6828
|
+
tone: "destructive",
|
|
6829
|
+
revealOnHover: true,
|
|
6830
|
+
onClick: onDelete,
|
|
6831
|
+
children: /* @__PURE__ */ jsx23(Trash22, {})
|
|
6832
|
+
}
|
|
6833
|
+
)
|
|
6834
|
+
] })
|
|
6835
|
+
] });
|
|
6836
|
+
}
|
|
6837
|
+
function PairLists({
|
|
6838
|
+
nodeALabel,
|
|
6839
|
+
nodeBLabel,
|
|
6840
|
+
nodeAId,
|
|
6841
|
+
active,
|
|
6842
|
+
available,
|
|
6843
|
+
editableTypes,
|
|
6844
|
+
onEdit,
|
|
6845
|
+
onDelete,
|
|
6846
|
+
onCreate
|
|
6847
|
+
}) {
|
|
6848
|
+
const empty = active.length === 0 && available.length === 0;
|
|
6849
|
+
const labelCounts = available.reduce((acc, e) => {
|
|
6850
|
+
acc[e.label] = (acc[e.label] ?? 0) + 1;
|
|
6851
|
+
return acc;
|
|
6852
|
+
}, {});
|
|
6853
|
+
const rowLabel = (entry) => {
|
|
6854
|
+
if (labelCounts[entry.label] <= 1) return entry.label;
|
|
6855
|
+
const from = entry.sourceId === nodeAId ? nodeALabel : nodeBLabel;
|
|
6856
|
+
const to = entry.sourceId === nodeAId ? nodeBLabel : nodeALabel;
|
|
6857
|
+
return `${entry.label} (${from} -> ${to})`;
|
|
6858
|
+
};
|
|
6859
|
+
return /* @__PURE__ */ jsxs21("div", { className: "flex flex-col gap-4", children: [
|
|
6860
|
+
empty && /* @__PURE__ */ jsxs21("p", { className: "text-xs text-muted-foreground", children: [
|
|
6861
|
+
"No edges are defined between ",
|
|
6862
|
+
nodeALabel,
|
|
6863
|
+
" and ",
|
|
6864
|
+
nodeBLabel,
|
|
6865
|
+
"."
|
|
6866
|
+
] }),
|
|
6867
|
+
active.length > 0 && /* @__PURE__ */ jsxs21("div", { children: [
|
|
6868
|
+
/* @__PURE__ */ jsx23(SectionTitle2, { children: "Active edges" }),
|
|
6869
|
+
/* @__PURE__ */ jsx23("div", { className: "rounded-lg border divide-y", children: active.map((edge) => /* @__PURE__ */ jsx23(
|
|
6870
|
+
EdgeRow,
|
|
6871
|
+
{
|
|
6872
|
+
label: edge.label_,
|
|
6873
|
+
onEdit: editableTypes[edge.label_] ? () => onEdit(edge) : void 0,
|
|
6874
|
+
onDelete: () => onDelete(edge)
|
|
6875
|
+
},
|
|
6876
|
+
edge.id_
|
|
6877
|
+
)) })
|
|
6878
|
+
] }),
|
|
6879
|
+
available.length > 0 && /* @__PURE__ */ jsxs21("div", { children: [
|
|
6880
|
+
/* @__PURE__ */ jsx23(SectionTitle2, { children: "Available edges" }),
|
|
6881
|
+
/* @__PURE__ */ jsx23("div", { className: "rounded-lg border divide-y", children: available.map((entry) => /* @__PURE__ */ jsx23(
|
|
6882
|
+
EdgeRow,
|
|
6883
|
+
{
|
|
6884
|
+
label: rowLabel(entry),
|
|
6885
|
+
onAdd: () => onCreate(entry)
|
|
6886
|
+
},
|
|
6887
|
+
`${entry.label}:${entry.sourceId}:${entry.targetId}`
|
|
6888
|
+
)) })
|
|
6889
|
+
] })
|
|
6890
|
+
] });
|
|
6891
|
+
}
|
|
6892
|
+
function DraftForm({
|
|
6893
|
+
draft,
|
|
6894
|
+
schema,
|
|
6895
|
+
widgets,
|
|
6896
|
+
onSave,
|
|
6897
|
+
onCancel
|
|
6898
|
+
}) {
|
|
6899
|
+
const label = draft.mode === "create" ? draft.entry.label : draft.edge.label_;
|
|
6900
|
+
const data = draft.mode === "edit" ? draft.edge.properties_ ?? {} : {};
|
|
6901
|
+
return /* @__PURE__ */ jsxs21("div", { children: [
|
|
6902
|
+
/* @__PURE__ */ jsxs21(SectionTitle2, { children: [
|
|
6903
|
+
draft.mode === "create" ? "Add" : "Edit",
|
|
6904
|
+
" ",
|
|
6905
|
+
label
|
|
6906
|
+
] }),
|
|
6907
|
+
/* @__PURE__ */ jsx23(
|
|
6908
|
+
JsonSchemaFormRenderer3,
|
|
6909
|
+
{
|
|
6910
|
+
schema,
|
|
6911
|
+
data,
|
|
6912
|
+
widgets,
|
|
6913
|
+
showActions: true,
|
|
6914
|
+
allowPristineSave: draft.mode === "create",
|
|
6915
|
+
saveLabel: draft.mode === "create" ? "Add" : "Save",
|
|
6916
|
+
cancelLabel: "Cancel",
|
|
6917
|
+
showExtraProperties: true,
|
|
6918
|
+
onUpdate: onSave,
|
|
6919
|
+
onCancel
|
|
6920
|
+
},
|
|
6921
|
+
label
|
|
6922
|
+
)
|
|
6923
|
+
] });
|
|
6924
|
+
}
|
|
6925
|
+
function EdgeFormSheet({
|
|
6926
|
+
open,
|
|
6927
|
+
onOpenChange,
|
|
6928
|
+
nodeA,
|
|
6929
|
+
nodeB,
|
|
6930
|
+
port,
|
|
6931
|
+
schemaProvider,
|
|
6932
|
+
widgets,
|
|
6933
|
+
onChanged
|
|
6934
|
+
}) {
|
|
6935
|
+
const [reloadKey, setReloadKey] = useState15(0);
|
|
6936
|
+
const [draft, setDraft] = useState15(null);
|
|
6937
|
+
const [error, setError] = useState15("");
|
|
6938
|
+
const [pendingDelete, setPendingDelete] = useState15(null);
|
|
6939
|
+
const { active, available } = usePairState(
|
|
6940
|
+
port,
|
|
6941
|
+
schemaProvider,
|
|
6942
|
+
open,
|
|
6943
|
+
nodeA.id_,
|
|
6944
|
+
nodeA.label_,
|
|
6945
|
+
nodeB.id_,
|
|
6946
|
+
nodeB.label_,
|
|
6947
|
+
reloadKey
|
|
6948
|
+
);
|
|
6949
|
+
const schema = useEdgeSchema(schemaProvider, draft);
|
|
6950
|
+
const editableTypes = useEditableEdgeTypes(schemaProvider, active);
|
|
6951
|
+
const nodeAId = nodeA.id_;
|
|
6952
|
+
const nodeBId = nodeB.id_;
|
|
6953
|
+
useEffect7(() => {
|
|
6954
|
+
if (open) {
|
|
6955
|
+
setDraft(null);
|
|
6956
|
+
setError("");
|
|
6957
|
+
setPendingDelete(null);
|
|
6958
|
+
}
|
|
6959
|
+
}, [open, nodeAId, nodeBId]);
|
|
6960
|
+
const reload = useCallback9(() => {
|
|
6961
|
+
setReloadKey((k) => k + 1);
|
|
6962
|
+
onChanged?.();
|
|
6963
|
+
}, [onChanged]);
|
|
6964
|
+
const handleSave = useCallback9(
|
|
6965
|
+
(formData) => {
|
|
6966
|
+
if (!draft) return;
|
|
6967
|
+
const { properties } = extractRelationFields(formData);
|
|
6968
|
+
void (async () => {
|
|
6969
|
+
try {
|
|
6970
|
+
if (draft.mode === "create") {
|
|
6971
|
+
await port.createEdge({
|
|
6972
|
+
label_: draft.entry.label,
|
|
6973
|
+
start_id_: draft.entry.sourceId,
|
|
6974
|
+
end_id_: draft.entry.targetId,
|
|
6975
|
+
properties_: properties
|
|
6976
|
+
});
|
|
6977
|
+
} else {
|
|
6978
|
+
await port.updateEdge({
|
|
6979
|
+
id_: draft.edge.id_,
|
|
6980
|
+
label_: draft.edge.label_,
|
|
6981
|
+
start_id_: draft.edge.start_id_,
|
|
6982
|
+
end_id_: draft.edge.end_id_,
|
|
6983
|
+
properties_: properties
|
|
6984
|
+
});
|
|
6985
|
+
}
|
|
6986
|
+
setDraft(null);
|
|
6987
|
+
setError("");
|
|
6988
|
+
reload();
|
|
6989
|
+
} catch (e) {
|
|
6990
|
+
setError(e instanceof Error ? e.message : "Failed to save edge");
|
|
6991
|
+
}
|
|
6992
|
+
})();
|
|
6993
|
+
},
|
|
6994
|
+
[draft, port, reload]
|
|
6995
|
+
);
|
|
6996
|
+
const confirmDelete = useCallback9(async () => {
|
|
6997
|
+
const edge = pendingDelete;
|
|
6998
|
+
if (!edge || edge.id_ == null) return;
|
|
6999
|
+
try {
|
|
7000
|
+
await port.deleteEdge(edge.id_);
|
|
7001
|
+
setError("");
|
|
7002
|
+
setPendingDelete(null);
|
|
7003
|
+
reload();
|
|
7004
|
+
} catch (e) {
|
|
7005
|
+
setPendingDelete(null);
|
|
7006
|
+
setError(e instanceof Error ? e.message : "Failed to delete edge");
|
|
7007
|
+
}
|
|
7008
|
+
}, [pendingDelete, port, reload]);
|
|
7009
|
+
return /* @__PURE__ */ jsxs21(Sheet5, { open, onOpenChange, children: [
|
|
7010
|
+
/* @__PURE__ */ jsxs21(SheetContent5, { children: [
|
|
7011
|
+
/* @__PURE__ */ jsxs21(SheetHeader5, { children: [
|
|
7012
|
+
/* @__PURE__ */ jsx23(SheetTitle5, { children: "Edges" }),
|
|
7013
|
+
/* @__PURE__ */ jsx23(SheetDescription5, { children: "Manage the edges between the two selected nodes." })
|
|
7014
|
+
] }),
|
|
7015
|
+
/* @__PURE__ */ jsxs21(SheetBody5, { className: "flex flex-col gap-4", children: [
|
|
7016
|
+
/* @__PURE__ */ jsxs21("div", { className: "flex items-center gap-2 text-sm", children: [
|
|
7017
|
+
/* @__PURE__ */ jsx23("span", { className: "font-medium", children: extractDisplayLabel(nodeA) }),
|
|
7018
|
+
/* @__PURE__ */ jsx23(ArrowRight2, { className: "h-4 w-4 text-muted-foreground" }),
|
|
7019
|
+
/* @__PURE__ */ jsx23("span", { className: "font-medium", children: extractDisplayLabel(nodeB) })
|
|
7020
|
+
] }),
|
|
7021
|
+
!draft && /* @__PURE__ */ jsx23(
|
|
7022
|
+
PairLists,
|
|
7023
|
+
{
|
|
7024
|
+
nodeALabel: nodeA.label_,
|
|
7025
|
+
nodeBLabel: nodeB.label_,
|
|
7026
|
+
nodeAId: nodeA.id_,
|
|
7027
|
+
active,
|
|
7028
|
+
available,
|
|
7029
|
+
editableTypes,
|
|
7030
|
+
onEdit: (edge) => setDraft({ mode: "edit", edge }),
|
|
7031
|
+
onDelete: (edge) => setPendingDelete(edge),
|
|
7032
|
+
onCreate: (entry) => setDraft({ mode: "create", entry })
|
|
7033
|
+
}
|
|
7034
|
+
),
|
|
7035
|
+
draft && schema && /* @__PURE__ */ jsx23(
|
|
7036
|
+
DraftForm,
|
|
7037
|
+
{
|
|
7038
|
+
draft,
|
|
7039
|
+
schema,
|
|
7040
|
+
widgets,
|
|
7041
|
+
onSave: handleSave,
|
|
7042
|
+
onCancel: () => setDraft(null)
|
|
7043
|
+
}
|
|
7044
|
+
),
|
|
7045
|
+
error && /* @__PURE__ */ jsx23(Alert2, { variant: "destructive", children: error })
|
|
7046
|
+
] })
|
|
7047
|
+
] }),
|
|
7048
|
+
/* @__PURE__ */ jsx23(
|
|
7049
|
+
DeleteEdgeConfirm,
|
|
7050
|
+
{
|
|
7051
|
+
edge: pendingDelete,
|
|
7052
|
+
onCancel: () => setPendingDelete(null),
|
|
7053
|
+
onConfirm: () => void confirmDelete()
|
|
7054
|
+
}
|
|
7055
|
+
)
|
|
7056
|
+
] });
|
|
7057
|
+
}
|
|
7058
|
+
function DeleteEdgeConfirm({
|
|
7059
|
+
edge,
|
|
7060
|
+
onCancel,
|
|
7061
|
+
onConfirm
|
|
7062
|
+
}) {
|
|
7063
|
+
return /* @__PURE__ */ jsx23(Dialog, { open: !!edge, onOpenChange: (o) => !o && onCancel(), children: /* @__PURE__ */ jsxs21(DialogContent, { children: [
|
|
7064
|
+
/* @__PURE__ */ jsxs21(DialogHeader, { children: [
|
|
7065
|
+
/* @__PURE__ */ jsx23(DialogTitle, { children: "Delete edge" }),
|
|
7066
|
+
/* @__PURE__ */ jsxs21(DialogDescription, { children: [
|
|
7067
|
+
"Delete the edge ",
|
|
7068
|
+
/* @__PURE__ */ jsx23("strong", { children: edge?.label_ }),
|
|
7069
|
+
"? This removes the relationship between the two nodes."
|
|
7070
|
+
] })
|
|
7071
|
+
] }),
|
|
7072
|
+
/* @__PURE__ */ jsxs21(DialogFooter, { children: [
|
|
7073
|
+
/* @__PURE__ */ jsx23(Button7, { variant: "outline", onClick: onCancel, children: "Cancel" }),
|
|
7074
|
+
/* @__PURE__ */ jsx23(Button7, { variant: "destructive", onClick: onConfirm, children: "Delete" })
|
|
7075
|
+
] })
|
|
7076
|
+
] }) });
|
|
7077
|
+
}
|
|
7078
|
+
|
|
7079
|
+
// src/graph-edit/DeleteNodeDialog.tsx
|
|
7080
|
+
import { useCallback as useCallback10, useEffect as useEffect8, useState as useState16 } from "react";
|
|
7081
|
+
import {
|
|
7082
|
+
Dialog as Dialog2,
|
|
7083
|
+
DialogContent as DialogContent2,
|
|
7084
|
+
DialogHeader as DialogHeader2,
|
|
7085
|
+
DialogTitle as DialogTitle2,
|
|
7086
|
+
DialogDescription as DialogDescription2,
|
|
7087
|
+
DialogFooter as DialogFooter2,
|
|
7088
|
+
Button as Button8,
|
|
7089
|
+
Alert as Alert3
|
|
7090
|
+
} from "@petrarca/sonnet-ui";
|
|
7091
|
+
import { errorLog as errorLog6 } from "@petrarca/sonnet-core";
|
|
7092
|
+
import { jsx as jsx24, jsxs as jsxs22 } from "react/jsx-runtime";
|
|
7093
|
+
var EMPTY_DEPENDENT = {
|
|
7094
|
+
inbound: [],
|
|
7095
|
+
outbound: [],
|
|
7096
|
+
names: {}
|
|
7097
|
+
};
|
|
7098
|
+
function useDependentEdges(port, open, node) {
|
|
7099
|
+
const [dependent, setDependent] = useState16(EMPTY_DEPENDENT);
|
|
7100
|
+
useEffect8(() => {
|
|
7101
|
+
if (!open) return;
|
|
7102
|
+
if (node.id_ == null) {
|
|
7103
|
+
setDependent(EMPTY_DEPENDENT);
|
|
7104
|
+
return;
|
|
7105
|
+
}
|
|
7106
|
+
let cancelled = false;
|
|
7107
|
+
void (async () => {
|
|
7108
|
+
try {
|
|
7109
|
+
const [inbound, outbound] = await Promise.all([
|
|
7110
|
+
port.getInboundEdges(node.id_),
|
|
7111
|
+
port.getOutboundEdges(node.id_)
|
|
7112
|
+
]);
|
|
7113
|
+
if (cancelled) return;
|
|
7114
|
+
const neighbourIds = Array.from(
|
|
7115
|
+
/* @__PURE__ */ new Set([
|
|
7116
|
+
...inbound.map((e) => e.start_id_),
|
|
7117
|
+
...outbound.map((e) => e.end_id_)
|
|
7118
|
+
])
|
|
7119
|
+
);
|
|
7120
|
+
const names = await resolveNames(port, neighbourIds);
|
|
7121
|
+
if (cancelled) return;
|
|
7122
|
+
setDependent({ inbound, outbound, names });
|
|
7123
|
+
} catch (e) {
|
|
7124
|
+
if (cancelled) return;
|
|
7125
|
+
errorLog6("[DeleteNodeDialog] failed to load dependent edges", e);
|
|
7126
|
+
}
|
|
7127
|
+
})();
|
|
7128
|
+
return () => {
|
|
7129
|
+
cancelled = true;
|
|
7130
|
+
};
|
|
7131
|
+
}, [port, open, node.id_]);
|
|
7132
|
+
return dependent;
|
|
7133
|
+
}
|
|
7134
|
+
async function resolveNames(port, ids) {
|
|
7135
|
+
if (ids.length === 0) return {};
|
|
7136
|
+
const nodes = await port.getNodes(ids);
|
|
7137
|
+
const names = {};
|
|
7138
|
+
for (const n of nodes) {
|
|
7139
|
+
if (n.id_ != null) names[n.id_] = extractDisplayLabel(n);
|
|
7140
|
+
}
|
|
7141
|
+
return names;
|
|
7142
|
+
}
|
|
7143
|
+
function EdgeList({
|
|
7144
|
+
title,
|
|
7145
|
+
edges,
|
|
7146
|
+
names,
|
|
7147
|
+
direction
|
|
7148
|
+
}) {
|
|
7149
|
+
if (edges.length === 0) return null;
|
|
7150
|
+
return /* @__PURE__ */ jsxs22("div", { className: "mb-2", children: [
|
|
7151
|
+
/* @__PURE__ */ jsx24("span", { className: "heading-meta block", children: title }),
|
|
7152
|
+
/* @__PURE__ */ jsx24("ul", { className: "list-disc pl-6 text-sm", children: edges.map((edge) => {
|
|
7153
|
+
const neighbour = direction === "inbound" ? names[edge.start_id_] : names[edge.end_id_];
|
|
7154
|
+
const text = direction === "inbound" ? `${neighbour ?? "?"} \u2192 ${edge.label_}` : `${edge.label_} \u2192 ${neighbour ?? "?"}`;
|
|
7155
|
+
return /* @__PURE__ */ jsx24("li", { children: neighbour ? text : edge.label_ }, edge.id_);
|
|
7156
|
+
}) })
|
|
7157
|
+
] });
|
|
7158
|
+
}
|
|
7159
|
+
function DeleteNodeDialog({
|
|
7160
|
+
open,
|
|
7161
|
+
onOpenChange,
|
|
7162
|
+
node,
|
|
7163
|
+
port,
|
|
7164
|
+
onDeleted
|
|
7165
|
+
}) {
|
|
7166
|
+
const { inbound, outbound, names } = useDependentEdges(port, open, node);
|
|
7167
|
+
const [error, setError] = useState16("");
|
|
7168
|
+
const hasEdges = inbound.length > 0 || outbound.length > 0;
|
|
7169
|
+
const handleDelete = useCallback10(() => {
|
|
7170
|
+
if (node.id_ == null) return;
|
|
7171
|
+
void (async () => {
|
|
7172
|
+
try {
|
|
7173
|
+
await port.deleteNode(node.id_);
|
|
7174
|
+
setError("");
|
|
7175
|
+
onDeleted?.(node.id_);
|
|
7176
|
+
onOpenChange(false);
|
|
7177
|
+
} catch (e) {
|
|
7178
|
+
setError(e instanceof Error ? e.message : "Unable to delete node");
|
|
7179
|
+
}
|
|
7180
|
+
})();
|
|
7181
|
+
}, [port, node.id_, onDeleted, onOpenChange]);
|
|
7182
|
+
return /* @__PURE__ */ jsx24(Dialog2, { open, onOpenChange, children: /* @__PURE__ */ jsxs22(DialogContent2, { children: [
|
|
7183
|
+
/* @__PURE__ */ jsxs22(DialogHeader2, { children: [
|
|
7184
|
+
/* @__PURE__ */ jsx24(DialogTitle2, { children: "Delete node" }),
|
|
7185
|
+
/* @__PURE__ */ jsxs22(DialogDescription2, { children: [
|
|
7186
|
+
"Delete the node ",
|
|
7187
|
+
/* @__PURE__ */ jsx24("strong", { children: extractDisplayLabel(node) }),
|
|
7188
|
+
"?"
|
|
7189
|
+
] })
|
|
7190
|
+
] }),
|
|
7191
|
+
!hasEdges && /* @__PURE__ */ jsx24("p", { className: "text-sm text-muted-foreground", children: "No connected edges." }),
|
|
7192
|
+
/* @__PURE__ */ jsx24(
|
|
7193
|
+
EdgeList,
|
|
7194
|
+
{
|
|
7195
|
+
title: "Inbound edges",
|
|
7196
|
+
edges: inbound,
|
|
7197
|
+
names,
|
|
7198
|
+
direction: "inbound"
|
|
7199
|
+
}
|
|
7200
|
+
),
|
|
7201
|
+
/* @__PURE__ */ jsx24(
|
|
7202
|
+
EdgeList,
|
|
7203
|
+
{
|
|
7204
|
+
title: "Outbound edges",
|
|
7205
|
+
edges: outbound,
|
|
7206
|
+
names,
|
|
7207
|
+
direction: "outbound"
|
|
7208
|
+
}
|
|
7209
|
+
),
|
|
7210
|
+
error && /* @__PURE__ */ jsx24(Alert3, { variant: "destructive", children: error }),
|
|
7211
|
+
/* @__PURE__ */ jsxs22(DialogFooter2, { children: [
|
|
7212
|
+
/* @__PURE__ */ jsx24(Button8, { variant: "outline", onClick: () => onOpenChange(false), children: "Cancel" }),
|
|
7213
|
+
/* @__PURE__ */ jsx24(Button8, { variant: "destructive", onClick: handleDelete, children: "Delete" })
|
|
7214
|
+
] })
|
|
7215
|
+
] }) });
|
|
7216
|
+
}
|
|
7217
|
+
|
|
7218
|
+
// src/graph-edit/GraphEditToolbar.tsx
|
|
7219
|
+
import { Fragment as Fragment8, jsx as jsx25, jsxs as jsxs23 } from "react/jsx-runtime";
|
|
7220
|
+
function toPerms(caps) {
|
|
7221
|
+
const canCreateNode = caps.createNode === "edit";
|
|
7222
|
+
const canUpdateNode = caps.updateNode === "edit";
|
|
7223
|
+
const canDeleteNode = caps.deleteNode === "edit";
|
|
7224
|
+
const canEditEdges = caps.createEdge === "edit" || caps.updateEdge === "edit" || caps.deleteEdge === "edit";
|
|
7225
|
+
return {
|
|
7226
|
+
canCreateNode,
|
|
7227
|
+
canUpdateNode,
|
|
7228
|
+
canDeleteNode,
|
|
7229
|
+
canEditEdges,
|
|
7230
|
+
anyEditable: canCreateNode || canUpdateNode || canDeleteNode || canEditEdges
|
|
7231
|
+
};
|
|
7232
|
+
}
|
|
7233
|
+
function ToolbarButtons({
|
|
7234
|
+
perms,
|
|
7235
|
+
count,
|
|
7236
|
+
onOpen
|
|
7237
|
+
}) {
|
|
7238
|
+
return /* @__PURE__ */ jsxs23("div", { className: "flex gap-0.5", children: [
|
|
7239
|
+
perms.canCreateNode && /* @__PURE__ */ jsx25(
|
|
7240
|
+
IconButton4,
|
|
7241
|
+
{
|
|
7242
|
+
label: "Add node",
|
|
7243
|
+
variant: "outline",
|
|
7244
|
+
disabled: count > 0,
|
|
7245
|
+
onClick: () => onOpen("node-create"),
|
|
7246
|
+
children: /* @__PURE__ */ jsx25(Plus3, {})
|
|
7247
|
+
}
|
|
7248
|
+
),
|
|
7249
|
+
perms.canUpdateNode && /* @__PURE__ */ jsx25(
|
|
7250
|
+
IconButton4,
|
|
7251
|
+
{
|
|
7252
|
+
label: "Edit node",
|
|
7253
|
+
variant: "outline",
|
|
7254
|
+
disabled: count !== 1,
|
|
7255
|
+
onClick: () => onOpen("node-edit"),
|
|
7256
|
+
children: /* @__PURE__ */ jsx25(Edit, {})
|
|
7257
|
+
}
|
|
7258
|
+
),
|
|
7259
|
+
perms.canEditEdges && /* @__PURE__ */ jsx25(
|
|
7260
|
+
IconButton4,
|
|
7261
|
+
{
|
|
7262
|
+
label: "Edges",
|
|
7263
|
+
variant: "outline",
|
|
7264
|
+
disabled: count !== 2,
|
|
7265
|
+
onClick: () => onOpen("edge"),
|
|
7266
|
+
children: /* @__PURE__ */ jsx25(Link, {})
|
|
7267
|
+
}
|
|
7268
|
+
),
|
|
7269
|
+
perms.canDeleteNode && /* @__PURE__ */ jsx25(
|
|
7270
|
+
IconButton4,
|
|
7271
|
+
{
|
|
7272
|
+
label: "Delete node",
|
|
7273
|
+
variant: "outline",
|
|
7274
|
+
tone: "destructive",
|
|
7275
|
+
disabled: count !== 1,
|
|
7276
|
+
onClick: () => onOpen("delete"),
|
|
7277
|
+
children: /* @__PURE__ */ jsx25(Trash23, {})
|
|
7278
|
+
}
|
|
7279
|
+
)
|
|
7280
|
+
] });
|
|
7281
|
+
}
|
|
7282
|
+
function ToolbarDialogs({
|
|
7283
|
+
perms,
|
|
7284
|
+
dialog,
|
|
7285
|
+
firstNode,
|
|
7286
|
+
secondNode,
|
|
7287
|
+
port,
|
|
7288
|
+
schemaProvider,
|
|
7289
|
+
widgets,
|
|
7290
|
+
onClose,
|
|
7291
|
+
onMutated
|
|
7292
|
+
}) {
|
|
7293
|
+
const onChange = useCallback11(
|
|
7294
|
+
(o) => {
|
|
7295
|
+
if (!o) onClose();
|
|
7296
|
+
},
|
|
7297
|
+
[onClose]
|
|
7298
|
+
);
|
|
7299
|
+
return /* @__PURE__ */ jsxs23(Fragment8, { children: [
|
|
7300
|
+
(perms.canCreateNode || perms.canUpdateNode) && /* @__PURE__ */ jsx25(
|
|
7301
|
+
NodeFormSheet,
|
|
7302
|
+
{
|
|
7303
|
+
open: dialog === "node-create" || dialog === "node-edit",
|
|
7304
|
+
onOpenChange: onChange,
|
|
7305
|
+
node: dialog === "node-edit" ? firstNode : null,
|
|
7306
|
+
port,
|
|
7307
|
+
schemaProvider,
|
|
7308
|
+
widgets,
|
|
7309
|
+
onSaved: onMutated
|
|
7310
|
+
}
|
|
7311
|
+
),
|
|
7312
|
+
perms.canEditEdges && firstNode && secondNode && /* @__PURE__ */ jsx25(
|
|
7313
|
+
EdgeFormSheet,
|
|
7314
|
+
{
|
|
7315
|
+
open: dialog === "edge",
|
|
7316
|
+
onOpenChange: onChange,
|
|
7317
|
+
nodeA: firstNode,
|
|
7318
|
+
nodeB: secondNode,
|
|
7319
|
+
port,
|
|
7320
|
+
schemaProvider,
|
|
7321
|
+
widgets,
|
|
7322
|
+
onChanged: onMutated
|
|
7323
|
+
}
|
|
7324
|
+
),
|
|
7325
|
+
perms.canDeleteNode && firstNode && /* @__PURE__ */ jsx25(
|
|
7326
|
+
DeleteNodeDialog,
|
|
7327
|
+
{
|
|
7328
|
+
open: dialog === "delete",
|
|
7329
|
+
onOpenChange: onChange,
|
|
7330
|
+
node: firstNode,
|
|
7331
|
+
port,
|
|
7332
|
+
onDeleted: onMutated
|
|
7333
|
+
}
|
|
7334
|
+
)
|
|
7335
|
+
] });
|
|
7336
|
+
}
|
|
7337
|
+
function GraphEditToolbar({
|
|
7338
|
+
port,
|
|
7339
|
+
schemaProvider,
|
|
7340
|
+
capabilities,
|
|
7341
|
+
readOnly,
|
|
7342
|
+
widgets,
|
|
7343
|
+
onMutated
|
|
7344
|
+
}) {
|
|
7345
|
+
const useSelection3 = useWorkspaceSelection();
|
|
7346
|
+
const selectedNodeIds = useSelection3(
|
|
7347
|
+
(s) => s.selectedNodeIds
|
|
7348
|
+
);
|
|
7349
|
+
const live = useLiveGraphData();
|
|
7350
|
+
const [dialog, setDialog] = useState17(null);
|
|
7351
|
+
const perms = useMemo11(
|
|
7352
|
+
() => toPerms(resolveGraphCapabilities(capabilities, readOnly)),
|
|
7353
|
+
[capabilities, readOnly]
|
|
7354
|
+
);
|
|
7355
|
+
const selectedNodes = useMemo11(
|
|
7356
|
+
() => selectedNodeIds.map((id) => live.normalized.nodesById[id]).filter(Boolean),
|
|
7357
|
+
[selectedNodeIds, live.normalized]
|
|
7358
|
+
);
|
|
7359
|
+
const stableOnMutated = useStableRef(onMutated);
|
|
7360
|
+
if (!perms.anyEditable) return null;
|
|
7361
|
+
return /* @__PURE__ */ jsxs23(Fragment8, { children: [
|
|
7362
|
+
/* @__PURE__ */ jsx25(
|
|
7363
|
+
ToolbarButtons,
|
|
7364
|
+
{
|
|
7365
|
+
perms,
|
|
7366
|
+
count: selectedNodes.length,
|
|
7367
|
+
onOpen: setDialog
|
|
7368
|
+
}
|
|
7369
|
+
),
|
|
7370
|
+
/* @__PURE__ */ jsx25(
|
|
7371
|
+
ToolbarDialogs,
|
|
7372
|
+
{
|
|
7373
|
+
perms,
|
|
7374
|
+
dialog,
|
|
7375
|
+
firstNode: selectedNodes[0],
|
|
7376
|
+
secondNode: selectedNodes[1],
|
|
7377
|
+
port,
|
|
7378
|
+
schemaProvider,
|
|
7379
|
+
widgets,
|
|
7380
|
+
onClose: () => setDialog(null),
|
|
7381
|
+
onMutated: stableOnMutated
|
|
7382
|
+
}
|
|
7383
|
+
)
|
|
7384
|
+
] });
|
|
7385
|
+
}
|
|
4402
7386
|
export {
|
|
4403
7387
|
ActionPanel_default as ActionPanel,
|
|
7388
|
+
DeleteNodeDialog,
|
|
4404
7389
|
EMPTY_NORMALIZED,
|
|
7390
|
+
EdgeFormSheet,
|
|
4405
7391
|
EdgeInfoCard_default as EdgeInfoCard,
|
|
4406
7392
|
GraphA11yList,
|
|
7393
|
+
GraphEditToolbar,
|
|
7394
|
+
GraphModelEditor,
|
|
4407
7395
|
GraphRendererProvider,
|
|
4408
7396
|
GraphStoresContext,
|
|
4409
7397
|
GraphVisualizer_default as GraphVisualizer,
|
|
7398
|
+
ModelActionPanel,
|
|
7399
|
+
NodeFormSheet,
|
|
4410
7400
|
NodeInfoCard_default as NodeInfoCard,
|
|
7401
|
+
NodeTypeNameSheet,
|
|
4411
7402
|
Overview_default as Overview,
|
|
7403
|
+
PropertyFormSheet,
|
|
7404
|
+
RelationFormSheet,
|
|
7405
|
+
TypeDetailPanel,
|
|
7406
|
+
addEdgeType,
|
|
7407
|
+
addNodeType,
|
|
7408
|
+
addRelation,
|
|
4412
7409
|
buildColorPalette,
|
|
4413
7410
|
buildColorPaletteFromNodes,
|
|
7411
|
+
capabilitiesSchema,
|
|
7412
|
+
cardinalityLabel,
|
|
4414
7413
|
createActionPanelStore,
|
|
4415
7414
|
createGraphDataStore,
|
|
4416
7415
|
createGraphExplorationStore,
|
|
4417
7416
|
createGraphFilterStore,
|
|
4418
7417
|
createGraphFocusStore,
|
|
4419
7418
|
createSelectionStore,
|
|
7419
|
+
deleteEdgeType,
|
|
7420
|
+
deleteEdgeTypeProperty,
|
|
7421
|
+
deleteNodeProperty,
|
|
7422
|
+
deleteNodeType,
|
|
7423
|
+
deleteRelation,
|
|
7424
|
+
deleteRelationProperty,
|
|
7425
|
+
edgeTypeView,
|
|
7426
|
+
editableRelations,
|
|
4420
7427
|
enrichEdge,
|
|
4421
7428
|
enrichEdges,
|
|
4422
7429
|
enrichNode,
|
|
4423
7430
|
enrichNodes,
|
|
7431
|
+
enrichSchemaWithRelations,
|
|
4424
7432
|
extractDisplayLabel,
|
|
7433
|
+
extractRelationFields,
|
|
7434
|
+
findEdgeType,
|
|
7435
|
+
findType,
|
|
7436
|
+
graphCapabilitiesSchema,
|
|
7437
|
+
groupEdgesByRelation,
|
|
7438
|
+
hasProperties,
|
|
7439
|
+
hasRelationFields,
|
|
7440
|
+
initializeFormData,
|
|
7441
|
+
isEdgeType,
|
|
7442
|
+
isNodeType,
|
|
4425
7443
|
isRawGraphNode,
|
|
7444
|
+
isRelationField,
|
|
4426
7445
|
isVisualEdge,
|
|
4427
7446
|
isVisualNode,
|
|
7447
|
+
modelToGraph,
|
|
7448
|
+
nodeTypeView,
|
|
4428
7449
|
normalizeGraph,
|
|
7450
|
+
reconcileFilter,
|
|
7451
|
+
relationFieldName,
|
|
7452
|
+
relationTitle,
|
|
7453
|
+
relationTypeFromField,
|
|
7454
|
+
relationView,
|
|
7455
|
+
renameEdgeType,
|
|
7456
|
+
renameNodeType,
|
|
7457
|
+
resolveCapabilities,
|
|
7458
|
+
resolveGraphCapabilities,
|
|
7459
|
+
resolveRelationEdge,
|
|
7460
|
+
setEdgeTypeProperty,
|
|
7461
|
+
setNodeProperty,
|
|
7462
|
+
setRelationProperty,
|
|
4429
7463
|
toCytoscapeEdge,
|
|
4430
7464
|
toCytoscapeNode,
|
|
4431
7465
|
toReagraphEdge,
|
|
4432
7466
|
toReagraphNode,
|
|
7467
|
+
typeLabel,
|
|
7468
|
+
updateRelation,
|
|
4433
7469
|
useActionPanelStore,
|
|
4434
7470
|
useCytoscapeData,
|
|
4435
7471
|
GraphFocus_default as useGraphFocus,
|
|
4436
7472
|
useGraphRenderer,
|
|
4437
7473
|
useLiveGraphData,
|
|
4438
7474
|
useReagraphData,
|
|
7475
|
+
useStableRef,
|
|
4439
7476
|
useWorkspaceGraphData,
|
|
4440
7477
|
useWorkspaceGraphExploration,
|
|
4441
7478
|
useWorkspaceGraphFilter,
|
|
4442
|
-
useWorkspaceSelection
|
|
7479
|
+
useWorkspaceSelection,
|
|
7480
|
+
validateCapabilities,
|
|
7481
|
+
validateGraphCapabilities
|
|
4443
7482
|
};
|
|
4444
7483
|
//# sourceMappingURL=index.js.map
|