Merge "Remove modifier pressed check for bracket key"
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneAccountIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneAccountIndex.java
index fbde7be..be4f917 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneAccountIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneAccountIndex.java
@@ -81,7 +81,7 @@
     if (LuceneIndexModule.isInMemoryTest(cfg)) {
       return new RAMDirectory();
     }
-    Path indexDir = LuceneVersionManager.getDir(sitePaths, ACCOUNTS + "_", schema);
+    Path indexDir = LuceneVersionManager.getDir(sitePaths, ACCOUNTS, schema);
     return FSDirectory.open(indexDir);
   }
 
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 3afcb07..dc9f6c1 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -108,7 +108,7 @@
   static final String UPDATED_SORT_FIELD = sortFieldName(ChangeField.UPDATED);
   static final String ID_SORT_FIELD = sortFieldName(ChangeField.LEGACY_ID);
 
-  private static final String CHANGES_PREFIX = "changes_";
+  private static final String CHANGES = "changes";
   private static final String CHANGES_OPEN = "open";
   private static final String CHANGES_CLOSED = "closed";
   private static final String ADDED_FIELD = ChangeField.ADDED.getName();
@@ -178,7 +178,7 @@
           new ChangeSubIndex(
               schema, sitePaths, new RAMDirectory(), "ramClosed", closedConfig, searcherFactory);
     } else {
-      Path dir = LuceneVersionManager.getDir(sitePaths, CHANGES_PREFIX, schema);
+      Path dir = LuceneVersionManager.getDir(sitePaths, CHANGES, schema);
       openIndex =
           new ChangeSubIndex(
               schema, sitePaths, dir.resolve(CHANGES_OPEN), openConfig, searcherFactory);
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneGroupIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneGroupIndex.java
index c4f10ff..daece8c 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneGroupIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneGroupIndex.java
@@ -80,7 +80,7 @@
     if (LuceneIndexModule.isInMemoryTest(cfg)) {
       return new RAMDirectory();
     }
-    Path indexDir = LuceneVersionManager.getDir(sitePaths, GROUPS + "_", schema);
+    Path indexDir = LuceneVersionManager.getDir(sitePaths, GROUPS, schema);
     return FSDirectory.open(indexDir);
   }
 
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
index f6f0c28..ad13066 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -48,8 +48,8 @@
     }
   }
 
-  static Path getDir(SitePaths sitePaths, String prefix, Schema<?> schema) {
-    return sitePaths.index_dir.resolve(String.format("%s%04d", prefix, schema.getVersion()));
+  static Path getDir(SitePaths sitePaths, String name, Schema<?> schema) {
+    return sitePaths.index_dir.resolve(String.format("%s_%04d", name, schema.getVersion()));
   }
 
   @Inject
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
index 72dc0b23..975bb5e 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
@@ -20,24 +20,24 @@
 (function(window) {
   'use strict';
 
+  // Must be declared outside behavior implementation to be accessed inside
+  // behavior functions.
   const getKeyboardEvent = function(e) {
-    return Polymer.dom(e.detail ? e.detail.keyboardEvent : e);
+    e = Polymer.dom(e.detail ? e.detail.keyboardEvent : e);
+    // When e is a keyboardEvent, e.event is not null.
+    if (e.event) { e = e.event; }
+    return e;
   };
 
   /** @polymerBehavior KeyboardShortcutBehaviorImpl */
   const KeyboardShortcutBehaviorImpl = {
     modifierPressed(e) {
       e = getKeyboardEvent(e);
-      // When e is a keyboardEvent, e.event is not null.
-      if (e.event) { e = e.event; }
       return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;
     },
 
     isModifierPressed(e, modifier) {
-      e = getKeyboardEvent(e);
-      // When e is a keyboardEvent, e.event is not null.
-      if (e.event) { e = e.event; }
-      return e[modifier];
+      return getKeyboardEvent(e)[modifier];
     },
 
     shouldSuppressKeyboardShortcut(e) {
@@ -50,6 +50,11 @@
       }
       return false;
     },
+
+    // Alias for getKeyboardEvent.
+    getKeyboardEvent(e) {
+      return getKeyboardEvent(e);
+    },
   };
 
   window.Gerrit = window.Gerrit || {};
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
index df376ba..da04c37 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
@@ -21,7 +21,7 @@
 <script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../bower_components/web-component-tester/browser.js"></script>
 
-<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <link rel="import" href="keyboard-shortcut-behavior.html">
 
 <test-fixture id="basic">
@@ -69,7 +69,7 @@
     test('doesn’t block kb shortcuts for non-whitelisted els', done => {
       const divEl = document.createElement('div');
       element.appendChild(divEl);
-      element._handleKey = function(e) {
+      element._handleKey = e => {
         assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
         done();
       };
@@ -79,7 +79,7 @@
     test('blocks kb shortcuts for input els', done => {
       const inputEl = document.createElement('input');
       element.appendChild(inputEl);
-      element._handleKey = function(e) {
+      element._handleKey = e => {
         assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
         done();
       };
@@ -89,7 +89,7 @@
     test('blocks kb shortcuts for textarea els', done => {
       const textareaEl = document.createElement('textarea');
       element.appendChild(textareaEl);
-      element._handleKey = function(e) {
+      element._handleKey = e => {
         assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
         done();
       };
@@ -100,7 +100,7 @@
       const divEl = document.createElement('div');
       const element = overlay.querySelector('test-element');
       element.appendChild(divEl);
-      element._handleKey = function(e) {
+      element._handleKey = e => {
         assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
         done();
       };
@@ -109,7 +109,7 @@
 
     test('modifierPressed returns accurate values', () => {
       const spy = sandbox.spy(element, 'modifierPressed');
-      element._handleKey = function(e) {
+      element._handleKey = e => {
         element.modifierPressed(e);
       };
       MockInteractions.keyDownOn(element, 75, 'shift', 'k');
@@ -130,7 +130,7 @@
 
     test('isModifierPressed returns accurate value', () => {
       const spy = sandbox.spy(element, 'isModifierPressed');
-      element._handleKey = function(e) {
+      element._handleKey = e => {
         element.isModifierPressed(e, 'shiftKey');
       };
       MockInteractions.keyDownOn(element, 75, 'shift', 'k');
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
new file mode 100644
index 0000000..9ceee68
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
@@ -0,0 +1,107 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+
+<link rel="import" href="../../../styles/gr-form-styles.html">
+
+<dom-module id="gr-admin-project-list">
+  <template>
+    <style>
+      :host {
+        display: flex;
+        flex-direction: column;
+      }
+      tr.project-table {
+        border-bottom: 1px solid #eee;
+      }
+      #projectList {
+        border-collapse: collapse;
+        width: 100%;
+      }
+      td {
+        flex-shrink: 0;
+        padding: .3em .5em;
+      }
+      th {
+        background-color: #ddd;
+        border-bottom: 1px solid #eee;
+        font-weight: bold;
+        padding: .3em .5em;
+        text-align: left;
+      }
+      a {
+        color: var(--default-text-color);
+        text-decoration: none;
+      }
+      a:hover {
+        text-decoration: underline;
+      }
+      nav {
+        padding: .5em 0;
+        text-align: center;
+      }
+      nav a {
+        display: inline-block;
+      }
+      nav a:first-of-type {
+        margin-right: .5em;
+      }
+      .description {
+        width: 70%;
+      }
+    </style>
+    <table id="projectList">
+      <tr class="headerRow">
+        <th class="name topHeader">Project Name</th>
+        <th class="description topHeader">Project Description</th>
+        <th class="repositoryBrowser topHeader">Repository Browser</th>
+        <th class="readOnly topHeader">Read only</th>
+      </tr>
+      <template is="dom-repeat" items="[[_shownProjects]]">
+        <tr class="project-table">
+          <td class="name">
+            <a href$="[[_getUrl(item.id)]]">[[item.name]]</a>
+          </td>
+          <td class="description">[[item.description]]</td>
+          <td class="repositoryBrowser">
+            <template is="dom-repeat"
+                items="[[_computeWeblink(item)]]" as="link">
+              <a href$="[[link.url]]" class="webLink" rel="noopener" target="_blank">
+                ([[link.name]])
+              </a>
+            </template>
+          </td>
+          <td class="readOnly">[[_readOnly(item)]]</td>
+        </tr>
+      </template>
+    </table>
+    <nav>
+      <a id="prevArrow"
+          href$="[[_computeNavLink(_offset, -1, _projectsPerPage)]]"
+          hidden$="[[_hidePrevArrow(_offset)]]" hidden>&larr; Prev</a>
+      <a id="nextArrow"
+          href$="[[_computeNavLink(_offset, 1, _projectsPerPage)]]"
+          hidden$="[[_hideNextArrow(_loading, _projects)]]" hidden>
+        Next &rarr;</a>
+    </nav>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-admin-project-list.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
new file mode 100644
index 0000000..038b41f
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
@@ -0,0 +1,148 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-admin-project-list',
+
+    properties: {
+      /**
+       * URL params passed from the router.
+       */
+      params: {
+        type: Object,
+        observer: '_paramsChanged',
+      },
+
+      /**
+       * Offset of currently visible query results.
+       */
+      _offset: Number,
+
+      _projects: Array,
+
+      /**
+       * Because  we request one more than the projectsPerPage, _shownProjects
+       * maybe one less than _projects.
+       * */
+      _shownProjects: {
+        type: Array,
+        computed: '_computeShownProjects(_projects)',
+      },
+
+      _projectsPerPage: {
+        type: Number,
+        value: 25,
+      },
+
+      _loading: {
+        type: Boolean,
+        value: true,
+      },
+    },
+
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+      Gerrit.URLEncodingBehavior,
+    ],
+
+    listeners: {
+      'next-page': '_handleNextPage',
+      'previous-page': '_handlePreviousPage',
+    },
+
+    _paramsChanged(value) {
+      this._loading = true;
+
+      if (value && value.offset) {
+        this._offset = value.offset;
+      } else {
+        this._offset = 0;
+      }
+
+      return this.$.restAPI.getProjects(this._projectsPerPage, this._offset)
+          .then(projects => {
+            if (!projects) {
+              this._projects = [];
+              return;
+            }
+            this._projects = Object.keys(projects)
+             .map(key => {
+               const project = projects[key];
+               project.name = key;
+               return project;
+             });
+            this._loading = false;
+          });
+    },
+
+    _readOnly(item) {
+      return item.state === 'READ_ONLY' ? 'Y' : 'N';
+    },
+
+    _getUrl(item) {
+      return this.getBaseUrl() + '/admin/projects/' +
+          this.encodeURL(item, false);
+    },
+
+
+    _computeWeblink(project) {
+      if (!project.web_links) {
+        return '';
+      }
+      const webLinks = project.web_links;
+      return webLinks.length ? webLinks : null;
+    },
+
+    _computeNavLink(offset, direction, projectsPerPage) {
+      // Offset could be a string when passed from the router.
+      offset = +(offset || 0);
+      const newOffset = Math.max(0, offset + (projectsPerPage * direction));
+      let href = this.getBaseUrl() + '/admin/projects';
+      if (newOffset > 0) {
+        href += ',' + newOffset;
+      }
+      return href;
+    },
+
+    _computeShownProjects(projects) {
+      return projects.slice(0, 25);
+    },
+
+    _hidePrevArrow(offset) {
+      return offset === 0;
+    },
+
+    _hideNextArrow(loading, projects) {
+      let lastPage = false;
+      if (projects.length < this._projectsPerPage + 1) {
+        lastPage = true;
+      }
+      return loading || lastPage || !projects || !projects.length;
+    },
+
+    _handleNextPage() {
+      if (this.$.nextArrow.hidden) { return; }
+      page.show(this._computeNavLink(
+          this._offset, 1, this._projectsPerPage));
+    },
+
+    _handlePreviousPage() {
+      if (this.$.prevArrow.hidden) { return; }
+      page.show(this._computeNavLink(
+          this._offset, -1, this._projectsPerPage));
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
new file mode 100644
index 0000000..f9e08cd
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-admin-project-list</title>
+
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+
+<link rel="import" href="gr-admin-project-list.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-admin-project-list></gr-admin-project-list>
+  </template>
+</test-fixture>
+
+<script>
+  let counter = 0;
+  const projectGenerator = () => {
+    return {
+      id: `test${++counter}`,
+      state: 'ACTIVE',
+      web_links: [
+        {
+          name: 'diffusion',
+          url: `https://phabricator.example.org/r/project/test${counter}`,
+        },
+      ],
+    };
+  };
+
+  suite('gr-admin-project-list tests', () => {
+    let element;
+    let projects;
+    let value;
+
+    suite('list with projects', () => {
+      setup(done => {
+        projects = _.times(26, projectGenerator);
+
+        stub('gr-rest-api-interface', {
+          getProjects(num, offset) {
+            return Promise.resolve(projects);
+          },
+        });
+
+        element = fixture('basic');
+        element._paramsChanged(value).then(() => { flush(done); });
+      });
+
+      test('test for test project in the list', done => {
+        flush(() => {
+          assert.equal(element._projects[1].id, 'test2');
+          done();
+        });
+      });
+
+      test('test next button', done => {
+        flush(() => {
+          let loading;
+          assert.isFalse(element._hideNextArrow(loading, projects));
+          loading = true;
+          assert.isTrue(element._hideNextArrow(loading, projects));
+          loading = false;
+          assert.isFalse(element._hideNextArrow(loading, projects));
+          element._projects = [];
+          assert.isTrue(element._hideNextArrow(loading, element._projects));
+          projects = _.times(4, projectGenerator);
+          assert.isTrue(element._hideNextArrow(loading, projects));
+          done();
+        });
+      });
+
+      test('test for prev button', () => {
+        flush(() => {
+          let offset = 0;
+          assert.isTrue(element._hidePrevArrow(offset));
+          offset = 5;
+          assert.isFalse(element._hidePrevArrow(offset));
+        });
+      });
+
+      test('_shownProjects', () => {
+        assert.equal(element._shownProjects.length, 25);
+      });
+    });
+
+    suite('test with less then 25 projects', () => {
+      setup(done => {
+        projects = _.times(25, projectGenerator);
+
+        stub('gr-rest-api-interface', {
+          getProjects(num, offset) {
+            return Promise.resolve(projects);
+          },
+        });
+
+        element = fixture('basic');
+        element._paramsChanged(value).then(() => { flush(done); });
+      });
+
+      test('test next button', done => {
+        flush(() => {
+          let loading;
+          assert.isTrue(element._hideNextArrow(loading, projects));
+          projects = _.times(1, projectGenerator);
+          assert.isTrue(element._hideNextArrow(loading, projects));
+          projects = _.times(26, projectGenerator);
+          assert.isFalse(element._hideNextArrow(loading, projects));
+          done();
+        });
+      });
+
+      test('_shownProjects', () => {
+        assert.equal(element._shownProjects.length, 25);
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
index eb6e213..be66587 100644
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
@@ -67,6 +67,10 @@
       this.$.input.setText(text);
     },
 
+    getText() {
+      return this.$.input.text;
+    },
+
     _handleInputCommit(e) {
       this.fire('add', {value: e.detail.value});
     },
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
index b2f1b41..4e403e6 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
@@ -90,11 +90,13 @@
     },
 
     _handleAdd(e) {
-      const reviewer = e.detail.value;
+      this._addReviewer(e.detail.value);
+    },
+
+    _addReviewer(reviewer) {
       // Append new account or group to the accounts property. We add our own
       // internal properties to the account/group here, so we clone the object
       // to avoid cluttering up the shared change object.
-      // TODO(logan): Polyfill for Object.assign in IE.
       if (reviewer.account) {
         const account =
             Object.assign({}, reviewer.account, {_pendingAdd: true});
@@ -114,12 +116,14 @@
           this.$.entry.setText(reviewer);
           this.dispatchEvent(new CustomEvent('show-alert',
             {detail: {message: VALID_EMAIL_ALERT}, bubbles: true}));
+          return false;
         } else {
           const account = {email: reviewer, _pendingAdd: true};
           this.push('accounts', account);
         }
       }
       this.pendingConfirmation = null;
+      return true;
     },
 
     confirmGroup(group) {
@@ -245,6 +249,22 @@
       }
     },
 
+    /**
+     * Submit the text of the entry as a reviewer value, if it exists. If it is
+     * a successful submit of the text, clear the entry value.
+     *
+     * @return {boolean} If there is text in the entry, return true if the
+     *     submission was successful and false if not. If there is no text,
+     *     return true.
+     */
+    submitEntryText() {
+      const text = this.$.entry.getText();
+      if (!text.length) { return true; }
+      const wasSubmitted = this._addReviewer(text);
+      if (wasSubmitted) { this.$.entry.clear(); }
+      return wasSubmitted;
+    },
+
     additions() {
       return this.accounts.filter(account => {
         return account._pendingAdd;
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
index f35bbcd..5520254 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
@@ -175,6 +175,30 @@
       assert.isFalse(element._computeRemovable(newAccount));
     });
 
+    test('submitEntryText', () => {
+      element.allowAnyInput = true;
+      flushAsynchronousOperations();
+
+      const getTextStub = sandbox.stub(element.$.entry, 'getText');
+      getTextStub.onFirstCall().returns('');
+      getTextStub.onSecondCall().returns('test');
+      getTextStub.onThirdCall().returns('test@test');
+
+      // When entry is empty, return true.
+      const clearStub = sandbox.stub(element.$.entry, 'clear');
+      assert.isTrue(element.submitEntryText());
+      assert.isFalse(clearStub.called);
+
+      // When entry is invalid, return false.
+      assert.isFalse(element.submitEntryText());
+      assert.isFalse(clearStub.called);
+
+      // When entry is valid, return true and clear text.
+      assert.isTrue(element.submitEntryText());
+      assert.isTrue(clearStub.called);
+      assert.equal(element.additions()[0].account.email, 'test@test');
+    });
+
     test('additions returns sanitized new accounts and groups', () => {
       assert.equal(element.additions().length, 0);
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 77c2ca0..05c6c68 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -481,7 +481,7 @@
             document.documentElement.scrollTop =
                 document.body.scrollTop = this.viewState.scrollTop;
           } else {
-            this._maybeScrollToMessage();
+            this._maybeScrollToMessage(window.location.hash);
           }
         }, 1);
       });
@@ -513,10 +513,9 @@
       this.viewState.numFilesShown = numFilesShown;
     },
 
-    _maybeScrollToMessage() {
+    _maybeScrollToMessage(hash) {
       const msgPrefix = '#message-';
-      const hash = window.location.hash;
-      if (hash.startsWith(msgPrefix) === 0) {
+      if (hash.startsWith(msgPrefix)) {
         this.$.messageList.scrollToMessage(hash.substr(msgPrefix.length));
       }
     },
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 7794cd67..5847e73 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -1320,5 +1320,19 @@
             'header wip');
       });
     });
+
+    test('_maybeScrollToMessage', () => {
+      const scrollStub = sandbox.stub(element.$.messageList, 'scrollToMessage');
+
+      element._maybeScrollToMessage('');
+      assert.isFalse(scrollStub.called);
+
+      element._maybeScrollToMessage('message');
+      assert.isFalse(scrollStub.called);
+
+      element._maybeScrollToMessage('#message-TEST');
+      assert.isTrue(scrollStub.called);
+      assert.equal(scrollStub.lastCall.args[0], 'TEST');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
new file mode 100644
index 0000000..b4a6171
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-reply-dialog</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-reply-dialog.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-reply-dialog></gr-reply-dialog>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-reply-dialog tests', () => {
+    let element;
+    let changeNum;
+    let patchNum;
+
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+
+      changeNum = 42;
+      patchNum = 1;
+
+      stub('gr-rest-api-interface', {
+        getConfig() { return Promise.resolve({}); },
+        getAccount() { return Promise.resolve({}); },
+      });
+
+      element = fixture('basic');
+      element.change = {
+        _number: changeNum,
+        labels: {
+          'Verified': {
+            values: {
+              '-1': 'Fails',
+              ' 0': 'No score',
+              '+1': 'Verified',
+            },
+            default_value: 0,
+          },
+          'Code-Review': {
+            values: {
+              '-2': 'Do not submit',
+              '-1': 'I would prefer that you didn\'t submit this',
+              ' 0': 'No score',
+              '+1': 'Looks good to me, but someone else must approve',
+              '+2': 'Looks good to me, approved',
+            },
+            default_value: 0,
+          },
+        },
+      };
+      element.patchNum = patchNum;
+      element.permittedLabels = {
+        'Code-Review': [
+          '-1',
+          ' 0',
+          '+1',
+        ],
+        'Verified': [
+          '-1',
+          ' 0',
+          '+1',
+        ],
+      };
+      element.serverConfig = {note_db_enabled: true};
+
+      sandbox.stub(element, 'fetchIsLatestKnown', () => Promise.resolve(true));
+
+      // Allow the elements created by dom-repeat to be stamped.
+      flushAsynchronousOperations();
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('send blocked when invalid email is supplied to ccs', () => {
+      const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve());
+      // Stub the below function to avoid side effects from the send promise
+      // resolving.
+      sandbox.stub(element, '_purgeReviewersPendingRemove');
+
+      element.$$('#ccs').$.entry.setText('test');
+      MockInteractions.tap(element.$$('gr-button.send'));
+      assert.isFalse(sendStub.called);
+      flushAsynchronousOperations();
+
+      element.$$('#ccs').$.entry.setText('test@test.test');
+      MockInteractions.tap(element.$$('gr-button.send'));
+      assert.isTrue(sendStub.called);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 168e5b3..8218b43 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -136,6 +136,10 @@
         type: String,
         computed: '_computeSendButtonLabel(canBeStarted)',
       },
+      _ccsEnabled: {
+        type: Boolean,
+        computed: '_computeCCsEnabled(serverConfig)',
+      },
     },
 
     FocusTarget,
@@ -472,7 +476,7 @@
         }
       }
 
-      if (serverConfig.note_db_enabled) {
+      if (this._ccsEnabled) {
         this._ccs = ccs;
       } else {
         this._ccs = [];
@@ -500,9 +504,7 @@
       }
 
       const key = this._accountOrGroupKey(entry);
-      const finder = function(entry) {
-        return this._accountOrGroupKey(entry) === key;
-      }.bind(this);
+      const finder = entry => this._accountOrGroupKey(entry) === key;
 
       return this._reviewers.find(finder) === undefined &&
           this._ccs.find(finder) === undefined;
@@ -526,6 +528,11 @@
 
     _saveTapHandler(e) {
       e.preventDefault();
+      if (this._ccsEnabled && !this.$$('#ccs').submitEntryText()) {
+        // Do not proceed with the save if there is an invalid email entry in
+        // the text field of the CC entry.
+        return;
+      }
       this.send(this._includeComments).then(keepReviewers => {
         this._purgeReviewersPendingRemove(false, keepReviewers);
       });
@@ -533,14 +540,17 @@
 
     _sendTapHandler(e) {
       e.preventDefault();
+      if (this._ccsEnabled && !this.$$('#ccs').submitEntryText()) {
+        // Do not proceed with the send if there is an invalid email entry in
+        // the text field of the CC entry.
+        return;
+      }
       if (this.canBeStarted) {
-        this._startReview()
-            .then(() => {
-              return this.send(this._includeComments);
-            })
-            .then(keepReviewers => {
-              this._purgeReviewersPendingRemove(false, keepReviewers);
-            });
+        this._startReview().then(() => {
+          return this.send(this._includeComments);
+        }).then(keepReviewers => {
+          this._purgeReviewersPendingRemove(false, keepReviewers);
+        });
         return;
       }
       this.send(this._includeComments).then(keepReviewers => {
@@ -636,5 +646,9 @@
     _computeSendButtonLabel(canBeStarted) {
       return canBeStarted ? 'Start review' : 'Send';
     },
+
+    _computeCCsEnabled(serverConfig) {
+      return serverConfig && serverConfig.note_db_enabled;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 673a11e..fcdb209 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -108,6 +108,20 @@
       });
     });
 
+    // Matches /admin/projects[,<offset>][/].
+    page(/^\/admin\/projects(,(\d+))?(\/)?$/, loadUser, data => {
+      restAPI.getLoggedIn().then(loggedIn => {
+        if (loggedIn) {
+          app.params = {
+            view: 'gr-admin-project-list',
+            offset: data.params[1] || 0,
+          };
+        } else {
+          page.redirect('/login/' + encodeURIComponent(data.canonicalPath));
+        }
+      });
+    });
+
     page('/admin/(.*)', loadUser, data => {
       restAPI.getLoggedIn().then(loggedIn => {
         if (loggedIn) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index ef00f55..e90dc41d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -16,6 +16,7 @@
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../shared/gr-textarea/gr-textarea.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
@@ -122,7 +123,6 @@
         display: none;
       }
       .editing .editMessage {
-        background-color: #fff;
         display: block;
       }
       .show-hide {
@@ -168,7 +168,7 @@
       }
       #container.collapsed .actions,
       #container.collapsed gr-formatted-text,
-      #container.collapsed iron-autogrow-textarea {
+      #container.collapsed gr-textarea {
         display: none;
       }
       .resolve,
@@ -233,14 +233,14 @@
           [[comment.robot_id]]
         </div>
       </template>
-      <iron-autogrow-textarea
+      <gr-textarea
           id="editTextarea"
           class="editMessage"
           autocomplete="on"
           disabled="{{disabled}}"
           rows="4"
-          bind-value="{{_messageText}}"
-          on-keydown="_handleTextareaKeydown"></iron-autogrow-textarea>
+          text="{{_messageText}}"
+          on-keydown="_handleTextareaKeydown"></gr-textarea>
       <gr-formatted-text class="message"
           content="[[comment.message]]"
           no-trailing-margin="[[!comment.__draft]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index ac8e299..82ebfbb 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -129,6 +129,7 @@
 
     detached() {
       this.cancelDebouncer('fire-update');
+      this.$.editTextarea.closeDropdown();
     },
 
     _computeShowHideText(collapsed) {
@@ -229,13 +230,7 @@
     _editingChanged(editing, previousValue) {
       this.$.container.classList.toggle('editing', editing);
       if (editing) {
-        const textarea = this.$.editTextarea.textarea;
-        // Put the cursor at the end always.
-        textarea.selectionStart = textarea.value.length;
-        textarea.selectionEnd = textarea.selectionStart;
-        this.async(() => {
-          textarea.focus();
-        });
+        this.$.editTextarea.putCursorAtEnd();
       }
       if (this.comment && this.comment.id) {
         this.$$('.cancel').hidden = !editing;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index ce3b233..4a1df77 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -79,7 +79,7 @@
           'gr-formatted-text is not visible');
       assert.isFalse(isVisible(element.$$('.actions')),
           'actions are not visible');
-      assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+      assert.isFalse(isVisible(element.$$('gr-textarea')),
           'textarea is not visible');
 
       // The header middle content is only visible when comments are collapsed.
@@ -94,7 +94,7 @@
           'gr-formatted-text is visible');
       assert.isTrue(isVisible(element.$$('.actions')),
           'actions are visible');
-      assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+      assert.isFalse(isVisible(element.$$('gr-textarea')),
           'textarea is not visible');
       assert.isFalse(isVisible(element.$$('.collapsedContent')),
           'header middle content is not visible');
@@ -166,7 +166,7 @@
           'gr-formatted-text is not visible');
       assert.isFalse(isVisible(element.$$('.actions')),
           'actions are not visible');
-      assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+      assert.isFalse(isVisible(element.$$('gr-textarea')),
           'textarea is not visible');
       assert.isTrue(isVisible(element.$$('.collapsedContent')),
           'header middle content is visible');
@@ -177,7 +177,7 @@
           'gr-formatted-text is visible');
       assert.isTrue(isVisible(element.$$('.actions')),
           'actions are visible');
-      assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+      assert.isFalse(isVisible(element.$$('gr-textarea')),
           'textarea is not visible');
       assert.isFalse(isVisible(element.$$('.collapsedContent')),
           'header middle content is is not visible');
@@ -261,7 +261,7 @@
 
     test('delete comment', done => {
       sandbox.stub(
-          element.$.restAPI, 'deleteComment').returns(Promise.resolve());
+          element.$.restAPI, 'deleteComment').returns(Promise.resolve({}));
       sandbox.spy(element.$.overlay, 'open');
       element.changeNum = 42;
       element.patchNum = 0xDEADBEEF;
@@ -393,7 +393,7 @@
           'gr-formatted-text is not visible');
       assert.isFalse(isVisible(element.$$('.actions')),
           'actions are not visible');
-      assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+      assert.isFalse(isVisible(element.$$('gr-textarea')),
           'textarea is not visible');
       assert.isTrue(isVisible(element.$$('.collapsedContent')),
           'header middle content is visible');
@@ -404,7 +404,7 @@
           'gr-formatted-text is visible');
       assert.isTrue(isVisible(element.$$('.actions')),
           'actions are visible');
-      assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+      assert.isFalse(isVisible(element.$$('gr-textarea')),
           'textarea is not visible');
       assert.isFalse(isVisible(element.$$('.collapsedContent')),
           'header middle content is is not visible');
@@ -417,7 +417,7 @@
           'gr-formatted-text is not visible');
       assert.isTrue(isVisible(element.$$('.actions')),
           'actions are visible');
-      assert.isTrue(isVisible(element.$$('iron-autogrow-textarea')),
+      assert.isTrue(isVisible(element.$$('gr-textarea')),
           'textarea is visible');
       assert.isFalse(isVisible(element.$$('.collapsedContent')),
           'header middle content is not visible');
@@ -430,7 +430,7 @@
           'gr-formatted-text is not visible');
       assert.isFalse(isVisible(element.$$('.actions')),
           'actions are not visible');
-      assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+      assert.isFalse(isVisible(element.$$('gr-textarea')),
           'textarea is not visible');
       assert.isTrue(isVisible(element.$$('.collapsedContent')),
           'header middle content is visible');
@@ -442,7 +442,7 @@
           'gr-formatted-text is not visible');
       assert.isTrue(isVisible(element.$$('.actions')),
           'actions are visible');
-      assert.isTrue(isVisible(element.$$('iron-autogrow-textarea')),
+      assert.isTrue(isVisible(element.$$('gr-textarea')),
           'textarea is visible');
       assert.isFalse(isVisible(element.$$('.collapsedContent')),
           'header middle content is not visible');
@@ -500,7 +500,7 @@
       });
       element._messageText = 'is that the horse from horsing around??';
       MockInteractions.pressAndReleaseKeyOn(
-          element.$.editTextarea.textarea,
+          element.$.editTextarea.$.textarea.textarea,
           83, 'ctrl');  // 'ctrl + s'
     });
 
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 87a4687..0fe48fd 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -18,6 +18,7 @@
 <link rel="import" href="../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../styles/app-theme.html">
+<link rel="import" href="./admin/gr-admin-project-list/gr-admin-project-list.html">
 <link rel="import" href="./admin/gr-admin-view/gr-admin-view.html">
 <link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html">
 <link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html">
@@ -125,6 +126,11 @@
             on-account-detail-update="_handleAccountDetailUpdate">
         </gr-settings-view>
       </template>
+      <template is="dom-if" if="[[_showProjectListView]]" restamp="true">
+        <gr-admin-project-list
+            params="[[params]]"
+            id="projectList"></gr-admin-project-list>
+      </template>
       <template is="dom-if" if="[[_showAdminView]]" restamp="true">
         <gr-admin-view path="[[_path]]"></gr-admin-view>
       </template>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 945e010..846c186 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -48,6 +48,7 @@
       _showChangeView: Boolean,
       _showDiffView: Boolean,
       _showSettingsView: Boolean,
+      _showProjectListView: Boolean,
       _showAdminView: Boolean,
       _showCLAView: Boolean,
       _viewState: Object,
@@ -127,6 +128,7 @@
       this.set('_showChangeView', view === 'gr-change-view');
       this.set('_showDiffView', view === 'gr-diff-view');
       this.set('_showSettingsView', view === 'gr-settings-view');
+      this.set('_showProjectListView', view === 'gr-admin-project-list');
       this.set('_showAdminView', view === 'gr-admin-view');
       this.set('_showCLAView', view === 'gr-cla-view');
       if (this.params.justRegistered) {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
index 5702795..8be89c4 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -30,6 +30,7 @@
      */
 
     properties: {
+      index: Number,
       moveToRoot: Boolean,
       fixedPosition: Boolean,
       suggestions: {
@@ -63,6 +64,8 @@
     close() {
       if (this.moveToRoot) {
         Gerrit.getRootElement().removeChild(this);
+      } else {
+        this.hidden = true;
       }
     },
 
@@ -133,7 +136,9 @@
 
     _handleEscape() {
       this._fireClose();
-      this.close();
+      if (!this.hidden) {
+        this.close();
+      }
     },
 
     _handleTapItem(e) {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
index 41468d1..c071f0c 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
@@ -69,12 +69,14 @@
 
     test('escape key', () => {
       const listener = sandbox.spy();
+      element.hidden = false;
       element.addEventListener('dropdown-closed', listener);
       const closeSpy = sandbox.spy(element, 'close');
       MockInteractions.pressAndReleaseKeyOn(element, 27);
       flushAsynchronousOperations();
       assert.isTrue(listener.called);
       assert.isTrue(closeSpy.called);
+      assert.isTrue(element.hidden);
     });
 
     test('tab key', () => {
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 afbce45..df4df05 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -49,7 +49,7 @@
           on-item-selected="_handleItemSelect"
           suggestions="[[_suggestions]]"
           role="listbox"
-          index="[[index]]"
+          index="[[_index]]"
           hidden$="[[_computeSuggestionsHidden(_suggestions, _focused)]]">
       </gr-autocomplete-dropdown>
     </div>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
index 5c0535b..3cda49b 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
@@ -15,10 +15,10 @@
 -->
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-js-api-interface">
-  <template></template>
   <script src="gr-change-actions-js-api.js"></script>
   <script src="gr-change-reply-js-api.js"></script>
   <script src="gr-js-api-interface.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index c3013bd..40810b1 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -36,6 +36,8 @@
     let plugin;
     let errorStub;
     let sandbox;
+    let getResponseObjectStub;
+    let sendStub;
 
     const throwErrFn = function() {
       throw Error('Unfortunately, this handler has stopped');
@@ -43,10 +45,16 @@
 
     setup(() => {
       sandbox = sinon.sandbox.create();
+      getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
+      sendStub = sandbox.stub().returns(Promise.resolve());
       stub('gr-rest-api-interface', {
         getAccount() {
           return Promise.resolve({name: 'Judy Hopps'});
         },
+        getResponseObject: getResponseObjectStub,
+        send(...args) {
+          return sendStub(...args);
+        },
       });
       element = fixture('basic');
       errorStub = sandbox.stub(console, 'error');
@@ -67,6 +75,27 @@
           'http://test.com/plugins/testplugin/static/test.js');
     });
 
+    test('get', done => {
+      const response = {foo: 'foo'};
+      getResponseObjectStub.returns(Promise.resolve(response));
+      plugin.get('/url', r => {
+        assert.isTrue(sendStub.calledWith('GET', '/url'));
+        assert.strictEqual(r, response);
+        done();
+      });
+    });
+
+    test('post', done => {
+      const payload = {foo: 'foo'};
+      const response = {bar: 'bar'};
+      getResponseObjectStub.returns(Promise.resolve(response));
+      plugin.post('/url', payload, r => {
+        assert.isTrue(sendStub.calledWith('POST', '/url', payload));
+        assert.strictEqual(r, response);
+        done();
+      });
+    });
+
     test('history event', done => {
       plugin.on(element.EventType.HISTORY, throwErrFn);
       plugin.on(element.EventType.HISTORY, path => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index a84eedd..0811935 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -24,6 +24,14 @@
     GWT_PLUGIN_STUB[name] = warnNotSupported.bind(null, name);
   }
 
+  let _restAPI;
+  const getRestAPI = () => {
+    if (!_restAPI) {
+      _restAPI = document.createElement('gr-rest-api-interface');
+    }
+    return _restAPI;
+  };
+
   const API_VERSION = '0.1';
 
   // GWT JSNI uses $wnd to refer to window.
@@ -77,6 +85,20 @@
     return this._url.origin + '/plugins/' + this._name + (opt_path || '/');
   };
 
+  Plugin.prototype._send = function(method, url, callback, opt_payload) {
+    return getRestAPI().send(method, url, opt_payload)
+        .then(getRestAPI().getResponseObject)
+        .then(callback);
+  };
+
+  Plugin.prototype.get = function(url, callback) {
+    return this._send('GET', url, callback);
+  },
+
+  Plugin.prototype.post = function(url, payload, callback) {
+    return this._send('POST', url, callback, payload);
+  },
+
   Plugin.prototype.changeActions = function() {
     return new GrChangeActionsInterface(Plugin._sharedAPIElement.getElement(
         Plugin._sharedAPIElement.Element.CHANGE_ACTIONS));
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 43e2cf1..e371201 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -559,6 +559,13 @@
       });
     },
 
+    getProjects(projectsPerPage, opt_offset) {
+      const offset = opt_offset || 0;
+      return this._fetchSharedCacheURL(
+          `/projects/?d&n=${projectsPerPage + 1}&S=${offset}`
+      );
+    },
+
     getSuggestedGroups(inputVal, opt_n, opt_errFn, opt_ctx) {
       const params = {s: inputVal};
       if (opt_n) { params.n = opt_n; }
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
index 51e349b..8d0b1e3 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
@@ -38,8 +38,11 @@
         background-color: var(--background-color, none);
         width: 100%;
       }
+      /*This is needed to not add a scroll bar on the side of gr-textarea
+      since there is 2px of padding in iron-autogrow-textarea for the
+      native textarea*/
       iron-autogrow-textarea {
-        padding: 0;
+        padding: 2px;
       }
       #textarea.noBorder {
         border: none;
@@ -54,8 +57,7 @@
     </style>
     <gr-autocomplete-dropdown id="emojiSuggestions"
         suggestions="[[_suggestions]]"
-        index="[[index]]"
-        position="[[position]]"
+        index="[[_index]]"
         move-to-root
         fixed-position="[[fixedPositionDropdown]]"
         hidden>
@@ -72,4 +74,4 @@
         on-bind-value-changed="_onValueChanged"></iron-autogrow-textarea>
   </template>
   <script src="gr-textarea.js"></script>
-</dom-module>
\ No newline at end of file
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
index 6dbe47e..9158a57 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -133,6 +133,20 @@
       }
     },
 
+    getNativeTextarea() {
+      return this.$.textarea.textarea;
+    },
+
+    putCursorAtEnd() {
+      const textarea = this.getNativeTextarea();
+      // Put the cursor at the end always.
+      textarea.selectionStart = textarea.value.length;
+      textarea.selectionEnd = textarea.selectionStart;
+      this.async(() => {
+        textarea.focus();
+      });
+    },
+
     _handleEscKey(e) {
       if (this._hideAutocomplete) { return; }
       e.preventDefault();
@@ -192,6 +206,10 @@
           10);
     },
 
+    _getScrollTop() {
+      return document.body.scrollTop;
+    },
+
     /**
      * This positions the dropdown to be just below the cursor position. It is
      * calculated by having a hidden element with the same width and styling of
@@ -205,7 +223,12 @@
       const caratPosition = this._getPositionOfCursor();
       const fontSize = this._getFontSize();
 
-      const top = caratPosition.top + fontSize + VERTICAL_OFFSET + 'px';
+      let top = caratPosition.top + fontSize + VERTICAL_OFFSET;
+
+      if (!this.fixedPositionDropdown) {
+        top += this._getScrollTop();
+      }
+      top += 'px';
       const left = caratPosition.left + 'px';
       this.$.emojiSuggestions.setPosition(top, left);
     },
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
index c92a479..1c5dea3 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -181,9 +181,12 @@
       sandbox.stub(element, '_getPositionOfCursor', () => {
         return {top: 100, left: 30};
       });
-      sandbox.stub(element, '_getFontSize', () => {
-        return 12;
-      });
+      sandbox.stub(element, '_getFontSize', () => 12);
+      sandbox.stub(element, '_getScrollTop', () => 100);
+      element._updateSelectorPosition();
+      assert.isTrue(setPositionSpy.lastCall.calledWithExactly('219px', '30px'));
+
+      element.fixedPositionDropdown = true;
       element._updateSelectorPosition();
       assert.isTrue(setPositionSpy.lastCall.calledWithExactly('119px', '30px'));
     });
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index a44cf27..9a2e404 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -30,6 +30,7 @@
     // This seemed to be flakey when it was farther down the list. Keep at the
     // beginning.
     'gr-app_test.html',
+    'admin/gr-admin-project-list/gr-admin-project-list_test.html',
     'change-list/gr-change-list-item/gr-change-list-item_test.html',
     'change-list/gr-change-list-view/gr-change-list-view_test.html',
     'change-list/gr-change-list/gr-change-list_test.html',
@@ -51,6 +52,7 @@
     'change/gr-messages-list/gr-messages-list_test.html',
     'change/gr-related-changes-list/gr-related-changes-list_test.html',
     'change/gr-reply-dialog/gr-reply-dialog_test.html',
+    'change/gr-reply-dialog/gr-reply-dialog-it_test.html',
     'change/gr-reviewer-list/gr-reviewer-list_test.html',
     'core/gr-account-dropdown/gr-account-dropdown_test.html',
     'core/gr-error-manager/gr-error-manager_test.html',
@@ -123,6 +125,7 @@
   // Behaviors tests.
   const behaviors = [
     'base-url-behavior/base-url-behavior_test.html',
+    'keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html',
     'rest-client-behavior/rest-client-behavior_test.html',
     'gr-change-table-behavior/gr-change-table-behavior_test.html',
     'gr-patch-set-behavior/gr-patch-set-behavior_test.html',