From 6dba4a2aa1951ad3594c98ed24e2f15c35e567d9 Mon Sep 17 00:00:00 2001
From: Piotr Gawron <piotr.gawron@uni.lu>
Date: Wed, 4 Jan 2017 13:47:13 +0100
Subject: [PATCH] comments functionality works via API

---
 frontend-js/src/main/js/ServerConnector.js    |  99 +++++-
 frontend-js/src/main/js/gui/CommentDialog.js  | 289 ++++++++++++++++++
 .../src/main/js/map/AbstractCustomMap.js      |   7 +-
 frontend-js/src/main/js/map/CustomMap.js      |  33 ++
 frontend-js/src/main/js/map/data/Alias.js     |   4 +
 frontend-js/src/main/js/map/data/MapModel.js  |  94 +++++-
 frontend-js/src/main/js/map/data/Reaction.js  |  65 ++++
 frontend-js/src/test/js/GuiConnector-mock.js  |   5 +
 .../src/test/js/ServerConnector-mock.js       |  31 +-
 .../src/test/js/ServerConnector-test.js       |   8 +-
 .../src/test/js/gui/CommentDialog-test.js     |  31 ++
 frontend-js/src/test/js/map/CustomMap-test.js |  32 ++
 frontend-js/src/test/js/mocha-config.js       |   5 +
 ...alse&projectId=sample&token=MOCK_TOKEN_ID& |   1 +
 ...=102&projectId=sample&token=MOCK_TOKEN_ID& |   1 +
 ...9168&projectId=sample&token=MOCK_TOKEN_ID& |   1 +
 ...9173&projectId=sample&token=MOCK_TOKEN_ID& |   1 +
 ...9173&projectId=sample&token=MOCK_TOKEN_ID& |   1 +
 ...3511&projectId=sample&token=MOCK_TOKEN_ID& |   1 +
 .../java/lcsb/mapviewer/bean/ExportBean.java  |   3 +-
 .../components/map/feedbackDialog.xhtml       |  54 +---
 21 files changed, 691 insertions(+), 75 deletions(-)
 create mode 100644 frontend-js/src/main/js/gui/CommentDialog.js
 create mode 100644 frontend-js/src/test/js/gui/CommentDialog-test.js
 create mode 100644 frontend-js/testFiles/apiCalls/comment/addComment/content=&coordinates=2,12&elementId=&elementType=POINT&email=&modelId=102&name=&pinned=false&projectId=sample&token=MOCK_TOKEN_ID&
 create mode 100644 frontend-js/testFiles/apiCalls/project/getClosestElementsByCoordinates/coordinates=2,12&modelId=102&projectId=sample&token=MOCK_TOKEN_ID&
 create mode 100644 frontend-js/testFiles/apiCalls/project/getElements/columns=&id=329168&projectId=sample&token=MOCK_TOKEN_ID&
 create mode 100644 frontend-js/testFiles/apiCalls/project/getElements/columns=&id=329173&projectId=sample&token=MOCK_TOKEN_ID&
 create mode 100644 frontend-js/testFiles/apiCalls/project/getElements/columns=id,bounds,modelId&id=329168,329173&projectId=sample&token=MOCK_TOKEN_ID&
 create mode 100644 frontend-js/testFiles/apiCalls/project/getReactions/columns=&id=153511&projectId=sample&token=MOCK_TOKEN_ID&

diff --git a/frontend-js/src/main/js/ServerConnector.js b/frontend-js/src/main/js/ServerConnector.js
index 30be4d5fa9..1ef52399d4 100644
--- a/frontend-js/src/main/js/ServerConnector.js
+++ b/frontend-js/src/main/js/ServerConnector.js
@@ -690,6 +690,21 @@ ServerConnector.readFile = function(url) {
   });
 };
 
+ServerConnector.sendPostRequest = function(url, params) {
+  return new Promise(function(resolve, reject) {
+    request.post({url:url, form:params}, function(error, response, body) {
+      if (error) {
+        reject(error);
+
+      } else if (response.statusCode !== 200) {
+        reject(response);
+      } else {
+        resolve(body);
+      }
+    });
+  });
+};
+
 ServerConnector.getToken = function() {
   var self = this;
   return new Promise(function(resolve) {
@@ -738,8 +753,11 @@ ServerConnector.getApiUrl = function(paramObj) {
   var type = paramObj.type;
   var method = paramObj.method;
   var params = this.createGetParams(paramObj.params);
-
-  var result = this.getApiBaseUrl() + "/"+type+"/"+method+"?"+params;
+  
+  var result = this.getApiBaseUrl() + "/"+type+"/"+method;
+  if (params!=="") {
+    result+="?"+params;
+  }
   return result;
 };
 
@@ -754,6 +772,12 @@ ServerConnector.getProjectUrl = function(projectId, token) {
   });
 };
 
+ServerConnector.addCommentUrl = function() {
+  return this.getApiUrl({type:"comment",
+    method: "addComment",
+  });
+};
+
 ServerConnector.getOverlaysUrl = function(projectId, token) {
   return this.getApiUrl({type:"overlay",
     method: "getOverlayList",
@@ -807,9 +831,12 @@ ServerConnector.getOverlayElementsUrl = function(overlayId, projectId, token) {
 ServerConnector.idsToString = function (ids) {
   var result = "";
   if (ids!==undefined) {
+    ids.sort(function(a, b){return a-b});
     for (var i = 0; i < ids.length; i++) {
       if (result !== "") {
-        result = result + "," + ids[i];
+        if (ids[i-1]!=ids[i]) {
+          result = result + "," + ids[i];
+        } // we ignore duplicates
       } else {
         result = ids[i];
       }
@@ -818,6 +845,10 @@ ServerConnector.idsToString = function (ids) {
   return result;
 };
 
+ServerConnector.pointToString = function (point) {
+  return point.x+","+point.y;
+};
+
 ServerConnector.columnsToString = function (columns) {
   if (columns === undefined) {
     return "";
@@ -865,6 +896,21 @@ ServerConnector.getConfigurationUrl = function(token) {
   });
   return result;
 };
+ServerConnector.getClosestElementsByCoordinatesUrl = function(params) {
+  var coordinates = this.pointToString(params.coordinates);
+  var projectId = params.projectId;
+  var modelId = params.modelId;
+  var token = params.token;
+
+  return this.getApiUrl({type:"project",
+    method:"getClosestElementsByCoordinates",
+    params: {
+      projectId: projectId, 
+      coordinates: coordinates, 
+      modelId: modelId, 
+      token: token},
+  });
+};
 
 ServerConnector.getConfigurationParam = function(paramId) {
   var self = this;
@@ -1078,4 +1124,51 @@ ServerConnector.getSessionData = function() {
   }
   return this._sessionData;
 };
+
+ServerConnector.getClosestElementsByCoordinates = function(params) {
+  var self = this;
+  var projectId;
+  return new Promise(function(resolve, reject) {
+    return self.getProjectId(params.projectId).then(function(result) {
+      projectId = result;
+      return self.getToken();
+    }).then(function(token) {
+      return self.readFile(self.getClosestElementsByCoordinatesUrl({projectId:projectId, token:token, modelId:params.modelId, coordinates: params.coordinates}));
+    }).then(function(content) {
+      var array=JSON.parse(content);
+      var result = [];
+      for (var i = 0; i < array.length; i++) {
+        result.push(new IdentifiedElement(array[i]));
+      }
+      resolve(result);
+    }).catch(function(exception){
+      reject(exception);
+    });
+  });
+};
+
+ServerConnector.addComment = function(params) {
+  var self = this;
+  var projectId;
+  return new Promise(function(resolve, reject) {
+    return self.getProjectId(params.projectId).then(function(result) {
+      params.projectId = result;
+      return self.getToken();
+    }).then(function(token) {
+      params.token = token;
+      params.coordinates =self.pointToString(params.coordinates); 
+      return self.sendPostRequest(self.addCommentUrl(),params);
+    }).then(function(content) {
+      var response=JSON.parse(content);
+      if (response.status==="OK") {
+        resolve();
+      } else {
+        reject(response);
+      }
+    }).catch(function(exception){
+      reject(exception);
+    });
+  });
+};
+
 module.exports = ServerConnector;
diff --git a/frontend-js/src/main/js/gui/CommentDialog.js b/frontend-js/src/main/js/gui/CommentDialog.js
new file mode 100644
index 0000000000..cdb2f59e89
--- /dev/null
+++ b/frontend-js/src/main/js/gui/CommentDialog.js
@@ -0,0 +1,289 @@
+"use strict";
+
+var Promise = require("bluebird");
+
+var Alias = require('../map/data/Alias');
+var Reaction = require('../map/data/Reaction');
+var logger = require('../logger');
+var Functions = require('../Functions');
+
+function CommentDialog(element, customMap) {
+  var self = this;
+
+  this.setElement(element);
+  this.setMap(customMap);
+  var table = document.createElement('table');
+
+  var typeLabel = document.createElement('label');
+  typeLabel.innerHTML = "Type";
+  var typeOptions = document.createElement("select");
+  this.setTypeOptions(typeOptions);
+
+  table.appendChild(createRow([ typeLabel, typeOptions ]));
+
+  var detailDiv = document.createElement('div');
+
+  table.appendChild(createRow([ document.createElement('div'), detailDiv ]));
+
+  var pinnedLabel = document.createElement('label');
+  pinnedLabel.innerHTML = "Pinned";
+  var pinnedCheckbox = document.createElement('input');
+  pinnedCheckbox.type = "checkbox";
+
+  table.appendChild(createRow([ pinnedLabel, pinnedCheckbox ]));
+  this.setPinnedCheckbox(pinnedCheckbox);
+
+  var nameLabel = document.createElement('label');
+  nameLabel.innerHTML = "Name:<br/>(Visible to moderators only)";
+  var nameInput = document.createElement('input');
+  nameInput.type = "text";
+
+  table.appendChild(createRow([ nameLabel, nameInput ]));
+  this.setNameInput(nameInput);
+  nameInput.onchange = function(el) {
+    logger.debug(nameInput.value);
+  }
+
+  var emailLabel = document.createElement('label');
+  emailLabel.innerHTML = "Email:<br/>(Visible to moderators only)";
+  var emailInput = document.createElement('input');
+  emailInput.type = "text";
+
+  table.appendChild(createRow([ emailLabel, emailInput ]));
+  this.setEmailInput(emailInput);
+
+  var contentLabel = document.createElement('label');
+  contentLabel.innerHTML = "Content:";
+  var contentInput = document.createElement('textarea');
+  contentInput.cols = "80";
+  contentInput.rows = "3";
+
+  table.appendChild(createRow([ contentLabel, contentInput ]));
+  this.setContentInput(contentInput);
+
+  var sendButton = document.createElement('button');
+  sendButton.innerHTML = "Send";
+  sendButton.onclick = function() {
+    self.addComment().then(function() {
+      if (self.close !== undefined) {
+        self.close();
+      } else {
+        logger.warn("Cannot close dialog");
+      }
+    });
+  }
+
+  table.appendChild(createRow([ sendButton ]));
+
+  element.appendChild(table);
+
+  typeOptions.onchange = function() {
+    var option = self.getSelectedType();
+    var text = "";
+    if (option instanceof Alias) {
+      if (option.getFullName() !== undefined) {
+        text = option.getFullName();
+      }
+    } else if (option instanceof Reaction) {
+      text = "Reactants: ";
+      var reactants = option.getReactants();
+      for (var i = 0; i < reactants.length; i++) {
+        text += reactants[i].getName() + ",";
+      }
+      text += "<br/>";
+      text += "Modifiers: ";
+      var modifiers = option.getModifiers();
+      for (var i = 0; i < modifiers.length; i++) {
+        text += modifiers[i].getName() + ",";
+      }
+      text += "<br/>";
+      text += "Products: ";
+      var products = option.getProducts();
+      for (var i = 0; i < products.length; i++) {
+        text += products[i].getName() + ",";
+      }
+      text += "<br/>";
+    }
+    detailDiv.innerHTML = text;
+  };
+
+}
+
+CommentDialog.GENERAL = "<General>";
+
+function createRow(elements) {
+  var row = document.createElement('tr');
+  for (var i = 0; i < elements.length; i++) {
+    var container = document.createElement('td');
+    container.appendChild(elements[i]);
+    row.appendChild(container);
+  }
+  return row;
+};
+
+CommentDialog.prototype.setMap = function(map) {
+  this._map = map;
+}
+
+CommentDialog.prototype.getMap = function() {
+  return this._map;
+}
+
+CommentDialog.prototype.setElement = function(element) {
+  this._element = element;
+}
+
+CommentDialog.prototype.getElement = function() {
+  return this._element;
+}
+
+CommentDialog.prototype.open = function(types) {
+  var self = this;
+  self.setTypes([ CommentDialog.GENERAL ]);
+
+  var promises = [ CommentDialog.GENERAL ];
+  for (var i = 0; i < types.length; i++) {
+    var ie = types[i];
+    if (ie.getType() === "ALIAS") {
+      promises.push(self.getMap().getSubmodelById(ie.getModelId()).getModel().getAliasById(ie.getId(), true));
+    } else if (ie.getType() === "REACTION") {
+      promises.push(self.getMap().getSubmodelById(ie.getModelId()).getModel().getReactionById(ie.getId(), true));
+    } else {
+      throw new Error("Unknown element type: " + ie.getType());
+    }
+  }
+  return Promise.all(promises).then(function(elements) {
+    self.setTypes(elements);
+  });
+};
+CommentDialog.prototype.setTypes = function(types) {
+  var typeOptions = this.getTypeOptions();
+  while (typeOptions.firstChild) {
+    typeOptions.removeChild(typeOptions.firstChild);
+  }
+
+  for (var i = 0; i < types.length; i++) {
+    var option = document.createElement("option");
+    option.value = i;
+    var element = types[i];
+    var text = element;
+    if (element instanceof Alias) {
+      text = element.getType() + ": " + element.getName();
+    } else if (element instanceof Reaction) {
+      text = "Reaction: " + element.getReactionId();
+    }
+    option.text = text;
+    typeOptions.appendChild(option);
+  }
+  typeOptions.value = 0;
+
+  this._types = types;
+}
+
+CommentDialog.prototype.getTypes = function() {
+  return this._types;
+};
+
+CommentDialog.prototype.getSelectedType = function() {
+  return this._types[this.getTypeOptions().value];
+};
+
+CommentDialog.prototype.setSelectedType = function(value) {
+  if (Functions.isInt(value)) {
+    this.getTypeOptions().value = value;
+    this.getTypeOptions().onchange();
+  } else {
+    throw new Error("Unknown value type: " + value)
+  }
+};
+
+CommentDialog.prototype.getTypeOptions = function() {
+  return this._typeOptions;
+};
+CommentDialog.prototype.setTypeOptions = function(typeOptions) {
+  this._typeOptions = typeOptions;
+};
+CommentDialog.prototype.setContentInput = function(contentInput) {
+  this._contentInput = contentInput;
+};
+CommentDialog.prototype.getContentInput = function() {
+  return this._contentInput;
+};
+CommentDialog.prototype.setNameInput = function(nameInput) {
+  this._nameInput = nameInput;
+};
+CommentDialog.prototype.getNameInput = function() {
+  return this._nameInput;
+};
+CommentDialog.prototype.setEmailInput = function(emailInput) {
+  this._emailInput = emailInput;
+};
+CommentDialog.prototype.getEmailInput = function() {
+  return this._emailInput;
+};
+CommentDialog.prototype.setPinnedCheckbox = function(pinnedCheckbox) {
+  this._pinnedCheckbox = pinnedCheckbox;
+};
+
+CommentDialog.prototype.getPinnedCheckbox = function() {
+  return this._pinnedCheckbox;
+};
+
+CommentDialog.prototype.getTypes = function(types) {
+  return this._types;
+};
+
+CommentDialog.prototype.getName = function() {
+  return this.getNameInput().value;
+};
+
+CommentDialog.prototype.getEmail = function() {
+  return this.getEmailInput().value;
+};
+
+CommentDialog.prototype.getContent = function() {
+  return this.getContentInput().value;
+};
+
+CommentDialog.prototype.isPinned = function() {
+  return this.getPinnedCheckbox().checked;
+};
+CommentDialog.prototype.getSelectedTypeId = function() {
+  var selected = this.getSelectedType();
+  if (selected instanceof Alias) {
+    return selected.getId();
+  } else if (selected instanceof Reaction) {
+    return selected.getId();
+  } else {
+    return "";
+  }
+};
+
+CommentDialog.prototype.getSelectedTypeClass = function() {
+  var selected = this.getSelectedType();
+  if (selected instanceof Alias) {
+    return "ALIAS";
+  } else if (selected instanceof Reaction) {
+    return "REACTION";
+  } else {
+    return "POINT";
+  }
+};
+
+CommentDialog.prototype.addComment = function() {
+  var self = this;
+  var name = self.getName();
+  return ServerConnector.addComment({
+    modelId : self.getMap().getActiveSubmapId(),
+    coordinates : self.getMap().getActiveSubmapClickCoordinates(),
+    name : name,
+    email : self.getEmail(),
+    content : self.getContent(),
+    pinned : self.isPinned(),
+    elementId : self.getSelectedTypeId(),
+    elementType : self.getSelectedTypeClass()
+
+  });
+};
+
+module.exports = CommentDialog;
diff --git a/frontend-js/src/main/js/map/AbstractCustomMap.js b/frontend-js/src/main/js/map/AbstractCustomMap.js
index a2016e0ffe..34ed259182 100644
--- a/frontend-js/src/main/js/map/AbstractCustomMap.js
+++ b/frontend-js/src/main/js/map/AbstractCustomMap.js
@@ -317,13 +317,15 @@ AbstractCustomMap.prototype.registerMapClickEvents = function() {
   });
 
   // select last clicked map
-  google.maps.event.addListener(this.getGoogleMap(), 'click', function() {
+  google.maps.event.addListener(this.getGoogleMap(), 'click', function(mouseEvent) {
     customMap.setActiveSubmapId(self.getId());
+    customMap.setActiveSubmapClickCoordinates(self.fromLatLngToPoint(mouseEvent.latLng));
   });
 
   // select last clicked map
-  google.maps.event.addListener(this.getGoogleMap(), 'rightclick', function() {
+  google.maps.event.addListener(this.getGoogleMap(), 'rightclick', function(mouseEvent) {
     customMap.setActiveSubmapId(self.getId());
+    customMap.setActiveSubmapClickCoordinates(self.fromLatLngToPoint(mouseEvent.latLng));
   });
 
   // prepare for image export
@@ -356,7 +358,6 @@ AbstractCustomMap.prototype.registerMapClickEvents = function() {
 
   // context menu event
   google.maps.event.addListener(this.getGoogleMap(), 'rightclick', function(mouseEvent) {
-    ServerConnector.requestUpdateCommentList(self.getId(), mouseEvent.latLng);
     GuiConnector.showRightClickMenu(GuiConnector.xPos, GuiConnector.yPos);
   });
 };
diff --git a/frontend-js/src/main/js/map/CustomMap.js b/frontend-js/src/main/js/map/CustomMap.js
index d0ab5c7f9e..48d7ef1662 100644
--- a/frontend-js/src/main/js/map/CustomMap.js
+++ b/frontend-js/src/main/js/map/CustomMap.js
@@ -7,6 +7,7 @@ var functions = require('../Functions');
 
 var AbstractCustomMap = require('./AbstractCustomMap');
 var AliasMarker = require('./marker/AliasMarker');
+var CommentDialog = require('../gui/CommentDialog');
 var ControlType = require('./ControlType');
 var CustomMapOptions = require('./CustomMapOptions');
 var IdentifiedElement = require('./data/IdentifiedElement');
@@ -83,7 +84,11 @@ function CustomMap(options) {
   // list of reference genomes
   this._referenceGenome = [];
   
+  var commentDialog = new CommentDialog(document.getElementById("feedbackContent"), this); 
 
+  this.setCommentDialog(commentDialog);
+  
+  
   ServerConnector.actualizeSessionData();
 }
 
@@ -1660,6 +1665,16 @@ CustomMap.prototype.setActiveSubmapId = function(submapId) {
   this._activeSubmapId = submapId;
 };
 
+CustomMap.prototype.setActiveSubmapClickCoordinates = function(coordinates) {
+  if (!(coordinates instanceof google.maps.Point)) {
+    throw new Error("Coordinates must be provided as google.maps.Point object, but found: "+coordinates );
+  }
+  this._activeSubmapCoordinates = coordinates;
+};
+CustomMap.prototype.getActiveSubmapClickCoordinates = function() {
+  return this._activeSubmapCoordinates;
+};
+
 CustomMap.prototype.updateAliasesForLayout = function(layoutId, jsonAliases) {
   logger.debug("Updating aliases for layout: " + layoutId);
 
@@ -1739,4 +1754,22 @@ CustomMap.prototype.getControl = function(type) {
   return this._controls[type];
 };
 
+CustomMap.prototype.setCommentDialog = function(commentDialog) {
+  this._commentDialog = commentDialog;
+  commentDialog.close = function(){
+    jsfCommentDialog.hide();
+  }
+};
+
+CustomMap.prototype.getCommentDialog = function() {
+  return this._commentDialog;
+};
+
+CustomMap.prototype.openCommentDialog = function() {
+  var self = this;
+  return ServerConnector.getClosestElementsByCoordinates({modelId:this.getActiveSubmapId(), coordinates:this.getActiveSubmapClickCoordinates()}).then(function(elements){
+    return self.getCommentDialog().open(elements);
+  });
+};
+
 module.exports = CustomMap;
diff --git a/frontend-js/src/main/js/map/data/Alias.js b/frontend-js/src/main/js/map/data/Alias.js
index 9ddea3e571..7b426ecc9b 100644
--- a/frontend-js/src/main/js/map/data/Alias.js
+++ b/frontend-js/src/main/js/map/data/Alias.js
@@ -110,6 +110,10 @@ Alias.prototype.getName = function() {
   return this.name;
 };
 
+Alias.prototype.getFullName = function() {
+  return this.fullName;
+};
+
 Alias.prototype.setType = function(type) {
   this.type = type;
 };
diff --git a/frontend-js/src/main/js/map/data/MapModel.js b/frontend-js/src/main/js/map/data/MapModel.js
index 411450ae13..c2fcb21d92 100644
--- a/frontend-js/src/main/js/map/data/MapModel.js
+++ b/frontend-js/src/main/js/map/data/MapModel.js
@@ -136,17 +136,18 @@ MapModel.prototype.getAliasById = function(id, complete) {
 MapModel.prototype.getCompleteAliasById = function(id) {
   var self = this;
   return new Promise(function(resolve, reject) {
-    if (self._aliases[id].isComplete()) {
+    if (self._aliases[id]!==undefined && self._aliases[id].isComplete()) {
       resolve(self._aliases[id]);
+    } else {
+      ServerConnector.getAliases([id]).then(function(aliases){
+        if (self._aliases[id] === undefined) {
+          self._aliases[id] = aliases[0];
+        } else {
+          self._aliases[id] .update(aliases[0]);
+        }
+        resolve(self._aliases[id]);
+      }, reject);
     }
-    ServerConnector.getAliases([id]).then(function(aliases){
-      if (self._aliases[id] === undefined) {
-        self._aliases[id] = aliases[0];
-      } else {
-        self._aliases[id] .update(aliases[0]);
-      }
-      resolve(self._aliases[id]);
-    }, reject);
   });
 };
 
@@ -157,8 +158,11 @@ MapModel.prototype.getCompleteAliasById = function(id) {
  *          identifier of the {@link Reaction}
  * @returns {@link Reaction} by identifier
  */
-MapModel.prototype.getReactionById = function(id) {
+MapModel.prototype.getReactionById = function(id, complete) {
   var self = this;
+  if (complete) {
+    return this.getCompleteReactionById(id);
+  }
   return new Promise(function(resolve, reject) {
     if (self._reactions[id] !== undefined) {
       resolve(self._reactions[id]);
@@ -170,6 +174,69 @@ MapModel.prototype.getReactionById = function(id) {
   });
 };
 
+MapModel.prototype.getCompleteReactionById = function(id) {
+  var self = this;
+  return new Promise(function(resolve, reject) {
+    if (self._reactions[id]!==undefined && self._reactions[id].isComplete()) {
+      resolve(self._reactions[id]);
+    } else {
+      var result;
+      ServerConnector.getReactions([id]).then(function(reactions){
+        if (self._reactions[id] === undefined) {
+          self._reactions[id] = reactions[0];
+        } else {
+          self._reactions[id] .update(reactions[0]);
+        }
+        var ids =[];
+        var i;
+        result =self._reactions[id]; 
+        for (i=0;i<result.getReactants().length;i++) {
+          if (!(result.getReactants()[i] instanceof Alias)) {
+            if (self._aliases[result.getReactants()[i]]===undefined || !self._aliases[result.getReactants()[i]].isComplete()) {
+              ids.push(result.getReactants()[i]);
+            }
+          }
+        }
+        for (i=0;i<result.getProducts().length;i++) {
+          if (!(result.getProducts()[i] instanceof Alias)) {
+            if (self._aliases[result.getProducts()[i]]===undefined || !self._aliases[result.getProducts()[i]].isComplete()) {
+              ids.push(result.getProducts()[i]);
+            }
+          }
+        }
+        for (i=0;i<result.getModifiers().length;i++) {
+          if (!(result.getModifiers()[i] instanceof Alias)) {
+            if (self._aliases[result.getModifiers()[i]]===undefined || !self._aliases[result.getModifiers()[i]].isComplete()) {
+              ids.push(result.getModifiers()[i]);
+            }
+          }
+        }
+        return self.getMissingElements({aliasIds:ids, complete : true});
+      }).then(function(){
+        var i;
+        result =self._reactions[id]; 
+        for (i=0;i<result.getReactants().length;i++) {
+          if (!(result.getReactants()[i] instanceof Alias)) {
+            result.getReactants()[i] = self._aliases[result.getReactants()[i]];
+          }
+        }
+        for (i=0;i<result.getProducts().length;i++) {
+          if (!(result.getProducts()[i] instanceof Alias)) {
+            result.getProducts()[i] = self._aliases[result.getProducts()[i]];
+          }
+        }
+        for (i=0;i<result.getModifiers().length;i++) {
+          if (!(result.getModifiers()[i] instanceof Alias)) {
+            result.getModifiers()[i] = self._aliases[result.getModifiers()[i]];
+          }
+        }
+        resolve(result);
+      });
+    }
+  });
+};
+
+
 MapModel.prototype.getMissingElements = function(elements) {
   var self = this;
 
@@ -221,7 +288,12 @@ MapModel.prototype.getMissingElements = function(elements) {
 
   var aliasPromise =  null;
   if (aliasIds.length>0){ 
-    aliasPromise =  ServerConnector.getLightAliases(aliasIds);
+    if (elements.complete){  
+      aliasPromise =  ServerConnector.getAliases(aliasIds);
+    } else {
+      aliasPromise =  ServerConnector.getLightAliases(aliasIds);
+      
+    }
   }
 
   
diff --git a/frontend-js/src/main/js/map/data/Reaction.js b/frontend-js/src/main/js/map/data/Reaction.js
index ec7c176696..8b6aae5848 100644
--- a/frontend-js/src/main/js/map/data/Reaction.js
+++ b/frontend-js/src/main/js/map/data/Reaction.js
@@ -39,6 +39,8 @@ function Reaction(javaObject) {
     }
     this.setCenter(javaObject.centerPoint);
     this.setModelId(javaObject.modelId);
+    this.setCompletness(false);
+    this.update(javaObject);
   }
 }
 
@@ -87,4 +89,67 @@ Reaction.prototype.setModelId = function(modelId) {
   this._modelId = modelId;
 };
 
+Reaction.prototype.update = function(javaObject) {
+  if (javaObject.reactionId === undefined) {
+    return;
+  }
+  this.setReactionId(javaObject.reactionId);
+
+  if (javaObject.reactants !== "") {
+    this.setReactants(javaObject.reactants.split(","));
+  } else {
+    this.setReactants([]);
+  }
+  if (javaObject.products !== "") {
+    this.setProducts(javaObject.products.split(","));
+  } else {
+    this.setProducts([]);
+  }
+  if (javaObject.modifiers !== "") {
+    this.setModifiers(javaObject.modifiers.split(","));
+  } else {
+    this.setModifiers([]);
+  }
+};
+
+Reaction.prototype.getCompletness = function() {
+  return this._completness;
+};
+
+Reaction.prototype.setCompletness = function(completness) {
+  this._completness = completness;
+};
+
+Reaction.prototype.getReactionId = function() {
+  return this._reactionId;
+};
+
+Reaction.prototype.setReactionId = function(reactionId) {
+  this._reactionId = reactionId;
+};
+
+Reaction.prototype.getReactants = function() {
+  return this._reactants;
+};
+
+Reaction.prototype.setReactants = function(reactants) {
+  this._reactants = reactants;
+};
+
+Reaction.prototype.setProducts = function(products) {
+  this._products = products;
+};
+
+Reaction.prototype.getProducts = function() {
+  return this._products;
+};
+
+Reaction.prototype.setModifiers = function(modifiers) {
+  this._modifiers = modifiers;
+};
+
+Reaction.prototype.getModifiers = function() {
+  return this._modifiers;
+};
+
 module.exports = Reaction;
diff --git a/frontend-js/src/test/js/GuiConnector-mock.js b/frontend-js/src/test/js/GuiConnector-mock.js
index d098d2aabd..a0eec6f133 100644
--- a/frontend-js/src/test/js/GuiConnector-mock.js
+++ b/frontend-js/src/test/js/GuiConnector-mock.js
@@ -141,4 +141,9 @@ GuiConnectorMock.getCustomMap = function() {
   return this._customMap;
 };
 
+GuiConnectorMock.alert = function(message) {
+  logger.error(message);
+  throw new Error(message);
+};
+
 module.exports = GuiConnectorMock;
diff --git a/frontend-js/src/test/js/ServerConnector-mock.js b/frontend-js/src/test/js/ServerConnector-mock.js
index b9151e2300..d1e35939b0 100644
--- a/frontend-js/src/test/js/ServerConnector-mock.js
+++ b/frontend-js/src/test/js/ServerConnector-mock.js
@@ -160,10 +160,6 @@ ServerConnectorMock.sendOverlayDetailDataRequest = function(overlayName, identif
   ServerConnectorMock.callListeners("onSendOverlayDetailDataRequest", overlayName, identifiedElement);
 };
 
-ServerConnectorMock.requestUpdateCommentList = function(modelId, latLngCoordinates) {
-  ServerConnectorMock.callListeners("onRequestUpdateCommentList", [ modelId, latLngCoordinates ]);
-};
-
 ServerConnectorMock.searchByCoord = function(modelId, latLngCoordinates) {
   ServerConnectorMock.callListeners("onSearchByCoord", [ modelId, latLngCoordinates ]);
 };
@@ -216,6 +212,33 @@ ServerConnectorMock.readFile = function(url) {
   });
 };
 
+ServerConnectorMock.sendPostRequest = function(url, params) {
+  var self = this;
+  return new Promise(function(resolve, reject) {
+    if (url.indexOf("http") === 0) {
+      request.post({url:url, form:params}, function(error, response, body) {
+        if (error) {
+          reject(error);
+
+        } else if (response.statusCode !== 200) {
+          reject(response);
+        } else {
+          resolve(body);
+        }
+      });
+    } else {
+      var mockUrl = url+"/"+self.createGetParams(params); 
+      fs.readFile(mockUrl, 'utf8', function(err, content) {
+        if (err) {
+          reject(err);
+        } else {
+          resolve(content);
+        }
+      });
+    }
+  });
+};
+
 ServerConnectorMock.getApiBaseUrl = function() {
   return "./testFiles/apiCalls/";
 };
diff --git a/frontend-js/src/test/js/ServerConnector-test.js b/frontend-js/src/test/js/ServerConnector-test.js
index bac17fa99f..c2c9f5a3e7 100644
--- a/frontend-js/src/test/js/ServerConnector-test.js
+++ b/frontend-js/src/test/js/ServerConnector-test.js
@@ -50,7 +50,7 @@ describe('ServerConnector', function() {
       assert.equal(reaction.getModelId(), 15781);
     });
   });
-  
+
   it('getOverlayElements', function() {
     return ServerConnector.getOverlayElements(101).then(function(result) {
       assert.equal(result.length, 1);
@@ -62,4 +62,10 @@ describe('ServerConnector', function() {
     });
   });
 
+  it('idsToString', function() {
+    var ids = [ 3, 2, 9, 1, 6, 8, 3, 2, 9, 1, 7, 3 ];
+    var str = ServerConnector.idsToString(ids);
+    assert.equal(str, "1,2,3,6,7,8,9")
+  });
+
 });
diff --git a/frontend-js/src/test/js/gui/CommentDialog-test.js b/frontend-js/src/test/js/gui/CommentDialog-test.js
new file mode 100644
index 0000000000..d94f3234ae
--- /dev/null
+++ b/frontend-js/src/test/js/gui/CommentDialog-test.js
@@ -0,0 +1,31 @@
+"use strict";
+
+var Helper = require('../helper');
+
+require("../mocha-config.js");
+
+var CommentDialog = require('../../../main/js/gui/CommentDialog');
+
+var chai = require('chai');
+var assert = chai.assert;
+var logger = require('../logger');
+
+describe('CommentDialog', function() {
+
+  var helper;
+  before(function() {
+    helper = new Helper();
+  });
+
+  it('contructor', function() {
+    var map = helper.createCustomMap();
+    
+    var dialog = new CommentDialog(testDiv, map);
+    logger.debug(testDiv.innerHTML);
+  });
+  
+  it('getName', function() {
+    var dialog = new CommentDialog(testDiv);
+    assert.ok(dialog.getName()!==undefined);
+  });
+});
diff --git a/frontend-js/src/test/js/map/CustomMap-test.js b/frontend-js/src/test/js/map/CustomMap-test.js
index 4e6040d712..d0fe6f60b1 100644
--- a/frontend-js/src/test/js/map/CustomMap-test.js
+++ b/frontend-js/src/test/js/map/CustomMap-test.js
@@ -626,4 +626,36 @@ describe('CustomMap', function() {
     });
   });
 
+  it("openCommentDialog", function() {
+    var map = helper.createCustomMap();
+    map.getModel().setId(102);
+    map.setActiveSubmapId(102);
+    map.setActiveSubmapClickCoordinates(new google.maps.Point(2,12));
+    return map.openCommentDialog().then(function(){
+      var types = map.getCommentDialog().getTypes();
+      assert.equal(types.length, 3);
+      var selected = map.getCommentDialog().getSelectedType();
+      assert.ok(selected === "<General>");
+      
+      map.getCommentDialog().setSelectedType(1); 
+      selected = map.getCommentDialog().getSelectedType();
+      assert.notOk(selected === "<General>");
+      
+      map.getCommentDialog().setSelectedType(2); 
+      selected = map.getCommentDialog().getSelectedType();
+      assert.notOk(selected === "<General>");
+    });
+  });
+
+  it("addComment", function() {
+    var map = helper.createCustomMap();
+    map.getModel().setId(102);
+    map.setActiveSubmapId(102);
+    map.setActiveSubmapClickCoordinates(new google.maps.Point(2,12));
+    return map.openCommentDialog().then(function(){
+      return map.getCommentDialog().addComment();
+    });
+  });
+
+  
 });
diff --git a/frontend-js/src/test/js/mocha-config.js b/frontend-js/src/test/js/mocha-config.js
index f8a84ea24b..0ef06b7c38 100644
--- a/frontend-js/src/test/js/mocha-config.js
+++ b/frontend-js/src/test/js/mocha-config.js
@@ -33,11 +33,16 @@ beforeEach(function() {
   global.testDiv.id = "test";
   document.body.appendChild(testDiv);
   
+  global.dialogDiv = document.createElement("div");
+  global.dialogDiv.id = "feedbackContent";
+  document.body.appendChild(global.dialogDiv);
+  
   ServerConnector.init();
   ServerConnector.setToken("MOCK_TOKEN_ID");
 });
 
 after(function() {
   document.body.removeChild(global.testDiv);
+  document.body.removeChild(global.dialogDiv);
   delete global.testDiv;
 });
diff --git a/frontend-js/testFiles/apiCalls/comment/addComment/content=&coordinates=2,12&elementId=&elementType=POINT&email=&modelId=102&name=&pinned=false&projectId=sample&token=MOCK_TOKEN_ID& b/frontend-js/testFiles/apiCalls/comment/addComment/content=&coordinates=2,12&elementId=&elementType=POINT&email=&modelId=102&name=&pinned=false&projectId=sample&token=MOCK_TOKEN_ID&
new file mode 100644
index 0000000000..48aa9beb26
--- /dev/null
+++ b/frontend-js/testFiles/apiCalls/comment/addComment/content=&coordinates=2,12&elementId=&elementType=POINT&email=&modelId=102&name=&pinned=false&projectId=sample&token=MOCK_TOKEN_ID&
@@ -0,0 +1 @@
+{"status":"OK"}
\ No newline at end of file
diff --git a/frontend-js/testFiles/apiCalls/project/getClosestElementsByCoordinates/coordinates=2,12&modelId=102&projectId=sample&token=MOCK_TOKEN_ID& b/frontend-js/testFiles/apiCalls/project/getClosestElementsByCoordinates/coordinates=2,12&modelId=102&projectId=sample&token=MOCK_TOKEN_ID&
new file mode 100644
index 0000000000..5b65788a70
--- /dev/null
+++ b/frontend-js/testFiles/apiCalls/project/getClosestElementsByCoordinates/coordinates=2,12&modelId=102&projectId=sample&token=MOCK_TOKEN_ID&
@@ -0,0 +1 @@
+[{"modelId":102,"id":329173,"type":"ALIAS"},{"modelId":102,"id":153511,"type":"REACTION"}]
\ No newline at end of file
diff --git a/frontend-js/testFiles/apiCalls/project/getElements/columns=&id=329168&projectId=sample&token=MOCK_TOKEN_ID& b/frontend-js/testFiles/apiCalls/project/getElements/columns=&id=329168&projectId=sample&token=MOCK_TOKEN_ID&
new file mode 100644
index 0000000000..f85b05bb4b
--- /dev/null
+++ b/frontend-js/testFiles/apiCalls/project/getElements/columns=&id=329168&projectId=sample&token=MOCK_TOKEN_ID&
@@ -0,0 +1 @@
+[{"formerSymbols":[],"references":[],"modelId":15781,"synonyms":[],"description":"","type":"RNA","name":"s5","bounds":{"x":0.0,"y":118.5,"width":90.0,"height":25.0},"id":329168}]
\ No newline at end of file
diff --git a/frontend-js/testFiles/apiCalls/project/getElements/columns=&id=329173&projectId=sample&token=MOCK_TOKEN_ID& b/frontend-js/testFiles/apiCalls/project/getElements/columns=&id=329173&projectId=sample&token=MOCK_TOKEN_ID&
new file mode 100644
index 0000000000..1dd70f6d37
--- /dev/null
+++ b/frontend-js/testFiles/apiCalls/project/getElements/columns=&id=329173&projectId=sample&token=MOCK_TOKEN_ID&
@@ -0,0 +1 @@
+[{"formerSymbols":[],"references":[],"modelId":102,"synonyms":[],"description":"description of S1\r\nthird line","type":"Protein","name":"s1","bounds":{"x":12.0,"y":6.0,"width":80.0,"height":40.0},"id":329173}]
\ No newline at end of file
diff --git a/frontend-js/testFiles/apiCalls/project/getElements/columns=id,bounds,modelId&id=329168,329173&projectId=sample&token=MOCK_TOKEN_ID& b/frontend-js/testFiles/apiCalls/project/getElements/columns=id,bounds,modelId&id=329168,329173&projectId=sample&token=MOCK_TOKEN_ID&
new file mode 100644
index 0000000000..40ddfd8017
--- /dev/null
+++ b/frontend-js/testFiles/apiCalls/project/getElements/columns=id,bounds,modelId&id=329168,329173&projectId=sample&token=MOCK_TOKEN_ID&
@@ -0,0 +1 @@
+[{"modelId":102,"bounds":{"x":12.0,"y":6.0,"width":80.0,"height":40.0},"id":329173},{"modelId":102,"bounds":{"x":0.0,"y":118.5,"width":90.0,"height":25.0},"id":329168}]
\ No newline at end of file
diff --git a/frontend-js/testFiles/apiCalls/project/getReactions/columns=&id=153511&projectId=sample&token=MOCK_TOKEN_ID& b/frontend-js/testFiles/apiCalls/project/getReactions/columns=&id=153511&projectId=sample&token=MOCK_TOKEN_ID&
new file mode 100644
index 0000000000..1aa69bc2e9
--- /dev/null
+++ b/frontend-js/testFiles/apiCalls/project/getReactions/columns=&id=153511&projectId=sample&token=MOCK_TOKEN_ID&
@@ -0,0 +1 @@
+[{"modelId":102,"reactants":"329168","reactionId":"re2","id":153511,"type":"State transition","lines":[{"start":{"x":45.833333333333336,"y":118.49999999999999},"end":{"x":47.983923957904906,"y":86.24114063142645},"type":"START"},{"start":{"x":48.516076042095094,"y":78.25885936857357},"end":{"x":50.666666666666664,"y":46.0},"type":"END"}],"modifiers":"","centerPoint":{"x":48.25,"y":82.25},"products":"329173"}]
\ No newline at end of file
diff --git a/web/src/main/java/lcsb/mapviewer/bean/ExportBean.java b/web/src/main/java/lcsb/mapviewer/bean/ExportBean.java
index a516eed1bd..5cae8b6263 100644
--- a/web/src/main/java/lcsb/mapviewer/bean/ExportBean.java
+++ b/web/src/main/java/lcsb/mapviewer/bean/ExportBean.java
@@ -986,8 +986,7 @@ public class ExportBean extends AbstractManagedBean {
 		MenuModel result = new DefaultMenuModel();
 
 		DefaultMenuItem addCommentItem = new DefaultMenuItem("Add comment");
-		addCommentItem.setOncomplete("commentDialog.show();");
-		addCommentItem.setUpdate(":feedbackForm:feedbackDialog");
+		addCommentItem.setOncomplete("jsfCommentDialog.show();customMap.openCommentDialog();");
 		addCommentItem.setStyleClass("addCommentContext");
 		addCommentItem.setCommand("#{exportMB.nop()}");
 		addCommentItem.setAjax(true);
diff --git a/web/src/main/webapp/WEB-INF/components/map/feedbackDialog.xhtml b/web/src/main/webapp/WEB-INF/components/map/feedbackDialog.xhtml
index 7985b50d88..ce04a24bc9 100644
--- a/web/src/main/webapp/WEB-INF/components/map/feedbackDialog.xhtml
+++ b/web/src/main/webapp/WEB-INF/components/map/feedbackDialog.xhtml
@@ -5,56 +5,8 @@
 		xmlns:c="http://java.sun.com/jsp/jstl/core"
 	xmlns:p="http://primefaces.org/ui">
 
-	<h:form id = "feedbackForm">
- 		<p:dialog id="feedbackDialog" header="Comment" widgetVar="commentDialog" modal="true" onShow="updateFeedbackForm();">	
-			<!-- This is a workaround, because update in menuitem it had some issues-->
-			<p:remoteCommand name="updateFeedbackForm" update="feedbackList,feedbackDescription1,feedbackDescription2,feedbackDescription3" />
-
-			<h:panelGrid columns="2" cellpadding="5">	
-				<h:outputLabel value="Type:" />	
-				<p:selectOneMenu id="feedbackList" value="#{feedbackMB.feedbackElement}" style="width:300px;padding-right: 8px;">
-					<p:ajax update="feedbackDescription1,feedbackDescription2,feedbackDescription3" event="change" />
-		 			<f:selectItems value="#{feedbackMB.feedbackElementStringList}"/>
-				</p:selectOneMenu>	
-				<h:outputLabel value="" />	
-				<h:outputLabel id="feedbackDescription1" value="#{feedbackMB.elementDescription1}" />	
-				<h:outputLabel value="" />	
-				<h:outputLabel id="feedbackDescription2" value="#{feedbackMB.elementDescription2}" />	
-				<h:outputLabel value="" />	
-				<h:outputLabel id="feedbackDescription3" value="#{feedbackMB.elementDescription3}" />
-				<h:outputText value="Pinned: " />	
-				<p:selectBooleanButton value="#{feedbackMB.pinned}" onLabel="Yes" offLabel="No" onIcon="ui-icon-check" offIcon="ui-icon-close"/>	
-				<h:panelGroup>
-					<h:outputLabel for="feedbackName" value="Name:" />
-					<br/>
-					<h:outputLabel for="feedbackName" value="(Visible to moderators only)" />	
-				</h:panelGroup>
-				
-				<p:inputText id="feedbackName"	value="#{feedbackMB.name}" label="username" style="font-size: 120%;width:100%"/>	
-				<h:panelGroup>
-					<h:outputLabel for="feedbackEmail" value="Email:" />	
-					<br/>
-					<h:outputLabel for="feedbackName" value="(Visible to moderators only)" />	
-				</h:panelGroup>
-				<p:inputText id="feedbackEmail" value="#{feedbackMB.email}" label="email" style="font-size: 120%;width:100%"/>	
-				<h:outputLabel for="feedbackContent" value="Content:" />	
-				<p:inputTextarea	id="feedbackContent" value="#{feedbackMB.content}" label="content" style="font-size: 120%;width:100%"/>	
-				<f:facet name="footer">	
-					<p:commandButton id="sendFedbackButton" value="Send" actionListener="#{feedbackMB.sendFeedback}"	onsuccess="commentDialog.hide()" />	
-				</f:facet>	
-			</h:panelGrid>	
+ 		<p:dialog id="feedbackDialog" header="Comment" widgetVar="jsfCommentDialog" modal="true" >	
+			<div id="feedbackContent"/>
 		</p:dialog>			
-	</h:form>
-
-	<h:form id="feedbackUpdateForm">
-		<p:remoteCommand name="_updateCommentList" actionListener="#{feedbackMB.updateCommentList}" update=":feedbackForm:feedbackDialog"/>
-	</h:form>
 
-<h:form id="_commentConnector">
-  <p:remoteCommand name="_requestCommentDetailDataFunction" actionListener="#{feedbackMB.requestDetailData}"/>
-	<p:remoteCommand name="_refreshCommentOverlayCollection" actionListener="#{feedbackMB.refreshOverlayCollection}"/>
-	<p:remoteCommand name="_registerCommentOverlayCollection" actionListener="#{feedbackMB.registerOverlayCollection}"/>
-	<p:remoteCommand name="_clearCommentOverlayCollection" actionListener="#{feedbackMB.clear}" />
-	<p:remoteCommand name="_setShowComments " actionListener="#{feedbackMB.setShowComments}" />
-</h:form>
-</html>
\ No newline at end of file
+</html>
-- 
GitLab