|
|
@@ -0,0 +1,148 @@
|
|
|
+// The React state and reducer of a D3Graph component showing an onion graph state.
|
|
|
+
|
|
|
+import {PrimitiveValue} from "onion/types";
|
|
|
+import {INodeState, IValueState, GraphStateListener} from "onion/graph_state";
|
|
|
+import {D3GraphData, D3NodeData, D3LinkData} from "../d3graph";
|
|
|
+
|
|
|
+export type D3OnionNodeData = D3NodeData<INodeState|IValueState>;
|
|
|
+export type D3OnionLinkData = D3LinkData<null>;
|
|
|
+export type D3OnionGraphData = D3GraphData<INodeState|IValueState,null>;
|
|
|
+
|
|
|
+interface AddNode {type:'addNode', ns: INodeState, x: number, y: number}
|
|
|
+interface RemoveNode {type:'removeNode', id: PrimitiveValue}
|
|
|
+interface AddValue {type:'addValue', vs: IValueState, x: number, y: number}
|
|
|
+interface RemoveValue {type:'removeValue', value: PrimitiveValue}
|
|
|
+interface AddLinkToNode {type:'addLinkToNode', sourceId: PrimitiveValue, label: string, targetId: PrimitiveValue}
|
|
|
+interface AddLinkToValue {type:'addLinkToValue', sourceId: PrimitiveValue, label: string, targetValue: PrimitiveValue}
|
|
|
+interface RemoveLink {type:'removeLink', sourceId: PrimitiveValue, label: string}
|
|
|
+
|
|
|
+export type D3OnionGraphAction =
|
|
|
+ Readonly<AddNode>
|
|
|
+ | Readonly<RemoveNode>
|
|
|
+ | Readonly<AddValue>
|
|
|
+ | Readonly<RemoveValue>
|
|
|
+ | Readonly<AddLinkToNode>
|
|
|
+ | Readonly<AddLinkToValue>
|
|
|
+ | Readonly<RemoveLink>;
|
|
|
+
|
|
|
+export function onionGraphReducer(prevState: D3OnionGraphData, action: D3OnionGraphAction): D3OnionGraphData {
|
|
|
+ switch (action.type) {
|
|
|
+ case 'addNode': {
|
|
|
+ return {
|
|
|
+ nodes: [...prevState.nodes, {
|
|
|
+ id: nodeNodeId(action.ns.creation.id.value),
|
|
|
+ label: JSON.stringify(action.ns.creation.id.value),
|
|
|
+ x: action.x,
|
|
|
+ y: action.y,
|
|
|
+ color: "darkturquoise",
|
|
|
+ obj: action.ns,
|
|
|
+ highlight: false,
|
|
|
+ }],
|
|
|
+ links: prevState.links,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ case 'removeNode': {
|
|
|
+ return {
|
|
|
+ nodes: prevState.nodes.filter(n => !n.obj.isNode(action.id)),
|
|
|
+ links: prevState.links,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ case 'addValue': {
|
|
|
+ return {
|
|
|
+ nodes: [...prevState.nodes, {
|
|
|
+ id: valueNodeId(action.vs.value),
|
|
|
+ label: JSON.stringify(action.vs.value),
|
|
|
+ x: action.x,
|
|
|
+ y: action.y,
|
|
|
+ color: "darkorange",
|
|
|
+ obj: action.vs,
|
|
|
+ highlight: false,
|
|
|
+ }],
|
|
|
+ links: prevState.links,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ case 'removeValue': {
|
|
|
+ return {
|
|
|
+ nodes: prevState.nodes.filter(n => !n.obj.isValue(action.value)),
|
|
|
+ links: prevState.links,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ case 'addLinkToNode': {
|
|
|
+ return {
|
|
|
+ nodes: prevState.nodes,
|
|
|
+ links: [...prevState.links, {
|
|
|
+ source: prevState.nodes.find(n => n.obj.isNode(action.sourceId)), // AR: here is the problem!
|
|
|
+ target: prevState.nodes.find(n => n.obj.isNode(action.targetId)),
|
|
|
+ label: action.label,
|
|
|
+ color: 'black',
|
|
|
+ obj: null,
|
|
|
+ }],
|
|
|
+ };
|
|
|
+ }
|
|
|
+ case 'addLinkToValue': {
|
|
|
+ return {
|
|
|
+ nodes: prevState.nodes,
|
|
|
+ links: [...prevState.links, {
|
|
|
+ source: prevState.nodes.find(n => n.obj.isNode(action.sourceId)),
|
|
|
+ target: prevState.nodes.find(n => n.obj.isValue(action.targetValue)),
|
|
|
+ label: action.label,
|
|
|
+ color: 'black',
|
|
|
+ obj: null,
|
|
|
+ }],
|
|
|
+ };
|
|
|
+ }
|
|
|
+ case 'removeLink': {
|
|
|
+ return {
|
|
|
+ nodes: prevState.nodes,
|
|
|
+ links: prevState.links.filter(l => l.source.obj.creation.id.value !== action.sourceId || l.label !== action.label),
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function nodeNodeId(nodeId: PrimitiveValue) {
|
|
|
+ return "N"+JSON.stringify(nodeId);
|
|
|
+}
|
|
|
+
|
|
|
+function valueNodeId(value: PrimitiveValue) {
|
|
|
+ return "V"+JSON.stringify(value);
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+// Responds to changes to a GraphState object by updating the React state of a d3Graph component.
|
|
|
+export class D3GraphUpdater implements GraphStateListener {
|
|
|
+ readonly setGraph: (cb: (prevGraph: D3OnionGraphData) => D3OnionGraphData) => void;
|
|
|
+
|
|
|
+ // SVG coordinates for newly created nodes
|
|
|
+ // This information cannot be part of our NodeCreation deltas, but it must come from somewhere...
|
|
|
+ x: number;
|
|
|
+ y: number;
|
|
|
+
|
|
|
+ constructor(setGraph: (cb: (prevGraph: D3OnionGraphData) => D3OnionGraphData) => void, x, y) {
|
|
|
+ this.setGraph = setGraph;
|
|
|
+ this.x = x;
|
|
|
+ this.y = y;
|
|
|
+ }
|
|
|
+
|
|
|
+ createNode(ns: INodeState) {
|
|
|
+ this.setGraph(prevGraph => onionGraphReducer(prevGraph, {type: 'addNode', ns, x: this.x, y: this.y}));
|
|
|
+ }
|
|
|
+ createValue(vs: IValueState) {
|
|
|
+ this.setGraph(prevGraph => onionGraphReducer(prevGraph, {type: 'addValue', vs, x: this.x, y: this.y}));
|
|
|
+ }
|
|
|
+ deleteNode(id: PrimitiveValue) {
|
|
|
+ this.setGraph(prevGraph => onionGraphReducer(prevGraph, {type: 'removeNode', id}));
|
|
|
+ }
|
|
|
+ deleteValue(value: PrimitiveValue) {
|
|
|
+ this.setGraph(prevGraph => onionGraphReducer(prevGraph, {type: 'removeValue', value}));
|
|
|
+ }
|
|
|
+ createLinkToNode(sourceId: PrimitiveValue, label: string, targetId: PrimitiveValue) {
|
|
|
+ this.setGraph(prevGraph => onionGraphReducer(prevGraph, {type: 'addLinkToNode', sourceId, label, targetId}));
|
|
|
+ }
|
|
|
+ createLinkToValue(sourceId: PrimitiveValue, label: string, targetValue: PrimitiveValue) {
|
|
|
+ this.setGraph(prevGraph => onionGraphReducer(prevGraph, {type: 'addLinkToValue', sourceId, label, targetValue}));
|
|
|
+ }
|
|
|
+ deleteLink(sourceId: PrimitiveValue, label: string) {
|
|
|
+ this.setGraph(prevGraph => onionGraphReducer(prevGraph, {type: 'removeLink', sourceId, label}));
|
|
|
+ }
|
|
|
+}
|