Skip to content

Commit e3e5c4b

Browse files
committed
Graph methods in JSON OT
1 parent 67b2b12 commit e3e5c4b

File tree

5 files changed

+257
-39
lines changed

5 files changed

+257
-39
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"@types/react-dom": "^16.9.8",
1010
"@types/uuid": "^8.0.0",
1111
"express": "^4.17.1",
12+
"graphlib": "^2.1.8",
1213
"jsonwebtoken": "^8.5.1",
1314
"random-words": "^1.1.1",
1415
"react": "^0.0.0-experimental-4c8c98ab9",
@@ -22,6 +23,8 @@
2223
"ws": "^7.3.1"
2324
},
2425
"devDependencies": {
26+
"@types/graphlib": "^2.1.6",
27+
"@types/react-router-dom": "^5.1.5",
2528
"concurrently": "^5.2.0"
2629
},
2730
"scripts": {

src/App.tsx

Lines changed: 99 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,31 @@
11
import randomWords from "random-words";
2-
import { v4 as uuid } from "uuid";
32
import React, { useMemo, useState, useEffect, useCallback } from "react";
43
import { connectToDB, getConnection } from "./sharedb";
54
import { useTransition } from "./react-experimental";
5+
import {
6+
Flow,
7+
Node,
8+
insertNodeOp,
9+
removeNodeOp,
10+
setFlowOp,
11+
connectOp,
12+
} from "./flow";
613
import {
714
Link,
815
BrowserRouter as Router,
916
useHistory,
1017
useLocation,
1118
} from "react-router-dom";
1219

13-
interface Node {
14-
text: string;
15-
}
16-
17-
type Flow = {
18-
nodes: Record<string, Node>;
19-
edges: Array<[string | null, string]>;
20-
};
21-
2220
// Custom hook for talking to a flow in ShareDB
2321
function useFlow(config: {
2422
id: string;
2523
}): {
2624
state: Flow | null;
27-
addNode: () => void;
25+
insertNode: () => void;
2826
removeNode: (id: string) => void;
29-
reset: (flow: Flow) => void;
27+
connectNodes: (src: string, tgt: string) => void;
28+
setFlow: (flow: Flow) => void;
3029
isPending: boolean;
3130
} {
3231
// Setup
@@ -56,20 +55,27 @@ function useFlow(config: {
5655

5756
// Methods
5857

59-
const addNode = useCallback(() => {
60-
doc.submitOp([{ p: ["nodes", uuid()], oi: { text: randomWords() } }]);
58+
const insertNode = useCallback(() => {
59+
doc.submitOp(insertNodeOp());
6160
}, [doc]);
6261

6362
const removeNode = useCallback(
6463
(id) => {
65-
doc.submitOp([{ p: ["nodes", id], od: {} }]);
64+
doc.submitOp(removeNodeOp(id, doc.data));
65+
},
66+
[doc]
67+
);
68+
69+
const connectNodes = useCallback(
70+
(src, tgt) => {
71+
doc.submitOp(connectOp(src, tgt, doc.data));
6672
},
6773
[doc]
6874
);
6975

70-
const reset = useCallback(
76+
const setFlow = useCallback(
7177
(flow) => {
72-
doc.submitOp([{ p: [], od: doc.data, oi: flow }]);
78+
doc.submitOp(setFlowOp(flow, doc.data));
7379
},
7480
[doc]
7581
);
@@ -78,16 +84,33 @@ function useFlow(config: {
7884

7985
return {
8086
state,
81-
addNode,
87+
insertNode,
8288
removeNode,
83-
reset,
89+
setFlow,
90+
connectNodes,
8491
isPending,
8592
};
8693
}
8794

88-
const Flow: React.FC<{ id: string }> = ({ id }) => {
95+
const FlowView: React.FC<{ id: string }> = ({ id }) => {
8996
const flow = useFlow({ id });
9097

98+
const [selected, setSelected] = useState<string | null>(null);
99+
100+
const onSelect = useCallback(
101+
(id: string) => {
102+
if (selected === null) {
103+
setSelected(id);
104+
} else if (selected === id) {
105+
setSelected(null);
106+
} else {
107+
flow.connectNodes(selected, id);
108+
setSelected(null);
109+
}
110+
},
111+
[selected, setSelected]
112+
);
113+
91114
if (flow.state === null) {
92115
return <p>Loading...</p>;
93116
}
@@ -97,7 +120,7 @@ const Flow: React.FC<{ id: string }> = ({ id }) => {
97120
<main>
98121
<button
99122
onClick={() => {
100-
flow.addNode();
123+
flow.insertNode();
101124
}}
102125
>
103126
Add
@@ -106,31 +129,47 @@ const Flow: React.FC<{ id: string }> = ({ id }) => {
106129
onClick={() => {
107130
fetch("/flow.json")
108131
.then((res) => res.json())
109-
.then((flowData) => {
110-
flow.reset(flowData);
132+
.then((flowData: Flow) => {
133+
flow.setFlow(flowData);
111134
});
112135
}}
113136
>
114137
Import flow
115138
</button>
116139
<button
117140
onClick={() => {
118-
flow.reset({
141+
flow.setFlow({
119142
nodes: {},
120143
edges: [],
121144
});
122145
}}
123146
>
124147
Reset
125148
</button>
126-
{Object.keys(flow.state.nodes).map((k) => (
127-
<NodeView
128-
key={k}
129-
onRemove={flow.removeNode}
130-
id={k}
131-
node={flow.state.nodes[k]}
132-
/>
133-
))}
149+
<div className="row mt">
150+
<div>
151+
<h3>Nodes</h3>
152+
{Object.keys(flow.state.nodes).map((k) => (
153+
<NodeView
154+
key={k}
155+
onRemove={flow.removeNode}
156+
onSelect={onSelect}
157+
id={k}
158+
node={flow.state.nodes[k]}
159+
activeId={selected}
160+
/>
161+
))}
162+
</div>
163+
<div>
164+
<h3>Edges</h3>
165+
{flow.state.edges.map(([src, tgt]) => (
166+
<p>
167+
{src ? src.slice(0, 6) : "root"} -{" "}
168+
{tgt ? tgt.slice(0, 6) : "root"}
169+
</p>
170+
))}
171+
</div>
172+
</div>
134173
</main>
135174
{flow.isPending && <div className="overlay" />}
136175
</>
@@ -142,12 +181,16 @@ const NodeView = React.memo(
142181
id,
143182
node,
144183
onRemove,
184+
onSelect,
185+
activeId,
145186
}: {
146187
id: string;
147188
node: Node;
148189
onRemove: (id: string) => void;
190+
onSelect: (id: string) => void;
191+
activeId: string | null;
149192
}) => (
150-
<div className="node">
193+
<div className={`node ${id === activeId ? "node--active" : ""}`}>
151194
<button
152195
className="remove-button"
153196
onClick={() => {
@@ -156,14 +199,33 @@ const NodeView = React.memo(
156199
>
157200
×
158201
</button>
159-
<p>
160-
{node.text || "unset"} {Math.round(Math.random() * 1000)}
161-
</p>
202+
<div>
203+
<p>
204+
{node.text || "unset"}{" "}
205+
<small>{Math.round(Math.random() * 1000)}</small>
206+
</p>
207+
<p>
208+
<small>{id.slice(0, 6)}..</small>
209+
</p>
210+
</div>
211+
<button
212+
onClick={() => {
213+
onSelect(id);
214+
}}
215+
>
216+
{activeId === id
217+
? "Deselect"
218+
: activeId !== null
219+
? "Connect"
220+
: "Select"}
221+
</button>
162222
</div>
163223
),
164224
(prevProps, nextProps) =>
165225
prevProps.id === nextProps.id &&
166226
prevProps.onRemove === nextProps.onRemove &&
227+
prevProps.onSelect === nextProps.onSelect &&
228+
prevProps.activeId === nextProps.activeId &&
167229
JSON.stringify(prevProps.node) === JSON.stringify(nextProps.node)
168230
);
169231

@@ -209,7 +271,7 @@ const App = () => {
209271
New flow
210272
</button>
211273
</nav>
212-
<Flow id={id} />
274+
<FlowView id={id} />
213275
</div>
214276
);
215277
};

src/flow.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import randomWords from "random-words";
2+
import { Graph, alg } from "graphlib";
3+
import { v4 as uuid } from "uuid";
4+
5+
export interface Node {
6+
text: string;
7+
}
8+
9+
export type Flow = {
10+
nodes: Record<string, Node>;
11+
edges: Array<[string | null, string]>;
12+
};
13+
14+
export interface Op {
15+
p: Array<any>;
16+
li?: any;
17+
ld?: any;
18+
od?: any;
19+
oi?: any;
20+
}
21+
22+
const toGraphlib = (flow: Flow): Graph => {
23+
// create a graph with the existing nodes and edges
24+
const g = new Graph({ directed: true, multigraph: false });
25+
Object.keys(flow.nodes).forEach((key) => {
26+
g.setNode(key);
27+
});
28+
flow.edges.forEach(([src, tgt]) => {
29+
g.setEdge(src, tgt);
30+
});
31+
return g;
32+
};
33+
34+
export const insertNodeOp = (): Array<Op> => [
35+
{ p: ["nodes", uuid()], oi: { text: randomWords() } },
36+
];
37+
38+
export const removeNodeOp = (id: string, flow: Flow): Array<Op> => {
39+
const graph = toGraphlib(flow);
40+
const [, ...children] = alg.preorder(graph, [id]);
41+
const affectedNodes = [id, ...children];
42+
const affectedEdgeIndices = flow.edges
43+
.map(([src, tgt], index) =>
44+
affectedNodes.includes(src) || affectedNodes.includes(tgt) ? index : null
45+
)
46+
.filter((val) => val !== null)
47+
// Sort in descending order so that indices don't shift after each ShareDB operation
48+
.sort((a, b) => b - a);
49+
return [
50+
{ p: ["nodes", id], od: flow.nodes[id] },
51+
...children.map((childId) => ({
52+
p: ["nodes", childId],
53+
od: flow.nodes[childId],
54+
})),
55+
...affectedEdgeIndices.map((edgeIndex) => ({
56+
p: ["edges", edgeIndex],
57+
ld: flow.edges[edgeIndex],
58+
})),
59+
];
60+
};
61+
62+
export const setFlowOp = (flow: Flow, prevFlow: Flow): Array<Op> => [
63+
{ p: [], od: prevFlow, oi: flow },
64+
];
65+
66+
export const connectOp = (src: string, tgt: string, flow: Flow): Array<Op> => {
67+
if (src === tgt) {
68+
return [];
69+
}
70+
if (flow.edges.find(([s, t]) => s === src && t === tgt)) {
71+
return [];
72+
}
73+
const graph = toGraphlib(flow);
74+
graph.setEdge(src, tgt);
75+
if (!alg.isAcyclic(graph)) {
76+
return [];
77+
}
78+
return [
79+
{
80+
p: ["edges", flow.edges.length],
81+
li: [src, tgt],
82+
},
83+
];
84+
};

0 commit comments

Comments
 (0)