Merge "Add more commit message conditions"
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
index 249ec7e..c51e33a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
@@ -40,6 +40,7 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -49,6 +50,7 @@
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.update.BatchUpdateReviewDb;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
@@ -410,6 +412,12 @@
     if (PrimaryStorage.of(update.getChange()).equals(PrimaryStorage.REVIEW_DB)) {
       PatchLineComment.Key key =
           new PatchLineComment.Key(new Patch.Key(psId, commentKey.filename), commentKey.uuid);
+
+      if (db instanceof BatchUpdateReviewDb) {
+        db = ((BatchUpdateReviewDb) db).unsafeGetDelegate();
+      }
+      db = ReviewDbUtil.unwrapDb(db);
+
       PatchLineComment patchLineComment = db.patchComments().get(key);
 
       if (!patchLineComment.getStatus().equals(PUBLISHED)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
index d65f512..c1eabad 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
@@ -14,7 +14,7 @@
 (function() {
   'use strict';
 
-  var NOTIFICATION_TYPES = [
+  const NOTIFICATION_TYPES = [
     {name: 'Changes', key: 'notify_new_changes'},
     {name: 'Patches', key: 'notify_new_patch_sets'},
     {name: 'Comments', key: 'notify_all_comments'},
@@ -35,24 +35,24 @@
       _projects: Array,
       _projectsToRemove: {
         type: Array,
-        value: function() { return []; },
+        value() { return []; },
       },
       _query: {
         type: Function,
-        value: function() {
+        value() {
           return this._getProjectSuggestions.bind(this);
         },
       },
     },
 
-    loadData: function() {
-      return this.$.restAPI.getWatchedProjects().then(function(projs) {
+    loadData() {
+      return this.$.restAPI.getWatchedProjects().then(projs => {
         this._projects = projs;
-      }.bind(this));
+      });
     },
 
-    save: function() {
-      var deletePromise;
+    save() {
+      let deletePromise;
       if (this._projectsToRemove.length) {
         deletePromise = this.$.restAPI.deleteWatchedProjects(
             this._projectsToRemove);
@@ -61,56 +61,57 @@
       }
 
       return deletePromise
-          .then(function() {
+          .then(() => {
             return this.$.restAPI.saveWatchedProjects(this._projects);
-          }.bind(this))
-          .then(function(projects) {
+          })
+          .then(projects => {
             this._projects = projects;
             this._projectsToRemove = [];
             this.hasUnsavedChanges = false;
-          }.bind(this));
+          });
     },
 
-    _getTypes: function() {
+    _getTypes() {
       return NOTIFICATION_TYPES;
     },
 
-    _getTypeCount: function() {
+    _getTypeCount() {
       return this._getTypes().length;
     },
 
-    _computeCheckboxChecked: function(project, key) {
+    _computeCheckboxChecked(project, key) {
       return project.hasOwnProperty(key);
     },
 
-    _getProjectSuggestions: function(input) {
+    _getProjectSuggestions(input) {
       return this.$.restAPI.getSuggestedProjects(input)
-        .then(function(response) {
-          var projects = [];
-          for (var key in response) {
-            projects.push({
-              name: key,
-              value: response[key],
-            });
-          }
-          return projects;
-        });
+          .then(response => {
+            const projects = [];
+            for (const key in response) {
+              if (!response.hasOwnProperty(key)) { continue; }
+              projects.push({
+                name: key,
+                value: response[key],
+              });
+            }
+            return projects;
+          });
     },
 
-    _handleRemoveProject: function(e) {
-      var index = parseInt(e.target.getAttribute('data-index'), 10);
-      var project = this._projects[index];
+    _handleRemoveProject(e) {
+      const index = parseInt(e.target.getAttribute('data-index'), 10);
+      const project = this._projects[index];
       this.splice('_projects', index, 1);
       this.push('_projectsToRemove', project);
       this.hasUnsavedChanges = true;
     },
 
-    _canAddProject: function(project, filter) {
+    _canAddProject(project, filter) {
       if (!project || !project.id) { return false; }
 
       // Check if the project with filter is already in the list. Compare
       // filters using == to coalesce null and undefined.
-      for (var i = 0; i < this._projects.length; i++) {
+      for (let i = 0; i < this._projects.length; i++) {
         if (this._projects[i].project === project.id &&
             this._projects[i].filter == filter) {
           return false;
@@ -120,8 +121,9 @@
       return true;
     },
 
-    _getNewProjectIndex: function(name, filter) {
-      for (var i = 0; i < this._projects.length; i++) {
+    _getNewProjectIndex(name, filter) {
+      let i;
+      for (i = 0; i < this._projects.length; i++) {
         if (this._projects[i].project > name ||
             (this._projects[i].project === name &&
                 this._projects[i].filter > filter)) {
@@ -131,18 +133,18 @@
       return i;
     },
 
-    _handleAddProject: function() {
-      var newProject = this.$.newProject.value;
-      var newProjectName = this.$.newProject.text;
-      var filter = this.$.newFilter.value || null;
+    _handleAddProject() {
+      const newProject = this.$.newProject.value;
+      const newProjectName = this.$.newProject.text;
+      const filter = this.$.newFilter.value || null;
 
       if (!this._canAddProject(newProject, filter)) { return; }
 
-      var insertIndex = this._getNewProjectIndex(newProjectName, filter);
+      const insertIndex = this._getNewProjectIndex(newProjectName, filter);
 
       this.splice('_projects', insertIndex, 0, {
         project: newProjectName,
-        filter: filter,
+        filter,
         _is_local: true,
       });
 
@@ -151,16 +153,16 @@
       this.hasUnsavedChanges = true;
     },
 
-    _handleCheckboxChange: function(e) {
-      var index = parseInt(e.target.getAttribute('data-index'), 10);
-      var key = e.target.getAttribute('data-key');
-      var checked = e.target.checked;
+    _handleCheckboxChange(e) {
+      const index = parseInt(e.target.getAttribute('data-index'), 10);
+      const key = e.target.getAttribute('data-key');
+      const checked = e.target.checked;
       this.set(['_projects', index, key], !!checked);
       this.hasUnsavedChanges = true;
     },
 
-    _handleNotifCellTap: function(e) {
-      var checkbox = Polymer.dom(e.target).querySelector('input');
+    _handleNotifCellTap(e) {
+      const checkbox = Polymer.dom(e.target).querySelector('input');
       if (checkbox) { checkbox.click(); }
     },
   });
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
index 59e87b0..1e6ed7a 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
@@ -33,11 +33,12 @@
 </test-fixture>
 
 <script>
-  suite('gr-watched-projects-editor tests', function() {
-    var element;
+  suite('gr-watched-projects-editor tests', () => {
+    let element;
 
-    setup(function(done) {
-      var projects = [{
+    setup(done => {
+      const projects = [
+        {
           project: 'project a',
           notify_submitted_changes: true,
           notify_abandoned_changes: true,
@@ -57,8 +58,8 @@
       ];
 
       stub('gr-rest-api-interface', {
-        getSuggestedProjects: function(input) {
-          if (input.indexOf('the') === 0) {
+        getSuggestedProjects(input) {
+          if (input.startsWith('the')) {
             return Promise.resolve({'the project': {
               id: 'the project',
               state: 'ACTIVE',
@@ -68,27 +69,27 @@
             return Promise.resolve({});
           }
         },
-        getWatchedProjects: function() {
+        getWatchedProjects() {
           return Promise.resolve(projects);
         },
       });
 
       element = fixture('basic');
 
-      element.loadData().then(function() { flush(done); });
+      element.loadData().then(() => { flush(done); });
     });
 
-    test('renders', function() {
-      var rows = element.$$('table').querySelectorAll('tbody tr');
+    test('renders', () => {
+      const rows = element.$$('table').querySelectorAll('tbody tr');
       assert.equal(rows.length, 4);
 
       function getKeysOfRow(row) {
-        var boxes = rows[row].querySelectorAll('input[checked]');
+        const boxes = rows[row].querySelectorAll('input[checked]');
         return Array.prototype.map.call(boxes,
-            function(e) { return e.getAttribute('data-key'); });
+            e => { return e.getAttribute('data-key'); });
       }
 
-      var checkedKeys = getKeysOfRow(0);
+      let checkedKeys = getKeysOfRow(0);
       assert.equal(checkedKeys.length, 2);
       assert.equal(checkedKeys[0], 'notify_submitted_changes');
       assert.equal(checkedKeys[1], 'notify_abandoned_changes');
@@ -107,22 +108,22 @@
       assert.equal(checkedKeys[2], 'notify_all_comments');
     });
 
-    test('_getProjectSuggestions empty', function(done) {
-      element._getProjectSuggestions('nonexistent').then(function(projects) {
+    test('_getProjectSuggestions empty', done => {
+      element._getProjectSuggestions('nonexistent').then(projects => {
         assert.equal(projects.length, 0);
         done();
       });
     });
 
-    test('_getProjectSuggestions non-empty', function(done) {
-      element._getProjectSuggestions('the project').then(function(projects) {
+    test('_getProjectSuggestions non-empty', done => {
+      element._getProjectSuggestions('the project').then(projects => {
         assert.equal(projects.length, 1);
         assert.equal(projects[0].name, 'the project');
         done();
       });
     });
 
-    test('_canAddProject', function() {
+    test('_canAddProject', () => {
       assert.isFalse(element._canAddProject(null, null));
       assert.isFalse(element._canAddProject({}, null));
 
@@ -144,7 +145,7 @@
       assert.isTrue(element._canAddProject({id: 'project b'}, 'filter 3'));
     });
 
-    test('_getNewProjectIndex', function() {
+    test('_getNewProjectIndex', () => {
       // Projects are sorted in ASCII order.
       assert.equal(element._getNewProjectIndex('project A', 'filter'), 0);
       assert.equal(element._getNewProjectIndex('project a', 'filter'), 1);
@@ -158,7 +159,7 @@
       assert.equal(element._getNewProjectIndex('project c', 'filter'), 4);
     });
 
-    test('_handleAddProject', function() {
+    test('_handleAddProject', () => {
       element.$.newProject.value = {id: 'project d'};
       element.$.newProject.setText('project d');
       element.$.newFilter.bindValue = '';
@@ -171,7 +172,7 @@
       assert.isTrue(element._projects[4]._is_local);
     });
 
-    test('_handleAddProject with invalid inputs', function() {
+    test('_handleAddProject with invalid inputs', () => {
       element.$.newProject.value = {id: 'project b'};
       element.$.newProject.setText('project b');
       element.$.newFilter.bindValue = 'filter 1';
@@ -181,14 +182,14 @@
       assert.equal(element._projects.length, 4);
     });
 
-    test('_handleRemoveProject', function() {
+    test('_handleRemoveProject', () => {
       assert.equal(element._projectsToRemove, 0);
-      var button = element.$$('table tbody tr:nth-child(2) gr-button');
+      const button = element.$$('table tbody tr:nth-child(2) gr-button');
       MockInteractions.tap(button);
 
       flushAsynchronousOperations();
 
-      var rows = element.$$('table tbody').querySelectorAll('tr');
+      const rows = element.$$('table tbody').querySelectorAll('tr');
       assert.equal(rows.length, 3);
 
       assert.equal(element._projectsToRemove.length, 1);
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
index d19afb7..81379af 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -16,6 +16,7 @@
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
 <link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
 
 <dom-module id="gr-autocomplete">
@@ -31,25 +32,6 @@
         border: none;
         outline: none;
       }
-      #suggestions {
-        background-color: #fff;
-        box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
-        position: absolute;
-        z-index: 10;
-      }
-      ul {
-        list-style: none;
-      }
-      li {
-        cursor: pointer;
-        padding: .5em .75em;
-      }
-      li:focus {
-        outline: none;
-      }
-      li.selected {
-        background-color: #eee;
-      }
     </style>
     <input
         id="input"
@@ -61,29 +43,13 @@
         on-keydown="_handleKeydown"
         on-focus="_onInputFocus"
         autocomplete="off" />
-    <div
-        id="suggestions"
+    <gr-autocomplete-dropdown id="suggestions"
+        on-item-selected="_handleItemSelect"
+        suggestions="[[_suggestions]]"
         role="listbox"
+        index="[[index]]"
         hidden$="[[_computeSuggestionsHidden(_suggestions, _focused)]]">
-      <ul>
-        <template is="dom-repeat" items="[[_suggestions]]">
-          <li
-              data-index$="[[index]]"
-              tabindex="-1"
-              aria-label$="[[item.name]]"
-              on-keydown="_handleKeydown"
-              role="option"
-              on-tap="_handleSuggestionTap">[[item.name]]</li>
-        </template>
-      </ul>
-    </div>
-    <gr-cursor-manager
-        id="cursor"
-        index="{{_index}}"
-        cursor-target-class="selected"
-        scroll-behavior="keep-visible"
-        focus-on-move
-        stops="[[_suggestionEls]]"></gr-cursor-manager>
+    </gr-autocomplete-dropdown>
   </template>
   <script src="gr-autocomplete.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 9cf67b4..56a3c23 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -116,17 +116,15 @@
       },
 
       _index: Number,
-
       _disableSuggestions: {
         type: Boolean,
         value: false,
       },
-
       _focused: {
         type: Boolean,
         value: false,
       },
-
+      _selected: Object,
     },
 
     attached() {
@@ -153,6 +151,18 @@
       this.text = '';
     },
 
+    _handleItemSelect(e) {
+      let silent = false;
+      if (e.detail.trigger === 'tab' && this.tabCompleteWithoutCommit) {
+        silent = true;
+      }
+      this._selected = e.detail.selected;
+      this._commit(silent);
+      if (e.detail.trigger === 'tap') {
+        this.focus();
+      }
+    },
+
     /**
      * Set the text of the input without triggering the suggestion dropdown.
      * @param {String} text The new text for the input.
@@ -182,10 +192,11 @@
           // Late response.
           return;
         }
+        for (const suggestion of suggestions) {
+          suggestion.text = suggestion.name;
+        }
         this._suggestions = suggestions;
         Polymer.dom.flush();
-        this._suggestionEls = this.$.suggestions.querySelectorAll('li');
-        this.$.cursor.moveToStart();
         if (this._index === -1) {
           this.value = null;
         }
@@ -209,11 +220,11 @@
       switch (e.keyCode) {
         case 38: // Up
           e.preventDefault();
-          this.$.cursor.previous();
+          this.$.suggestions.cursorUp();
           break;
         case 40: // Down
           e.preventDefault();
-          this.$.cursor.next();
+          this.$.suggestions.cursorDown();
           break;
         case 27: // Escape
           e.preventDefault();
@@ -222,12 +233,12 @@
         case 9: // Tab
           if (this._suggestions.length > 0) {
             e.preventDefault();
-            this._commit(this.tabCompleteWithoutCommit);
+            this._handleEnter(this.tabCompleteWithoutCommit);
           }
           break;
         case 13: // Enter
           e.preventDefault();
-          this._commit();
+          this._handleEnter();
           break;
         default:
           // For any normal keypress, return focus to the input to allow for
@@ -245,9 +256,15 @@
       }
     },
 
-    _updateValue(suggestions, index) {
-      if (!suggestions.length || index === -1) { return; }
-      const completed = suggestions[index].value;
+    _handleEnter(opt_tabCompleteWithoutCommit) {
+      this._selected = this.$.suggestions.getCursorTarget();
+      this._commit(opt_tabCompleteWithoutCommit);
+      this.focus();
+    },
+
+    _updateValue(suggestion, suggestions) {
+      if (!suggestion) { return; }
+      const completed = suggestions[suggestion.dataset.index].value;
       if (this.multi) {
         // Append the completed text to the end of the string.
         // Allow spaces within quoted terms.
@@ -286,7 +303,7 @@
     _commit(silent) {
       // Allow values that are not in suggestion list iff suggestions are empty.
       if (this._suggestions.length > 0) {
-        this._updateValue(this._suggestions, this._index);
+        this._updateValue(this._selected, this._suggestions);
       } else {
         this.value = this.text || '';
       }
@@ -297,8 +314,8 @@
       if (this.multi) {
         this.setText(this.value);
       } else {
-        if (!this.clearOnCommit && this._suggestions[this._index]) {
-          this.setText(this._suggestions[this._index].name);
+        if (!this.clearOnCommit && this._selected) {
+          this.setText(this._suggestions[this._selected.dataset.index].name);
         } else {
           this.clear();
         }
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index 6a3ae81..62d4079 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -58,9 +58,8 @@
         ]);
       });
       element.query = queryStub;
-
-      assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
-      assert.equal(element.$.cursor.index, -1);
+      assert.isTrue(element.$.suggestions.hidden);
+      assert.equal(element.$.suggestions.$.cursor.index, -1);
 
       element.text = 'blah';
 
@@ -69,15 +68,15 @@
 
       promise.then(() => {
         assert.isFalse(element.$.suggestions.hasAttribute('hidden'));
-
-        const suggestions = element.$.suggestions.querySelectorAll('li');
+        const suggestions =
+            Polymer.dom(element.$.suggestions.root).querySelectorAll('li');
         assert.equal(suggestions.length, 5);
 
         for (let i = 0; i < 5; i++) {
           assert.equal(suggestions[i].textContent, 'blah ' + i);
         }
 
-        assert.notEqual(element.$.cursor.index, -1);
+        assert.notEqual(element.$.suggestions.$.cursor.index, -1);
         done();
       });
     });
@@ -91,20 +90,20 @@
       });
       element.query = queryStub;
 
-      assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
+      assert.isTrue(element.$.suggestions.hidden);
 
       element._focused = true;
       element.text = 'blah';
 
       promise.then(() => {
-        assert.isFalse(element.$.suggestions.hasAttribute('hidden'));
+        assert.isFalse(element.$.suggestions.hidden);
 
         const cancelHandler = sandbox.spy();
         element.addEventListener('cancel', cancelHandler);
 
         MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
         assert.isFalse(cancelHandler.called);
-        assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
+        assert.isTrue(element.$.suggestions.hidden);
         assert.equal(element._suggestions.length, 0);
 
         MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
@@ -126,32 +125,32 @@
       });
       element.query = queryStub;
 
-      assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
-      assert.equal(element.$.cursor.index, -1);
+      assert.isTrue(element.$.suggestions.hidden);
+      assert.equal(element.$.suggestions.$.cursor.index, -1);
       element._focused = true;
       element.text = 'blah';
 
       promise.then(() => {
-        assert.isFalse(element.$.suggestions.hasAttribute('hidden'));
+        assert.isFalse(element.$.suggestions.hidden);
 
         const commitHandler = sandbox.spy();
         element.addEventListener('commit', commitHandler);
 
-        assert.equal(element.$.cursor.index, 0);
+        assert.equal(element.$.suggestions.$.cursor.index, 0);
 
         MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null,
             'down');
 
-        assert.equal(element.$.cursor.index, 1);
+        assert.equal(element.$.suggestions.$.cursor.index, 1);
 
         MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null,
             'down');
 
-        assert.equal(element.$.cursor.index, 2);
+        assert.equal(element.$.suggestions.$.cursor.index, 2);
 
         MockInteractions.pressAndReleaseKeyOn(element.$.input, 38, null, 'up');
 
-        assert.equal(element.$.cursor.index, 1);
+        assert.equal(element.$.suggestions.$.cursor.index, 1);
 
         MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
             'enter');
@@ -159,7 +158,7 @@
         assert.equal(element.value, 1);
         assert.isTrue(commitHandler.called);
         assert.equal(commitHandler.getCall(0).args[0].detail.value, 1);
-        assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
+        assert.isTrue(element.$.suggestions.hidden);
 
         done();
       });
@@ -304,12 +303,12 @@
       element._focused = true;
       element._suggestions = [{name: 'first suggestion'}];
       Polymer.dom.flush();
-      assert.isFalse(element.$.suggestions.hasAttribute('hidden'));
-      MockInteractions.tap(element.$$('#suggestions li:first-child'));
+      assert.isFalse(element.$.suggestions.hidden);
+      MockInteractions.tap(element.$.suggestions.$$('li:first-child'));
       flushAsynchronousOperations();
       assert.isTrue(focusSpy.called);
       assert.isTrue(commitSpy.called);
-      assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
+      assert.isTrue(element.$.suggestions.hidden);
       assert.isTrue(element._focused);
     });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
index 77d1c05..2e7f17c 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -15,10 +15,10 @@
   'use strict';
 
   // Date cutoff is one day:
-  var DRAFT_MAX_AGE = 24 * 60 * 60 * 1000;
+  const DRAFT_MAX_AGE = 24 * 60 * 60 * 1000;
 
   // Clean up old entries no more frequently than one day.
-  var CLEANUP_THROTTLE_INTERVAL = 24 * 60 * 60 * 1000;
+  const CLEANUP_THROTTLE_INTERVAL = 24 * 60 * 60 * 1000;
 
   Polymer({
     is: 'gr-storage',
@@ -27,7 +27,7 @@
       _lastCleanup: Number,
       _storage: {
         type: Object,
-        value: function() {
+        value() {
           return window.localStorage;
         },
       },
@@ -37,42 +37,43 @@
       },
     },
 
-    getDraftComment: function(location) {
+    getDraftComment(location) {
       this._cleanupDrafts();
       return this._getObject(this._getDraftKey(location));
     },
 
-    setDraftComment: function(location, message) {
-      var key = this._getDraftKey(location);
-      this._setObject(key, {message: message, updated: Date.now()});
+    setDraftComment(location, message) {
+      const key = this._getDraftKey(location);
+      this._setObject(key, {message, updated: Date.now()});
     },
 
-    eraseDraftComment: function(location) {
-      var key = this._getDraftKey(location);
+    eraseDraftComment(location) {
+      const key = this._getDraftKey(location);
       this._storage.removeItem(key);
     },
 
-    getPreferences: function() {
+    getPreferences() {
       return this._getObject('localPrefs');
     },
 
-    savePreferences: function(localPrefs) {
+    savePreferences(localPrefs) {
       this._setObject('localPrefs', localPrefs || null);
     },
 
-    _getDraftKey: function(location) {
-      var range = location.range ? location.range.start_line + '-' +
-          location.range.start_character + '-' + location.range.end_character +
-          '-' + location.range.end_line : null;
-      var key = ['draft', location.changeNum, location.patchNum, location.path,
-          location.line || ''].join(':');
+    _getDraftKey(location) {
+      const range = location.range ?
+          `${location.range.start_line}-${location.range.start_character}` +
+              `-${location.range.end_character}-${location.range.end_line}` :
+          null;
+      let key = ['draft', location.changeNum, location.patchNum, location.path,
+        location.line || ''].join(':');
       if (range) {
         key = key + ':' + range;
       }
       return key;
     },
 
-    _cleanupDrafts: function() {
+    _cleanupDrafts() {
       // Throttle cleanup to the throttle interval.
       if (this._lastCleanup &&
           Date.now() - this._lastCleanup < CLEANUP_THROTTLE_INTERVAL) {
@@ -80,9 +81,9 @@
       }
       this._lastCleanup = Date.now();
 
-      var draft;
-      for (var key in this._storage) {
-        if (key.indexOf('draft:') === 0) {
+      let draft;
+      for (const key in this._storage) {
+        if (key.startsWith('draft:')) {
           draft = this._getObject(key);
           if (Date.now() - draft.updated > DRAFT_MAX_AGE) {
             this._storage.removeItem(key);
@@ -91,13 +92,13 @@
       }
     },
 
-    _getObject: function(key) {
-      var serial = this._storage.getItem(key);
+    _getObject(key) {
+      const serial = this._storage.getItem(key);
       if (!serial) { return null; }
       return JSON.parse(serial);
     },
 
-    _setObject: function(key, obj) {
+    _setObject(key, obj) {
       if (this._exceededQuota) { return; }
       try {
         this._storage.setItem(key, JSON.stringify(obj));
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
index 6d77c55..4171939 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
@@ -31,43 +31,44 @@
 </test-fixture>
 
 <script>
-  suite('gr-storage tests', function() {
-    var element;
+  suite('gr-storage tests', () => {
+    let element;
 
     function mockStorage(opt_quotaExceeded) {
       return {
-        getItem: function(key) { return this[key]; },
-        removeItem: function(key) { delete this[key]; },
-        setItem: function(key, value) {
+        getItem(key) { return this[key]; },
+        removeItem(key) { delete this[key]; },
+        setItem(key, value) {
+          // eslint-disable-next-line no-throw-literal
           if (opt_quotaExceeded) { throw {code: 22}; /* Quota exceeded */ }
           this[key] = value;
         },
       };
     }
 
-    setup(function() {
+    setup(() => {
       element = fixture('basic');
       element._storage = mockStorage();
     });
 
-    test('storing, retrieving and erasing drafts', function() {
-      var changeNum = 1234;
-      var patchNum = 5;
-      var path = 'my_source_file.js';
-      var line = 123;
-      var location = {
-        changeNum: changeNum,
-        patchNum: patchNum,
-        path: path,
-        line: line,
+    test('storing, retrieving and erasing drafts', () => {
+      const changeNum = 1234;
+      const patchNum = 5;
+      const path = 'my_source_file.js';
+      const line = 123;
+      const location = {
+        changeNum,
+        patchNum,
+        path,
+        line,
       };
 
       // The key is in the expected format.
-      var key = element._getDraftKey(location);
+      const key = element._getDraftKey(location);
       assert.equal(key, ['draft', changeNum, patchNum, path, line].join(':'));
 
       // There should be no draft initially.
-      var draft = element.getDraftComment(location);
+      const draft = element.getDraftComment(location);
       assert.isNotOk(draft);
 
       // Setting the draft stores it under the expected key.
@@ -82,24 +83,24 @@
       assert.isNotOk(element._storage.getItem(key));
     });
 
-    test('automatically removes old drafts', function() {
-      var changeNum = 1234;
-      var patchNum = 5;
-      var path = 'my_source_file.js';
-      var line = 123;
-      var location = {
-        changeNum: changeNum,
-        patchNum: patchNum,
-        path: path,
-        line: line,
+    test('automatically removes old drafts', () => {
+      const changeNum = 1234;
+      const patchNum = 5;
+      const path = 'my_source_file.js';
+      const line = 123;
+      const location = {
+        changeNum,
+        patchNum,
+        path,
+        line,
       };
 
-      var key = element._getDraftKey(location);
+      const key = element._getDraftKey(location);
 
       // Make sure that the call to cleanup doesn't get throttled.
       element._lastCleanup = 0;
 
-      var cleanupSpy = sinon.spy(element, '_cleanupDrafts');
+      const cleanupSpy = sinon.spy(element, '_cleanupDrafts');
 
       // Create a message with a timestamp that is a second behind the max age.
       element._storage.setItem(key, JSON.stringify({
@@ -108,7 +109,7 @@
       }));
 
       // Getting the draft should cause it to be removed.
-      var draft = element.getDraftComment(location);
+      const draft = element.getDraftComment(location);
 
       assert.isTrue(cleanupSpy.called);
       assert.isNotOk(draft);
@@ -117,18 +118,18 @@
       cleanupSpy.restore();
     });
 
-    test('_getDraftKey', function() {
-      var changeNum = 1234;
-      var patchNum = 5;
-      var path = 'my_source_file.js';
-      var line = 123;
-      var location = {
-        changeNum: changeNum,
-        patchNum: patchNum,
-        path: path,
-        line: line,
+    test('_getDraftKey', () => {
+      const changeNum = 1234;
+      const patchNum = 5;
+      const path = 'my_source_file.js';
+      const line = 123;
+      const location = {
+        changeNum,
+        patchNum,
+        path,
+        line,
       };
-      var expectedResult = 'draft:1234:5:my_source_file.js:123';
+      let expectedResult = 'draft:1234:5:my_source_file.js:123';
       assert.equal(element._getDraftKey(location), expectedResult);
       location.range = {
         start_character: 1,
@@ -140,21 +141,21 @@
       assert.equal(element._getDraftKey(location), expectedResult);
     });
 
-    test('exceeded quota disables storage', function() {
+    test('exceeded quota disables storage', () => {
       element._storage = mockStorage(true);
       assert.isFalse(element._exceededQuota);
 
-      var changeNum = 1234;
-      var patchNum = 5;
-      var path = 'my_source_file.js';
-      var line = 123;
-      var location = {
-        changeNum: changeNum,
-        patchNum: patchNum,
-        path: path,
-        line: line,
+      const changeNum = 1234;
+      const patchNum = 5;
+      const path = 'my_source_file.js';
+      const line = 123;
+      const location = {
+        changeNum,
+        patchNum,
+        path,
+        line,
       };
-      var key = element._getDraftKey(location);
+      const key = element._getDraftKey(location);
       element.setDraftComment(location, 'my comment');
       assert.isTrue(element._exceededQuota);
       assert.isNotOk(element._storage.getItem(key));