@petrarca/sonnet-graph 0.4.1 → 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 +3323 -277
- 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,8 +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
|
|
1691
|
-
const
|
|
1704
|
+
const expandBtnRef = useRef2(null);
|
|
1705
|
+
const dragRef = useRef2(null);
|
|
1706
|
+
const onPointerDown = useCallback3(
|
|
1692
1707
|
(e) => {
|
|
1693
1708
|
if (e.target.closest("button")) return;
|
|
1694
1709
|
e.preventDefault();
|
|
@@ -1702,13 +1717,13 @@ function ExpandOptionsPanel({
|
|
|
1702
1717
|
},
|
|
1703
1718
|
[offset]
|
|
1704
1719
|
);
|
|
1705
|
-
const onPointerMove =
|
|
1720
|
+
const onPointerMove = useCallback3((e) => {
|
|
1706
1721
|
if (!dragRef.current) return;
|
|
1707
1722
|
const dx = e.clientX - dragRef.current.startX;
|
|
1708
1723
|
const dy = e.clientY - dragRef.current.startY;
|
|
1709
1724
|
setOffset({ x: dragRef.current.origX + dx, y: dragRef.current.origY + dy });
|
|
1710
1725
|
}, []);
|
|
1711
|
-
const onPointerUp =
|
|
1726
|
+
const onPointerUp = useCallback3(() => {
|
|
1712
1727
|
dragRef.current = null;
|
|
1713
1728
|
}, []);
|
|
1714
1729
|
useEffect2(() => {
|
|
@@ -1736,6 +1751,11 @@ function ExpandOptionsPanel({
|
|
|
1736
1751
|
};
|
|
1737
1752
|
const allSelected = selectedTypes.length === edgeTypes.length;
|
|
1738
1753
|
const noneSelected = edgeTypes.length > 0 && selectedTypes.length === 0;
|
|
1754
|
+
useEffect2(() => {
|
|
1755
|
+
if (!loading && !noneSelected) {
|
|
1756
|
+
expandBtnRef.current?.focus();
|
|
1757
|
+
}
|
|
1758
|
+
}, [loading, noneSelected]);
|
|
1739
1759
|
return /* @__PURE__ */ jsxs4(
|
|
1740
1760
|
"div",
|
|
1741
1761
|
{
|
|
@@ -1825,6 +1845,7 @@ function ExpandOptionsPanel({
|
|
|
1825
1845
|
/* @__PURE__ */ jsx5(
|
|
1826
1846
|
"button",
|
|
1827
1847
|
{
|
|
1848
|
+
ref: expandBtnRef,
|
|
1828
1849
|
className: "w-full py-1.5 text-[11px] font-medium rounded bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer",
|
|
1829
1850
|
onClick: handleExpand,
|
|
1830
1851
|
disabled: loading || noneSelected,
|
|
@@ -1858,9 +1879,9 @@ function ReaContextMenu({
|
|
|
1858
1879
|
}, [onClose]);
|
|
1859
1880
|
const handleFocus = () => {
|
|
1860
1881
|
if (elementType === "node") {
|
|
1861
|
-
const
|
|
1862
|
-
devLog7("[ReaContextMenu] Focus on node and neighborhood:",
|
|
1863
|
-
useGraphExploration.getState().setFocusModeWithNeighborhood(
|
|
1882
|
+
const nodeId2 = data.id;
|
|
1883
|
+
devLog7("[ReaContextMenu] Focus on node and neighborhood:", nodeId2);
|
|
1884
|
+
useGraphExploration.getState().setFocusModeWithNeighborhood(nodeId2);
|
|
1864
1885
|
} else {
|
|
1865
1886
|
const startNodeId = data.data.start_id_;
|
|
1866
1887
|
devLog7("[ReaContextMenu] Focus on edge start node:", startNodeId);
|
|
@@ -1886,58 +1907,58 @@ function ReaContextMenu({
|
|
|
1886
1907
|
};
|
|
1887
1908
|
const handleHideNode = () => {
|
|
1888
1909
|
if (elementType === "node") {
|
|
1889
|
-
const
|
|
1890
|
-
devLog7("[ReaContextMenu] Hide node:",
|
|
1891
|
-
useGraphExploration.getState().hideElements([
|
|
1910
|
+
const nodeId2 = data.id;
|
|
1911
|
+
devLog7("[ReaContextMenu] Hide node:", nodeId2);
|
|
1912
|
+
useGraphExploration.getState().hideElements([nodeId2]);
|
|
1892
1913
|
}
|
|
1893
1914
|
onClose();
|
|
1894
1915
|
};
|
|
1895
1916
|
const handleHideIncoming = () => {
|
|
1896
1917
|
if (elementType === "node") {
|
|
1897
|
-
const
|
|
1898
|
-
devLog7("[ReaContextMenu] Hide incoming edges:",
|
|
1899
|
-
useGraphExploration.getState().hideNodeIncoming(
|
|
1918
|
+
const nodeId2 = data.id;
|
|
1919
|
+
devLog7("[ReaContextMenu] Hide incoming edges:", nodeId2);
|
|
1920
|
+
useGraphExploration.getState().hideNodeIncoming(nodeId2);
|
|
1900
1921
|
}
|
|
1901
1922
|
onClose();
|
|
1902
1923
|
};
|
|
1903
1924
|
const handleHideOutgoing = () => {
|
|
1904
1925
|
if (elementType === "node") {
|
|
1905
|
-
const
|
|
1906
|
-
devLog7("[ReaContextMenu] Hide outgoing edges:",
|
|
1907
|
-
useGraphExploration.getState().hideNodeOutgoing(
|
|
1926
|
+
const nodeId2 = data.id;
|
|
1927
|
+
devLog7("[ReaContextMenu] Hide outgoing edges:", nodeId2);
|
|
1928
|
+
useGraphExploration.getState().hideNodeOutgoing(nodeId2);
|
|
1908
1929
|
}
|
|
1909
1930
|
onClose();
|
|
1910
1931
|
};
|
|
1911
1932
|
const handleExpandWithOptions = (options) => {
|
|
1912
1933
|
if (elementType === "node") {
|
|
1913
|
-
const
|
|
1914
|
-
devLog7("[ReaContextMenu] Expand node with options:",
|
|
1915
|
-
useGraphExploration.getState().expandNode(
|
|
1934
|
+
const nodeId2 = data.id;
|
|
1935
|
+
devLog7("[ReaContextMenu] Expand node with options:", nodeId2, options);
|
|
1936
|
+
useGraphExploration.getState().expandNode(nodeId2, options);
|
|
1916
1937
|
}
|
|
1917
1938
|
onClose();
|
|
1918
1939
|
};
|
|
1919
1940
|
const handleHideForwardPath = () => {
|
|
1920
1941
|
if (elementType === "edge") {
|
|
1921
|
-
const
|
|
1922
|
-
devLog7("[ReaContextMenu] Hide forward path from edge:",
|
|
1923
|
-
useGraphExploration.getState().hideEdgeForwardPath(
|
|
1942
|
+
const edgeId2 = data.id;
|
|
1943
|
+
devLog7("[ReaContextMenu] Hide forward path from edge:", edgeId2);
|
|
1944
|
+
useGraphExploration.getState().hideEdgeForwardPath(edgeId2);
|
|
1924
1945
|
}
|
|
1925
1946
|
onClose();
|
|
1926
1947
|
};
|
|
1927
1948
|
const handleHideReversePath = () => {
|
|
1928
1949
|
if (elementType === "edge") {
|
|
1929
|
-
const
|
|
1930
|
-
devLog7("[ReaContextMenu] Hide reverse path from edge:",
|
|
1931
|
-
useGraphExploration.getState().hideEdgeReversePath(
|
|
1950
|
+
const edgeId2 = data.id;
|
|
1951
|
+
devLog7("[ReaContextMenu] Hide reverse path from edge:", edgeId2);
|
|
1952
|
+
useGraphExploration.getState().hideEdgeReversePath(edgeId2);
|
|
1932
1953
|
}
|
|
1933
1954
|
onClose();
|
|
1934
1955
|
};
|
|
1935
1956
|
const handleFocusOnEdgeType = () => {
|
|
1936
1957
|
if (elementType === "edge") {
|
|
1937
|
-
const
|
|
1958
|
+
const edgeId2 = data.id;
|
|
1938
1959
|
const edgeLabel = data.data?.label_ || "edge";
|
|
1939
|
-
devLog7("[ReaContextMenu] Focus on edge type:",
|
|
1940
|
-
useGraphExploration.getState().focusOnEdgeType(
|
|
1960
|
+
devLog7("[ReaContextMenu] Focus on edge type:", edgeId2, edgeLabel);
|
|
1961
|
+
useGraphExploration.getState().focusOnEdgeType(edgeId2);
|
|
1941
1962
|
}
|
|
1942
1963
|
onClose();
|
|
1943
1964
|
};
|
|
@@ -2063,7 +2084,7 @@ function ReaGraph({
|
|
|
2063
2084
|
extraToolbarContent,
|
|
2064
2085
|
getNodeEdgeTypes
|
|
2065
2086
|
}) {
|
|
2066
|
-
const isMountedRef =
|
|
2087
|
+
const isMountedRef = useRef3(false);
|
|
2067
2088
|
const { nodes, edges } = useReagraphData();
|
|
2068
2089
|
const { normalized } = useLiveGraphData();
|
|
2069
2090
|
useEffect3(() => {
|
|
@@ -2071,7 +2092,7 @@ function ReaGraph({
|
|
|
2071
2092
|
if (nodes.length > 0) devLog8("[ReGraph] sample node", nodes[0]);
|
|
2072
2093
|
if (edges.length > 0) devLog8("[ReGraph] sample edge", edges[0]);
|
|
2073
2094
|
}, [nodes, edges]);
|
|
2074
|
-
const canvas =
|
|
2095
|
+
const canvas = useRef3(null);
|
|
2075
2096
|
const [hoveredNode, setHoveredNode] = useState5(
|
|
2076
2097
|
void 0
|
|
2077
2098
|
);
|
|
@@ -2132,7 +2153,7 @@ function ReaGraph({
|
|
|
2132
2153
|
clearSelections();
|
|
2133
2154
|
clearSelectionStore();
|
|
2134
2155
|
}, [lastDataChangeTime, clearSelections, clearSelectionStore]);
|
|
2135
|
-
const fitToScreen =
|
|
2156
|
+
const fitToScreen = useCallback4(
|
|
2136
2157
|
(reason) => {
|
|
2137
2158
|
if (!isMountedRef.current) return;
|
|
2138
2159
|
if (nodes.length === 0) return;
|
|
@@ -2178,6 +2199,20 @@ function ReaGraph({
|
|
|
2178
2199
|
isMountedRef.current = false;
|
|
2179
2200
|
};
|
|
2180
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]);
|
|
2181
2216
|
useEffect3(() => {
|
|
2182
2217
|
if (isMountedRef.current) fitToScreen("counts-changed");
|
|
2183
2218
|
}, [nodes.length, edges.length, fitToScreen]);
|
|
@@ -2293,7 +2328,7 @@ function ReaGraph({
|
|
|
2293
2328
|
window.removeEventListener("resize", handleResize);
|
|
2294
2329
|
};
|
|
2295
2330
|
}, [fitToScreen]);
|
|
2296
|
-
const firstDataRef =
|
|
2331
|
+
const firstDataRef = useRef3(true);
|
|
2297
2332
|
useEffect3(() => {
|
|
2298
2333
|
if (!isMountedRef.current) return;
|
|
2299
2334
|
if (nodes.length > 0) {
|
|
@@ -2394,9 +2429,9 @@ var ReaGraph_default = ReaGraph;
|
|
|
2394
2429
|
// src/components/cyto/CytoscapeGraph.tsx
|
|
2395
2430
|
import {
|
|
2396
2431
|
useEffect as useEffect4,
|
|
2397
|
-
useRef as
|
|
2432
|
+
useRef as useRef4,
|
|
2398
2433
|
useState as useState6,
|
|
2399
|
-
useCallback as
|
|
2434
|
+
useCallback as useCallback5
|
|
2400
2435
|
} from "react";
|
|
2401
2436
|
import { devLog as devLog10, errorLog as errorLog3 } from "@petrarca/sonnet-core";
|
|
2402
2437
|
import cytoscape from "cytoscape";
|
|
@@ -2817,9 +2852,9 @@ var createNodeContextMenu = ({
|
|
|
2817
2852
|
{
|
|
2818
2853
|
content: "Focus",
|
|
2819
2854
|
select: (ele) => {
|
|
2820
|
-
const
|
|
2821
|
-
devLog9("[CxtMenu] Focus on node and neighborhood:",
|
|
2822
|
-
useGraphExploration.getState().setFocusModeWithNeighborhood(
|
|
2855
|
+
const nodeId2 = ele.id();
|
|
2856
|
+
devLog9("[CxtMenu] Focus on node and neighborhood:", nodeId2);
|
|
2857
|
+
useGraphExploration.getState().setFocusModeWithNeighborhood(nodeId2);
|
|
2823
2858
|
}
|
|
2824
2859
|
},
|
|
2825
2860
|
{
|
|
@@ -2837,38 +2872,38 @@ var createNodeContextMenu = ({
|
|
|
2837
2872
|
{
|
|
2838
2873
|
content: "Hide Node",
|
|
2839
2874
|
select: (ele) => {
|
|
2840
|
-
const
|
|
2841
|
-
devLog9("[CxtMenu] Hide node:",
|
|
2842
|
-
useGraphExploration.getState().hideElements([
|
|
2875
|
+
const nodeId2 = ele.id();
|
|
2876
|
+
devLog9("[CxtMenu] Hide node:", nodeId2);
|
|
2877
|
+
useGraphExploration.getState().hideElements([nodeId2]);
|
|
2843
2878
|
}
|
|
2844
2879
|
},
|
|
2845
2880
|
{
|
|
2846
2881
|
content: "Hide Incoming",
|
|
2847
2882
|
select: (ele) => {
|
|
2848
|
-
const
|
|
2849
|
-
devLog9("[CxtMenu] Hide incoming (predecessors) of node:",
|
|
2850
|
-
useGraphExploration.getState().hideNodeIncoming(
|
|
2883
|
+
const nodeId2 = ele.id();
|
|
2884
|
+
devLog9("[CxtMenu] Hide incoming (predecessors) of node:", nodeId2);
|
|
2885
|
+
useGraphExploration.getState().hideNodeIncoming(nodeId2);
|
|
2851
2886
|
}
|
|
2852
2887
|
},
|
|
2853
2888
|
{
|
|
2854
2889
|
content: "Hide Outgoing",
|
|
2855
2890
|
select: (ele) => {
|
|
2856
|
-
const
|
|
2857
|
-
devLog9("[CxtMenu] Hide outgoing (successors) of node:",
|
|
2858
|
-
useGraphExploration.getState().hideNodeOutgoing(
|
|
2891
|
+
const nodeId2 = ele.id();
|
|
2892
|
+
devLog9("[CxtMenu] Hide outgoing (successors) of node:", nodeId2);
|
|
2893
|
+
useGraphExploration.getState().hideNodeOutgoing(nodeId2);
|
|
2859
2894
|
}
|
|
2860
2895
|
},
|
|
2861
2896
|
{
|
|
2862
2897
|
content: "Expand",
|
|
2863
2898
|
select: (ele) => {
|
|
2864
|
-
const
|
|
2865
|
-
devLog9("[CxtMenu] Expand request for node:",
|
|
2899
|
+
const nodeId2 = ele.id();
|
|
2900
|
+
devLog9("[CxtMenu] Expand request for node:", nodeId2);
|
|
2866
2901
|
if (onExpandRequest) {
|
|
2867
2902
|
const pos = ele.renderedPosition();
|
|
2868
|
-
const label = ele.data("label") ||
|
|
2869
|
-
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 });
|
|
2870
2905
|
} else {
|
|
2871
|
-
useGraphExploration.getState().expandNode(
|
|
2906
|
+
useGraphExploration.getState().expandNode(nodeId2).catch((error) => {
|
|
2872
2907
|
errorLog2("[CxtMenu] Expansion failed:", error);
|
|
2873
2908
|
toast3.error("Failed to expand node");
|
|
2874
2909
|
});
|
|
@@ -2904,42 +2939,42 @@ var createEdgeContextMenu = ({
|
|
|
2904
2939
|
{
|
|
2905
2940
|
content: "Focus",
|
|
2906
2941
|
select: (ele) => {
|
|
2907
|
-
const
|
|
2942
|
+
const edgeId2 = ele.id();
|
|
2908
2943
|
devLog9(
|
|
2909
2944
|
"[CxtMenu] Focus on edge (auto-direction based on selection):",
|
|
2910
|
-
|
|
2945
|
+
edgeId2
|
|
2911
2946
|
);
|
|
2912
|
-
useGraphExploration.getState().focusEdge(
|
|
2947
|
+
useGraphExploration.getState().focusEdge(edgeId2);
|
|
2913
2948
|
}
|
|
2914
2949
|
},
|
|
2915
2950
|
{
|
|
2916
2951
|
content: "Focus Edge Type",
|
|
2917
2952
|
select: (ele) => {
|
|
2918
|
-
const
|
|
2919
|
-
devLog9("[CxtMenu] Focus on edge type:",
|
|
2920
|
-
useGraphExploration.getState().focusOnEdgeType(
|
|
2953
|
+
const edgeId2 = ele.id();
|
|
2954
|
+
devLog9("[CxtMenu] Focus on edge type:", edgeId2);
|
|
2955
|
+
useGraphExploration.getState().focusOnEdgeType(edgeId2);
|
|
2921
2956
|
}
|
|
2922
2957
|
},
|
|
2923
2958
|
{
|
|
2924
2959
|
content: "Hide Forward Path",
|
|
2925
2960
|
select: (ele) => {
|
|
2926
|
-
const
|
|
2961
|
+
const edgeId2 = ele.id();
|
|
2927
2962
|
devLog9(
|
|
2928
2963
|
"[CxtMenu] Hide forward path (edge + target + downstream):",
|
|
2929
|
-
|
|
2964
|
+
edgeId2
|
|
2930
2965
|
);
|
|
2931
|
-
useGraphExploration.getState().hideEdgeForwardPath(
|
|
2966
|
+
useGraphExploration.getState().hideEdgeForwardPath(edgeId2);
|
|
2932
2967
|
}
|
|
2933
2968
|
},
|
|
2934
2969
|
{
|
|
2935
2970
|
content: "Hide Reverse Path",
|
|
2936
2971
|
select: (ele) => {
|
|
2937
|
-
const
|
|
2972
|
+
const edgeId2 = ele.id();
|
|
2938
2973
|
devLog9(
|
|
2939
2974
|
"[CxtMenu] Hide reverse path (edge + source + upstream):",
|
|
2940
|
-
|
|
2975
|
+
edgeId2
|
|
2941
2976
|
);
|
|
2942
|
-
useGraphExploration.getState().hideEdgeReversePath(
|
|
2977
|
+
useGraphExploration.getState().hideEdgeReversePath(edgeId2);
|
|
2943
2978
|
}
|
|
2944
2979
|
}
|
|
2945
2980
|
],
|
|
@@ -2990,9 +3025,9 @@ function CytoscapeExpandOverlay({
|
|
|
2990
3025
|
{
|
|
2991
3026
|
nodeLabel: expandRequest.nodeLabel,
|
|
2992
3027
|
onExpand: (options) => {
|
|
2993
|
-
const { nodeId } = expandRequest;
|
|
3028
|
+
const { nodeId: nodeId2 } = expandRequest;
|
|
2994
3029
|
onClose();
|
|
2995
|
-
useGraphExploration.getState().expandNode(
|
|
3030
|
+
useGraphExploration.getState().expandNode(nodeId2, options).catch((err) => {
|
|
2996
3031
|
errorLog3("[CxtMenu] Expansion failed:", err);
|
|
2997
3032
|
toast4.error("Failed to expand node");
|
|
2998
3033
|
});
|
|
@@ -3322,7 +3357,7 @@ function useFullscreen(fitToScreen, cyRef) {
|
|
|
3322
3357
|
return {
|
|
3323
3358
|
fullscreen,
|
|
3324
3359
|
setFullscreen,
|
|
3325
|
-
toggleFullscreen:
|
|
3360
|
+
toggleFullscreen: useCallback5(() => setFullscreen((f) => !f), [])
|
|
3326
3361
|
};
|
|
3327
3362
|
}
|
|
3328
3363
|
function setupCytoscapeEventHandlers({
|
|
@@ -3385,8 +3420,8 @@ function setupCytoscapeEventHandlers({
|
|
|
3385
3420
|
});
|
|
3386
3421
|
cy.on("tap", "edge", (evt) => {
|
|
3387
3422
|
const edgeEle = evt.target;
|
|
3388
|
-
const
|
|
3389
|
-
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);
|
|
3390
3425
|
if (!edgeObj) return;
|
|
3391
3426
|
const sourceId = edgeEle.data("source");
|
|
3392
3427
|
const targetId = edgeEle.data("target");
|
|
@@ -3396,7 +3431,7 @@ function setupCytoscapeEventHandlers({
|
|
|
3396
3431
|
const selectedNodeId = currentlySelectedNodes[0].id();
|
|
3397
3432
|
if (selectedNodeId === sourceId || selectedNodeId === targetId) {
|
|
3398
3433
|
devLog10("[CytoscapeGraph] tap edge - node+edge selection", {
|
|
3399
|
-
edgeId,
|
|
3434
|
+
edgeId: edgeId2,
|
|
3400
3435
|
selectedNode: selectedNodeId,
|
|
3401
3436
|
direction: selectedNodeId === sourceId ? "outgoing" : "incoming"
|
|
3402
3437
|
});
|
|
@@ -3410,7 +3445,7 @@ function setupCytoscapeEventHandlers({
|
|
|
3410
3445
|
return;
|
|
3411
3446
|
}
|
|
3412
3447
|
}
|
|
3413
|
-
devLog10("[CytoscapeGraph] tap edge - default (both endpoints)", { edgeId });
|
|
3448
|
+
devLog10("[CytoscapeGraph] tap edge - default (both endpoints)", { edgeId: edgeId2 });
|
|
3414
3449
|
evtCy.batch(() => {
|
|
3415
3450
|
evtCy.elements().unselect();
|
|
3416
3451
|
evtCy.$id(sourceId).select();
|
|
@@ -3438,16 +3473,16 @@ function CytoscapeGraph({
|
|
|
3438
3473
|
extraToolbarContent,
|
|
3439
3474
|
getNodeEdgeTypes
|
|
3440
3475
|
}) {
|
|
3441
|
-
const containerRef =
|
|
3442
|
-
const cyRef =
|
|
3443
|
-
const isInitializedRef =
|
|
3476
|
+
const containerRef = useRef4(null);
|
|
3477
|
+
const cyRef = useRef4(null);
|
|
3478
|
+
const isInitializedRef = useRef4(false);
|
|
3444
3479
|
const [currentLayout, setCurrentLayout] = useState6(initialLayout);
|
|
3445
3480
|
const [expandRequest, setExpandRequest] = useState6(
|
|
3446
3481
|
null
|
|
3447
3482
|
);
|
|
3448
|
-
const nodesRef =
|
|
3449
|
-
const edgesRef =
|
|
3450
|
-
const pendingFocusActionRef =
|
|
3483
|
+
const nodesRef = useRef4([]);
|
|
3484
|
+
const edgesRef = useRef4([]);
|
|
3485
|
+
const pendingFocusActionRef = useRef4(null);
|
|
3451
3486
|
const cytoData = useCytoscapeData();
|
|
3452
3487
|
const live = useLiveGraphData();
|
|
3453
3488
|
const nodes = live.nodes;
|
|
@@ -3467,31 +3502,51 @@ function CytoscapeGraph({
|
|
|
3467
3502
|
setSelectedEdge: setSelectedEdgeStore,
|
|
3468
3503
|
clear: clearSelectionStore
|
|
3469
3504
|
} = useSelection3.getState();
|
|
3470
|
-
const selectedNodeIdsRef =
|
|
3505
|
+
const selectedNodeIdsRef = useRef4(
|
|
3471
3506
|
useSelection3.getState().selectedNodeIds
|
|
3472
3507
|
);
|
|
3473
|
-
const selectedEdgeRef =
|
|
3508
|
+
const selectedEdgeRef = useRef4(
|
|
3474
3509
|
useSelection3.getState().selectedEdge
|
|
3475
3510
|
);
|
|
3476
3511
|
useEffect4(() => {
|
|
3477
|
-
const
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
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;
|
|
3482
3526
|
cy.batch(() => {
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
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();
|
|
3491
3540
|
}
|
|
3492
3541
|
}
|
|
3493
3542
|
});
|
|
3494
|
-
}
|
|
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;
|
|
3495
3550
|
}, [useSelection3]);
|
|
3496
3551
|
useEffect4(() => {
|
|
3497
3552
|
nodesRef.current = nodes;
|
|
@@ -3505,8 +3560,10 @@ function CytoscapeGraph({
|
|
|
3505
3560
|
cyRef.current = cytoscape({
|
|
3506
3561
|
container: containerRef.current,
|
|
3507
3562
|
elements: [],
|
|
3508
|
-
|
|
3509
|
-
//
|
|
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",
|
|
3510
3567
|
style: [
|
|
3511
3568
|
// Base node style with data mappers for dynamic properties
|
|
3512
3569
|
{
|
|
@@ -3600,19 +3657,20 @@ function CytoscapeGraph({
|
|
|
3600
3657
|
{
|
|
3601
3658
|
selector: "edge:selected",
|
|
3602
3659
|
style: {
|
|
3603
|
-
"line-color":
|
|
3604
|
-
"target-arrow-color":
|
|
3605
|
-
width
|
|
3606
|
-
//
|
|
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,
|
|
3607
3665
|
"z-index": 999,
|
|
3608
3666
|
"font-size": "11px",
|
|
3609
3667
|
"font-weight": "bold",
|
|
3610
3668
|
"text-background-opacity": 1,
|
|
3611
3669
|
"text-background-color": "#ffffff",
|
|
3612
3670
|
"text-background-padding": "3px",
|
|
3613
|
-
"arrow-scale":
|
|
3671
|
+
"arrow-scale": 0.9,
|
|
3614
3672
|
opacity: 1,
|
|
3615
|
-
"overlay-color":
|
|
3673
|
+
"overlay-color": SELECTION_COLOR,
|
|
3616
3674
|
"overlay-opacity": 0.08
|
|
3617
3675
|
}
|
|
3618
3676
|
},
|
|
@@ -3621,13 +3679,13 @@ function CytoscapeGraph({
|
|
|
3621
3679
|
style: {
|
|
3622
3680
|
"border-width": 10,
|
|
3623
3681
|
// thicker circle outline
|
|
3624
|
-
"border-color":
|
|
3682
|
+
"border-color": SELECTION_COLOR,
|
|
3625
3683
|
"border-opacity": 1,
|
|
3626
3684
|
"background-opacity": 1,
|
|
3627
3685
|
"text-background-opacity": 1,
|
|
3628
3686
|
"text-background-color": "#ffffff",
|
|
3629
3687
|
"text-background-padding": "4px",
|
|
3630
|
-
"overlay-color":
|
|
3688
|
+
"overlay-color": SELECTION_COLOR,
|
|
3631
3689
|
"overlay-opacity": 0.18,
|
|
3632
3690
|
width: 48,
|
|
3633
3691
|
// slight size bump from 40
|
|
@@ -3659,9 +3717,9 @@ function CytoscapeGraph({
|
|
|
3659
3717
|
cyRef.current = null;
|
|
3660
3718
|
};
|
|
3661
3719
|
}, []);
|
|
3662
|
-
const elementIdsRef =
|
|
3663
|
-
const lastLayoutRef =
|
|
3664
|
-
const lastFitPaddingRef =
|
|
3720
|
+
const elementIdsRef = useRef4("");
|
|
3721
|
+
const lastLayoutRef = useRef4("");
|
|
3722
|
+
const lastFitPaddingRef = useRef4(fitPadding);
|
|
3665
3723
|
useEffect4(() => {
|
|
3666
3724
|
const cy = cyRef.current;
|
|
3667
3725
|
if (!cy || !isInitializedRef.current) return;
|
|
@@ -3759,7 +3817,7 @@ function CytoscapeGraph({
|
|
|
3759
3817
|
const updateLayout = (newLayout) => {
|
|
3760
3818
|
setCurrentLayout(newLayout);
|
|
3761
3819
|
};
|
|
3762
|
-
const fitToScreen =
|
|
3820
|
+
const fitToScreen = useCallback5(() => {
|
|
3763
3821
|
if (cyRef.current) {
|
|
3764
3822
|
cyRef.current.fit(void 0, fitPadding);
|
|
3765
3823
|
}
|
|
@@ -3816,9 +3874,9 @@ function CytoscapeGraph({
|
|
|
3816
3874
|
extraToolbarContent,
|
|
3817
3875
|
zoomIn: () => {
|
|
3818
3876
|
if (cyRef.current) {
|
|
3819
|
-
const
|
|
3877
|
+
const z3 = cyRef.current.zoom();
|
|
3820
3878
|
cyRef.current.zoom({
|
|
3821
|
-
level: Math.min(
|
|
3879
|
+
level: Math.min(z3 * 1.2, 5),
|
|
3822
3880
|
renderedPosition: {
|
|
3823
3881
|
x: cyRef.current.width() / 2,
|
|
3824
3882
|
y: cyRef.current.height() / 2
|
|
@@ -3828,9 +3886,9 @@ function CytoscapeGraph({
|
|
|
3828
3886
|
},
|
|
3829
3887
|
zoomOut: () => {
|
|
3830
3888
|
if (cyRef.current) {
|
|
3831
|
-
const
|
|
3889
|
+
const z3 = cyRef.current.zoom();
|
|
3832
3890
|
cyRef.current.zoom({
|
|
3833
|
-
level: Math.max(
|
|
3891
|
+
level: Math.max(z3 / 1.2, 0.1),
|
|
3834
3892
|
renderedPosition: {
|
|
3835
3893
|
x: cyRef.current.width() / 2,
|
|
3836
3894
|
y: cyRef.current.height() / 2
|
|
@@ -4028,18 +4086,11 @@ function NodeLabelBadges({
|
|
|
4028
4086
|
labels,
|
|
4029
4087
|
labelCounts,
|
|
4030
4088
|
colorPalette,
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
toggleNodeLabelFilter
|
|
4089
|
+
shown,
|
|
4090
|
+
onToggleLabel
|
|
4034
4091
|
}) {
|
|
4035
4092
|
return labels.map((label) => {
|
|
4036
|
-
const bgColor =
|
|
4037
|
-
allLabelsDisabled,
|
|
4038
|
-
nodeLabelFilters,
|
|
4039
|
-
label,
|
|
4040
|
-
colorPalette,
|
|
4041
|
-
label
|
|
4042
|
-
);
|
|
4093
|
+
const bgColor = shown.has(label) ? colorPalette[label] : colorPalette["faded"];
|
|
4043
4094
|
return /* @__PURE__ */ jsxs12(
|
|
4044
4095
|
Badge,
|
|
4045
4096
|
{
|
|
@@ -4049,7 +4100,7 @@ function NodeLabelBadges({
|
|
|
4049
4100
|
backgroundColor: bgColor,
|
|
4050
4101
|
borderRadius: "10px"
|
|
4051
4102
|
},
|
|
4052
|
-
onClick: () =>
|
|
4103
|
+
onClick: () => onToggleLabel(label),
|
|
4053
4104
|
children: [
|
|
4054
4105
|
label,
|
|
4055
4106
|
" (",
|
|
@@ -4099,12 +4150,53 @@ function EdgeLabelBadges({
|
|
|
4099
4150
|
);
|
|
4100
4151
|
});
|
|
4101
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
|
+
}
|
|
4102
4192
|
function GraphOverview({
|
|
4103
4193
|
stats,
|
|
4104
4194
|
colorPalette,
|
|
4105
4195
|
labelCounts,
|
|
4106
4196
|
edgeLabelCounts,
|
|
4107
|
-
edgeToNodeLabels
|
|
4197
|
+
edgeToNodeLabels,
|
|
4198
|
+
nodeBuckets,
|
|
4199
|
+
edgeSectionTitle = "Relations"
|
|
4108
4200
|
}) {
|
|
4109
4201
|
const useGraphFilter2 = useWorkspaceGraphFilter();
|
|
4110
4202
|
const nodeLabelFilters = useGraphFilter2((state) => state.nodeLabelFilters);
|
|
@@ -4113,24 +4205,15 @@ function GraphOverview({
|
|
|
4113
4205
|
const allEdgeLabelsDisabled = useGraphFilter2(
|
|
4114
4206
|
(state) => state.allEdgeLabelsDisabled
|
|
4115
4207
|
);
|
|
4116
|
-
const toggleNodeLabelFilter = useGraphFilter2(
|
|
4117
|
-
(state) => state.toggleNodeLabelFilter
|
|
4118
|
-
);
|
|
4119
|
-
const clearNodeLabelFilters = useGraphFilter2(
|
|
4120
|
-
(state) => state.clearNodeLabelFilters
|
|
4121
|
-
);
|
|
4122
|
-
const toggleAllLabelsDisabled = useGraphFilter2(
|
|
4123
|
-
(state) => state.toggleAllLabelsDisabled
|
|
4124
|
-
);
|
|
4125
4208
|
const toggleEdgeLabelFilter = useGraphFilter2(
|
|
4126
4209
|
(state) => state.toggleEdgeLabelFilter
|
|
4127
4210
|
);
|
|
4128
|
-
const clearEdgeLabelFilters = useGraphFilter2(
|
|
4129
|
-
(state) => state.clearEdgeLabelFilters
|
|
4130
|
-
);
|
|
4131
4211
|
const toggleAllEdgeLabelsDisabled = useGraphFilter2(
|
|
4132
4212
|
(state) => state.toggleAllEdgeLabelsDisabled
|
|
4133
4213
|
);
|
|
4214
|
+
const setNodeFilterState = useGraphFilter2(
|
|
4215
|
+
(state) => state.setNodeFilterState
|
|
4216
|
+
);
|
|
4134
4217
|
const allLabels = Object.keys(labelCounts).sort();
|
|
4135
4218
|
devLog11("[Overview] Rendering labels", {
|
|
4136
4219
|
labels: allLabels,
|
|
@@ -4139,52 +4222,63 @@ function GraphOverview({
|
|
|
4139
4222
|
stats
|
|
4140
4223
|
});
|
|
4141
4224
|
const allEdgeLabels = Object.keys(edgeLabelCounts || {}).sort();
|
|
4142
|
-
const
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
toggleAllEdgeLabelsDisabled();
|
|
4153
|
-
clearEdgeLabelFilters();
|
|
4154
|
-
} else {
|
|
4155
|
-
toggleAllEdgeLabelsDisabled();
|
|
4156
|
-
}
|
|
4157
|
-
};
|
|
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;
|
|
4158
4235
|
return /* @__PURE__ */ jsxs12(Fragment3, { children: [
|
|
4159
|
-
/* @__PURE__ */ jsx13("p", { className: "mt-2
|
|
4160
|
-
allLabels.length > 0 && /* @__PURE__ */
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
/* @__PURE__ */ jsx13(
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
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
|
+
}) }),
|
|
4186
4280
|
allEdgeLabels.length > 0 && /* @__PURE__ */ jsxs12("div", { children: [
|
|
4187
|
-
/* @__PURE__ */ jsx13("p", { className: "
|
|
4281
|
+
/* @__PURE__ */ jsx13("p", { className: "heading-meta mb-2", children: edgeSectionTitle }),
|
|
4188
4282
|
/* @__PURE__ */ jsxs12("div", { className: "flex flex-col items-start gap-2", children: [
|
|
4189
4283
|
/* @__PURE__ */ jsx13(
|
|
4190
4284
|
AllToggleBadge,
|
|
@@ -4212,7 +4306,7 @@ function GraphOverview({
|
|
|
4212
4306
|
] })
|
|
4213
4307
|
] });
|
|
4214
4308
|
}
|
|
4215
|
-
function Overview() {
|
|
4309
|
+
function Overview({ nodeBuckets, edgeSectionTitle } = {}) {
|
|
4216
4310
|
const { stats, colorPalette, labelCounts, edgeLabelCounts, baseNormalized } = useLiveGraphData();
|
|
4217
4311
|
const edgeToNodeLabels = useMemo4(() => {
|
|
4218
4312
|
if (!baseNormalized?.nodesById || !baseNormalized?.edgesById) return {};
|
|
@@ -4223,7 +4317,7 @@ function Overview() {
|
|
|
4223
4317
|
}, [baseNormalized]);
|
|
4224
4318
|
if (stats && stats.values != null && stats.values > 0) {
|
|
4225
4319
|
return /* @__PURE__ */ jsxs12(Fragment3, { children: [
|
|
4226
|
-
/* @__PURE__ */ jsx13("p", { className: "mt-2
|
|
4320
|
+
/* @__PURE__ */ jsx13("p", { className: "mt-2 heading-component", children: "Overview" }),
|
|
4227
4321
|
/* @__PURE__ */ jsxs12("p", { className: "text-sm text-muted-foreground", children: [
|
|
4228
4322
|
formatNumber(stats.values),
|
|
4229
4323
|
" Values"
|
|
@@ -4238,7 +4332,9 @@ function Overview() {
|
|
|
4238
4332
|
colorPalette,
|
|
4239
4333
|
labelCounts,
|
|
4240
4334
|
edgeLabelCounts,
|
|
4241
|
-
edgeToNodeLabels
|
|
4335
|
+
edgeToNodeLabels,
|
|
4336
|
+
nodeBuckets,
|
|
4337
|
+
edgeSectionTitle
|
|
4242
4338
|
}
|
|
4243
4339
|
);
|
|
4244
4340
|
}
|
|
@@ -4284,11 +4380,12 @@ function ActionPanel({ actions }) {
|
|
|
4284
4380
|
var ActionPanel_default = ActionPanel;
|
|
4285
4381
|
|
|
4286
4382
|
// src/components/GraphA11yList/GraphA11yList.tsx
|
|
4287
|
-
import { useMemo as useMemo6, useRef as
|
|
4383
|
+
import { useCallback as useCallback6, useMemo as useMemo6, useRef as useRef5 } from "react";
|
|
4288
4384
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
4289
4385
|
import { cn as cn2 } from "@petrarca/sonnet-core";
|
|
4290
4386
|
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
4291
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;
|
|
4292
4389
|
function GraphA11yList({
|
|
4293
4390
|
visible = false,
|
|
4294
4391
|
rowHeight = 28,
|
|
@@ -4322,28 +4419,46 @@ function GraphA11yList({
|
|
|
4322
4419
|
}));
|
|
4323
4420
|
return [...nodeRows, ...edgeRows];
|
|
4324
4421
|
}, [live.nodes, live.edges]);
|
|
4325
|
-
const parentRef =
|
|
4422
|
+
const parentRef = useRef5(null);
|
|
4326
4423
|
const virtualizer = useVirtualizer({
|
|
4327
4424
|
count: rows.length,
|
|
4328
4425
|
getScrollElement: () => parentRef.current,
|
|
4329
4426
|
estimateSize: () => rowHeight,
|
|
4330
4427
|
overscan: 8
|
|
4331
4428
|
});
|
|
4332
|
-
const
|
|
4333
|
-
|
|
4429
|
+
const activateNode = useCallback6(
|
|
4430
|
+
(id, additive) => {
|
|
4334
4431
|
setSelectedEdge(null);
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
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
|
+
);
|
|
4341
4455
|
const isSelected = (row) => row.kind === "node" ? selectedNodeIds.includes(row.id) : selectedEdge?.id_ === row.edge.id_;
|
|
4342
4456
|
return /* @__PURE__ */ jsx15(
|
|
4343
4457
|
"div",
|
|
4344
4458
|
{
|
|
4345
4459
|
role: "listbox",
|
|
4346
4460
|
"aria-label": "Graph elements",
|
|
4461
|
+
"aria-multiselectable": true,
|
|
4347
4462
|
className: cn2(visible ? "rounded-md border" : SR_ONLY, className),
|
|
4348
4463
|
children: /* @__PURE__ */ jsx15(
|
|
4349
4464
|
"div",
|
|
@@ -4371,7 +4486,7 @@ function GraphA11yList({
|
|
|
4371
4486
|
"aria-label": `${row.kind === "node" ? "Node" : "Edge"} ${row.label}`,
|
|
4372
4487
|
"data-testid": `graph-${row.kind}-${row.id}`,
|
|
4373
4488
|
...row.kind === "node" ? { "data-graph-node-id": row.id } : { "data-graph-edge-id": row.id },
|
|
4374
|
-
onClick: () => selectRow(row),
|
|
4489
|
+
onClick: (e) => selectRow(row, e),
|
|
4375
4490
|
className: cn2(
|
|
4376
4491
|
"absolute left-0 top-0 flex w-full items-center px-2 text-left text-sm",
|
|
4377
4492
|
visible && "hover:bg-accent aria-selected:bg-state-selected aria-selected:text-foreground"
|
|
@@ -4392,46 +4507,2977 @@ function GraphA11yList({
|
|
|
4392
4507
|
}
|
|
4393
4508
|
);
|
|
4394
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
|
+
}
|
|
4395
7386
|
export {
|
|
4396
7387
|
ActionPanel_default as ActionPanel,
|
|
7388
|
+
DeleteNodeDialog,
|
|
4397
7389
|
EMPTY_NORMALIZED,
|
|
7390
|
+
EdgeFormSheet,
|
|
4398
7391
|
EdgeInfoCard_default as EdgeInfoCard,
|
|
4399
7392
|
GraphA11yList,
|
|
7393
|
+
GraphEditToolbar,
|
|
7394
|
+
GraphModelEditor,
|
|
4400
7395
|
GraphRendererProvider,
|
|
4401
7396
|
GraphStoresContext,
|
|
4402
7397
|
GraphVisualizer_default as GraphVisualizer,
|
|
7398
|
+
ModelActionPanel,
|
|
7399
|
+
NodeFormSheet,
|
|
4403
7400
|
NodeInfoCard_default as NodeInfoCard,
|
|
7401
|
+
NodeTypeNameSheet,
|
|
4404
7402
|
Overview_default as Overview,
|
|
7403
|
+
PropertyFormSheet,
|
|
7404
|
+
RelationFormSheet,
|
|
7405
|
+
TypeDetailPanel,
|
|
7406
|
+
addEdgeType,
|
|
7407
|
+
addNodeType,
|
|
7408
|
+
addRelation,
|
|
4405
7409
|
buildColorPalette,
|
|
4406
7410
|
buildColorPaletteFromNodes,
|
|
7411
|
+
capabilitiesSchema,
|
|
7412
|
+
cardinalityLabel,
|
|
4407
7413
|
createActionPanelStore,
|
|
4408
7414
|
createGraphDataStore,
|
|
4409
7415
|
createGraphExplorationStore,
|
|
4410
7416
|
createGraphFilterStore,
|
|
4411
7417
|
createGraphFocusStore,
|
|
4412
7418
|
createSelectionStore,
|
|
7419
|
+
deleteEdgeType,
|
|
7420
|
+
deleteEdgeTypeProperty,
|
|
7421
|
+
deleteNodeProperty,
|
|
7422
|
+
deleteNodeType,
|
|
7423
|
+
deleteRelation,
|
|
7424
|
+
deleteRelationProperty,
|
|
7425
|
+
edgeTypeView,
|
|
7426
|
+
editableRelations,
|
|
4413
7427
|
enrichEdge,
|
|
4414
7428
|
enrichEdges,
|
|
4415
7429
|
enrichNode,
|
|
4416
7430
|
enrichNodes,
|
|
7431
|
+
enrichSchemaWithRelations,
|
|
4417
7432
|
extractDisplayLabel,
|
|
7433
|
+
extractRelationFields,
|
|
7434
|
+
findEdgeType,
|
|
7435
|
+
findType,
|
|
7436
|
+
graphCapabilitiesSchema,
|
|
7437
|
+
groupEdgesByRelation,
|
|
7438
|
+
hasProperties,
|
|
7439
|
+
hasRelationFields,
|
|
7440
|
+
initializeFormData,
|
|
7441
|
+
isEdgeType,
|
|
7442
|
+
isNodeType,
|
|
4418
7443
|
isRawGraphNode,
|
|
7444
|
+
isRelationField,
|
|
4419
7445
|
isVisualEdge,
|
|
4420
7446
|
isVisualNode,
|
|
7447
|
+
modelToGraph,
|
|
7448
|
+
nodeTypeView,
|
|
4421
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,
|
|
4422
7463
|
toCytoscapeEdge,
|
|
4423
7464
|
toCytoscapeNode,
|
|
4424
7465
|
toReagraphEdge,
|
|
4425
7466
|
toReagraphNode,
|
|
7467
|
+
typeLabel,
|
|
7468
|
+
updateRelation,
|
|
4426
7469
|
useActionPanelStore,
|
|
4427
7470
|
useCytoscapeData,
|
|
4428
7471
|
GraphFocus_default as useGraphFocus,
|
|
4429
7472
|
useGraphRenderer,
|
|
4430
7473
|
useLiveGraphData,
|
|
4431
7474
|
useReagraphData,
|
|
7475
|
+
useStableRef,
|
|
4432
7476
|
useWorkspaceGraphData,
|
|
4433
7477
|
useWorkspaceGraphExploration,
|
|
4434
7478
|
useWorkspaceGraphFilter,
|
|
4435
|
-
useWorkspaceSelection
|
|
7479
|
+
useWorkspaceSelection,
|
|
7480
|
+
validateCapabilities,
|
|
7481
|
+
validateGraphCapabilities
|
|
4436
7482
|
};
|
|
4437
7483
|
//# sourceMappingURL=index.js.map
|