Allows topic editing in the change view

Adds an editable text label to the gr-change-metadata for modifying the
topic of a change.  Introduces the gr-editable-label as a UI element for
editable text labels.  Editing the topic is only allowed if the user is
logged in and has the topic-edit permission.

This is a redo of change 78110, which had been rebased wrong and had to
be reverted. https://gerrit-review.googlesource.com/c/78110/

Bug: Issue 4102
Change-Id: Ia7bb7b803a9fdd15a5f9f0f8eecf77bdd3ee4ba7
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 98e8e9a..e28768e 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
@@ -18,6 +18,8 @@
 <link rel="import" href="../../../behaviors/rest-client-behavior.html">
 <link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
+<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html">
 
 <dom-module id="gr-change-metadata">
@@ -117,7 +119,13 @@
     </section>
     <section>
       <span class="title">Topic</span>
-      <span class="value">[[change.topic]]</span>
+      <span class="value">
+        <gr-editable-label
+            value="{{change.topic}}"
+            placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]"
+            read-only="[[_topicReadOnly]]"
+            on-changed="_handleTopicChanged"></gr-editable-label>
+      </span>
     </section>
     <section class="strategy" hidden$="[[_computeHideStrategy(change)]]" hidden>
       <span class="title">Strategy</span>
@@ -141,6 +149,7 @@
         </span>
       </section>
     </template>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-change-metadata.js"></script>
 </dom-module>
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 825b88b..5004a95 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
@@ -38,6 +38,10 @@
         type: String,
         computed: '_computeWebLink(change, commitInfo, serverConfig)',
       },
+      _topicReadOnly: {
+        type: Boolean,
+        computed: '_computeTopicReadOnly(mutable, change)',
+      },
     },
 
     behaviors: [
@@ -102,5 +106,18 @@
       });
       return result;
     },
+
+    _handleTopicChanged: function(e, topic) {
+      if (!topic.length) { topic = null; }
+      this.$.restAPI.setChangeTopic(this.change.id, topic);
+    },
+
+    _computeTopicReadOnly: function(mutable, change) {
+      return !mutable || !change.actions.topic || !change.actions.topic.enabled;
+    },
+
+    _computeTopicPlaceholder: function(_topicReadOnly) {
+      return _topicReadOnly ? 'No Topic' : 'Click to add topic';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
new file mode 100644
index 0000000..33180d8
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
@@ -0,0 +1,48 @@
+<!--
+Copyright (C) 2016 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="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<dom-module id="gr-editable-label">
+  <template>
+    <style>
+      input {
+        font: inherit;
+        max-width: 8em;
+      }
+      label {
+        color: #777;
+      }
+      label.editable {
+        cursor: pointer;
+      }
+      label.editable.placeholder {
+        color: #00f;
+        text-decoration: underline;
+      }
+    </style>
+    <input
+        is="iron-input"
+        id="input"
+        hidden$="[[!editing]]"
+        on-keydown="_handleInputKeydown"
+        bind-value="{{_inputText}}">
+    <label
+        hidden$="[[editing]]"
+        class$="[[_computeLabelClass(readOnly, value, placeholder)]]"
+        on-tap="_open">[[_computeLabel(value, placeholder)]]</label>
+  </template>
+  <script src="gr-editable-label.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
new file mode 100644
index 0000000..c87535a
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -0,0 +1,104 @@
+// Copyright (C) 2016 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-editable-label',
+
+    /**
+     * Fired when the value is changed.
+     *
+     * @event changed
+     */
+
+    properties: {
+      editing: {
+        type: Boolean,
+        value: false,
+      },
+      value: {
+        type: String,
+        notify: true,
+        value: null,
+      },
+      placeholder: {
+        type: String,
+        value: null,
+      },
+      readOnly: {
+        type: Boolean,
+        value: false,
+      },
+      _inputText: String,
+    },
+
+    _usePlaceholder: function(value, placeholder) {
+      return (!value || !value.length) && placeholder;
+    },
+
+    _computeLabel: function(value, placeholder) {
+      if (this._usePlaceholder(value, placeholder)) {
+        return placeholder;
+      }
+      return value;
+    },
+
+    _open: function() {
+      if (this.readOnly || this.editing) { return; }
+
+      this._inputText = this.value;
+      this.editing = true;
+
+      this.async(function() {
+        this.$.input.focus();
+        this.$.input.setSelectionRange(0, this.$.input.value.length)
+      });
+    },
+
+    _save: function() {
+      if (!this.editing) { return; };
+
+      this.value = this._inputText;
+      this.editing = false;
+      this.fire('changed', this.value);
+    },
+
+    _cancel: function() {
+      if (!this.editing) { return; }
+
+      this.editing = false;
+      this._inputText = this.value;
+    },
+
+    _handleInputKeydown: function(e) {
+      if (e.keyCode === 13) {  // Enter key
+        e.preventDefault();
+        this._save();
+      } else if (e.keyCode === 27) { // Escape key
+        e.preventDefault();
+        this._cancel();
+      }
+    },
+
+    _computeLabelClass: function(readOnly, value, placeholder) {
+      var classes = [];
+      if (!readOnly) { classes.push('editable'); }
+      if (this._usePlaceholder(value, placeholder)) {
+        classes.push('placeholder');
+      }
+      return classes.join(' ');
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
new file mode 100644
index 0000000..f46d677
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 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-editable-label</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/iron-test-helpers/mock-interactions.js"></script>
+
+<link rel="import" href="gr-editable-label.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-editable-label
+        value="value text"
+        placeholder="label text"></gr-editable-label>
+  </template>
+</test-fixture>
+
+<test-fixture id="read-only">
+  <template>
+    <gr-editable-label
+        read-only
+        value="value text"
+        placeholder="label text"></gr-editable-label>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-linked-text tests', function() {
+    var element;
+    var input;
+    var label;
+
+    setup(function() {
+      element = fixture('basic');
+
+      input = element.$$('input');
+      label = element.$$('label');
+    });
+
+    test('element render', function() {
+      // The input is hidden and the label is visible:
+      assert.isNotNull(input.getAttribute('hidden'));
+      assert.isNull(label.getAttribute('hidden'));
+
+      assert.isTrue(label.classList.contains('editable'));
+
+      assert.equal(label.textContent, 'value text');
+
+      MockInteractions.tap(label);
+
+      Polymer.dom.flush();
+
+      // The input is visible and the label is hidden:
+      assert.isNull(input.getAttribute('hidden'));
+      assert.isNotNull(label.getAttribute('hidden'));
+
+      assert.equal(input.value, 'value text');
+    });
+
+    test('edit value', function(done) {
+      var editedStub = sinon.stub();
+      element.addEventListener('changed', editedStub);
+
+      MockInteractions.tap(label);
+
+      Polymer.dom.flush();
+
+      element._inputText = 'new text';
+
+      assert.isFalse(editedStub.called);
+
+      element.async(function() {
+        assert.isTrue(editedStub.called);
+        assert.equal(input.value, 'new text');
+        done();
+      });
+
+      // Press enter:
+      MockInteractions.keyDownOn(input, 13);
+    });
+  });
+
+  suite('gr-linked-text tests', function() {
+    var element;
+    var input;
+    var label;
+
+    setup(function() {
+      element = fixture('read-only');
+
+      input = element.$$('input');
+      label = element.$$('label');
+    });
+
+    test('disallows edit when read-only', function() {
+      // The input is hidden and the label is visible:
+      assert.isNotNull(input.getAttribute('hidden'));
+      assert.isNull(label.getAttribute('hidden'));
+
+      MockInteractions.tap(label);
+
+      Polymer.dom.flush();
+
+      // The input is still hidden and the label is still visible:
+      assert.isNotNull(input.getAttribute('hidden'));
+      assert.isNull(label.getAttribute('hidden'));
+    });
+
+    test('label is not marked as editable', function() {
+      assert.isFalse(label.classList.contains('editable'));
+    });
+  });
+</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 9604ded..162c954 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
@@ -761,5 +761,10 @@
           return {baseImage: baseImage, revisionImage: revisionImage};
         }.bind(this));
     },
+
+    setChangeTopic: function(changeNum, topic) {
+      return this.send('PUT', '/changes/' + encodeURIComponent(changeNum) +
+          '/topic', {topic: topic});
+    },
   });
 })();
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 781f457..171ec07 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -60,6 +60,7 @@
     'shared/gr-cursor-manager/gr-cursor-manager_test.html',
     'shared/gr-date-formatter/gr-date-formatter_test.html',
     'shared/gr-js-api-interface/gr-js-api-interface_test.html',
+    'shared/gr-editable-label/gr-editable-label_test.html',
     'shared/gr-linked-text/gr-linked-text_test.html',
     'shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
     'shared/gr-storage/gr-storage_test.html',