commit 78a6bb5904aea2bb8c05d99f9c6253bae66aacb6
Author: Pavel Fidranský <fidransky@outlook.com>
Date:   Tue Aug 28 12:48:37 2018 +0200

    UI for saving diagrams to database

diff --git a/sources/src/main/webapp/js/app.js b/sources/src/main/webapp/js/app.js
index 983904c..85ae958 100644
--- a/sources/src/main/webapp/js/app.js
+++ b/sources/src/main/webapp/js/app.js
@@ -29,6 +29,8 @@ function App() {
 	/** @prop {object} API Application programming interface paths. */
 	this.API = {
 		loadGraphData: 'api/load-graph-data',
+		saveDiagram: 'api/save-diagram',
+		removeDiagram: 'api/remove-diagram',
 	};
 
 	/** @prop {float} headerHeight Current height of the application header. */
@@ -58,6 +60,9 @@ function App() {
 	/** TODO: jsDoc */
 	this.attributeTypeList = [];
 
+	/** TODO: jsDoc */
+	this.possibleEnumValues = {};
+
 	/**
 	 * Loads graph using diagram (if available).
 	 * @param diagramId Diagram identifier.
@@ -273,7 +278,42 @@ function App() {
 				scale: 1,
 			});
 		});
-		
+
+		// save to database button
+		var saveDiagramToDatabaseButton = document.getElementById('btnSaveDiagramToDatabase');
+		if (saveDiagramToDatabaseButton) {
+			saveDiagramToDatabaseButton.addEventListener('click', function(e) {
+				var diagramId = app.utils.getQueryVariable('diagramId');
+				if (diagramId === false) diagramId = null;
+
+				var diagramName = prompt('Enter new diagram name:');
+				if (diagramName === null || diagramName === '') return false;
+
+				$.ajax({
+					'type': 'POST',
+					'url': app.API.saveDiagram,
+					'data': {
+						'diagram_id': diagramId,
+						'name': diagramName,
+						'graph_json': JSON.stringify(self.graphExporter.run()),
+						'public': 0,
+					},
+					'success': function() {
+						alert('Saved.');
+					},
+					'error': function(xhr) {
+						switch (xhr.status) {
+							case 401:
+								alert('You are either not logged in or not an owner of this diagram.');
+								break;
+							default:
+								alert('Something went wrong.');
+						}
+					},
+				});
+			});
+		}
+
 		// window resize
 		window.addEventListener('resize', function(e) {
 			self.headerHeight = getHeaderHeight();
diff --git a/sources/src/main/webapp/js/components/edge.js b/sources/src/main/webapp/js/components/edge.js
index bec89a7..249dc0f 100644
--- a/sources/src/main/webapp/js/components/edge.js
+++ b/sources/src/main/webapp/js/components/edge.js
@@ -238,6 +238,20 @@ function Edge(props) {
 		rootElement.remove();
 	};
 
+	/**
+	 * Exports the edge to a new, plain JS object.
+	 * @returns {Object} exported edge
+	 */
+	this.export = function() {
+		return {
+			subedgeInfo: props.subedgeInfo,
+			from: this.getFrom().id,
+			to: this.getTo().id,
+			text: '',
+			id: this.id,
+		};
+	};
+
 	/**
 	 * Edge click interaction. Highlights the edge and vertices related to it. Reveals edge popover.
 	 * @param {Event} e Click event.
diff --git a/sources/src/main/webapp/js/components/group.js b/sources/src/main/webapp/js/components/group.js
index a481047..5c31520 100644
--- a/sources/src/main/webapp/js/components/group.js
+++ b/sources/src/main/webapp/js/components/group.js
@@ -262,6 +262,13 @@ function Group(props) {
 		}
 	};
 
+	/**
+	 * @returns true if the group is currently highlighted (in any way), otherwise false
+	 */
+	this.isHighlighted = function() {
+		return highlighted;
+	};
+
 	/**
 	 * Toggles highlighting of the group.
 	 * @param {boolean} newValue True to highlight the group, false to unhighlight.
@@ -444,6 +451,25 @@ function Group(props) {
 		});
 	};
 
+	/**
+	 * Exports the group to a new, plain JS object.
+	 * @returns {Object} exported group
+	 */
+	this.export = function() {
+		var verticesId = vertexList.map(function(vertex) {
+			return vertex.id;
+		});
+
+		return {
+			groupId: this.id,
+			id: this.id,
+			name: this.name,
+			verticesId: verticesId,
+			verticesEdgeFromId: [],	// TODO: what to put in here?
+			verticesEdgeToId: [],	// TODO: what to put in here?
+		};
+	};
+
 	/**
 	 * @returns {Element} SVG DOM element.
 	 */
diff --git a/sources/src/main/webapp/js/components/vertex.js b/sources/src/main/webapp/js/components/vertex.js
index 2f6d7d3..b5f1613 100644
--- a/sources/src/main/webapp/js/components/vertex.js
+++ b/sources/src/main/webapp/js/components/vertex.js
@@ -9,7 +9,7 @@ function Vertex(props) {
 	/** @prop {integer} archetype Identifier of the vertex archetype. */
 	this.archetype = props.archetype;
 	/** @prop {string} name Name of the vertex. */
-	this.name = props.title.split(':')[1];
+	this.name = props.title.split(':')[1];	// TODO: archetype prefix is useless
 	/** @prop {array} symbol Symbol of the group. */
 	this.symbol = app.markSymbol.getMarkSymbol();
 
@@ -256,6 +256,13 @@ function Vertex(props) {
 		}
 	};
 
+	/**
+	 * @returns true if the vertex is currently highlighted (in any way), otherwise false
+	 */
+	this.isHighlighted = function() {
+		return highlighted;
+	};
+
 	/**
 	 * Toggles highlighting of the vertex.
 	 * @param {boolean} newValue True to highlight the vertex, false to unhighlight.
@@ -455,6 +462,21 @@ function Vertex(props) {
 		});
 	};
 
+	/**
+	 * Exports the vertex to a new, plain JS object.
+	 * @returns {Object} exported vertex
+	 */
+	this.export = function() {
+		return {
+			archetype: this.archetype,
+			attributes: props.attributes,
+			id: this.id,
+			originalId: this.id,
+			text: '',
+			title: 'XXX:' + this.name,	// TODO: archetype prefix is useless
+		};
+	};
+
 	/**
 	 * @returns {Element} SVG DOM element.
 	 */
diff --git a/sources/src/main/webapp/js/graphExporter.js b/sources/src/main/webapp/js/graphExporter.js
index 4c8eb59..b9ecf1d 100644
--- a/sources/src/main/webapp/js/graphExporter.js
+++ b/sources/src/main/webapp/js/graphExporter.js
@@ -4,30 +4,66 @@
 function GraphExporter() {
 
 	/**
-	 * Exports positions of edges and vertices in graph to JSON.
+	 * Exports graph to JSON.
 	 */
 	this.run = function() {
-		var edgesExport = app.edgeList.map(function(edge) {
+		// vertices
+		var vertices = app.vertexList.map(function(vertex) {
+			return vertex.export();
+		});
+
+		// edges
+		var edges = app.edgeList.map(function(edge) {
+			return edge.export();
+		});
+
+		// groups
+		var groups = app.groupList.map(function(group) {
+			return group.export();
+		});
+
+		// positions of nodes
+		var positions = app.nodeList.map(function(node) {
+			if (node instanceof Vertex) {
+				var id = 'vertex' + node.id;
+			} else if (node instanceof Group) {
+				var id = 'group' + node.id;
+			} else {
+				var id = node.id;
+			}
+
+			var position = node.getPosition();
+
 			return {
-				id: edge.id,
-				from: edge.getFrom().id,
-				to: edge.getTo().id,
+				id: id,
+				x: position.x,
+				y: position.y,
 			};
 		});
 
-		var verticesExport = app.vertexList.map(function(vertex) {
+		// nodes excluded to the sidebar
+		var excludedNodeList = app.sidebarComponent.excludedNodeListComponent.getNodeList();
+		var sideBar = excludedNodeList.map(function(node) {
 			return {
-				id: vertex.id,
-				name: vertex.name,
-				position: vertex.getPosition(),
-				isExcluded: vertex.isExcluded(),
+				id: node.id,
+				isHighlighted: node.isHighlighted(),
 			};
 		});
 
 		return {
-			edges: edgesExport,
-			vertices: verticesExport,
+			attributeTypes: app.attributeTypeList,
+			edgeArchetypes: app.archetype.edge,
+			vertexArchetypes: app.archetype.vertex,
+			vertices: vertices,
+			edges: edges,
+			possibleEnumValues: app.possibleEnumValues,
+			archetypeIcons: app.archetype.icon,
+			groups: groups,
+			positions: positions,
+			sideBar: sideBar,
+			selectedVertex: null,	// TODO: selected == highlighted
+			selectedEdge: null,	// TODO selected == highlighted
 		};
-	};
+	}
 
 }
diff --git a/sources/src/main/webapp/js/graphLoader.js b/sources/src/main/webapp/js/graphLoader.js
index c00a2bd..c436c07 100644
--- a/sources/src/main/webapp/js/graphLoader.js
+++ b/sources/src/main/webapp/js/graphLoader.js
@@ -15,22 +15,34 @@ function GraphLoader() {
 
 		var canvasSize = ((data.vertices.length * 75) / Math.round(Math.sqrt(data.vertices.length))) + 1000;
 
-		app.attributeTypeList = data.attributeTypes;
-
 		// store archetypes
 		app.archetype.vertex = data.vertexArchetypes;
 		app.archetype.edge = data.edgeArchetypes;
 		app.archetype.icon = data.archetypeIcons;
 
+		app.attributeTypeList = data.attributeTypes;
+		app.possibleEnumValues = data.possibleEnumValues;
+
 		// construct vertices
 		var vertexMap = {};
 		data.vertices.forEach(function(component) {
 			var vertex = new Vertex(component);
 
-			vertex.setPosition(new Coordinates(
-				Math.floor(Math.random() * canvasSize),
-				Math.floor(Math.random() * canvasSize),
-			));
+			// position
+			var position = data.positions.find(function(position) {
+				return position.id === ('vertex' + vertex.id);
+			});
+
+			if (app.utils.isDefined(position)) {
+				vertex.setPosition(new Coordinates(position.x, position.y));
+
+			} else {
+				// set random
+				vertex.setPosition(new Coordinates(
+					Math.floor(Math.random() * canvasSize),
+					Math.floor(Math.random() * canvasSize),
+				));
+			}
 
 			app.nodeList.push(vertex);
 			app.vertexList.push(vertex);
@@ -71,9 +83,6 @@ function GraphLoader() {
 			app.viewportComponent.addEdge(edge);
 		});
 
-		// center viewport
-		app.viewportComponent.center();
-
 		// find unconnected vertices
 		app.vertexList.filter(function(vertex) {
 			return vertex.isUnconnected();
@@ -85,20 +94,52 @@ function GraphLoader() {
 		// construct groups
 		data.groups.forEach(function(component) {
 			var group = new Group(component);
-			group.setExcluded(true);
 
+			// vertices
 			app.vertexList.filter(function(vertex) {
 				return component.verticesId.indexOf(vertex.id) > -1;
 			}).forEach(function(vertex) {
 				group.addVertex(vertex);
 			});
 
+			// position
+			var position = data.positions.find(function(position) {
+				return position.id === ('group' + group.id);
+			});
+
+			if (app.utils.isDefined(position)) {
+				group.setPosition(new Coordinates(position.x, position.y));
+
+			} else {
+				// set random
+				group.setPosition(new Coordinates(
+					Math.floor(Math.random() * canvasSize),
+					Math.floor(Math.random() * canvasSize),
+				));
+			}
+
 			app.nodeList.push(group);
 			app.groupList.push(group);
 
-			app.sidebarComponent.excludedNodeListComponent.add(group);
+			app.viewportComponent.addGroup(group);
+		});
+
+		// exclude nodes
+		data.sideBar.forEach(function(excludedNode) {
+			var node = app.nodeList.find(function(node) {
+				return node.id === excludedNode.id;
+			});
+
+			if (app.utils.isDefined(node)) {
+				node.exclude();
+
+				app.sidebarComponent.excludedNodeListComponent.add(node);
+			}
 		});
 
+		// center viewport
+		app.viewportComponent.center();
+
 		// update status bar
 		app.sidebarComponent.statusBarComponent.setComponentCount(data.vertices.length);
 	};
diff --git a/sources/src/main/webapp/js/utils/utils.js b/sources/src/main/webapp/js/utils/utils.js
index f07420e..0404ded 100644
--- a/sources/src/main/webapp/js/utils/utils.js
+++ b/sources/src/main/webapp/js/utils/utils.js
@@ -73,4 +73,26 @@ function Utils() {
 		})
 	};
 
+	/**
+	 * Extracts value of a query parameter from the current URL.
+	 * {@link https://css-tricks.com/snippets/javascript/get-url-variables/}
+	 * 
+	 * @param {string} variable 
+	 * @returns value of the query parameter or false if the parameter does not exist
+	 */
+	this.getQueryVariable = function(variable) {
+		var query = window.location.search.substring(1);
+		var vars = query.split('&');
+
+		for (var i = 0; i < vars.length; i++) {
+			var pair = vars[i].split('=');
+
+			if (pair[0] == variable) {
+				return pair[1];
+			}
+		}
+
+		return false;
+	}
+
 }
diff --git a/sources/src/main/webapp/showGraph.jsp b/sources/src/main/webapp/showGraph.jsp
index f3e98b2..38aab43 100644
--- a/sources/src/main/webapp/showGraph.jsp
+++ b/sources/src/main/webapp/showGraph.jsp
@@ -145,7 +145,10 @@
 						</li>
 						<c:if test="${isLoggedIn}">
 							<li>
-								<button class="btn save-diagram" id="btnSaveDiagramToDb" title="Save diagram.">
+								<hr class="navbar-separator">
+							</li>
+							<li>
+								<button class="btn save-diagram" id="btnSaveDiagramToDatabase" title="Save diagram.">
 									<img src="images/icon_save.png" id="applyLayoutImg" alt="Save diagram">
 								</button>
 							</li>
