Add 'Quick Approve' button to change actions

Adds Quick Approve button that sets maximal review score for all
permitted labels with one click.

Feature: Issue 4484
Change-Id: I8182e05d9dc34e01ed8a253a847375afadf766c8
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 2d5c0a7..d6b1505 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -49,6 +49,15 @@
 
   var ADDITIONAL_ACTION_KEY_PREFIX = '__additionalAction_';
 
+  var QUICK_APPROVE_ACTION = {
+    __key: 'review',
+    __type: 'change',
+    key: 'review',
+    label: 'Quick Approve',
+    method: 'POST',
+    title: 'Set maximal score to all labels you can.',
+  };
+
   Polymer({
     is: 'gr-change-actions',
 
@@ -98,7 +107,7 @@
       _changeActionValues: {
         type: Array,
         computed: '_computeChangeActionValues(actions.*, ' +
-            'primaryActionKeys.*, _additionalActions.*)',
+            'primaryActionKeys.*, _additionalActions.*, change)',
       },
       _additionalActions: {
         type: Array,
@@ -243,9 +252,14 @@
     },
 
     _computeChangeActionValues: function(actionsChangeRecord,
-        primariesChangeRecord, additionalActionsChangeRecord) {
-      return this._getActionValues(actionsChangeRecord, primariesChangeRecord,
-          additionalActionsChangeRecord, ActionType.CHANGE);
+        primariesChangeRecord, additionalActionsChangeRecord, change) {
+      var actions = this._getActionValues(
+        actionsChangeRecord, primariesChangeRecord,
+        additionalActionsChangeRecord, ActionType.CHANGE, change);
+      if (actions.length && this._canQuickApprove(change)) {
+        actions.unshift(QUICK_APPROVE_ACTION);
+      }
+      return actions;
     },
 
     _getActionValues: function(actionsChangeRecord, primariesChangeRecord,
@@ -289,6 +303,17 @@
       return result.concat(additionalActions);
     },
 
+    _canQuickApprove: function(change) {
+      if (!change || !change.labels || !change.permitted_labels) {
+        return false;
+      }
+      var missingApprovals = Object.keys(change.labels).filter(function(label) {
+        return !change.labels[label].approved;
+      });
+      return missingApprovals.some(
+          function(label) { return label in change.permitted_labels; });
+    },
+
     _computeLoadingLabel: function(action) {
       return ActionLoadingLabels[action] || 'Working...';
     },
@@ -344,6 +369,18 @@
         this.showRevertDialog();
       } else if (key === ChangeActions.ABANDON) {
         this._showActionDialog(this.$.confirmAbandonDialog);
+      } else if (key === QUICK_APPROVE_ACTION.key) {
+        var review = {
+          drafts: 'PUBLISH_ALL_REVISIONS',
+          labels: {},
+        };
+        var permittedLabels = this.change.permitted_labels;
+        Object.keys(permittedLabels).forEach(function(label) {
+          // Set label to maximal score permitted for it.
+          review.labels[label] = permittedLabels[label].slice(-1)[0];
+        });
+        this._fireAction(
+            this._prependSlash(key), QUICK_APPROVE_ACTION, true, review);
       } else {
         this._fireAction(this._prependSlash(key), this.actions[key], false);
       }
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 00e61d9..64443ff 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -84,6 +84,7 @@
       });
 
       element = fixture('basic');
+      element.change = {};
       element.changeNum = '42';
       element.patchNum = '2';
       element.actions = {
@@ -488,5 +489,79 @@
         assert.isFalse(fireActionStub.called);
       });
     });
+
+    suite('quick approve', function() {
+      setup(function() {
+        element.change = {
+          current_revision: 'abc1234',
+        };
+        element.change = {
+          current_revision: 'abc1234',
+          labels: {
+            'foo': {},
+          },
+          permitted_labels: {
+            'foo': ['-1', '0', '+1'],
+          },
+        };
+        flushAsynchronousOperations();
+      });
+
+      test('added when can approve', function() {
+        var approveButton = element.$$('gr-button[data-action-key=\'review\']');
+        assert.isNotNull(approveButton);
+      });
+
+      test('not added when no actions available', function() {
+        element.actions = [];
+        flushAsynchronousOperations();
+        var approveButton = element.$$('gr-button[data-action-key=\'review\']');
+        assert.isNull(approveButton);
+      });
+
+      test('not added when already approved', function() {
+        element.change = {
+          current_revision: 'abc1234',
+          labels: {
+            'foo': {
+              approved: {},
+            },
+          },
+          permitted_labels: {
+            'foo': [],
+          },
+        };
+        flushAsynchronousOperations();
+        var approveButton = element.$$('gr-button[data-action-key=\'review\']');
+        assert.isNull(approveButton);
+      });
+
+      test('not added when can not approve', function() {
+        element.change = {
+          current_revision: 'abc1234',
+          labels: {
+            'foo': {},
+          },
+          permitted_labels: {
+            'bar': [],
+          },
+        };
+        flushAsynchronousOperations();
+        var approveButton = element.$$('gr-button[data-action-key=\'review\']');
+        assert.isNull(approveButton);
+      });
+
+      test('approves when taped', function() {
+        var fireActionStub = sinon.stub(element, '_fireAction');
+        MockInteractions.tap(
+            element.$$('gr-button[data-action-key=\'review\']'));
+        flushAsynchronousOperations();
+        assert.isTrue(fireActionStub.called);
+        assert.isTrue(fireActionStub.calledWith('/review'));
+        var payload = fireActionStub.lastCall.args[3];
+        assert.deepEqual(payload.labels, {foo: '+1'});
+        fireActionStub.restore();
+      });
+    });
   });
 </script>