Add submit button to PolyGerrit

+ Lays the groundwork for other actions as well.
+ Another change seemingly unrelated is within gr-reply-dropdown,
  which is in here because when a change is submitted, the
  permittedLabels property on the change goes to {}, which needs
  to be accounted for.

Change-Id: I14c4c62ad7e52df6d56894abb38b708d81964d1b
diff --git a/polygerrit-ui/app/elements/gr-change-actions.html b/polygerrit-ui/app/elements/gr-change-actions.html
new file mode 100644
index 0000000..c37a931
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-change-actions.html
@@ -0,0 +1,138 @@
+<!--
+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="gr-ajax.html">
+<link rel="import" href="gr-request.html">
+
+<dom-module id="gr-change-actions">
+  <template>
+    <style>
+      :host {
+        display: block;
+      }
+      button {
+        background-color: #448aff;
+        border: none;
+        border-radius: 2px;
+        color: #fff;
+        cursor: pointer;
+        font: inherit;
+        padding: .5em .75em;
+      }
+      button[disabled] {
+        opacity: .5;
+      }
+    </style>
+    <gr-ajax id="actionsXHR"
+        url="[[_computeActionsPath(changeNum, patchNum)]]"
+        last-response="{{_actions}}"
+        loading="{{_loading}}"></gr-ajax>
+    <div>
+      <template is="dom-repeat" items="[[_computeActionValues(_actions)]]" as="action">
+        <button title$="[[action.title]]"
+            hidden$="[[!action.enabled]]"
+            data-action-key$="[[action.__key]]"
+            disabled$="[[_loading]]"
+            on-tap="_handleActionTap">[[action.label]]</button>
+      </template>
+    </div>
+  </template>
+  <script>
+  (function() {
+    'use strict';
+
+    Polymer({
+      is: 'gr-change-actions',
+
+      /**
+       * Fired when the change should be reloaded.
+       *
+       * @event reload-change
+       */
+
+      properties: {
+        changeNum: String,
+        patchNum: String,
+        _actions: {
+          type: Object,
+          observer: '_actionsChanged',
+        },
+        _loading: {
+          type: Boolean,
+          value: false,
+        },
+      },
+
+      reload: function() {
+        if (!!this.changeNum && !!this.patchNum) {
+          this.$.actionsXHR.generateRequest();
+        }
+      },
+
+      _actionsChanged: function(actions) {
+        this.hidden = actions.submit == null;
+      },
+
+      _computeActionsPath: function(changeNum, patchNum) {
+        return Changes.baseURL(changeNum, patchNum) + '/actions';
+      },
+
+      _computeActionValues: function(actions) {
+        var result = [];
+        for (var a in actions) {
+          // TODO(andybons): Add the rest of the actions.
+          if (a != 'submit') { continue; }
+          actions[a].__key = a;
+          result.push(actions[a]);
+        }
+        return result;
+      },
+
+      _handleActionTap: function(e) {
+        e.preventDefault();
+        var el = Polymer.dom(e).rootTarget;
+        var key = el.getAttribute('data-action-key');
+        if (key == 'submit') {
+          this._submitChange('/' + key, this._actions[key]);
+        }
+      },
+
+      _submitChange: function(endpoint, action) {
+        this._send(action.method, {}, endpoint).then(
+          function() {
+            this.fire('reload-change', null, {bubbles: false});
+          }.bind(this)).catch(function(err) {
+            alert('Oops. Something went wrong. Check the console and bug the ' +
+                'PolyGerrit team for assistance.');
+            throw err;
+        });
+      },
+
+      _send: function(method, payload, actionEndpoint) {
+        var xhr = document.createElement('gr-request');
+        this._xhrPromise = xhr.send({
+          method: method,
+          url: Changes.baseURL(this.changeNum, this.patchNum) + actionEndpoint,
+          body: payload,
+        });
+
+        return this._xhrPromise;
+      },
+    });
+  })();
+  </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-view.html b/polygerrit-ui/app/elements/gr-change-view.html
index 81034bc..fbf005c 100644
--- a/polygerrit-ui/app/elements/gr-change-view.html
+++ b/polygerrit-ui/app/elements/gr-change-view.html
@@ -17,6 +17,7 @@
 <link rel="import" href="../bower_components/polymer/polymer.html">
 <link rel="import" href="../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
 <link rel="import" href="gr-ajax.html">
+<link rel="import" href="gr-change-actions.html">
 <link rel="import" href="gr-date-formatter.html">
 <link rel="import" href="gr-file-list.html">
 <link rel="import" href="gr-linked-text.html">
@@ -112,6 +113,9 @@
       .notApproved {
         background-color: #ffd4d4;
       }
+      gr-change-actions {
+        margin-top: 1em;
+      }
       .summary {
         border-top: 1px solid #ddd;
         border-bottom: 1px solid #ddd;
@@ -229,6 +233,10 @@
             </tr>
           </template>
         </table>
+        <gr-change-actions id="actions"
+            change-num="[[_changeNum]]"
+            patch-num="[[_patchNum]]"
+            on-reload-change="_reload"></gr-change-actions>
       </section>
       <section class="summary">
         <gr-linked-text pre
@@ -457,16 +465,17 @@
       _reload: function() {
         var detailCompletes = this.$.detailXHR.generateRequest().completes;
         this.$.commentsXHR.generateRequest();
-        var reloadCommitInfoAndFileList = function() {
+        var reloadPatchNumDependentResources = function() {
           this.$.commitInfoXHR.generateRequest();
+          this.$.actions.reload();
           this.$.fileList.reload();
         }.bind(this);
 
         if (this._patchNum) {
-          reloadCommitInfoAndFileList();
+          reloadPatchNumDependentResources();
         } else {
           // The patch number is reliant on the change detail request.
-          detailCompletes.then(reloadCommitInfoAndFileList);
+          detailCompletes.then(reloadPatchNumDependentResources);
         }
       },
 
diff --git a/polygerrit-ui/app/elements/gr-reply-dropdown.html b/polygerrit-ui/app/elements/gr-reply-dropdown.html
index b65cbcb..12ab5b3 100644
--- a/polygerrit-ui/app/elements/gr-reply-dropdown.html
+++ b/polygerrit-ui/app/elements/gr-reply-dropdown.html
@@ -249,7 +249,9 @@
           }
         }
 
-        for (var i = 0; i < permittedLabels[labelName].length; i++) {
+        var len = permittedLabels[labelName] != null ?
+            permittedLabels[labelName].length : 0;
+        for (var i = 0; i < len; i++) {
           var val = parseInt(permittedLabels[labelName][i], 10);
           if (val == labelValue) {
             return i;
diff --git a/polygerrit-ui/app/test/gr-change-actions-test.html b/polygerrit-ui/app/test/gr-change-actions-test.html
new file mode 100644
index 0000000..69e2491
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-change-actions-test.html
@@ -0,0 +1,119 @@
+<!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-change-actions</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../scripts/changes.js"></script>
+<script src="../scripts/util.js"></script>
+
+<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="../elements/gr-change-actions.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-change-actions></gr-change-actions>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-change-actions tests', function() {
+    var element;
+    var server;
+
+    setup(function() {
+      element = fixture('basic');
+      server = sinon.fakeServer.create();
+
+      server.respondWith(
+        'GET',
+        '/changes/42/revisions/2/actions',
+        [
+          200,
+          { 'Content-Type': 'application/json' },
+          ')]}\'\n' +
+          JSON.stringify({
+            cherrypick: {
+              method: 'POST',
+              label: 'Cherry Pick',
+              title: 'Cherry pick change to a different branch',
+              enabled: true
+            },
+            rebase: {
+              method: 'POST',
+              label: 'Rebase',
+              title: 'Rebase onto tip of branch or parent change'
+            },
+            submit: {
+              method: 'POST',
+              label: 'Submit',
+              title: 'Submit patch set 1 into master',
+              enabled: true
+            }
+          }),
+        ]
+      );
+
+      server.respondWith(
+        'POST',
+        '/changes/42/revisions/2/submit',
+        [
+          200,
+          { 'Content-Type': 'application/json' },
+          ')]}\'\n{}',  // The response is not used by the element.
+        ]
+      );
+    });
+
+    test('submit button shows', function(done) {
+      element.changeNum = '42';
+      element.patchNum = '2';
+      element.reload();
+
+      server.respond();
+
+      element.async(function() {
+        var buttonEls = Polymer.dom(element.root).querySelectorAll('button');
+        assert.equal(buttonEls.length, 1);
+        assert.isFalse(element.hidden);
+        done();
+      }, 1);
+    });
+
+    test('submit change', function(done) {
+      element.changeNum = '42';
+      element.patchNum = '2';
+      element.reload();
+
+      server.respond();
+
+      element.async(function() {
+        var submitButton = element.$$('button[data-action-key="submit"]');
+        assert.ok(submitButton);
+        MockInteractions.tap(submitButton);
+        server.respond();
+
+        // Upon success it should fire the reload-change event.
+        element.addEventListener('reload-change', function(e) {
+          done();
+        });
+      }, 1);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 85c0e1d..b1f6128 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -24,6 +24,7 @@
   var testFiles = [];
 
   [ 'gr-account-dropdown-test.html',
+    'gr-change-actions-test.html',
     'gr-change-list-item-test.html',
     'gr-change-list-test.html',
     'gr-change-view-test.html',