Merge changes Ib2bb868f,I517d804b

* changes:
  Allow custom actions in toast messages
  Check for newer patch sets before setting labels
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
index acf3a62..8807917 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
@@ -36,6 +36,40 @@
         }
       }
     },
+
+    computeAllPatchSets: function(change) {
+      var patchNums = [];
+      for (var commit in change.revisions) {
+        if (change.revisions.hasOwnProperty(commit)) {
+          patchNums.push({
+            num: change.revisions[commit]._number,
+            desc: change.revisions[commit].description,
+          });
+        }
+      }
+      return patchNums.sort(function(a, b) { return a.num - b.num; });
+    },
+
+    computeLatestPatchNum: function(allPatchSets) {
+      return allPatchSets[allPatchSets.length - 1].num;
+    },
+
+    /**
+     * Check whether there is no newer patch than the latest patch that was
+     * available when this change was loaded.
+     * @return {Promise} A promise that yields true if the latest patch has been
+     *     loaded, and false if a newer patch has been uploaded in the meantime.
+     */
+    fetchIsLatestKnown: function(change, restAPI) {
+      var knownLatest = this.computeLatestPatchNum(
+          this.computeAllPatchSets(change));
+      return restAPI.getChangeDetail(change._number)
+          .then(function(detail) {
+            var actualLatest = this.computeLatestPatchNum(
+                this.computeAllPatchSets(detail));
+            return actualLatest <= knownLatest;
+          }.bind(this));
+    },
   };
 
   window.Gerrit = window.Gerrit || {};
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
index 7ff9371..892d94b 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
@@ -34,5 +34,50 @@
       assert.deepEqual(get(revisions, 2), revisions[2]);
       assert.equal(get(revisions, '3'), undefined);
     });
+
+    test('fetchIsLatestKnown on latest', function(done) {
+      var knownChange = {
+        revisions: {
+          sha1: {description: 'patch 1', _number: 1},
+          sha2: {description: 'patch 2', _number: 2},
+        },
+      };
+      var mockRestApi = {
+        getChangeDetail: function() {
+          return Promise.resolve(knownChange);
+        },
+      };
+      Gerrit.PatchSetBehavior.fetchIsLatestKnown(knownChange, mockRestApi)
+          .then(function(isLatest) {
+            assert.isTrue(isLatest);
+            done();
+          });
+    });
+
+    test('fetchIsLatestKnown not on latest', function(done) {
+      var knownChange = {
+        revisions: {
+          sha1: {description: 'patch 1', _number: 1},
+          sha2: {description: 'patch 2', _number: 2},
+        },
+      };
+      var actualChange = {
+        revisions: {
+          sha1: {description: 'patch 1', _number: 1},
+          sha2: {description: 'patch 2', _number: 2},
+          sha3: {description: 'patch 3', _number: 3},
+        },
+      };
+      var mockRestApi = {
+        getChangeDetail: function() {
+          return Promise.resolve(actualChange);
+        },
+      };
+      Gerrit.PatchSetBehavior.fetchIsLatestKnown(knownChange, mockRestApi)
+          .then(function(isLatest) {
+            assert.isFalse(isLatest);
+            done();
+          });
+    });
   });
 </script>
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 2cfa5ba..894ca63 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
@@ -14,6 +14,7 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../bower_components/iron-input/iron-input.html">
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 3999dcf..a51632a 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
@@ -120,6 +120,12 @@
      * @event <action key>-tap
      */
 
+    /**
+     * Fires to show an alert when a send is attempted on the non-latest patch.
+     *
+     * @event show-alert
+     */
+
     properties: {
       change: Object,
       actions: {
@@ -227,6 +233,7 @@
     RevisionActions: RevisionActions,
 
     behaviors: [
+      Gerrit.PatchSetBehavior,
       Gerrit.RESTClientBehavior,
     ],
 
@@ -787,15 +794,33 @@
       }.bind(this));
     },
 
-    _send: function(method, payload, actionEndpoint, revisionAction,
-        cleanupFn, opt_errorFn) {
-      var url = this.$.restAPI.getChangeActionURL(this.changeNum,
-          revisionAction ? this.patchNum : null, actionEndpoint);
-      return this.$.restAPI.send(method, url, payload,
-          this._handleResponseError, this).then(function(response) {
-            cleanupFn.call(this);
-            return response;
-          }.bind(this));
+    _send: function(method, payload, actionEndpoint, revisionAction, cleanupFn,
+        opt_errorFn) {
+      return this.fetchIsLatestKnown(this.change, this.$.restAPI)
+          .then(function(isLatest) {
+            if (!isLatest) {
+              this.fire('show-alert', {
+                  message: 'Cannot set label: a newer patch has been ' +
+                      'uploaded to this change.',
+                  action: 'Reload',
+                  callback: function() {
+                    // Load the current change without any patch range.
+                    location.href = this.getBaseUrl() + '/c/' +
+                        this.change._number;
+                  }.bind(this),
+              });
+              cleanupFn();
+              return Promise.resolve();
+            }
+
+            var url = this.$.restAPI.getChangeActionURL(this.changeNum,
+                revisionAction ? this.patchNum : null, actionEndpoint);
+            return this.$.restAPI.send(method, url, payload,
+                this._handleResponseError, this).then(function(response) {
+                  cleanupFn.call(this);
+                  return response;
+            }.bind(this));
+        }.bind(this));
     },
 
     _handleAbandonTap: function() {
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 5b23ded..fa56bb5 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
@@ -228,6 +228,8 @@
     });
 
     test('submit change', function(done) {
+      sandbox.stub(element, 'fetchIsLatestKnown',
+          function() { return Promise.resolve(true); });
       element.change = {
         revisions: {
           rev1: {_number: 1},
@@ -350,9 +352,6 @@
         sandbox.stub(window, 'alert');
       });
 
-      teardown(function() {
-      });
-
       test('works', function() {
         element._handleCherrypickTap();
         var action = {
@@ -467,9 +466,6 @@
         return element.reload();
       });
 
-      teardown(function() {
-      });
-
       test('revert change with plugin hook', function(done) {
         element.change = {
           current_revision: 'abc1234',
@@ -484,7 +480,6 @@
           MockInteractions.tap(revertButton);
 
           assert.equal(element.$.confirmRevertDialog.message, newRevertMsg);
-
           done();
         });
       });
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 32cc5c1..2c89e5c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -333,7 +333,7 @@
                 change-num="[[_changeNum]]"
                 change-status="[[_change.status]]"
                 commit-num="[[_commitInfo.commit]]"
-                patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
+                patch-num="[[computeLatestPatchNum(_allPatchSets)]]"
                 commit-message="[[_latestCommitMessage]]"
                 on-reload-change="_handleReloadChange"
                 on-download-tap="_handleDownloadTap"></gr-change-actions>
@@ -387,7 +387,7 @@
                   has-parent="{{hasParent}}"
                   loading="{{_relatedChangesLoading}}"
                   on-update="_updateRelatedChangeMaxHeight"
-                  patch-num="[[_computeLatestPatchNum(_allPatchSets)]]">
+                  patch-num="[[computeLatestPatchNum(_allPatchSets)]]">
               </gr-related-changes-list>
               <div
                   id="relatedChangesToggle"
@@ -424,7 +424,7 @@
                     disabled$="[[_computePatchSetDisabled(patchNum.num, _patchRange.basePatchNum)]]">
                   [[patchNum.num]]
                   /
-                  [[_computeLatestPatchNum(_allPatchSets)]]
+                  [[computeLatestPatchNum(_allPatchSets)]]
                   [[_computePatchSetDescription(_change, patchNum.num)]]
                 </option>
               </template>
@@ -495,7 +495,7 @@
         with-backdrop>
       <gr-reply-dialog id="replyDialog"
           change="{{_change}}"
-          patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
+          patch-num="[[computeLatestPatchNum(_allPatchSets)]]"
           permitted-labels="[[_change.permitted_labels]]"
           diff-drafts="[[_diffDrafts]]"
           server-config="[[serverConfig]]"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index f355403..e862991 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -119,7 +119,7 @@
       _currentRevisionActions: Object,
       _allPatchSets: {
         type: Array,
-        computed: '_computeAllPatchSets(_change, _change.revisions.*)',
+        computed: 'computeAllPatchSets(_change, _change.revisions.*)',
       },
       _loggedIn: {
         type: Boolean,
@@ -418,7 +418,7 @@
 
       if (this._initialLoadComplete && patchChanged) {
         if (patchRange.patchNum == null) {
-          patchRange.patchNum = this._computeLatestPatchNum(this._allPatchSets);
+          patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets);
         }
         this._patchRange = patchRange;
         this._reloadPatchNumDependentResources().then(function() {
@@ -556,7 +556,7 @@
           this._patchRange.basePatchNum || 'PARENT');
       this.set('_patchRange.patchNum',
           this._patchRange.patchNum ||
-              this._computeLatestPatchNum(this._allPatchSets));
+              this.computeLatestPatchNum(this._allPatchSets));
 
       this._updateSelected();
 
@@ -578,7 +578,7 @@
           currentPatchNum =
               this._change.revisions[this._change.current_revision]._number;
         } else {
-          currentPatchNum = this._computeLatestPatchNum(this._allPatchSets);
+          currentPatchNum = this.computeLatestPatchNum(this._allPatchSets);
         }
         if (patchNum === currentPatchNum &&
             this._patchRange.basePatchNum === 'PARENT') {
@@ -658,13 +658,9 @@
       return CHANGE_ID_ERROR.MISSING;
     },
 
-    _computeLatestPatchNum: function(allPatchSets) {
-      return allPatchSets[allPatchSets.length - 1].num;
-    },
-
     _computePatchInfoClass: function(patchNum, allPatchSets) {
       if (parseInt(patchNum, 10) ===
-          this._computeLatestPatchNum(allPatchSets)) {
+          this.computeLatestPatchNum(allPatchSets)) {
         return '';
       }
       return 'patchInfo--oldPatchSet';
@@ -682,19 +678,6 @@
       return parseInt(patchNum, 10) <= parseInt(basePatchNum, 10);
     },
 
-    _computeAllPatchSets: function(change) {
-      var patchNums = [];
-      for (var commit in change.revisions) {
-        if (change.revisions.hasOwnProperty(commit)) {
-          patchNums.push({
-            num: change.revisions[commit]._number,
-            desc: change.revisions[commit].description,
-          });
-        }
-      }
-      return patchNums.sort(function(a, b) { return a.num - b.num; });
-    },
-
     _computeLabelNames: function(labels) {
       return Object.keys(labels).sort();
     },
@@ -938,7 +921,7 @@
 
     _getLatestCommitMessage: function() {
       return this.$.restAPI.getChangeCommitInfo(this._changeNum,
-          this._computeLatestPatchNum(this._allPatchSets)).then(
+          this.computeLatestPatchNum(this._allPatchSets)).then(
               function(commitInfo) {
                 this._latestCommitMessage =
                     this._prepareCommitMsgForLinkify(commitInfo.message);
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 5b27b72..04beae1 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -568,7 +568,7 @@
     test('related changes are updated and new patch selected after rebase',
         function(done) {
       element._changeNum = '42';
-      sandbox.stub(element, '_computeLatestPatchNum', function() {
+      sandbox.stub(element, 'computeLatestPatchNum', function() {
         return 1;
       });
       sandbox.stub(element, '_reload',
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 4393936..d08961c 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -14,6 +14,8 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
@@ -52,8 +54,7 @@
         width: 100%;
       }
       .peopleContainer,
-      .labelsContainer,
-      .actionsContainer {
+      .labelsContainer {
         flex-shrink: 0;
       }
       .peopleContainer {
@@ -113,14 +114,24 @@
       .draftsContainer h3 {
         margin-top: .25em;
       }
-      .actionsContainer {
-        display: flex;
-        justify-content: space-between;
-      }
       .action:link,
       .action:visited {
         color: #00e;
       }
+      #checkingStatusLabel,
+      #notLatestLabel {
+        margin-left: 1em;
+      }
+      #checkingStatusLabel {
+        color: #444;
+        font-style: italic;
+      }
+      #notLatestLabel {
+        color: red;
+      }
+      #cancelButton {
+        float:right;
+      }
       @media screen and (max-width: 50em) {
         :host {
           max-height: none;
@@ -232,8 +243,23 @@
             patch-num="[[patchNum]]"
             hidden$="[[!_includeComments]]"></gr-comment-list>
       </section>
-      <section class="actionsContainer">
-        <gr-button primary class="action send" on-tap="_sendTapHandler">Send</gr-button>
+      <section>
+        <gr-button
+            primary
+            disabled="[[!_isState(knownLatestState, 'latest')]]"
+            class="action send"
+            on-tap="_sendTapHandler">Send</gr-button>
+        <span
+            id="checkingStatusLabel"
+            hidden$="[[!_isState(knownLatestState, 'checking')]]">
+          Checking whether patch [[patchNum]] is latest...
+        </span>
+        <span
+            id="notLatestLabel"
+            hidden$="[[!_isState(knownLatestState, 'not-latest')]]">
+          Patch [[patchNum]] is not latest.
+          <gr-button link on-tap="_reload">Reload</gr-button>
+        </span>
         <gr-button
             id="cancelButton"
             class="action cancel"
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index c237610..0bb548b 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -28,6 +28,12 @@
     CC: 'CC',
   };
 
+  var LatestPatchState = {
+    LATEST: 'latest',
+    CHECKING: 'checking',
+    NOT_LATEST: 'not-latest',
+  };
+
   Polymer({
     is: 'gr-reply-dialog',
 
@@ -50,6 +56,12 @@
      * @event autogrow
      */
 
+    /**
+     * Fires to show an alert when a send is attempted on the non-latest patch.
+     *
+     * @event show-alert
+     */
+
     properties: {
       change: Object,
       patchNum: String,
@@ -77,6 +89,7 @@
       permittedLabels: Object,
       serverConfig: Object,
       projectConfig: Object,
+      knownLatestState: String,
 
       _account: Object,
       _ccs: Array,
@@ -112,7 +125,9 @@
     FocusTarget: FocusTarget,
 
     behaviors: [
+      Gerrit.BaseUrlBehavior,
       Gerrit.KeyboardShortcutBehavior,
+      Gerrit.PatchSetBehavior,
       Gerrit.RESTClientBehavior,
     ],
 
@@ -137,6 +152,13 @@
     },
 
     open: function(opt_focusTarget) {
+      this.knownLatestState = LatestPatchState.CHECKING;
+      this.fetchIsLatestKnown(this.change, this.$.restAPI)
+          .then(function(isUpToDate) {
+            this.knownLatestState = isUpToDate ?
+                LatestPatchState.LATEST : LatestPatchState.NOT_LATEST;
+          }.bind(this));
+
       this._focusOn(opt_focusTarget);
       if (!this.draft || !this.draft.length) {
         this.draft = this._loadStoredDraft();
@@ -256,6 +278,12 @@
     },
 
     send: function(includeComments) {
+      if (this.knownLatestState === 'not-latest') {
+        this.fire('show-alert',
+            {message: 'Cannot reply to non-latest patch.'});
+        return;
+      }
+
       var labels = this.$.labelScores.getLabelValues();
 
       var obj = {
@@ -545,5 +573,14 @@
         this.fire('autogrow');
       });
     },
+
+    _isState: function(knownLatestState, value) {
+      return knownLatestState === value;
+    },
+
+    _reload: function() {
+      // Load the current change without any patch range.
+      location.href = this.getBaseUrl() + '/c/' + this.change._number;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 0e53067..253f01c 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -102,6 +102,9 @@
       eraseDraftCommentStub = sandbox.stub(element.$.storage,
           'eraseDraftComment');
 
+      sandbox.stub(element, 'fetchIsLatestKnown',
+          function() { return Promise.resolve(true); });
+
       // Allow the elements created by dom-repeat to be stamped.
       flushAsynchronousOperations();
     });
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index 22349de..27dcd4a 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -96,7 +96,7 @@
     },
 
     _handleShowAlert: function(e) {
-      this._showAlert(e.detail.message);
+      this._showAlert(e.detail.message, e.detail.action, e.detail.callback);
     },
 
     _handleNetworkError: function(e) {
@@ -108,14 +108,14 @@
       return this.$.restAPI.getLoggedIn();
     },
 
-    _showAlert: function(text) {
+    _showAlert: function(text, opt_actionText, opt_actionCallback) {
       if (this._alertElement) { return; }
 
       this._clearHideAlertHandle();
       this._hideAlertHandle =
         this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS);
       var el = this._createToastAlert();
-      el.show(text);
+      el.show(text, opt_actionText, opt_actionCallback);
       this._alertElement = el;
     },
 
@@ -138,8 +138,8 @@
       if (this._alertElement) { return; }
 
       this._alertElement = this._createToastAlert();
-      this._alertElement.show(errorText, actionText);
-      this.listen(this._alertElement, 'action', '_createLoginPopup');
+      this._alertElement.show(errorText, actionText,
+          this._createLoginPopup.bind(this));
 
       this._refreshingCredentials = true;
       this._requestCheckLoggedIn();
@@ -219,7 +219,6 @@
     _handleCredentialRefreshed: function() {
       this.unlisten(window, 'focus', '_handleWindowFocus');
       this._refreshingCredentials = false;
-      this.unlisten(this._alertElement, 'action', '_createLoginPopup');
       this._hideAlert();
       this._showAlert('Credentials refreshed.');
     },
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index 191d67a..c8743fb 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -156,7 +156,7 @@
             Polymer.dom(toast.root).textContent, 'Refresh credentials.');
 
         assert.isFalse(windowOpen.called);
-        toast.fire('action');
+        MockInteractions.tap(toast.$$('gr-button.action'));
         assert.isTrue(windowOpen.called);
 
         // @see Issue 5822: noopener breaks closeAfterLogin
@@ -183,10 +183,13 @@
     });
 
     test('show alert', function() {
+      var alertObj = {message: 'foo'}
       sandbox.stub(element, '_showAlert');
       element.fire('show-alert', {message: 'foo'});
       assert.isTrue(element._showAlert.calledOnce);
-      assert.isTrue(element._showAlert.lastCall.calledWithExactly('foo'));
+      assert.equal(element._showAlert.lastCall.args[0], 'foo');
+      assert.isNotOk(element._showAlert.lastCall.args[1]);
+      assert.isNotOk(element._showAlert.lastCall.args[2]);
     });
 
     test('checks stale credentials on visibility change', function() {
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
index 84846fb..ee7fe97 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -43,6 +43,7 @@
         type: Function,
         value: function() { return this._handleTransitionEnd.bind(this); },
       },
+      _actionCallback: Function,
     },
 
     attached: function() {
@@ -54,10 +55,11 @@
           this._boundTransitionEndHandler);
     },
 
-    show: function(text, opt_actionText) {
+    show: function(text, opt_actionText, opt_actionCallback) {
       this.text = text;
       this.actionText = opt_actionText;
       this._hideActionButton = !opt_actionText;
+      this._actionCallback = opt_actionCallback;
       document.body.appendChild(this);
       this._setShown(true);
     },
@@ -84,7 +86,7 @@
 
     _handleActionTap: function(e) {
       e.preventDefault();
-      this.fire('action', null, {bubbles: false});
+      if (this._actionCallback) { this._actionCallback(); }
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
index 067ac5b..be21edd 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
@@ -50,9 +50,7 @@
 
     test('action event', function(done) {
       element.show();
-      element.addEventListener('action', function() {
-        done();
-      });
+      element._actionCallback = done;
       MockInteractions.tap(element.$$('.action'));
     });