Merge "Update the patchset description editor to use a linked chip"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
index da3a547..f46af09 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
@@ -21,6 +21,7 @@
 <link rel="import" href="../../diff/gr-patch-range-select/gr-patch-range-select.html">
 <link rel="import" href="../../edit/gr-edit-controls/gr-edit-controls.html">
 <link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
+<link rel="import" href="../../shared/gr-linked-chip/gr-linked-chip.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-select/gr-select.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -100,6 +101,9 @@
       gr-button.selected iron-icon {
         color: var(--color-link);
       }
+      gr-linked-chip {
+        --linked-chip-text-color: black;
+      }
       .expanded #collapseBtn,
       .openFile .fileViewActions {
         align-items: center;
@@ -164,14 +168,28 @@
           </span>
           <span class="container descriptionContainer hideOnEdit">
             <span class="separator"></span>
-            <gr-editable-label
-                id="descriptionLabel"
-                class="descriptionLabel"
-                label-text="Add patchset description"
-                value="[[_computePatchSetDescription(change, patchNum)]]"
-                placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]"
-                read-only="[[_descriptionReadOnly]]"
-                on-changed="_handleDescriptionChanged"></gr-editable-label>
+            <template
+                is="dom-if"
+                if="[[_patchsetDescription]]">
+              <gr-linked-chip
+                  id="descriptionChip"
+                  text="[[_patchsetDescription]]"
+                  removable="[[!_descriptionReadOnly]]"
+                  on-remove="_handleDescriptionRemoved"></gr-linked-chip>
+            </template>
+            <template
+                is="dom-if"
+                if="[[!_patchsetDescription]]">
+              <gr-editable-label
+                  id="descriptionLabel"
+                  uppercase
+                  class="descriptionLabel"
+                  label-text="Add patchset description"
+                  value="[[_patchsetDescription]]"
+                  placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]"
+                  read-only="[[_descriptionReadOnly]]"
+                  on-changed="_handleDescriptionChanged"></gr-editable-label>
+            </template>
           </span>
         </div>
         <div class$="rightControls [[_computeExpandedClass(filesExpanded)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
index e0b4d6c..123b156 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -48,6 +48,10 @@
         readOnly: true,
         value: 225,
       },
+      _patchsetDescription: {
+        type: String,
+        value: '',
+      },
       _descriptionReadOnly: {
         type: Boolean,
         computed: '_computeDescriptionReadOnly(loggedIn, change, account)',
@@ -67,6 +71,10 @@
       Gerrit.PatchSetBehavior,
     ],
 
+    observers: [
+      '_computePatchSetDescription(change, patchNum)',
+    ],
+
     _expandAllDiffs() {
       this._expanded = true;
       this.fire('expand-diffs');
@@ -111,10 +119,14 @@
 
     _computePatchSetDescription(change, patchNum) {
       const rev = this.getRevisionByPatchNum(change.revisions, patchNum);
-      return (rev && rev.description) ?
+      this._patchsetDescription = (rev && rev.description) ?
           rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
     },
 
+    _handleDescriptionRemoved(e) {
+      return this._updateDescription('', e);
+    },
+
     /**
      * @param {!Object} revisions The revisions object keyed by revision hashes
      * @param {?Object} patchSet A revision already fetched from {revisions}
@@ -131,15 +143,31 @@
 
     _handleDescriptionChanged(e) {
       const desc = e.detail.trim();
+      this._updateDescription(desc, e);
+    },
+
+    /**
+     * Update the patchset description with the rest API.
+     * @param {string} desc
+     * @param {?(Event|Node)} e
+     * @return {!Promise}
+     */
+    _updateDescription(desc, e) {
+      const target = Polymer.dom(e).rootTarget;
+      if (target) { target.disabled = true; }
       const rev = this.getRevisionByPatchNum(this.change.revisions,
           this.patchNum);
       const sha = this._getPatchsetHash(this.change.revisions, rev);
-      this.$.restAPI.setDescription(this.changeNum,
-          this.patchNum, desc)
+      return this.$.restAPI.setDescription(this.changeNum, this.patchNum, desc)
           .then(res => {
             if (res.ok) {
+              if (target) { target.disabled = false; }
               this.set(['_change', 'revisions', sha, 'description'], desc);
+              this._patchsetDescription = desc;
             }
+          }).catch(err => {
+            if (target) { target.disabled = false; }
+            return;
           });
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
index d47023f..8dbb06f 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -96,10 +96,9 @@
           'Add patchset description');
     });
 
-    test('_handleDescriptionChanged', () => {
+    test('description editing', () => {
       const putDescStub = sandbox.stub(element.$.restAPI, 'setDescription')
           .returns(Promise.resolve({ok: true}));
-      sandbox.stub(element, '_computeDescriptionReadOnly');
 
       element.changeNum = '42';
       element.basePatchNum = 'PARENT';
@@ -120,14 +119,44 @@
       element.loggedIn = true;
 
       flushAsynchronousOperations();
-      const label = element.$.descriptionLabel;
-      assert.equal(label.value, 'test');
-      label.editing = true;
-      label._inputText = 'test2';
-      label._save();
-      flushAsynchronousOperations();
-      assert.isTrue(putDescStub.called);
-      assert.equal(putDescStub.args[0][2], 'test2');
+
+      // The element has a description, so the account chip should be visible
+      // and the description label should not exist.
+      const chip = Polymer.dom(element.root).querySelector('#descriptionChip');
+      let label = Polymer.dom(element.root).querySelector('#descriptionLabel');
+
+      assert.equal(chip.text, 'test');
+      assert.isNotOk(label);
+
+      // Simulate tapping the remove button, but call function directly so that
+      // can determine what happens after the promise is resolved.
+      return element._handleDescriptionRemoved().then(() => {
+        // The API stub should be called with an empty string for the new
+        // description.
+        assert.equal(putDescStub.lastCall.args[2], '');
+
+        flushAsynchronousOperations();
+        // The editable label should now be visible and the chip hidden.
+        label = Polymer.dom(element.root).querySelector('#descriptionLabel');
+        assert.isOk(label);
+        assert.equal(getComputedStyle(chip).display, 'none');
+        assert.notEqual(getComputedStyle(label).display, 'none');
+        assert.isFalse(label.readOnly);
+        // Edit the label to have a new value of test2, and save.
+        label.editing = true;
+        label._inputText = 'test2';
+        label._save();
+        flushAsynchronousOperations();
+        // The API stub should be called with an `test2` for the new
+        // description.
+        assert.equal(putDescStub.callCount, 2);
+        assert.equal(putDescStub.lastCall.args[2], 'test2');
+      }).then(() => {
+        flushAsynchronousOperations();
+        // The chip should be visible again, and the label hidden.
+        assert.equal(getComputedStyle(label).display, 'none');
+        assert.notEqual(getComputedStyle(chip).display, 'none');
+      });
     });
 
     test('expandAllDiffs called when expand button clicked', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
index d30bad2..6159d22 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
@@ -68,6 +68,9 @@
         opacity: .6;
         pointer-events: none;
       }
+      a {
+       color: var(--linked-chip-text-color);
+      }
     </style>
     <div class$="container [[_getBackgroundClass(transparentBackground)]]">
       <a href$="[[href]]">