Merge "Reindex when edit reference is updated"
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index b7a76a2..f54cfc9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -19,6 +19,7 @@
 <link rel="import" href="../../../behaviors/rest-client-behavior.html">
 
 <link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -38,7 +39,8 @@
       section {
         display: inline-block;
       }
-      gr-button {
+      gr-button,
+      gr-dropdown {
         margin-left: .5em;
       }
       gr-button:before {
@@ -63,29 +65,29 @@
       }
     </style>
     <div>
-      <section hidden$="[[_shouldHideActions(actions.*, _additionalActions.*, _loading)]]">
-        <template is="dom-repeat" items="[[_changeActionValues]]" as="action">
+    <gr-dropdown
+        id="moreActions"
+        down-arrow
+        vertical-offset="32"
+        horizontal-align="right"
+        on-tap-item-abandon="_handleAbandonTap"
+        on-tap-item-cherrypick="_handleCherrypickTap"
+        on-tap-item-delete="_handleDeleteTap"
+        hidden$="[[_shouldHideActions(_menuActions.*, _loading)]]"
+        items="[[_menuActions]]">More</gr-dropdown>
+      <section hidden$="[[_shouldHideActions(_topLevelActions.*, _loading)]]">
+        <template
+            is="dom-repeat"
+            items="[[_topLevelActions]]"
+            as="action">
           <gr-button title$="[[action.title]]"
-              hidden$="[[_computeActionHidden(action.__key, _hiddenChangeActions.*)]]"
+              hidden$="[[_computeActionHidden(action.__key, _hiddenActions.*)]]"
               primary$="[[action.__primary]]"
-              hidden$="[[!action.enabled]]"
               data-action-key$="[[action.__key]]"
               data-action-type$="[[action.__type]]"
               data-label$="[[action.label]]"
               data-loading-label$="[[_computeLoadingLabel(action.__key)]]"
-              on-tap="_handleActionTap"></gr-button>
-        </template>
-      </section>
-      <section hidden$="[[_shouldHideActions(revisionActions.*, _additionalActions.*, _loading)]]">
-        <template is="dom-repeat" items="[[_revisionActionValues]]" as="action">
-          <gr-button title$="[[action.title]]"
-              hidden$="[[_computeActionHidden(action.__key, _hiddenRevisionActions.*)]]"
-              primary$="[[action.__primary]]"
               disabled$="[[!action.enabled]]"
-              data-action-key$="[[action.__key]]"
-              data-action-type$="[[action.__type]]"
-              data-label$="[[action.label]]"
-              data-loading-label$="[[_computeLoadingLabel(action.__key)]]"
               on-tap="_handleActionTap"></gr-button>
         </template>
       </section>
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 6fdc3ad..331c3c5 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
@@ -87,6 +87,16 @@
     method: 'POST',
   };
 
+  /**
+   * Keys for actions to appear in the overflow menu rather than the top-level
+   * set of action buttons.
+   */
+  var MENU_ACTION_KEYS = [
+    'abandon',
+    'cherrypick',
+    '/', // '/' is the key for the delete action.
+  ];
+
   Polymer({
     is: 'gr-change-actions',
 
@@ -96,6 +106,12 @@
      * @event reload-change
      */
 
+    /**
+     * Fired when an action is tapped.
+     *
+     * @event <action key>-tap
+     */
+
     properties: {
       change: Object,
       actions: {
@@ -129,25 +145,26 @@
         type: Boolean,
         value: true,
       },
-      _revisionActionValues: {
+
+      _allActionValues: {
         type: Array,
-        computed: '_computeRevisionActionValues(revisionActions.*, ' +
-            'primaryActionKeys.*, _additionalActions.*)',
-      },
-      _changeActionValues: {
-        type: Array,
-        computed: '_computeChangeActionValues(actions.*, ' +
+        computed: '_computeAllActions(actions.*, revisionActions.*,' +
             'primaryActionKeys.*, _additionalActions.*, change)',
       },
+      _topLevelActions: {
+        type: Array,
+        computed: '_computeTopLevelActions(_allActionValues.*)',
+      },
+      _menuActions: {
+        type: Array,
+        computed: '_computeMenuActions(_allActionValues.*, _hiddenActions.*)',
+      },
+
       _additionalActions: {
         type: Array,
         value: function() { return []; },
       },
-      _hiddenChangeActions: {
-        type: Array,
-        value: function() { return []; },
-      },
-      _hiddenRevisionActions: {
+      _hiddenActions: {
         type: Array,
         value: function() { return []; },
       },
@@ -220,20 +237,15 @@
     },
 
     setActionHidden: function(type, key, hidden) {
-      var path;
-      if (type === ActionType.CHANGE) {
-        path = '_hiddenChangeActions';
-      } else if (type === ActionType.REVISION) {
-        path = '_hiddenRevisionActions';
-      } else {
+      if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
         throw Error('Invalid action type given: ' + type);
       }
 
-      var idx = this.get(path).indexOf(key);
+      var idx = this._hiddenActions.indexOf(key);
       if (hidden && idx === -1) {
-        this.push(path, key);
+        this.push('_hiddenActions', key);
       } else if (!hidden && idx !== -1) {
-        this.splice(path, idx, 1);
+        this.splice('_hiddenActions', idx, 1);
       }
     },
 
@@ -251,16 +263,8 @@
           this.patchNum);
     },
 
-    _shouldHideActions: function(actionsRecord, additionalActionsRecord,
-        loading) {
-      if (loading) { return true; }
-      return !this._actionCount(actionsRecord, additionalActionsRecord);
-    },
-
-    _actionCount: function(actionsRecord, additionalActionsRecord) {
-      var additionalActions = (additionalActionsRecord &&
-          additionalActionsRecord.base) || [];
-      return this._keyCount(actionsRecord) + additionalActions.length;
+    _shouldHideActions: function(actions, loading) {
+      return loading || !actions || !actions.base || !actions.base.length;
     },
 
     _keyCount: function(changeRecord) {
@@ -282,24 +286,6 @@
       });
     },
 
-    _computeRevisionActionValues: function(actionsChangeRecord,
-        primariesChangeRecord, additionalActionsChangeRecord) {
-      return this._getActionValues(actionsChangeRecord, primariesChangeRecord,
-          additionalActionsChangeRecord, ActionType.REVISION);
-    },
-
-    _computeChangeActionValues: function(actionsChangeRecord,
-        primariesChangeRecord, additionalActionsChangeRecord, change) {
-      var actions = this._getActionValues(
-          actionsChangeRecord, primariesChangeRecord,
-          additionalActionsChangeRecord, ActionType.CHANGE, change);
-      var quickApprove = this._getQuickApproveAction();
-      if (quickApprove) {
-        actions.unshift(quickApprove);
-      }
-      return actions;
-    },
-
     _getLabelStatus: function(label) {
       if (label.approved) {
         return LabelStatus.OK;
@@ -466,14 +452,10 @@
       var type = el.getAttribute('data-action-type');
       if (type === ActionType.REVISION) {
         this._handleRevisionAction(key);
-      } else if (key === ChangeActions.DELETE) {
-        this._showActionDialog(this.$.confirmDeleteDialog);
       } else if (key === ChangeActions.REVERT) {
         this.showRevertDialog();
-      } else if (key === ChangeActions.ABANDON) {
-        this._showActionDialog(this.$.confirmAbandonDialog);
       } else if (key === QUICK_APPROVE_ACTION.key) {
-        var action = this._changeActionValues.find(function(o) {
+        var action = this._allActionValues.find(function(o) {
           return o.key === key;
         });
         this._fireAction(
@@ -488,10 +470,6 @@
         case RevisionActions.REBASE:
           this._showActionDialog(this.$.confirmRebase);
           break;
-        case RevisionActions.CHERRYPICK:
-          this.$.confirmCherrypick.branch = '';
-          this._showActionDialog(this.$.confirmCherrypick);
-          break;
         case RevisionActions.SUBMIT:
           if (!this._canSubmitChange()) {
             return;
@@ -664,5 +642,92 @@
         return response;
       }.bind(this)).then(this._handleResponseError.bind(this));
     },
+
+    _handleAbandonTap: function() {
+      this._showActionDialog(this.$.confirmAbandonDialog);
+    },
+
+    _handleCherrypickTap: function() {
+      this.$.confirmCherrypick.branch = '';
+      this._showActionDialog(this.$.confirmCherrypick);
+    },
+
+    _handleDeleteTap: function() {
+      this._showActionDialog(this.$.confirmDeleteDialog);
+    },
+
+    /**
+     * Merge sources of change actions into a single ordered array of action
+     * values.
+     * @param {splices} changeActionsRecord
+     * @param {splices} revisionActionsRecord
+     * @param {splices} primariesRecord
+     * @param {splices} additionalActionsRecord
+     * @param {Object} change The change object.
+     * @return {Array}
+     */
+    _computeAllActions: function(changeActionsRecord, revisionActionsRecord,
+        primariesRecord, additionalActionsRecord, change) {
+      var revisionActionValues = this._getActionValues(revisionActionsRecord,
+          primariesRecord, additionalActionsRecord, ActionType.REVISION);
+      var changeActionValues = this._getActionValues(changeActionsRecord,
+          primariesRecord, additionalActionsRecord, ActionType.CHANGE, change);
+      var quickApprove = this._getQuickApproveAction();
+      if (quickApprove) {
+        changeActionValues.unshift(quickApprove);
+      }
+      return revisionActionValues
+          .concat(changeActionValues)
+          .sort(this._actionComparator);
+    },
+
+    /**
+     * Sort comparator to define the order of change actions.
+     */
+    _actionComparator: function(actionA, actionB) {
+      // The code review action always appears first.
+      if (actionA.__key === 'review') {
+        return -1;
+      } else if (actionB.__key === 'review') {
+        return 1;
+      }
+
+      // Primary actions always appear last.
+      if (actionA.__primary) {
+        return 1;
+      } else if (actionB.__primary) {
+        return -1;
+      }
+
+      // Change actions appear before revision actions.
+      if (actionA.__type === 'change' && actionB.__type === 'revision') {
+        return -1;
+      } else if (actionA.__type === 'revision' && actionB.__type === 'change') {
+        return 1;
+      }
+
+      // Otherwise, sort by the button label.
+      return actionA.label > actionB.label ? 1 : -1;
+    },
+
+    _computeTopLevelActions: function(actionRecord) {
+      return actionRecord.base.filter(function(a) {
+        return MENU_ACTION_KEYS.indexOf(a.__key) === -1;
+      });
+    },
+
+    _computeMenuActions: function(actionRecord, hiddenActionsRecord) {
+      var hiddenActions = hiddenActionsRecord.base || [];
+      return actionRecord.base
+          .filter(function(a) {
+            return MENU_ACTION_KEYS.indexOf(a.__key) !== -1 &&
+                hiddenActions.indexOf(a.__key) === -1;
+          })
+          .map(function(action) {
+            var key = action.__key;
+            if (key === '/') { key = 'delete'; }
+            return {name: action.label, id: key, };
+          });
+    },
   });
 })();
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 7dbd903..dbdf00f 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
@@ -99,11 +99,9 @@
     });
 
     test('_shouldHideActions', function() {
-      assert.isTrue(element._shouldHideActions(undefined, undefined, true));
-      assert.isTrue(element._shouldHideActions({base: {}},
-          {base: []}, false));
-      assert.isFalse(element._shouldHideActions({base: { test: 'test' }},
-          {base: ['test']}, false));
+      assert.isTrue(element._shouldHideActions(undefined, true));
+      assert.isTrue(element._shouldHideActions({base: {}}, false));
+      assert.isFalse(element._shouldHideActions({base: ['test']}, false));
     });
 
     test('hide revision action', function(done) {
@@ -114,10 +112,10 @@
         assert.throws(element.setActionHidden.bind(element, 'invalid type'));
         element.setActionHidden(element.ActionType.REVISION,
             element.RevisionActions.SUBMIT, true);
-        assert.lengthOf(element._hiddenRevisionActions, 1);
+        assert.lengthOf(element._hiddenActions, 1);
         element.setActionHidden(element.ActionType.REVISION,
             element.RevisionActions.SUBMIT, true);
-        assert.lengthOf(element._hiddenRevisionActions, 1);
+        assert.lengthOf(element._hiddenActions, 1);
         flush(function() {
           var buttonEl = element.$$('[data-action-key="submit"]');
           assert.isOk(buttonEl);
@@ -135,29 +133,26 @@
       });
     });
 
-    test('hide change action', function(done) {
+    test('hide menu action', function(done) {
       flush(function() {
-        var buttonEl = element.$$('[data-action-key="/"]');
+        var buttonEl = element.$.moreActions.$$('span[data-id="delete"]');
         assert.isOk(buttonEl);
-        assert.isFalse(buttonEl.hasAttribute('hidden'));
         assert.throws(element.setActionHidden.bind(element, 'invalid type'));
         element.setActionHidden(element.ActionType.CHANGE,
             element.ChangeActions.DELETE, true);
-        assert.lengthOf(element._hiddenChangeActions, 1);
+        assert.lengthOf(element._hiddenActions, 1);
         element.setActionHidden(element.ActionType.CHANGE,
             element.ChangeActions.DELETE, true);
-        assert.lengthOf(element._hiddenChangeActions, 1);
+        assert.lengthOf(element._hiddenActions, 1);
         flush(function() {
-          var buttonEl = element.$$('[data-action-key="/"]');
-          assert.isOk(buttonEl);
-          assert.isTrue(buttonEl.hasAttribute('hidden'));
+          var buttonEl = element.$.moreActions.$$('span[data-id="delete"]');
+          assert.isNotOk(buttonEl);
 
           element.setActionHidden(element.ActionType.CHANGE,
             element.RevisionActions.DELETE, false);
           flush(function() {
-            var buttonEl = element.$$('[data-action-key="/"]');
+            var buttonEl = element.$.moreActions.$$('span[data-id="delete"]');
             assert.isOk(buttonEl);
-            assert.isFalse(buttonEl.hasAttribute('hidden'));
             done();
           });
         });
@@ -169,7 +164,8 @@
       flush(function() {
         var buttonEls = Polymer.dom(element.root)
             .querySelectorAll('gr-button');
-        assert.equal(buttonEls.length, 6);
+        var menuItems = element.$.moreActions.items;
+        assert.equal(buttonEls.length + menuItems.length, 6);
         assert.isFalse(element.hidden);
         done();
       });
@@ -177,18 +173,18 @@
 
     test('delete buttons have explicit labels', function(done) {
       flush(function() {
-        var buttonEls =
-            Polymer.dom(element.root).querySelectorAll('[data-action-key="/"]');
-        assert.equal(buttonEls.length, 2);
-        assert.notEqual(buttonEls[0].getAttribute('data-label'),
-            buttonEls[1].getAttribute['data-label']);
+        var deleteItems = element.$.moreActions.items.filter(function(item) {
+          return item.id === 'delete';
+        });
+        assert.equal(deleteItems.length, 2);
+        assert.notEqual(deleteItems[0].name, deleteItems[1].name)
         assert.isTrue(
-            buttonEls[0].getAttribute('data-label') === 'Delete Revision' ||
-            buttonEls[0].getAttribute('data-label') === 'Delete Change'
+            deleteItems[0].name === 'Delete Revision' ||
+            deleteItems[0].name === 'Delete Change'
         );
         assert.isTrue(
-            buttonEls[1].getAttribute('data-label') === 'Delete Revision' ||
-            buttonEls[1].getAttribute('data-label') === 'Delete Change'
+            deleteItems[1].name === 'Delete Revision' ||
+            deleteItems[1].name === 'Delete Change'
         );
         done();
       });
@@ -205,6 +201,18 @@
       assert.deepEqual(element._getRevision(change, '2'), revObj);
     });
 
+    test('_actionComparator sort order', function() {
+      var actions = [
+        {label: '123', type: 'change', __key: 'review'},
+        {label: 'abc',type: 'revision'},
+        {label: 'abc',type: 'change'},
+        {label: 'def', type: 'change'},
+        {label: 'def', type: 'change',__primary: true},
+      ];
+      var result = actions.slice().sort(element._actionComparator);
+      assert.deepEqual(result, actions);
+    });
+
     test('submit change', function(done) {
       element.change = {
         revisions: {
@@ -285,10 +293,7 @@
         flushAsynchronousOperations();
         assert.isFalse(element.$.confirmRebase.hidden);
 
-        var cherryPickButton =
-            element.$$('gr-button[data-action-key="cherrypick"]');
-        assert.ok(cherryPickButton);
-        MockInteractions.tap(cherryPickButton);
+        element._handleCherrypickTap();
         flushAsynchronousOperations();
         assert.isTrue(element.$.confirmRebase.hidden);
         assert.isFalse(element.$.confirmCherrypick.hidden);
@@ -311,9 +316,7 @@
       });
 
       test('works', function() {
-        var cherryPickButton =
-            element.$$('gr-button[data-action-key="cherrypick"]');
-        MockInteractions.tap(cherryPickButton);
+        element._handleCherrypickTap();
         var action = {
           __key: 'cherrypick',
           __type: 'revision',
@@ -350,12 +353,10 @@
       });
 
       test('branch name cleared when re-open cherrypick', function() {
-        var cherryPickButton =
-            element.$$('gr-button[data-action-key="cherrypick"]');
         var emptyBranchName = '';
         element.$.confirmCherrypick.branch = 'master';
 
-        MockInteractions.tap(cherryPickButton);
+        element._handleCherrypickTap();
         assert.equal(element.$.confirmCherrypick.branch, emptyBranchName);
       });
     });
@@ -457,12 +458,6 @@
       var fireActionStub;
       var deleteAction;
 
-      var tapDeleteAction = function() {
-        var deleteButton = element.$$('gr-button[data-action-key=\'/\']');
-        MockInteractions.tap(deleteButton);
-        flushAsynchronousOperations();
-      };
-
       setup(function() {
         fireActionStub = sinon.stub(element, '_fireAction');
         element.change = {
@@ -484,12 +479,12 @@
       });
 
       test('does not delete on action', function() {
-        tapDeleteAction();
+        element._handleDeleteTap();
         assert.isFalse(fireActionStub.called);
       });
 
       test('shows confirm dialog', function() {
-        tapDeleteAction();
+        element._handleDeleteTap();
         assert.isFalse(element.$$('#confirmDeleteDialog').hidden);
         MockInteractions.tap(
             element.$$('#confirmDeleteDialog').$$('gr-button[primary]'));
@@ -498,7 +493,7 @@
       });
 
       test('hides delete confirm on cancel', function() {
-        tapDeleteAction();
+        element._handleDeleteTap();
         MockInteractions.tap(
             element.$$('#confirmDeleteDialog').$$('gr-button:not([primary])'));
         flushAsynchronousOperations();
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
index 1882500..f640dd6 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -49,11 +49,6 @@
       gr-button[link] {
         padding: 1em 0;
       }
-      gr-button:focus:not([primary]):not([secondary]),
-      gr-button:hover:not([primary]):not([secondary]) {
-        background-color: transparent;
-        border-color: transparent;
-      }
       ul {
         list-style: none;
       }
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
index 93e676c..c02583a 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -49,6 +49,7 @@
 
     setup(function() {
       element = fixture('basic');
+      element.change = {};
       var plugin;
       Gerrit.install(function(p) { plugin = p; }, '0.1',
           'http://test.com/plugins/testplugin/static/test.js');