Merge "Handle network failures during project lookup"
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index 2d8da15..ae28a05 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -60,6 +60,10 @@
         type: Boolean,
         value: false,
       },
+      _isAdmin: {
+        type: Boolean,
+        value: false,
+      },
       _showGroup: Boolean,
       _showGroupAuditLog: Boolean,
       _showGroupList: Boolean,
@@ -155,7 +159,7 @@
               },
             ],
           };
-          if (this._groupOwner) {
+          if (this._isAdmin || this._groupOwner) {
             linkCopy.subsection.children.push(
                 {
                   name: 'Audit Log',
@@ -242,16 +246,20 @@
 
     _computeGroupName(groupId) {
       if (!groupId) { return ''; }
+      const promises = [];
       this.$.restAPI.getGroupConfig(groupId).then(group => {
         this._groupName = group.name;
         this.reload();
-        this.$.restAPI.getIsGroupOwner(group.name).then(
+        promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
+          this._isAdmin = isAdmin;
+        }));
+        promises.push(this.$.restAPI.getIsGroupOwner(group.name).then(
             isOwner => {
-              if (isOwner) {
-                this._groupOwner = true;
-                this.reload();
-              }
-            });
+              this._groupOwner = isOwner;
+            }));
+        return Promise.all(promises).then(() => {
+          this.reload();
+        });
       });
     },
 
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
index 9371b17..9e08381 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
@@ -67,9 +67,18 @@
         font-family: var(--font-family-bold);
         text-align: left;
       }
+      .canModify #groupMemberSearchInput,
+      .canModify #saveGroupMember,
+      .canModify .deleteHeader,
+      .canModify .deleteColumn,
+      .canModify #includedGroupSearchInput,
+      .canModify #saveIncludedGroups,
+      .canModify .deleteIncludedHeader,
+      .canModify #saveIncludedGroups {
+        display: none;
+      }
     </style>
-    <style include="gr-form-styles"></style>
-    <main class="gr-form-styles">
+    <main class$="gr-form-styles [[_computeHideItemClass(_groupOwner, _isAdmin)]]">
       <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
         Loading...
       </div>
@@ -84,15 +93,13 @@
                     id="groupMemberSearchInput"
                     text="{{_groupMemberSearch}}"
                     query="[[_queryMembers]]"
-                    placeholder="Name Or Email"
-                    hidden$="[[!_groupOwner]]">
+                    placeholder="Name Or Email">
                 </gr-autocomplete>
               </span>
               <gr-button
                   id="saveGroupMember"
                   on-tap="_handleSavingGroupMember"
-                  disabled="[[!_groupMemberSearch]]"
-                  hidden$="[[!_groupOwner]]">
+                  disabled="[[!_groupMemberSearch]]">
                 Add
               </gr-button>
               <div class="gr-form-styles">
@@ -100,9 +107,7 @@
                   <tr class="headerRow">
                     <th class="nameHeader">Name</th>
                     <th class="emailAddressHeader">Email Address</th>
-                    <th class="deleteHeader" hidden$="[[!_groupOwner]]">
-                      Delete Member
-                    </th>
+                    <th class="deleteHeader">Delete Member</th>
                   </tr>
                   <tbody>
                     <template is="dom-repeat" items="[[_groupMembers]]">
@@ -111,7 +116,7 @@
                           <gr-account-link account="[[item]]"></gr-account-link>
                         </td>
                         <td>[[item.email]]</td>
-                        <td hidden$="[[!_groupOwner]]">
+                        <td class="deleteColumn">
                           <gr-button
                               class="deleteButton"
                               on-tap="_handleDeleteMember">
@@ -133,15 +138,13 @@
                     id="includedGroupSearchInput"
                     text="{{_includedGroupSearch}}"
                     query="[[_queryIncludedGroup]]"
-                    placeholder="Group Name"
-                    hidden$="[[!_groupOwner]]">
+                    placeholder="Group Name">
                 </gr-autocomplete>
               </span>
               <gr-button
                   id="saveIncludedGroups"
                   on-tap="_handleSavingIncludedGroups"
-                  disabled="[[!_includedGroupSearch]]"
-                  hidden$="[[!_groupOwner]]">
+                  disabled="[[!_includedGroupSearch]]">
                 Add
               </gr-button>
               <div class="gr-form-styles">
@@ -149,7 +152,7 @@
                   <tr class="headerRow">
                     <th class="groupNameHeader">Group Name</th>
                     <th class="descriptionHeader">Description</th>
-                    <th class="deleteIncludedHeader" hidden$="[[!_groupOwner]]">
+                    <th class="deleteIncludedHeader">
                       Delete Included Group
                     </th>
                   </tr>
@@ -162,7 +165,7 @@
                           </a>
                         </td>
                         <td>[[item.description]]</td>
-                        <td hidden$="[[!_groupOwner]]">
+                        <td class="deleteColumn">
                           <gr-button
                               class="deleteIncludedGroupButton"
                               on-tap="_handleDeleteIncludedGroup">
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
index cde6c66..81bb902 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
@@ -48,6 +48,10 @@
         type: Boolean,
         value: false,
       },
+      _isAdmin: {
+        type: Boolean,
+        value: false,
+      },
     },
 
     behaviors: [
@@ -68,17 +72,29 @@
 
       return this.$.restAPI.getGroupConfig(this.groupId).then(
           config => {
+            if (!config.name) { return; }
+
             this._groupName = config.name;
+
+            promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
+              this._isAdmin = isAdmin ? true : false;
+            }));
+
             promises.push(this.$.restAPI.getIsGroupOwner(config.name)
-                .then(isOwner => { this._groupOwner = isOwner; }));
+                .then(isOwner => {
+                  this._groupOwner = isOwner ? true : false;
+                }));
+
             promises.push(this.$.restAPI.getGroupMembers(config.name).then(
                 members => {
                   this._groupMembers = members;
                 }));
+
             promises.push(this.$.restAPI.getIncludedGroup(config.name)
                 .then(includedGroup => {
                   this._includedGroups = includedGroup;
                 }));
+
             return Promise.all(promises).then(() => {
               this._loading = false;
             });
@@ -223,5 +239,9 @@
             return groups;
           });
     },
+
+    _computeHideItemClass(owner, admin) {
+      return admin || owner ? '' : 'canModify';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
index 76a182b..ef89e37 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
@@ -152,5 +152,23 @@
         done();
       });
     });
+
+    test('_computeHideItemClass returns string for admin', () => {
+      const admin = true;
+      const owner = false;
+      assert.equal(element._computeHideItemClass(owner, admin), '');
+    });
+
+    test('_computeHideItemClass returns hideItem for admin and owner', () => {
+      const admin = false;
+      const owner = false;
+      assert.equal(element._computeHideItemClass(owner, admin), 'canModify');
+    });
+
+    test('_computeHideItemClass returns string for owner', () => {
+      const admin = false;
+      const owner = true;
+      assert.equal(element._computeHideItemClass(owner, admin), '');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
index 2f3d59a..0449ffa 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
@@ -71,13 +71,15 @@
                 <gr-autocomplete
                     id="groupNameInput"
                     text="{{_groupConfig.name}}"
-                    disabled="[[!_groupOwner]]"></gr-autocomplete>
+                    disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]"></gr-autocomplete>
               </span>
-              <gr-button
-                  id="inputUpdateNameBtn"
-                  on-tap="_handleSaveName"
-                  disabled="[[_computeButtonDisabled(_groupOwner, _rename)]]">
-                Rename Group</gr-button>
+              <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
+                <gr-button
+                    id="inputUpdateNameBtn"
+                    on-tap="_handleSaveName"
+                    disabled="[[!_rename]]">
+                  Rename Group</gr-button>
+              </span>
             </fieldset>
             <h3 class$="[[_computeHeaderClass(_owner)]]">
               Owners
@@ -87,13 +89,15 @@
                 <gr-autocomplete
                     text="{{_groupConfig.owner}}"
                     query="[[_query]]"
-                    disabled$="[[!_groupOwner]]">
+                    disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
                 </gr-autocomplete>
               </span>
-              <gr-button
-                  on-tap="_handleSaveOwner"
-                  disabled="[[_computeButtonDisabled(_groupOwner, _owner)]]">
-                Change Owners</gr-button>
+              <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
+                <gr-button
+                    on-tap="_handleSaveOwner"
+                    disabled="[[!_owner]]">
+                  Change Owners</gr-button>
+              </span>
             </fieldset>
             <h3 class$="[[_computeHeaderClass(_description)]]">
               Description
@@ -104,14 +108,15 @@
                     class="description"
                     autocomplete="on"
                     bind-value="{{_groupConfig.description}}"
-                    disabled="[[!_groupOwner]]"></iron-autogrow-textarea>
+                    disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]"></iron-autogrow-textarea>
               </div>
-              <gr-button
-                  on-tap="_handleSaveDescription"
-                  disabled=
-                      "[[_computeButtonDisabled(_groupOwner, _description)]]">
-                Save Description
-              </gr-button>
+              <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
+                <gr-button
+                    on-tap="_handleSaveDescription"
+                    disabled="[[!_description]]">
+                  Save Description
+                </gr-button>
+              </span>
             </fieldset>
             <h3 id="options" class$="[[_computeHeaderClass(_options)]]">
               Group Options
@@ -124,7 +129,7 @@
                 <span class="value">
                   <gr-select
                       bind-value="{{_groupConfig.options.visible_to_all}}">
-                    <select disabled$="[[!_groupOwner]]">
+                    <select disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
                       <template is="dom-repeat" items="[[_submitTypes]]">
                         <option value="[[item.value]]">[[item.label]]</option>
                       </template>
@@ -132,12 +137,13 @@
                   </gr-select>
                 </span>
               </section>
-              <gr-button
-                  on-tap="_handleSaveOptions"
-                  disabled=
-                         "[[_computeButtonDisabled(_groupOwner, _options)]]">
-                Save Group Options
-              </gr-button>
+              <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
+                <gr-button
+                    on-tap="_handleSaveOptions"
+                    disabled="[[!_options]]">
+                  Save Group Options
+                </gr-button>
+              </span>
             </fieldset>
           </fieldset>
         </div>
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
index 55bf3b0..9e0e8c6 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
@@ -56,11 +56,6 @@
         type: Boolean,
         value: true,
       },
-      _loggedIn: {
-        type: Boolean,
-        value: false,
-        observer: '_loggedInChanged',
-      },
       /** @type {?} */
       _groupConfig: Object,
       _groupName: Object,
@@ -80,6 +75,10 @@
           return this._getGroupSuggestions.bind(this);
         },
       },
+      _isAdmin: {
+        type: Boolean,
+        value: false,
+      },
     },
 
     observers: [
@@ -98,12 +97,22 @@
 
       return this.$.restAPI.getGroupConfig(this.groupId).then(
           config => {
-            this._groupConfig = config;
             this._groupName = config.name;
-            this.fire('title-change', {title: config.name});
-            this._loading = false;
+
+            this.$.restAPI.getIsAdmin().then(isAdmin => {
+              this._isAdmin = isAdmin ? true : false;
+            });
+
             this.$.restAPI.getIsGroupOwner(config.name)
-                .then(isOwner => { this._groupOwner = isOwner; });
+                .then(isOwner => {
+                  this._groupOwner = isOwner ? true : false;
+                });
+
+            this._groupConfig = config;
+
+            this.fire('title-change', {title: config.name});
+
+            this._loading = false;
           });
     },
 
@@ -111,18 +120,10 @@
       return loading ? 'loading' : '';
     },
 
-    _loggedInChanged(_loggedIn) {
-      if (!_loggedIn) { return; }
-    },
-
     _isLoading() {
       return this._loading || this._loading === undefined;
     },
 
-    _getLoggedIn() {
-      return this.$.restAPI.getLoggedIn();
-    },
-
     _handleSaveName() {
       return this.$.restAPI.saveGroupName(this.groupId, this._groupConfig.name)
           .then(config => {
@@ -182,10 +183,6 @@
       this._options = true;
     },
 
-    _computeButtonDisabled(options, option) {
-      return !options || !option;
-    },
-
     _computeHeaderClass(configChanged) {
       return configChanged ? 'edited' : '';
     },
@@ -204,5 +201,9 @@
             return groups;
           });
     },
+
+    _computeGroupDisabled(owner, admin) {
+      return admin || owner ? false : true;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
index 75bd905..3c3b938 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
@@ -121,5 +121,29 @@
             done();
           });
     });
+
+    test('_computeGroupDisabled return false for admin', () => {
+      const admin = true;
+      const owner = false;
+      assert.equal(element._computeGroupDisabled(owner, admin), false);
+    });
+
+    test('_computeGroupDisabled return true for admin', () => {
+      const admin = false;
+      const owner = false;
+      assert.equal(element._computeGroupDisabled(owner, admin), true);
+    });
+
+    test('_computeGroupDisabled return false for owner', () => {
+      const admin = false;
+      const owner = true;
+      assert.equal(element._computeGroupDisabled(owner, admin), false);
+    });
+
+    test('_computeGroupDisabled return true for owner', () => {
+      const admin = false;
+      const owner = false;
+      assert.equal(element._computeGroupDisabled(owner, admin), true);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
index d5506ad..20fd147 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -16,6 +16,13 @@
 
   Polymer({
     is: 'gr-label-score-row',
+
+    /**
+     * Fired when any label is changed.
+     *
+     * @event labels-changed
+     */
+
     properties: {
       /**
        * @type {{ name: string }}
@@ -96,6 +103,7 @@
       // nothing and then to the new item.
       if (!e.target.selectedItem) { return; }
       this._selectedValueText = e.target.selectedItem.getAttribute('title');
+      this.fire('labels-changed');
     },
 
     _computeAnyPermittedLabelValues(permittedLabels, label) {
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
index 537bd25..da450e1 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
@@ -103,6 +103,8 @@
     });
 
     test('label picker', () => {
+      const labelsChangedHandler = sandbox.stub();
+      element.addEventListener('labels-changed', labelsChangedHandler);
       assert.ok(element.$$('iron-selector'));
       MockInteractions.tap(element.$$(
           'gr-button[value="-1"]'));
@@ -112,6 +114,7 @@
           .textContent.trim(), '-1');
       assert.strictEqual(
           element.$.selectedValueLabel.textContent.trim(), 'bad');
+      assert.isTrue(labelsChangedHandler.called);
     });
 
     test('correct item is selected', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 2f9f78c..24673ff 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -241,6 +241,7 @@
             id="labelScores"
             account="[[_account]]"
             change="[[change]]"
+            on-labels-changed="_handleLabelsChanged"
             permitted-labels=[[permittedLabels]]></gr-label-scores>
       </section>
       <section class="draftsContainer" hidden$="[[_computeHideDraftList(diffDrafts)]]">
@@ -265,7 +266,7 @@
       <section>
         <gr-button
             primary
-            disabled="[[_isState(knownLatestState, 'not-latest')]]"
+            disabled="[[_computeSendButtonDisabled(knownLatestState, _sendButtonLabel, diffDrafts, draft, _reviewersMutated, _labelsChanged)]]"
             class="action send"
             on-tap="_sendTapHandler">[[_sendButtonLabel]]</gr-button>
         <template is="dom-if" if="[[canBeStarted]]">
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 752f15a..9f4a79a 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
@@ -34,6 +34,11 @@
     NOT_LATEST: 'not-latest',
   };
 
+  const ButtonLabels = {
+    START_REVIEW: 'Start review',
+    SEND: 'Send',
+  };
+
   // TODO(logan): Remove once the fix for issue 6841 is stable on
   // googlesource.com.
   const START_REVIEW_MESSAGE = 'This change is ready for review.';
@@ -175,6 +180,14 @@
         computed: '_computeCCsEnabled(serverConfig)',
       },
       _savingComments: Boolean,
+      _reviewersMutated: {
+        type: Boolean,
+        value: false,
+      },
+      _labelsChanged: {
+        type: Boolean,
+        value: false,
+      },
     },
 
     FocusTarget,
@@ -268,12 +281,14 @@
 
     _ccsChanged(splices) {
       if (splices && splices.indexSplices) {
+        this._reviewersMutated = true;
         this._processReviewerChange(splices.indexSplices, ReviewerTypes.CC);
       }
     },
 
     _reviewersChanged(splices) {
       if (splices && splices.indexSplices) {
+        this._reviewersMutated = true;
         this._processReviewerChange(splices.indexSplices,
             ReviewerTypes.REVIEWER);
         let key;
@@ -768,6 +783,11 @@
       });
     },
 
+    _handleLabelsChanged() {
+      this._labelsChanged = Object.keys(
+          this.$.labelScores.getLabelValues()).length !== 0;
+    },
+
     _isState(knownLatestState, value) {
       return knownLatestState === value;
     },
@@ -778,7 +798,7 @@
     },
 
     _computeSendButtonLabel(canBeStarted) {
-      return canBeStarted ? 'Start review' : 'Send';
+      return canBeStarted ? ButtonLabels.START_REVIEW : ButtonLabels.SEND;
     },
 
     _computeCCsEnabled(serverConfig) {
@@ -788,5 +808,17 @@
     _computeSavingLabelClass(savingComments) {
       return savingComments ? 'saving' : '';
     },
+
+    _computeSendButtonDisabled(knownLatestState, buttonLabel, drafts, text,
+        reviewersMutated, labelsChanged) {
+      if (this._isState(knownLatestState, LatestPatchState.NOT_LATEST)) {
+        return true;
+      }
+      if (buttonLabel === ButtonLabels.START_REVIEW) {
+        return false;
+      }
+      return !(drafts.length || text.length || reviewersMutated ||
+          labelsChanged);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 0f90019..9a83259 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -55,6 +55,8 @@
       stub('gr-rest-api-interface', {
         getConfig() { return Promise.resolve({}); },
         getAccount() { return Promise.resolve({}); },
+        getChange() { return Promise.resolve({}); },
+        getChangeSuggestedReviewers() { return Promise.resolve([]); },
       });
 
       element = fixture('basic');
@@ -1007,5 +1009,20 @@
         });
       });
     });
+
+    test('_computeSendButtonDisabled', () => {
+      const fn = element._computeSendButtonDisabled.bind(element);
+      assert.isTrue(fn('not-latest'));
+      assert.isFalse(fn('latest', 'Start review'));
+      assert.isTrue(fn('latest', 'Send', [], '', false, false));
+      // Mock nonempty comment draft array.
+      assert.isFalse(fn('latest', 'Send', ['test'], '', false, false));
+      // Mock nonempty change message.
+      assert.isFalse(fn('latest', 'Send', [], 'test', false, false));
+      // Mock reviewers mutated.
+      assert.isFalse(fn('latest', 'Send', [], '', true, false));
+      // Mock labels changed.
+      assert.isFalse(fn('latest', 'Send', [], '', false, true));
+    });
   });
 </script>
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 93bddcb..3a724bb 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -447,7 +447,8 @@
       this._mapRoute(RoutePattern.GROUP_AUDIT_LOG, '_handleGroupAuditLogRoute',
           true);
 
-      this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute');
+      this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute',
+          true);
 
       this._mapRoute(RoutePattern.GROUP_LIST_OFFSET,
           '_handleGroupListOffsetRoute', true);
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index cb659f8..c294b22 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -133,6 +133,7 @@
         '_handleGroupListFilterOffsetRoute',
         '_handleGroupListFilterRoute',
         '_handleGroupListOffsetRoute',
+        '_handleGroupMembersRoute',
         '_handleGroupRoute',
         '_handlePluginListFilterOffsetRoute',
         '_handlePluginListFilterRoute',
@@ -153,7 +154,6 @@
         '_handleDefaultRoute',
         '_handleChangeLegacyRoute',
         '_handleDiffLegacyRoute',
-        '_handleGroupMembersRoute',
         '_handleImproperlyEncodedPlusRoute',
         '_handlePassThroughRoute',
         '_handleProjectAccessRoute',
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
index c93ed0c..43343de 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
@@ -49,6 +49,17 @@
           </tr>
         </thead>
         <tbody>
+          <tr>
+            <td>Number</td>
+            <td
+                class="checkboxContainer"
+                on-tap="_handleTargetTap">
+              <input
+                  type="checkbox"
+                  name="number"
+                  checked$="[[showNumber]]">
+            </td>
+          </tr>
           <template is="dom-repeat" items="[[columnNames]]">
             <tr>
               <td>[[item]]</td>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
index 50a1146..4d87f9e 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
@@ -22,6 +22,10 @@
         type: Array,
         notify: true,
       },
+      showNumber: {
+        type: Boolean,
+        notify: true,
+      },
     },
 
     behaviors: [
@@ -53,6 +57,12 @@
         // The target is the checkbox itself.
         checkbox = Polymer.dom(e).rootTarget;
       }
+
+      if (checkbox.name === 'number') {
+        this.showNumber = checkbox.checked;
+        return;
+      }
+
       this.set('displayedColumns',
           this._updateDisplayedColumns(
               this.displayedColumns, checkbox.name, checkbox.checked));
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
index 61093b9..00ef16f 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
@@ -62,15 +62,17 @@
       const rows = element.$$('tbody').querySelectorAll('tr');
       let tds;
 
-      assert.equal(rows.length, element.columnNames.length);
+      // The `+ 1` is for the number column, which isn't included in the change
+      // table behavior's list.
+      assert.equal(rows.length, element.columnNames.length + 1);
       for (let i = 0; i < columns.length; i++) {
-        tds = rows[i].querySelectorAll('td');
+        tds = rows[i + 1].querySelectorAll('td');
         assert.equal(tds[0].textContent, columns[i]);
       }
     });
 
     test('hide item', () => {
-      const checkbox = element.$$('table input');
+      const checkbox = element.$$('table tr:nth-child(2) input');
       const isChecked = checkbox.checked;
       const displayedLength = element.displayedColumns.length;
       assert.isTrue(isChecked);
@@ -78,8 +80,7 @@
       MockInteractions.tap(checkbox);
       flushAsynchronousOperations();
 
-      assert.equal(element.displayedColumns.length,
-          displayedLength - 1);
+      assert.equal(element.displayedColumns.length, displayedLength - 1);
     });
 
     test('show item', () => {
@@ -91,7 +92,7 @@
         'Updated',
       ]);
       flushAsynchronousOperations();
-      const checkbox = element.$$('table input');
+      const checkbox = element.$$('table tr:nth-child(2) input');
       const isChecked = checkbox.checked;
       const displayedLength = element.displayedColumns.length;
       assert.isFalse(isChecked);
@@ -105,9 +106,9 @@
     });
 
     test('_handleTargetTap', () => {
-      const checkbox = element.$$('table input');
+      const checkbox = element.$$('table tr:nth-child(2) input');
       let originalDisplayedColumns = element.displayedColumns;
-      const td = element.$$('table .checkboxContainer');
+      const td = element.$$('table tr:nth-child(2) .checkboxContainer');
       const displayedColumnStub =
           sandbox.stub(element, '_updateDisplayedColumns');
 
@@ -125,6 +126,20 @@
           checkbox.checked));
     });
 
+    test('_handleTargetTap on number', () => {
+      element.showNumber = false;
+      const checkbox = element.$$('table tr:nth-child(1) input');
+      const displayedColumnStub =
+          sandbox.stub(element, '_updateDisplayedColumns');
+
+      MockInteractions.tap(checkbox);
+      assert.isFalse(displayedColumnStub.called);
+      assert.isTrue(element.showNumber);
+
+      MockInteractions.tap(checkbox);
+      assert.isFalse(element.showNumber);
+    });
+
     test('_updateDisplayedColumns', () => {
       let name = 'Subject';
       let checked = false;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index d039a3b..cacccda 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -321,6 +321,7 @@
         </h2>
         <fieldset id="changeTableColumns">
           <gr-change-table-editor
+              show-number="{{_showNumber}}"
               displayed-columns="{{_localChangeTableColumns}}">
           </gr-change-table-editor>
           <gr-button
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index c3332b3..25dd259 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -121,6 +121,8 @@
        * For testing purposes.
        */
       _loadingPromise: Object,
+
+      _showNumber: Boolean,
     },
 
     behaviors: [
@@ -132,7 +134,7 @@
       '_handlePrefsChanged(_localPrefs.*)',
       '_handleDiffPrefsChanged(_diffPrefs.*)',
       '_handleMenuChanged(_localMenu.splices)',
-      '_handleChangeTableChanged(_localChangeTableColumns)',
+      '_handleChangeTableChanged(_localChangeTableColumns, _showNumber)',
     ],
 
     attached() {
@@ -147,6 +149,7 @@
 
       promises.push(this.$.restAPI.getPreferences().then(prefs => {
         this.prefs = prefs;
+        this._showNumber = !!prefs.legacycid_in_change_table;
         this._copyPrefs('_localPrefs', 'prefs');
         this._cloneMenu();
         this._cloneChangeTableColumns();
@@ -303,6 +306,7 @@
 
     _handleSaveChangeTable() {
       this.set('prefs.change_table', this._localChangeTableColumns);
+      this.set('prefs.legacycid_in_change_table', this._showNumber);
       this._cloneChangeTableColumns();
       return this.$.restAPI.savePreferences(this.prefs).then(() => {
         this._changeTableChanged = false;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index e1182c1..868a9e3 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -386,6 +386,25 @@
       assert.isTrue(element.$.emailEditor.loadData.calledOnce);
     });
 
+    test('_handleSaveChangeTable', () => {
+      let newColumns = ['Owner', 'Project', 'Branch'];
+      element._localChangeTableColumns = newColumns.slice(0);
+      element._showNumber = false;
+      const cloneStub = sandbox.stub(element, '_cloneChangeTableColumns');
+      element._handleSaveChangeTable();
+      assert.isTrue(cloneStub.calledOnce);
+      assert.deepEqual(element.prefs.change_table, newColumns);
+      assert.isNotOk(element.prefs.legacycid_in_change_table);
+
+      newColumns = ['Size'];
+      element._localChangeTableColumns = newColumns;
+      element._showNumber = true;
+      element._handleSaveChangeTable();
+      assert.isTrue(cloneStub.calledTwice);
+      assert.deepEqual(element.prefs.change_table, newColumns);
+      assert.isTrue(element.prefs.legacycid_in_change_table);
+    });
+
     suite('_getFilterDocsLink', () => {
       test('with http: docs base URL', () => {
         const base = 'http://example.com/';
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 a016037..06a3fcf 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
@@ -274,7 +274,7 @@
 
     getGroupConfig(group) {
       const encodeName = encodeURIComponent(group);
-      return this._fetchSharedCacheURL('/groups/' + encodeName + '/detail');
+      return this.fetchJSON(`/groups/${encodeName}/detail`);
     },
 
     /**
@@ -348,8 +348,8 @@
      */
     getIsGroupOwner(groupName) {
       const encodeName = encodeURIComponent(groupName);
-      return this._fetchSharedCacheURL('/groups/?owned&q=' + encodeName)
-          .then(configs => configs.hasOwnProperty(encodeName));
+      return this._fetchSharedCacheURL(`/groups/?owned&q=${encodeName}`)
+          .then(configs => configs.hasOwnProperty(groupName));
     },
 
     getGroupMembers(groupName) {
@@ -1645,7 +1645,7 @@
     setChangeTopic(changeNum, topic) {
       const p = {topic};
       return this.getChangeURLAndSend(changeNum, 'PUT', null, '/topic', p)
-          .then(this.getResponseObject);
+          .then(this.getResponseObject.bind(this));
     },
 
     /**
@@ -1655,7 +1655,7 @@
      */
     setChangeHashtag(changeNum, hashtag) {
       return this.getChangeURLAndSend(changeNum, 'POST', null, '/hashtags',
-          hashtag).then(this.getResponseObject);
+          hashtag).then(this.getResponseObject.bind(this));
     },
 
     deleteAccountHttpPassword() {
@@ -1669,7 +1669,7 @@
      */
     generateAccountHttpPassword() {
       return this.send('PUT', '/accounts/self/password.http', {generate: true})
-          .then(this.getResponseObject);
+          .then(this.getResponseObject.bind(this));
     },
 
     getAccountSSHKeys() {
@@ -1772,7 +1772,7 @@
       const endpoint = `/comments/${commentID}/delete`;
       const payload = {reason};
       return this.getChangeURLAndSend(changeNum, 'POST', patchNum, endpoint,
-          payload).then(this.getResponseObject);
+          payload).then(this.getResponseObject.bind(this));
     },
 
     /**
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 3f47ea8..eb4f418 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -958,5 +958,29 @@
         assert.deepEqual(result, obj);
       });
     });
+
+    test('setChangeTopic', () => {
+      const sendSpy = sandbox.spy(element, 'getChangeURLAndSend');
+      return element.setChangeTopic(123, 'foo-bar').then(() => {
+        assert.isTrue(sendSpy.calledOnce);
+        assert.deepEqual(sendSpy.lastCall.args[4], {topic: 'foo-bar'});
+      });
+    });
+
+    test('setChangeHashtag', () => {
+      const sendSpy = sandbox.spy(element, 'getChangeURLAndSend');
+      return element.setChangeHashtag(123, 'foo-bar').then(() => {
+        assert.isTrue(sendSpy.calledOnce);
+        assert.equal(sendSpy.lastCall.args[4], 'foo-bar');
+      });
+    });
+
+    test('generateAccountHttpPassword', () => {
+      const sendSpy = sandbox.spy(element, 'send');
+      return element.generateAccountHttpPassword().then(() => {
+        assert.isTrue(sendSpy.calledOnce);
+        assert.deepEqual(sendSpy.lastCall.args[2], {generate: true});
+      });
+    });
   });
 </script>