From c98725f35530175d25a34b2c64959d25339ad4a2 Mon Sep 17 00:00:00 2001
From: Matthias Konitzny <konitzny@ibr.cs.tu-bs.de>
Date: Fri, 16 Sep 2022 12:27:33 +0200
Subject: [PATCH] Implemented system to merge checkpoints.

---
 src/editor/components/nodedetails.tsx | 27 +++++++++--
 src/editor/components/sidepanel.tsx   |  8 +++-
 src/editor/editor.tsx                 | 68 +++++++++++++++++++++------
 3 files changed, 84 insertions(+), 19 deletions(-)

diff --git a/src/editor/components/nodedetails.tsx b/src/editor/components/nodedetails.tsx
index 76a830c..70083d9 100644
--- a/src/editor/components/nodedetails.tsx
+++ b/src/editor/components/nodedetails.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useState } from "react";
 import { Node } from "../../common/graph/node";
 import { NodeType } from "../../common/graph/nodetype";
 import "./nodedetails.css";
@@ -7,14 +7,21 @@ import { NodeDataChangeRequest } from "../editor";
 type NodeDetailsProps = {
     selectedNodes: Node[];
     idToObjectType: Map<number, NodeType>;
-    onNodeDataChange: { (requests: NodeDataChangeRequest[]): void };
+    onNodeDataChange: (
+        requests: NodeDataChangeRequest[],
+        createCheckpoint?: boolean
+    ) => void;
+    createCheckpoint: (description: string) => void;
 };
 
 function NodeDetails({
     selectedNodes,
     idToObjectType,
     onNodeDataChange,
+    createCheckpoint,
 }: NodeDetailsProps) {
+    const [changed, setChanged] = useState(false);
+
     if (selectedNodes.length == 0) {
         return <div id="nodedetails">No node selected.</div>;
     }
@@ -45,6 +52,10 @@ function NodeDetails({
         key: keyof NodeDataChangeRequest,
         value: ValueType
     ) {
+        if (!changed) {
+            setChanged(true);
+        }
+
         Object.assign(referenceData, { [key]: value });
 
         onNodeDataChange(
@@ -67,12 +78,20 @@ function NodeDetails({
                 return Object.assign({}, defaults, update, {
                     id: node.id,
                 });
-            })
+            }),
+            false
         );
     };
 
+    const handleBlur = () => {
+        if (changed) {
+            createCheckpoint(`Modified ${selectedNodes.length} node(s) data.`);
+            setChanged(false);
+        }
+    };
+
     return (
-        <div id="nodedetails">
+        <div id="nodedetails" onBlur={handleBlur}>
             {selectedNodes.length === 1 ? (
                 <div>
                     <label htmlFor="node-name" hidden>
diff --git a/src/editor/components/sidepanel.tsx b/src/editor/components/sidepanel.tsx
index a0e1d63..4022b64 100644
--- a/src/editor/components/sidepanel.tsx
+++ b/src/editor/components/sidepanel.tsx
@@ -18,7 +18,11 @@ interface SidepanelProps {
     onUndo: () => void;
     onNodeTypeSelect: (type: NodeType) => void;
     onSettingsChange: (settings: EditorSettings) => void;
-    onNodeDataChange: { (requests: NodeDataChangeRequest[]): void };
+    onNodeDataChange: (
+        requests: NodeDataChangeRequest[],
+        createCheckpoint?: boolean
+    ) => void;
+    createCheckpoint: (description: string) => void;
     onSave: () => void;
     selectedNodes: Node[];
     settings: EditorSettings;
@@ -33,6 +37,7 @@ function Sidepanel({
     onSettingsChange,
     onNodeDataChange,
     onSave,
+    createCheckpoint,
     selectedNodes,
     settings,
 }: SidepanelProps) {
@@ -52,6 +57,7 @@ function Sidepanel({
                 selectedNodes={selectedNodes}
                 idToObjectType={graph.idToObjectGroup}
                 onNodeDataChange={onNodeDataChange}
+                createCheckpoint={createCheckpoint}
             />
             <hr />
             <NodeTypesEditor
diff --git a/src/editor/editor.tsx b/src/editor/editor.tsx
index 976693a..556bfe9 100644
--- a/src/editor/editor.tsx
+++ b/src/editor/editor.tsx
@@ -90,6 +90,7 @@ export class Editor extends React.PureComponent<any, stateTypes> {
         this.handleCheckpointRequest = this.handleCheckpointRequest.bind(this);
         this.handleUndo = this.handleUndo.bind(this);
         this.handleRedo = this.handleRedo.bind(this);
+        this.createCheckpoint = this.createCheckpoint.bind(this);
 
         document.addEventListener("keydown", (e) => {
             this.keyPressed(e.key);
@@ -232,7 +233,10 @@ export class Editor extends React.PureComponent<any, stateTypes> {
         this.selectNodes(nodesWithType);
     }
 
-    private handleNodeDataChange(nodeData: NodeDataChangeRequest[]) {
+    private handleNodeDataChange(
+        nodeData: NodeDataChangeRequest[],
+        createCheckpoint = true
+    ) {
         if (nodeData.length == 0) {
             return;
         }
@@ -246,16 +250,25 @@ export class Editor extends React.PureComponent<any, stateTypes> {
             Object.assign(node, request);
         }
 
-        graph.createCheckpoint(`Modified ${nodeData.length} node(s) data.`);
+        // Create checkpoint
+        if (createCheckpoint) {
+            graph.createCheckpoint(`Modified ${nodeData.length} node(s) data.`);
+        }
 
         // Push shallow copy to state
         this.setState({ graph: graph });
     }
 
-    private handleNodeCreation(position?: Coordinate2D): Node {
+    private handleNodeCreation(
+        position?: Coordinate2D,
+        createCheckpoint = true
+    ): Node {
         const graph = Object.assign(new DynamicGraph(), this.state.graph);
         const node = graph.createNode(undefined, position.x, position.y, 0, 0);
-        graph.createCheckpoint("Created new node.");
+
+        if (createCheckpoint) {
+            graph.createCheckpoint("Created new node.");
+        }
 
         this.setState({
             graph: graph,
@@ -263,7 +276,7 @@ export class Editor extends React.PureComponent<any, stateTypes> {
         return node;
     }
 
-    private handleNodeDeletion(ids: number[]) {
+    private handleNodeDeletion(ids: number[], createCheckpoint = true) {
         if (ids.length == 0) {
             return;
         }
@@ -273,29 +286,42 @@ export class Editor extends React.PureComponent<any, stateTypes> {
         const selectedNodes = this.state.selectedNodes.filter(
             (node) => !ids.includes(node.id)
         );
-        graph.createCheckpoint(`Deleted ${ids.length} nodes.`);
+
+        if (createCheckpoint) {
+            graph.createCheckpoint(`Deleted ${ids.length} nodes.`);
+        }
 
         this.setState({ graph: graph, selectedNodes: selectedNodes });
     }
 
-    private handleLinkCreation(source: number, target: number): Link {
+    private handleLinkCreation(
+        source: number,
+        target: number,
+        createCheckpoint = true
+    ): Link {
         const graph = Object.assign(new DynamicGraph(), this.state.graph);
         const link = graph.createLink(source, target);
-        graph.createCheckpoint(
-            `Created link between ${graph.node(source).name} and ${
-                graph.node(target).name
-            }.`
-        );
+
+        if (createCheckpoint) {
+            graph.createCheckpoint(
+                `Created link between ${graph.node(source).name} and ${
+                    graph.node(target).name
+                }.`
+            );
+        }
 
         this.setState({ graph: graph });
 
         return link;
     }
 
-    private handleLinkDeletion(ids: number[]) {
+    private handleLinkDeletion(ids: number[], createCheckpoint = true) {
         const graph = Object.assign(new DynamicGraph(), this.state.graph);
         ids.forEach((id) => graph.deleteLink(id));
-        graph.createCheckpoint(`Deleted ${ids.length} link(s).`);
+
+        if (createCheckpoint) {
+            graph.createCheckpoint(`Deleted ${ids.length} link(s).`);
+        }
 
         this.setState({ graph: graph });
     }
@@ -303,8 +329,11 @@ export class Editor extends React.PureComponent<any, stateTypes> {
     private loadGraphFromCheckpoint(checkpoint: Checkpoint<SimGraphData>) {
         const graph = new DynamicGraph();
         graph.fromSerializedObject(checkpoint.data);
+
+        // Transfer checkpoints to new graph object
         graph.history.copyCheckpointsFromHistory(this.state.graph.history);
 
+        // Restore selected nodes
         const selectedNodes = this.state.selectedNodes
             .map((node) => graph.node(node.id))
             .filter((node) => node != undefined);
@@ -336,6 +365,16 @@ export class Editor extends React.PureComponent<any, stateTypes> {
         this.setState({ graph: graph });
     }
 
+    /**
+     * Creates a new Checkpoint in the graph history
+     * @param description Checkpoint description.
+     */
+    private createCheckpoint(description: string) {
+        const graph = Object.assign(new DynamicGraph(), this.state.graph);
+        graph.createCheckpoint(description);
+        this.setState({ graph: graph });
+    }
+
     render(): React.ReactNode {
         return (
             <div id="ks-editor">
@@ -393,6 +432,7 @@ export class Editor extends React.PureComponent<any, stateTypes> {
                             settings={this.state.settings}
                             onNodeDataChange={this.handleNodeDataChange}
                             onSave={this.saveSpace}
+                            createCheckpoint={this.createCheckpoint}
                         />
                     </div>
                 )}
-- 
GitLab