Merge "Merge branch 'stable-2.14' into stable-2.15" into stable-2.15
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index c373cbc..4234ab2 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -34,10 +34,10 @@
 == Getting started
 
 To get started with the development of a plugin clone the sample
-plugin:
+plugins:
 
 ----
-$ git clone https://gerrit.googlesource.com/plugins/cookbook-plugin
+$ git clone https://gerrit.googlesource.com/plugins/examples
 ----
 
 This is a project that demonstrates the various features of the
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 4de9a95..a76904e 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
@@ -117,7 +117,7 @@
                     <td>[[item.email]]</td>
                     <td class="deleteColumn">
                       <gr-button
-                          class="deleteButton"
+                          class="deleteMembersButton"
                           on-tap="_handleDeleteMember">
                         Delete
                       </gr-button>
@@ -132,7 +132,8 @@
             <span class="value">
               <gr-autocomplete
                   id="includedGroupSearchInput"
-                  text="{{_includedGroupSearch}}"
+                  text="{{_includedGroupSearchName}}"
+                  value="{{_includedGroupSearchId}}"
                   query="[[_queryIncludedGroup]]"
                   placeholder="Group Name">
               </gr-autocomplete>
@@ -140,7 +141,7 @@
             <gr-button
                 id="saveIncludedGroups"
                 on-tap="_handleSavingIncludedGroups"
-                disabled="[[!_includedGroupSearch]]">
+                disabled="[[!_includedGroupSearchId]]">
               Add
             </gr-button>
             <table id="includedGroups">
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 5257e69..14ffa8b 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
@@ -27,7 +27,8 @@
       groupId: Number,
       _groupMemberSearchId: String,
       _groupMemberSearchName: String,
-      _includedGroupSearch: String,
+      _includedGroupSearchId: String,
+      _includedGroupSearchName: String,
       _loading: {
         type: Boolean,
         value: true,
@@ -136,6 +137,7 @@
             this.$.restAPI.getGroupMembers(this._groupName).then(members => {
               this._groupMembers = members;
             });
+            this._groupMemberSearchName = '';
             this._groupMemberSearchId = '';
           });
     },
@@ -155,9 +157,9 @@
             });
       } else if (this._itemType === 'includedGroup') {
         return this.$.restAPI.deleteIncludedGroup(this._groupName,
-            this._itemName)
+            this._itemId)
             .then(itemDeleted => {
-              if (itemDeleted.status === 204) {
+              if (itemDeleted.status === 204 || itemDeleted.status === 205) {
                 this.$.restAPI.getIncludedGroup(this._groupName)
                     .then(includedGroup => {
                       this._includedGroups = includedGroup;
@@ -188,7 +190,7 @@
 
     _handleSavingIncludedGroups() {
       return this.$.restAPI.saveIncludedGroup(this._groupName,
-          this._includedGroupSearch, err => {
+          this._includedGroupSearchId, err => {
             if (err.status === 404) {
               this.dispatchEvent(new CustomEvent('show-alert', {
                 detail: {message: SAVING_ERROR_TEXT},
@@ -206,16 +208,18 @@
                 .then(includedGroup => {
                   this._includedGroups = includedGroup;
                 });
-            this._includedGroupSearch = '';
+            this._includedGroupSearchName = '';
+            this._includedGroupSearchId = '';
           });
     },
 
     _handleDeleteIncludedGroup(e) {
+      const id = decodeURIComponent(e.model.get('item.id'));
       const name = e.model.get('item.name');
-      if (!name) {
-        return '';
-      }
-      this._itemName = name;
+      const item = name || id;
+      if (!item) { return ''; }
+      this._itemName = item;
+      this._itemId = id;
       this._itemType = 'includedGroup';
       this.$.overlay.open();
     },
@@ -252,7 +256,7 @@
               if (!response.hasOwnProperty(key)) { continue; }
               groups.push({
                 name: key,
-                value: response[key],
+                value: decodeURIComponent(response[key].id),
               });
             }
             return groups;
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 d670d4d..9d7a797 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
@@ -114,6 +114,20 @@
             return Promise.resolve({});
           }
         },
+        getSuggestedGroups(input) {
+          if (input.startsWith('test')) {
+            return Promise.resolve({
+              'test-admin': {
+                id: '1ce023d3fb4e4260776fb92cd08b52bbd21ce70a',
+              },
+              'test/Administrator (admin)': {
+                id: 'test%3Aadmin',
+              },
+            });
+          } else {
+            return Promise.resolve({});
+          }
+        },
         getLoggedIn() { return Promise.resolve(true); },
         getConfig() {
           return Promise.resolve();
@@ -156,7 +170,7 @@
           'https://test/site/group/url');
     });
 
-    test('save correctly', () => {
+    test('save members correctly', () => {
       element._groupOwner = true;
 
       const memberName = 'test-admin';
@@ -166,7 +180,7 @@
             return Promise.resolve({});
           });
 
-      const button = Polymer.dom(element.root).querySelector('gr-button');
+      const button = element.$.saveGroupMember;
 
       assert.isTrue(button.hasAttribute('disabled'));
 
@@ -183,6 +197,33 @@
       });
     });
 
+    test('save included groups correctly', () => {
+      element._groupOwner = true;
+
+      const includedGroupName = 'testName';
+
+      const saveIncludedGroupStub = sandbox.stub(
+          element.$.restAPI, 'saveIncludedGroup', () => {
+            return Promise.resolve({});
+          });
+
+      const button = element.$.saveIncludedGroups;
+
+      assert.isTrue(button.hasAttribute('disabled'));
+
+      element.$.includedGroupSearchInput.text = includedGroupName;
+      element.$.includedGroupSearchInput.value = 'testId';
+
+      assert.isFalse(button.hasAttribute('disabled'));
+
+      return element._handleSavingIncludedGroups().then(() => {
+        assert.isTrue(button.hasAttribute('disabled'));
+        assert.isFalse(element.$.Title.classList.contains('edited'));
+        assert.equal(saveIncludedGroupStub.lastCall.args[0], 'Administrators');
+        assert.equal(saveIncludedGroupStub.lastCall.args[1], 'testId');
+      });
+    });
+
     test('add included group 404 shows helpful error text', () => {
       element._groupOwner = true;
 
@@ -217,6 +258,20 @@
       });
     });
 
+    test('_getGroupSuggestions empty', () => {
+      return element._getGroupSuggestions('nonexistent').then(groups => {
+        assert.equal(groups.length, 0);
+      });
+    });
+
+    test('_getGroupSuggestions non-empty', () => {
+      return element._getGroupSuggestions('test').then(groups => {
+        assert.equal(groups.length, 2);
+        assert.equal(groups[0].name, 'test-admin');
+        assert.equal(groups[1].name, 'test/Administrator (admin)');
+      });
+    });
+
     test('_computeHideItemClass returns string for admin', () => {
       const admin = true;
       const owner = false;
@@ -237,7 +292,7 @@
 
     test('delete member', () => {
       const deletelBtns = Polymer.dom(element.root)
-          .querySelectorAll('.deleteButton');
+          .querySelectorAll('.deleteMembersButton');
       MockInteractions.tap(deletelBtns[0]);
       assert.equal(element._itemId, '1000097');
       assert.equal(element._itemName, 'jane');
@@ -251,5 +306,19 @@
       assert.equal(element._itemId, '1000098');
       assert.equal(element._itemName, '1000098');
     });
+
+    test('delete included groups', () => {
+      const deletelBtns = Polymer.dom(element.root)
+          .querySelectorAll('.deleteIncludedGroupButton');
+      MockInteractions.tap(deletelBtns[0]);
+      assert.equal(element._itemId, 'testId');
+      assert.equal(element._itemName, 'testName');
+      MockInteractions.tap(deletelBtns[1]);
+      assert.equal(element._itemId, 'testId2');
+      assert.equal(element._itemName, 'testName2');
+      MockInteractions.tap(deletelBtns[2]);
+      assert.equal(element._itemId, 'testId3');
+      assert.equal(element._itemName, 'testName3');
+    });
   });
 </script>