Support assignee field in PolyGerrit

Adds max-count prop to gr-account-list and utilizes it to create the
Assignee field in gr-change-metadata with autocompletion.

Feature: Issue 5229
Change-Id: Ifc796703322ff6d68040d10e0294e7aa8f6ce0ea
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
index 43714f2..4ad9053 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
@@ -49,7 +49,7 @@
     </template>
     <gr-account-entry
         borderless
-        hidden$="[[readonly]]"
+        hidden$="[[_computeEntryHidden(maxCount, accounts.*, readonly)]]"
         id="entry"
         change="[[change]]"
         filter="[[filter]]"
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 89820fc..f7e8fde 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
@@ -31,7 +31,7 @@
         value: null,
         notify: true,
       },
-      readonly:  {
+      readonly: {
         type: Boolean,
         value: false,
       },
@@ -40,6 +40,10 @@
        * undefined, all values are removable.
        */
       removableValues: Array,
+      maxCount: {
+        type: Number,
+        value: 0,
+      },
     },
 
     listeners: {
@@ -198,5 +202,9 @@
         }
       });
     },
+
+    _computeEntryHidden: function(maxCount, accountsRecord, readonly) {
+      return (maxCount && maxCount <= accountsRecord.base.length) || readonly;
+    },
   });
 })();
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 9b32856..a334b03 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
@@ -250,6 +250,20 @@
       assert.equal(element.accounts.length, 1);
     });
 
+    test('max-count', function() {
+      element.maxCount = 1;
+      var acct = makeAccount();
+      element._handleAdd({
+        detail: {
+          value: {
+            account: acct,
+          },
+        },
+      });
+      flushAsynchronousOperations();
+      assert.isTrue(element.$.entry.hasAttribute('hidden'));
+    });
+
     suite('keyboard interactions', function() {
 
       test('backspace at text input start removes last account', function() {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index ce17b3a..4cb2d19 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -107,6 +107,19 @@
     </section>
     <template is="dom-if" if="[[_showReviewersByState]]">
       <section>
+        <span class="title">Assignee</span>
+        <span class="value">
+          <gr-account-list
+              max-count="1"
+              id="assigneeValue"
+              placeholder="Add assignee..."
+              accounts="{{_assignee}}"
+              filter="[[_filterAssigneeSuggestion]]"
+              change="[[change]]"
+              readonly="[[!mutable]]"></gr-account-list>
+        </span>
+      </section>
+      <section>
         <span class="title">Reviewers</span>
         <span class="value">
           <gr-reviewer-list
@@ -127,6 +140,19 @@
     </template>
     <template is="dom-if" if="[[!_showReviewersByState]]">
       <section>
+        <span class="title">Assignee</span>
+        <span class="value">
+          <gr-account-list
+              max-count="1"
+              id="assigneeValue"
+              placeholder="Add assignee..."
+              accounts="{{_assignee}}"
+              filter="[[_filterAssigneeSuggestion]]"
+              change="[[change]]"
+              readonly="[[!mutable]]"></gr-account-list>
+        </span>
+      </section>
+      <section>
         <span class="title">Reviewers</span>
         <span class="value">
           <gr-reviewer-list
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index b56324a..16915ab 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -41,12 +41,48 @@
         type: Boolean,
         computed: '_computeShowLabelStatus(change)',
       },
+      /**
+       * Groups are not valid assignees.
+       */
+      _filterAssigneeSuggestion: {
+        type: Function,
+        value: function() {
+          return function(suggestion) { return suggestion.account; };
+        },
+      },
+
+      _assignee: Array,
     },
 
     behaviors: [
       Gerrit.RESTClientBehavior,
     ],
 
+    observers: [
+      '_changeChanged(change)',
+      '_assigneeChanged(_assignee.*)',
+    ],
+
+    _changeChanged: function(change) {
+      this._assignee = change.assignee ? [change.assignee] : [];
+    },
+
+    _assigneeChanged: function(assigneeRecord) {
+      if (!this.change) { return; }
+      var assignee = assigneeRecord.base;
+      if (assignee.length) {
+        var acct = assignee[0];
+        if (this.change.assignee &&
+            acct._account_id === this.change.assignee._account_id) { return; }
+        this.set(['change', 'assignee'], acct);
+        this.$.restAPI.setAssignee(this.change.change_id, acct._account_id);
+      } else {
+        if (!this.change.assignee) { return; }
+        this.set(['change', 'assignee'], undefined);
+        this.$.restAPI.deleteAssignee(this.change.change_id);
+      }
+    },
+
     _computeHideStrategy: function(change) {
       return !this.changeIsOpen(change.status);
     },
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index d354fd7..b2c0fc9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -182,6 +182,41 @@
         assert.equal(element.change.topic, '');
         assert.isTrue(topicStub.called);
       });
+
+      suite('assignee field', function() {
+        var dummyAccount = {
+          _account_id: 1,
+          name: 'bojack',
+        };
+        var deleteStub;
+        var setStub;
+        setup(function() {
+          deleteStub = sandbox.stub(element.$.restAPI, 'deleteAssignee');
+          setStub = sandbox.stub(element.$.restAPI, 'setAssignee');
+        });
+
+        test('changing change recomputes _assignee', function() {
+          assert.isFalse(!!element._assignee.length);
+          var change = element.change;
+          change.assignee = dummyAccount;
+          element._changeChanged(change);
+          assert.deepEqual(element._assignee[0], dummyAccount);
+        });
+
+        test('modifying _assignee calls API', function() {
+          assert.isFalse(!!element._assignee.length);
+          element.set('_assignee', [dummyAccount]);
+          assert.isTrue(setStub.calledOnce);
+          assert.deepEqual(element.change.assignee, dummyAccount);
+          element.set('_assignee', [dummyAccount]);
+          assert.isTrue(setStub.calledOnce);
+          element.set('_assignee', []);
+          assert.isTrue(deleteStub.calledOnce);
+          assert.equal(element.change.assignee, undefined);
+          element.set('_assignee', []);
+          assert.isTrue(deleteStub.calledOnce);
+        });
+      });
     });
   });
 </script>
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 4b52503..96661fd 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
@@ -952,5 +952,16 @@
             return null;
           });
     },
+
+    setAssignee: function(changeNum, assignee) {
+      return this.send('PUT',
+          this.getChangeActionURL(changeNum, null, '/assignee'),
+          {assignee: assignee});
+    },
+
+    deleteAssignee: function(changeNum) {
+      return this.send('DELETE',
+          this.getChangeActionURL(changeNum, null, '/assignee'));
+    },
   });
 })();