|
@@ -1,7 +1,9 @@
|
|
|
import * as React from "react";
|
|
|
import * as _ from "lodash";
|
|
|
|
|
|
-import {Grid, Text, Title, Group, Stack} from "@mantine/core";
|
|
|
+import {Grid, Text, Title, Group, Stack, SimpleGrid, Button, Space} from "@mantine/core";
|
|
|
+
|
|
|
+import {IconPlayerTrackPrev, IconPlayerTrackNext} from "@tabler/icons";
|
|
|
|
|
|
import {d3Types, Graph} from "./graph"
|
|
|
import {EditableGraph, GraphType, NodeType, LinkType} from "./editable_graph";
|
|
@@ -35,6 +37,7 @@ class D3StateManipulator implements GraphStateManipulator {
|
|
|
y: this.y,
|
|
|
color: "darkturquoise",
|
|
|
obj: ns,
|
|
|
+ highlight: false,
|
|
|
}],
|
|
|
links: prevGraph.links,
|
|
|
}));
|
|
@@ -53,7 +56,8 @@ class D3StateManipulator implements GraphStateManipulator {
|
|
|
links: [...prevGraph.links, {
|
|
|
source: prevGraph.nodes.find(n => n.obj.creation.id.value === sourceId),
|
|
|
target: prevGraph.nodes.find(n => n.obj.creation.id.value === targetId),
|
|
|
- label,
|
|
|
+ label: label,
|
|
|
+ color: 'black',
|
|
|
obj: null,
|
|
|
}],
|
|
|
}));
|
|
@@ -78,11 +82,29 @@ const emptyGraph = {
|
|
|
links: [],
|
|
|
};
|
|
|
|
|
|
+type HistoryGraphType = d3Types.d3Graph<Version,Delta|null>;
|
|
|
+type DependencyGraphType = d3Types.d3Graph<Delta,null>;
|
|
|
+
|
|
|
const initialHistoryGraph = {
|
|
|
- nodes: [versionToNode(initialVersion)],
|
|
|
- links: [],
|
|
|
+ nodes: [
|
|
|
+ {id: "cur", label: "", color: "grey", obj: null},
|
|
|
+ versionToNode(initialVersion),
|
|
|
+ ],
|
|
|
+ links: [
|
|
|
+ currentVersionLink(initialVersion),
|
|
|
+ ],
|
|
|
};
|
|
|
|
|
|
+
|
|
|
+function updateCurrentVersionLink(prevHistoryGraph: HistoryGraphType, newCurrentVersion) {
|
|
|
+ return {
|
|
|
+ nodes: prevHistoryGraph.nodes,
|
|
|
+ links: prevHistoryGraph.links.filter(link => link.source.id !== "cur").concat(
|
|
|
+ currentVersionLink(newCurrentVersion)
|
|
|
+ ),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
function fullVersionId(version: Version): string {
|
|
|
return version.hash.toString('base64');
|
|
|
}
|
|
@@ -97,8 +119,12 @@ function versionToNode(version: Version): d3Types.d3Node<Version> {
|
|
|
label: version.hash.toString('hex').slice(0,8),
|
|
|
color: "purple",
|
|
|
obj: version,
|
|
|
+ highlight: false,
|
|
|
}
|
|
|
}
|
|
|
+function currentVersionLink(version: Version): d3Types.d3Link<null> {
|
|
|
+ return { source: "cur", label: "", target: fullVersionId(version), color: 'grey', obj: null };
|
|
|
+}
|
|
|
|
|
|
function shortDeltaId(delta: Delta) {
|
|
|
return delta.getHash().toString('hex').slice(0,8);
|
|
@@ -133,7 +159,7 @@ function getDeltaColor(delta: Delta) {
|
|
|
}
|
|
|
|
|
|
interface BranchProps {
|
|
|
- getUuid: () => UUID;
|
|
|
+ generateUUID: () => UUID;
|
|
|
}
|
|
|
|
|
|
export function Branch(props: BranchProps) {
|
|
@@ -141,62 +167,212 @@ export function Branch(props: BranchProps) {
|
|
|
const [graph, setGraph] = React.useState<GraphType>(_.cloneDeep(emptyGraph));
|
|
|
const [manipulator, setManipulator] = React.useState<D3StateManipulator>(new D3StateManipulator(setGraph));
|
|
|
const [graphDeltaExecutor, setGraphDeltaExecutor] = React.useState<GraphDeltaExecutor>(new GraphDeltaExecutor(manipulator));
|
|
|
- const [historyGraph, setHistoryGraph] = React.useState<d3Types.d3Graph<Version,Delta>>(_.cloneDeep(initialHistoryGraph));
|
|
|
- const [dependencyGraph, setDependencyGraph] = React.useState<d3Types.d3Graph<Delta,null>>(_.cloneDeep(emptyGraph));
|
|
|
+ const [historyGraph, setHistoryGraph] = React.useState<HistoryGraphType>(_.cloneDeep(initialHistoryGraph));
|
|
|
+ const [dependencyGraph, setDependencyGraph] = React.useState<DependencyGraphType>(_.cloneDeep(emptyGraph));
|
|
|
|
|
|
const newVersionHandler = (version: Version) => {
|
|
|
setHistoryGraph(prevHistoryGraph => {
|
|
|
- return {
|
|
|
- nodes: prevHistoryGraph.nodes.concat(versionToNode(version)),
|
|
|
- links: prevHistoryGraph.links.concat(...version.parents.map(([parentVersion,delta]) => ({source:fullVersionId(version), label:"", target:fullVersionId(parentVersion), obj: delta}))),
|
|
|
- };
|
|
|
+ const newLinks = version.parents.map(([parentVersion,delta]) => ({source:fullVersionId(version), label:"", target:fullVersionId(parentVersion), color: 'black', obj: delta})).filter(link=>!prevHistoryGraph.links.some(prevLink => prevLink.source.id===link.source&&prevLink.target.id===link.target));
|
|
|
+
|
|
|
+ return updateCurrentVersionLink({
|
|
|
+ // only add node if the version does not yet exist in the history graph:
|
|
|
+ nodes: prevHistoryGraph.nodes.some(node => node.id === fullVersionId(version)) ?
|
|
|
+ prevHistoryGraph.nodes : prevHistoryGraph.nodes.concat(versionToNode(version)),
|
|
|
+
|
|
|
+ // create parent link if that link does not yet exist
|
|
|
+ links: prevHistoryGraph.links.concat(...newLinks),
|
|
|
+ }, version);
|
|
|
});
|
|
|
setVersion(version);
|
|
|
};
|
|
|
const newDeltaHandler = (delta: Delta) => {
|
|
|
+ if (dependencyGraph.nodes.some(node => node.id === fullDeltaId(delta))) {
|
|
|
+ // We already have this delta (remember that delta's are identified by the hash of their contents, so it is possible that different people concurrently create the same deltas, e.g., by deleting the same node concurrently)
|
|
|
+ return; // do nothing
|
|
|
+ }
|
|
|
setDependencyGraph(prevDepGraph => {
|
|
|
const color = getDeltaColor(delta);
|
|
|
return {
|
|
|
- nodes: prevDepGraph.nodes.concat({id: fullDeltaId(delta), label: delta.getDescription(), color, obj: delta}),
|
|
|
- links: prevDepGraph.links.concat(...delta.getTypedDependencies().map(([dep,depType]) => ({source: fullDeltaId(delta), label: depType, target: fullDeltaId(dep), obj: null}))),
|
|
|
+ // add one extra node that represents the new delta:
|
|
|
+ nodes: prevDepGraph.nodes.concat({id: fullDeltaId(delta), label: delta.getDescription(), color, highlight: false, obj: delta}),
|
|
|
+ // for every dependency and conflict, add a link:
|
|
|
+ links: prevDepGraph.links.concat(
|
|
|
+ ...delta.getTypedDependencies().map(([dep,depSummary]) => ({source: fullDeltaId(delta), label: depSummary, color: 'black', target: fullDeltaId(dep), obj: null})),
|
|
|
+ ...delta.getConflicts().map(conflictingDelta => ({source: fullDeltaId(delta), label: "", color: 'DarkGoldenRod', bidirectional: true, target: fullDeltaId(conflictingDelta), obj: null})),
|
|
|
+ ),
|
|
|
};
|
|
|
});
|
|
|
+ highlightCurrentDeltas();
|
|
|
+ };
|
|
|
+
|
|
|
+ const highlightCurrentDeltas = () => {
|
|
|
+ // const deltas = [...version];
|
|
|
+ // setDependencyGraph(prevDepGraph => {
|
|
|
+ // const nodes = prevDepGraph.nodes.map(node => {
|
|
|
+ // const {id, highlight: oldHighlight, ...rest} = node;
|
|
|
+ // const highlight = deltas.some(d => fullDeltaId(d) === id);
|
|
|
+ // return {
|
|
|
+ // id,
|
|
|
+ // highlight,
|
|
|
+ // ...rest,
|
|
|
+ // };
|
|
|
+ // // node.highlight = deltas.some(d => fullDeltaId(d) === node.id);
|
|
|
+ // // return node;
|
|
|
+ // });
|
|
|
+ // return {
|
|
|
+ // nodes,
|
|
|
+ // links: prevDepGraph.links,
|
|
|
+ // // .map(link => {
|
|
|
+ // // const {source, target, ...rest} = link;
|
|
|
+ // // return {
|
|
|
+ // // source: nodes.find(n => n.id === source.id),
|
|
|
+ // // target: nodes.find(n => n.id === target.id),
|
|
|
+ // // ...rest,
|
|
|
+ // // };
|
|
|
+ // // }),
|
|
|
+ // };
|
|
|
+ // });
|
|
|
}
|
|
|
|
|
|
// "physics" stuff (for graph layout)
|
|
|
- // const editableGraphForces = {charge: -10, center:0.01, link:0.05};
|
|
|
const historyGraphForces = {charge: -100, center:0.1, link:2};
|
|
|
const depGraphForces = {charge: -200, center:0.1, link:0.2};
|
|
|
|
|
|
- const historyMouseUpHandler = (event, {x,y}, node: d3Types.d3Node<Version> | undefined) => {
|
|
|
- if (node !== undefined) {
|
|
|
- // the user clicked on a version
|
|
|
- const versionClicked = node.obj;
|
|
|
- const deltas = [...versionClicked].reverse(); // all deltas of versionClicked, from early to late.
|
|
|
-
|
|
|
- function execDelta(d: Delta) {
|
|
|
- if (d instanceof CompositeDelta) {
|
|
|
- d.deltas.forEach(execDelta);
|
|
|
- }
|
|
|
- else if (d instanceof NodeCreation) {
|
|
|
- graphDeltaExecutor.execNodeCreation(d);
|
|
|
- }
|
|
|
- else if (d instanceof NodeDeletion) {
|
|
|
- graphDeltaExecutor.execNodeDeletion(d);
|
|
|
- }
|
|
|
- else if (d instanceof EdgeCreation) {
|
|
|
- graphDeltaExecutor.execEdgeCreation(d);
|
|
|
- }
|
|
|
- else if (d instanceof EdgeUpdate) {
|
|
|
- graphDeltaExecutor.execEdgeUpdate(d);
|
|
|
- }
|
|
|
- }
|
|
|
+ const exec = (d) => {
|
|
|
+ manipulator.x = 0;
|
|
|
+ manipulator.y = 0;
|
|
|
|
|
|
- manipulator.reset();
|
|
|
- deltas.forEach(execDelta);
|
|
|
- setVersion(versionClicked);
|
|
|
+ if (d instanceof CompositeDelta) {
|
|
|
+ d.deltas.forEach(exec);
|
|
|
+ }
|
|
|
+ else if (d instanceof NodeCreation) {
|
|
|
+ graphDeltaExecutor.execNodeCreation(d);
|
|
|
+ }
|
|
|
+ else if (d instanceof NodeDeletion) {
|
|
|
+ graphDeltaExecutor.execNodeDeletion(d);
|
|
|
+ }
|
|
|
+ else if (d instanceof EdgeCreation) {
|
|
|
+ graphDeltaExecutor.execEdgeCreation(d);
|
|
|
+ }
|
|
|
+ else if (d instanceof EdgeUpdate) {
|
|
|
+ graphDeltaExecutor.execEdgeUpdate(d);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ throw new Error("Assertion failed: Unexpected delta type");
|
|
|
}
|
|
|
}
|
|
|
+ const unexec = (d) => {
|
|
|
+ manipulator.x = 0;
|
|
|
+ manipulator.y = 0;
|
|
|
+
|
|
|
+ if (d instanceof CompositeDelta) {
|
|
|
+ [...d.deltas].reverse().forEach(unexec);
|
|
|
+ }
|
|
|
+ else if (d instanceof NodeCreation) {
|
|
|
+ graphDeltaExecutor.unexecNodeCreation(d);
|
|
|
+ }
|
|
|
+ else if (d instanceof NodeDeletion) {
|
|
|
+ graphDeltaExecutor.unexecNodeDeletion(d);
|
|
|
+ }
|
|
|
+ else if (d instanceof EdgeCreation) {
|
|
|
+ graphDeltaExecutor.unexecEdgeCreation(d);
|
|
|
+ }
|
|
|
+ else if (d instanceof EdgeUpdate) {
|
|
|
+ graphDeltaExecutor.unexecEdgeUpdate(d);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ throw new Error("Assertion failed: Unexpected delta type");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const historyMouseUpHandler = (event, {x,y}, node: d3Types.d3Node<Version> | undefined) => {
|
|
|
+ // if (node !== undefined) {
|
|
|
+ // // the user clicked on a version -> the clicked version becomes the "current version"
|
|
|
+ // const versionClicked = node.obj;
|
|
|
+ // const deltas = [...versionClicked].reverse(); // all deltas of versionClicked, from early to late.
|
|
|
+
|
|
|
+
|
|
|
+ // let graph2 = _.cloneDeep(emptyGraph);
|
|
|
+ // const setGraph2 = (updateFunction) => {
|
|
|
+ // graph2 = updateFunction(graph2);
|
|
|
+ // }
|
|
|
+ // const manipulator2 = new D3StateManipulator(setGraph2);
|
|
|
+ // const executor2 = new GraphDeltaExecutor(manipulator2);
|
|
|
+
|
|
|
+
|
|
|
+ // function execDelta(d: Delta) {
|
|
|
+ // if (d instanceof CompositeDelta) {
|
|
|
+ // d.deltas.forEach(execDelta);
|
|
|
+ // }
|
|
|
+ // else if (d instanceof NodeCreation) {
|
|
|
+ // executor2.execNodeCreation(d);
|
|
|
+ // }
|
|
|
+ // else if (d instanceof NodeDeletion) {
|
|
|
+ // executor2.execNodeDeletion(d);
|
|
|
+ // }
|
|
|
+ // else if (d instanceof EdgeCreation) {
|
|
|
+ // executor2.execEdgeCreation(d);
|
|
|
+ // }
|
|
|
+ // else if (d instanceof EdgeUpdate) {
|
|
|
+ // executor2.execEdgeUpdate(d);
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+ // deltas.forEach(execDelta);
|
|
|
+
|
|
|
+ // setGraph(prevGraph => {
|
|
|
+ // const preservedLinks = prevGraph.links.filter(link => graph2.links.some(link2 => link.source === link2.source && link.label === link2.label));
|
|
|
+ // const newLinks = graph2.links.filter(link2 => graph.links.every(link => !(link.source === link2.source && link.label === link2.label)));
|
|
|
+ // newLinks.forEach(link => {
|
|
|
+ // link.source = link
|
|
|
+ // })
|
|
|
+ // console.log("preservedLinks:", preservedLinks);
|
|
|
+ // console.log("newLinks:", newLinks);
|
|
|
+ // return {
|
|
|
+ // nodes: prevGraph.nodes.filter(node => graph2.nodes.some(node2 => node.id === node2.id))
|
|
|
+ // .concat(graph2.nodes.filter(node2 => graph.nodes.every(node => node.id !== node2.id))),
|
|
|
+ // links: preservedLinks.concat(newLinks),
|
|
|
+ // };
|
|
|
+ // });
|
|
|
+
|
|
|
+ // setVersion(versionClicked);
|
|
|
+ // setHistoryGraph(prevHistoryGraph => updateCurrentVersionLink(prevHistoryGraph, versionClicked));
|
|
|
+ // }
|
|
|
+ }
|
|
|
+
|
|
|
+ const onUndo = (parentVersion, deltaToUndo) => {
|
|
|
+ unexec(deltaToUndo);
|
|
|
+ setVersion(parentVersion);
|
|
|
+ highlightCurrentDeltas();
|
|
|
+ setHistoryGraph(prevHistoryGraph => updateCurrentVersionLink(prevHistoryGraph, parentVersion));
|
|
|
+ }
|
|
|
+
|
|
|
+ const onRedo = (childVersion, deltaToRedo) => {
|
|
|
+ exec(deltaToRedo);
|
|
|
+ setVersion(childVersion);
|
|
|
+ highlightCurrentDeltas();
|
|
|
+ setHistoryGraph(prevHistoryGraph => updateCurrentVersionLink(prevHistoryGraph, childVersion));
|
|
|
+ }
|
|
|
+
|
|
|
+ const undoButtons = version.parents.map(([parentVersion,deltaToUndo]) => {
|
|
|
+ return (
|
|
|
+ <div key={fullVersionId(parentVersion)}>
|
|
|
+ <Button fullWidth={true} compact={true} leftIcon={<IconPlayerTrackPrev size={18}/>} onClick={onUndo.bind(null, parentVersion,deltaToUndo)}>
|
|
|
+ UNDO {deltaToUndo.getDescription()}
|
|
|
+ </Button>
|
|
|
+ <Space h="xs"/>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ });
|
|
|
+ const redoButtons = version.children.map(([childVersion,deltaToRedo]) => {
|
|
|
+ return (
|
|
|
+ <div key={fullVersionId(childVersion)}>
|
|
|
+ <Button style={{width: "100%"}} compact={true} rightIcon={<IconPlayerTrackNext size={18}/>} onClick={onRedo.bind(null, childVersion,deltaToRedo)}>
|
|
|
+ REDO {deltaToRedo.getDescription()}
|
|
|
+ </Button>
|
|
|
+ <Space h="xs"/>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ });
|
|
|
|
|
|
return (
|
|
|
<Grid grow>
|
|
@@ -208,7 +384,7 @@ export function Branch(props: BranchProps) {
|
|
|
graph={graph}
|
|
|
graphDeltaExecutor={graphDeltaExecutor}
|
|
|
forces={historyGraphForces}
|
|
|
- getUuid={props.getUuid}
|
|
|
+ generateUUID={props.generateUUID}
|
|
|
setNextNodePosition={(x,y)=>{manipulator.x = x; manipulator.y = y;}}
|
|
|
newVersionHandler={newVersionHandler}
|
|
|
newDeltaHandler={newDeltaHandler}
|
|
@@ -222,6 +398,14 @@ export function Branch(props: BranchProps) {
|
|
|
<Grid.Col span={1}>
|
|
|
<Title order={4}>History Graph</Title>
|
|
|
<Graph graph={historyGraph} forces={historyGraphForces} mouseDownHandler={()=>{}} mouseUpHandler={historyMouseUpHandler} />
|
|
|
+ <SimpleGrid cols={2}>
|
|
|
+ <div>
|
|
|
+ {undoButtons}
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ {redoButtons}
|
|
|
+ </div>
|
|
|
+ </SimpleGrid>
|
|
|
<Text>All links are parent links.</Text>
|
|
|
<Text>Right or middle mouse button: Load version.</Text>
|
|
|
</Grid.Col>
|
|
@@ -234,12 +418,12 @@ export function Branch(props: BranchProps) {
|
|
|
}
|
|
|
|
|
|
export function App() {
|
|
|
- const getUuid = mockUuid();
|
|
|
+ const generateUUID = mockUuid();
|
|
|
|
|
|
return (
|
|
|
<Stack>
|
|
|
<Title order={2}>Onion VCS Demo</Title>
|
|
|
- <Branch getUuid={getUuid} />
|
|
|
+ <Branch generateUUID={generateUUID} />
|
|
|
<RountangleEditor />
|
|
|
</Stack>
|
|
|
);
|