Add rebase action with confirmation dialog

TODO in follow-up change
+ Autocomplete revisions/change numbers from the input
+ Consolidate button classes (it’s enough repeated code
  that it’s becomming a nuisance).
+ Perhaps move confirmation dialog functions into
  a behavior.

Change-Id: I382cc63591cd537dbe1d29a0451f392c1e77f287
diff --git a/polygerrit-ui/app/elements/gr-change-actions.html b/polygerrit-ui/app/elements/gr-change-actions.html
index 850ecb7..997bfb22 100644
--- a/polygerrit-ui/app/elements/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/gr-change-actions.html
@@ -15,8 +15,11 @@
 -->
 
 <link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../behaviors/rest-client-behavior.html">
 <link rel="import" href="gr-ajax.html">
+<link rel="import" href="gr-confirm-rebase-dialog.html">
+<link rel="import" href="gr-overlay.html">
 <link rel="import" href="gr-request.html">
 
 <dom-module id="gr-change-actions">
@@ -27,6 +30,7 @@
       }
       .primary {
         background-color: #448aff;
+        border-color: #448aff;
         color: #fff;
       }
       button:before {
@@ -40,16 +44,22 @@
         font: inherit;
         padding: .5em .75em;
       }
+      button[loading],
+      button[disabled] {
+        opacity: .75;
+      }
       button[loading] {
         cursor: wait;
-        opacity: .5;
       }
       button[loading]:before {
         content: attr(data-loading-label);
       }
       button[disabled] {
-        background-color: #555961;
-        opacity: .5;
+        cursor: default;
+      }
+      button[loading],
+      button[loading][disabled] {
+        cursor: wait;
       }
     </style>
     <gr-ajax id="actionsXHR"
@@ -75,6 +85,13 @@
             on-tap="_handleActionTap"></button>
       </template>
     </div>
+    <gr-overlay id="overlay" with-backdrop>
+      <gr-confirm-rebase-dialog id="confirmRebase"
+          class="confirmDialog"
+          on-confirm="_handleRebaseConfirm"
+          on-cancel="_handleConfirmDialogCancel"
+          hidden></gr-confirm-rebase-dialog>
+    </gr-overlay>
   </template>
   <script>
   (function() {
@@ -88,6 +105,7 @@
 
     // TODO(andybons): Add the rest of the revision actions.
     var RevisionActions = {
+      REBASE: 'rebase',
       SUBMIT: 'submit',
     };
 
@@ -110,9 +128,7 @@
           type: Boolean,
           value: true,
         },
-        _revisionActions: {
-          type: Object,
-        },
+        _revisionActions: Object,
       },
 
       behaviors: [
@@ -131,7 +147,9 @@
       },
 
       _actionsChanged: function(actions, revisionActions) {
-        this.hidden = revisionActions.submit == null &&
+        this.hidden =
+            revisionActions.rebase == null &&
+            revisionActions.submit == null &&
             actions.abandon == null &&
             actions.restore == null;
       },
@@ -160,6 +178,7 @@
 
       _computeLoadingLabel: function(action) {
         return {
+          'rebase': 'Rebasing...',
           'submit': 'Submitting...',
         }[action];
       },
@@ -173,12 +192,45 @@
         var el = Polymer.dom(e).rootTarget;
         var key = el.getAttribute('data-action-key');
         if (this._getValuesFor(RevisionActions).indexOf(key) > -1) {
+          if (key == RevisionActions.REBASE) {
+            this._showRebaseDialog();
+            return;
+          }
           this._fireRevisionAction('/' + key, this._revisionActions[key]);
         } else {
           this._fireChangeAction('/' + key, this.actions[key]);
         }
       },
 
+      _handleConfirmDialogCancel: function() {
+        var dialogEls =
+            Polymer.dom(this.root).querySelectorAll('.confirmDialog');
+        for (var i = 0; i < dialogEls.length; i++) {
+          dialogEls[i].hidden = true;
+        }
+        this.$.overlay.close();
+      },
+
+      _handleRebaseConfirm: function() {
+        var payload = {};
+        var el = this.$.confirmRebase;
+        if (el.clearParent) {
+          // There is a subtle but important difference between setting the base
+          // to an empty string and omitting it entirely from the payload. An
+          // empty string implies that the parent should be cleared and the
+          // change should be rebased on top of the target branch. Leaving out
+          // the base implies that it should be rebased on top of its current
+          // parent.
+          payload.base = '';
+        } else if (el.base && el.base.length > 0) {
+          payload.base = el.base;
+        }
+        this.$.overlay.close();
+        el.hidden = false;
+        this._fireRevisionAction('/rebase', this._revisionActions.rebase,
+            payload);
+      },
+
       _fireChangeAction: function(endpoint, action) {
         this._send(action.method, {}, endpoint).then(
           function() {
@@ -190,25 +242,33 @@
           });
       },
 
-      _fireRevisionAction: function(endpoint, action) {
+      _fireRevisionAction: function(endpoint, action, opt_payload) {
         var buttonEl = this.$$('[data-action-key="' + action.__key + '"]');
         buttonEl.setAttribute('loading', true);
         buttonEl.disabled = true;
+        function enableButton() {
+          buttonEl.removeAttribute('loading');
+          buttonEl.disabled = false;
+        }
 
-        this._send(action.method, {}, endpoint, true).then(
+        this._send(action.method, opt_payload, endpoint, true).then(
           function() {
             this.fire('reload-change', null, {bubbles: false});
-            buttonEl.setAttribute('loading', false);
-            buttonEl.disabled = false;
+            enableButton();
           }.bind(this)).catch(function(err) {
+            // TODO(andybons): Handle merge conflict (409 status);
             alert('Oops. Something went wrong. Check the console and bug the ' +
                 'PolyGerrit team for assistance.');
-            buttonEl.setAttribute('loading', false);
-            buttonEl.disabled = false;
+            enableButton();
             throw err;
           });
       },
 
+      _showRebaseDialog: function() {
+        this.$.confirmRebase.hidden = false;
+        this.$.overlay.open();
+      },
+
       _send: function(method, payload, actionEndpoint, revisionAction) {
         var xhr = document.createElement('gr-request');
         this._xhrPromise = xhr.send({
diff --git a/polygerrit-ui/app/elements/gr-change-view.html b/polygerrit-ui/app/elements/gr-change-view.html
index 9275666..fb0ca27 100644
--- a/polygerrit-ui/app/elements/gr-change-view.html
+++ b/polygerrit-ui/app/elements/gr-change-view.html
@@ -281,7 +281,7 @@
               actions="[[_change.actions]]"
               change-num="[[_changeNum]]"
               patch-num="[[_patchNum]]"
-              on-reload-change="_reload"></gr-change-actions>
+              on-reload-change="_handleReloadChange"></gr-change-actions>
         </div>
         <div class="changeInfo-column commitMessage">
           <h4>Commit message</h4>
@@ -596,6 +596,10 @@
         }
       },
 
+      _handleReloadChange: function() {
+        page.show(this._computeChangePath(this._changeNum));
+      },
+
       _reload: function() {
         var detailCompletes = this.$.detailXHR.generateRequest().completes;
         this.$.commentsXHR.generateRequest();
diff --git a/polygerrit-ui/app/elements/gr-confirm-dialog.html b/polygerrit-ui/app/elements/gr-confirm-dialog.html
new file mode 100644
index 0000000..beea8d2
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-confirm-dialog.html
@@ -0,0 +1,97 @@
+<!--
+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">
+
+<dom-module id="gr-confirm-dialog">
+  <template>
+    <style>
+      :host {
+        display: block;
+      }
+      .header {
+        border-bottom: 1px solid #ddd;
+        font-weight: bold;
+      }
+      .header,
+      .mainContent,
+      .footer {
+        padding: .5em .65em;
+      }
+      .footer {
+        display: flex;
+        justify-content: space-between;
+      }
+      button {
+        background-color: #f1f2f3;
+        border: 1px solid #aaa;
+        border-radius: 2px;
+        cursor: pointer;
+        font: inherit;
+        padding: .5em .75em;
+      }
+      .confirm {
+        background-color: #448aff;
+        border-color: #448aff;
+        color: #fff;
+      }
+    </style>
+    <div class="header"><content select=".header"></content></div>
+    <div class="mainContent"><content select=".main"></content></div>
+    <div class="footer">
+      <button class="confirm" on-tap="_handleConfirmTap">[[confirmLabel]]</button>
+      <button class="cancel" on-tap="_handleCancelTap">Cancel</button>
+    </div>
+  </template>
+  <script>
+  (function() {
+    'use strict';
+
+    Polymer({
+      is: 'gr-confirm-dialog',
+
+      /**
+       * Fired when the confirm button is pressed.
+       *
+       * @event confirm
+       */
+
+      /**
+       * Fired when the cancel button is pressed.
+       *
+       * @event cancel
+       */
+
+      properties: {
+        confirmLabel: {
+          type: String,
+          value: 'Confirm',
+        }
+      },
+
+      _handleConfirmTap: function(e) {
+        e.preventDefault();
+        this.fire('confirm', null, {bubbles: false});
+      },
+
+      _handleCancelTap: function(e) {
+        e.preventDefault();
+        this.fire('cancel', null, {bubbles: false});
+      },
+    });
+  })();
+  </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/gr-confirm-rebase-dialog.html
new file mode 100644
index 0000000..96c7188
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-confirm-rebase-dialog.html
@@ -0,0 +1,119 @@
+<!--
+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-confirm-dialog.html">
+
+<dom-module id="gr-confirm-rebase-dialog">
+  <template>
+    <style>
+      :host {
+        display: block;
+        width: 30em;
+      }
+      :host([disabled]) {
+        opacity: .5;
+        pointer-events: none;
+      }
+      label {
+        cursor: pointer;
+      }
+      .parentRevisionContainer label,
+      .parentRevisionContainer input[type="text"] {
+        display: block;
+        font: inherit;
+        width: 100%;
+      }
+      .parentRevisionContainer label {
+        margin-bottom: .2em;
+      }
+      .clearParentContainer {
+        margin: .5em 0;
+      }
+    </style>
+    <gr-confirm-dialog
+        confirm-label="Rebase"
+        on-confirm="_handleConfirmTap"
+        on-cancel="_handleCancelTap">
+      <div class="header">Confirm rebase</div>
+      <div class="main">
+        <div class="parentRevisionContainer">
+          <label for="parentInput">
+            Parent revision (optional)
+          </label>
+          <input is="iron-input"
+              type="text"
+              id="parentInput"
+              bind-value="{{base}}"
+              placeholder="Change number">
+        </div>
+        <div class="clearParentContainer">
+          <input id="clearParent"
+              type="checkbox"
+              on-tap="_handleClearParentTap">
+          <label for="clearParent">
+            Rebase on top of current branch (clear parent revision).
+          </label>
+        </div>
+      </div>
+    </gr-confirm-dialog>
+  </template>
+  <script>
+  (function() {
+    'use strict';
+
+    Polymer({
+      is: 'gr-confirm-rebase-dialog',
+
+      /**
+       * Fired when the confirm button is pressed.
+       *
+       * @event confirm
+       */
+
+      /**
+       * Fired when the cancel button is pressed.
+       *
+       * @event cancel
+       */
+
+      properties: {
+        base: String,
+        clearParent: Boolean,
+      },
+
+      _handleConfirmTap: function(e) {
+        e.preventDefault();
+        this.fire('confirm', null, {bubbles: false});
+      },
+
+      _handleCancelTap: function(e) {
+        e.preventDefault();
+        this.fire('cancel', null, {bubbles: false});
+      },
+
+      _handleClearParentTap: function(e) {
+        var clear = Polymer.dom(e).rootTarget.checked;
+        if (clear) {
+          this.base = '';
+        }
+        this.$.parentInput.disabled = clear;
+        this.clearParent = clear;
+      },
+    });
+  })();
+  </script>
+</dom-module>
diff --git a/polygerrit-ui/app/test/gr-change-actions-test.html b/polygerrit-ui/app/test/gr-change-actions-test.html
index 14dd010..3fc5ef1 100644
--- a/polygerrit-ui/app/test/gr-change-actions-test.html
+++ b/polygerrit-ui/app/test/gr-change-actions-test.html
@@ -78,30 +78,34 @@
           ')]}\'\n{}',  // The response is not used by the element.
         ]
       );
-    });
 
-    test('submit button shows', function(done) {
+      server.respondWith(
+        'POST',
+        '/changes/42/revisions/2/rebase',
+        [
+          200,
+          {'Content-Type': 'application/json'},
+          ')]}\'\n{}',  // The response is not used by the element.
+        ]
+      );
+
       element.changeNum = '42';
       element.patchNum = '2';
       element.reload();
 
       server.respond();
+    });
 
+    test('submit and rebase buttons show', function(done) {
       element.async(function() {
         var buttonEls = Polymer.dom(element.root).querySelectorAll('button');
-        assert.equal(buttonEls.length, 1);
+        assert.equal(buttonEls.length, 2);
         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);
@@ -114,5 +118,38 @@
         });
       }, 1);
     });
+
+    test('rebase change', function(done) {
+      element.async(function() {
+        var rebaseButton = element.$$('button[data-action-key="rebase"]');
+        MockInteractions.tap(rebaseButton);
+
+        element.$.confirmRebase.base = '1234';
+        element._handleRebaseConfirm();
+        server.respond();
+        var lastRequest = server.requests[server.requests.length - 1];
+        assert.equal(lastRequest.requestBody, '{"base":"1234"}');
+
+        element.$.confirmRebase.base = '';
+        element._handleRebaseConfirm();
+        server.respond();
+        lastRequest = server.requests[server.requests.length - 1];
+        assert.equal(lastRequest.requestBody, '{}');
+
+        element.$.confirmRebase.base = 'does not matter';
+        element.$.confirmRebase.clearParent = true;
+        element._handleRebaseConfirm();
+        server.respond();
+        lastRequest = server.requests[server.requests.length - 1];
+        assert.equal(lastRequest.requestBody, '{"base":""}');
+
+        // Upon each request success it should fire the reload-change event.
+        var numEvents = 0;
+        element.addEventListener('reload-change', function(e) {
+          if (++numEvents == 3) { done(); }
+        });
+      }, 1);
+    });
+
   });
 </script>
diff --git a/polygerrit-ui/app/test/gr-confirm-dialog-test.html b/polygerrit-ui/app/test/gr-confirm-dialog-test.html
new file mode 100644
index 0000000..b61bb17
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-confirm-dialog-test.html
@@ -0,0 +1,53 @@
+<!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-confirm-dialog</title>
+
+<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="../elements/gr-confirm-dialog.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-confirm-dialog></gr-confirm-dialog>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-confirm-dialog tests', function() {
+    var element;
+
+    setup(function() {
+      element = fixture('basic');
+    });
+
+    test('events', function(done) {
+      var numEvents = 0;
+      function handler() { if (++numEvents == 2) { done(); } }
+
+      element.addEventListener('confirm', handler);
+      element.addEventListener('cancel', handler);
+
+      MockInteractions.tap(element.$$('.confirm'));
+      MockInteractions.tap(element.$$('.cancel'));
+    });
+
+  });
+</script>
diff --git a/polygerrit-ui/app/test/gr-confirm-rebase-dialog-test.html b/polygerrit-ui/app/test/gr-confirm-rebase-dialog-test.html
new file mode 100644
index 0000000..7db82e9
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-confirm-rebase-dialog-test.html
@@ -0,0 +1,51 @@
+<!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-confirm-rebase-dialog</title>
+
+<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="../elements/gr-confirm-rebase-dialog.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-confirm-rebase-dialog></gr-confirm-rebase-dialog>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-confirm-rebase-dialog tests', function() {
+    var element;
+
+    setup(function() {
+      element = fixture('basic');
+    });
+
+    test('controls', function() {
+      assert.isFalse(element.$.parentInput.hasAttribute('disabled'));
+      assert.isFalse(element.$.clearParent.checked);
+      element.base = 'something great';
+      MockInteractions.tap(element.$.clearParent);
+      assert.isTrue(element.$.parentInput.hasAttribute('disabled'));
+      assert.isTrue(element.$.clearParent.checked);
+      assert.equal(element.base, '');
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 9c2099d..bf700c1 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -33,6 +33,8 @@
     'gr-change-list-test.html',
     'gr-change-star-test.html',
     'gr-change-view-test.html',
+    'gr-confirm-dialog-test.html',
+    'gr-confirm-rebase-dialog-test.html',
     'gr-date-formatter-test.html',
     'gr-diff-comment-test.html',
     'gr-diff-comment-thread-test.html',