| /** | 
 |  * @license | 
 |  * 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. | 
 |  */ | 
 | (function() { | 
 |   'use strict'; | 
 |  | 
 |   const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.'; | 
 |   const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.'; | 
 |   const ERR_REVISION_ACTIONS = 'Couldn’t load revision actions.'; | 
 |   /** | 
 |    * @enum {string} | 
 |    */ | 
 |   const LabelStatus = { | 
 |     /** | 
 |      * This label provides what is necessary for submission. | 
 |      */ | 
 |     OK: 'OK', | 
 |     /** | 
 |      * This label prevents the change from being submitted. | 
 |      */ | 
 |     REJECT: 'REJECT', | 
 |     /** | 
 |      * The label may be set, but it's neither necessary for submission | 
 |      * nor does it block submission if set. | 
 |      */ | 
 |     MAY: 'MAY', | 
 |     /** | 
 |      * The label is required for submission, but has not been satisfied. | 
 |      */ | 
 |     NEED: 'NEED', | 
 |     /** | 
 |      * The label is required for submission, but is impossible to complete. | 
 |      * The likely cause is access has not been granted correctly by the | 
 |      * project owner or site administrator. | 
 |      */ | 
 |     IMPOSSIBLE: 'IMPOSSIBLE', | 
 |     OPTIONAL: 'OPTIONAL', | 
 |   }; | 
 |  | 
 |   // TODO(davido): Add the rest of the change actions. | 
 |   const ChangeActions = { | 
 |     ABANDON: 'abandon', | 
 |     DELETE: '/', | 
 |     DELETE_EDIT: 'deleteEdit', | 
 |     EDIT: 'edit', | 
 |     FOLLOW_UP: 'followup', | 
 |     IGNORE: 'ignore', | 
 |     MOVE: 'move', | 
 |     PRIVATE: 'private', | 
 |     PRIVATE_DELETE: 'private.delete', | 
 |     PUBLISH_EDIT: 'publishEdit', | 
 |     REBASE_EDIT: 'rebaseEdit', | 
 |     RESTORE: 'restore', | 
 |     REVERT: 'revert', | 
 |     REVIEWED: 'reviewed', | 
 |     STOP_EDIT: 'stopEdit', | 
 |     UNIGNORE: 'unignore', | 
 |     UNREVIEWED: 'unreviewed', | 
 |     WIP: 'wip', | 
 |   }; | 
 |  | 
 |   // TODO(andybons): Add the rest of the revision actions. | 
 |   const RevisionActions = { | 
 |     CHERRYPICK: 'cherrypick', | 
 |     REBASE: 'rebase', | 
 |     SUBMIT: 'submit', | 
 |     DOWNLOAD: 'download', | 
 |   }; | 
 |  | 
 |   const ActionLoadingLabels = { | 
 |     abandon: 'Abandoning...', | 
 |     cherrypick: 'Cherry-picking...', | 
 |     delete: 'Deleting...', | 
 |     move: 'Moving..', | 
 |     rebase: 'Rebasing...', | 
 |     restore: 'Restoring...', | 
 |     revert: 'Reverting...', | 
 |     submit: 'Submitting...', | 
 |   }; | 
 |  | 
 |   const ActionType = { | 
 |     CHANGE: 'change', | 
 |     REVISION: 'revision', | 
 |   }; | 
 |  | 
 |   const ADDITIONAL_ACTION_KEY_PREFIX = '__additionalAction_'; | 
 |  | 
 |   const QUICK_APPROVE_ACTION = { | 
 |     __key: 'review', | 
 |     __type: 'change', | 
 |     enabled: true, | 
 |     key: 'review', | 
 |     label: 'Quick approve', | 
 |     method: 'POST', | 
 |   }; | 
 |  | 
 |   const ActionPriority = { | 
 |     CHANGE: 2, | 
 |     DEFAULT: 0, | 
 |     PRIMARY: 3, | 
 |     REVIEW: -3, | 
 |     REVISION: 1, | 
 |   }; | 
 |  | 
 |   const DOWNLOAD_ACTION = { | 
 |     enabled: true, | 
 |     label: 'Download patch', | 
 |     title: 'Open download dialog', | 
 |     __key: 'download', | 
 |     __primary: false, | 
 |     __type: 'revision', | 
 |   }; | 
 |  | 
 |   const REBASE_EDIT = { | 
 |     enabled: true, | 
 |     label: 'Rebase edit', | 
 |     title: 'Rebase change edit', | 
 |     __key: 'rebaseEdit', | 
 |     __primary: false, | 
 |     __type: 'change', | 
 |     method: 'POST', | 
 |   }; | 
 |  | 
 |   const PUBLISH_EDIT = { | 
 |     enabled: true, | 
 |     label: 'Publish edit', | 
 |     title: 'Publish change edit', | 
 |     __key: 'publishEdit', | 
 |     __primary: false, | 
 |     __type: 'change', | 
 |     method: 'POST', | 
 |   }; | 
 |  | 
 |   const DELETE_EDIT = { | 
 |     enabled: true, | 
 |     label: 'Delete edit', | 
 |     title: 'Delete change edit', | 
 |     __key: 'deleteEdit', | 
 |     __primary: false, | 
 |     __type: 'change', | 
 |     method: 'DELETE', | 
 |   }; | 
 |  | 
 |   const EDIT = { | 
 |     enabled: true, | 
 |     label: 'Edit', | 
 |     title: 'Edit this change', | 
 |     __key: 'edit', | 
 |     __primary: false, | 
 |     __type: 'change', | 
 |   }; | 
 |  | 
 |   const STOP_EDIT = { | 
 |     enabled: true, | 
 |     label: 'Stop editing', | 
 |     title: 'Stop editing this change', | 
 |     __key: 'stopEdit', | 
 |     __primary: false, | 
 |     __type: 'change', | 
 |   }; | 
 |  | 
 |   // Set of keys that have icons. As more icons are added to gr-icons.html, this | 
 |   // set should be expanded. | 
 |   const ACTIONS_WITH_ICONS = new Set([ | 
 |     ChangeActions.ABANDON, | 
 |     ChangeActions.DELETE_EDIT, | 
 |     ChangeActions.EDIT, | 
 |     ChangeActions.PUBLISH_EDIT, | 
 |     ChangeActions.REBASE_EDIT, | 
 |     ChangeActions.RESTORE, | 
 |     ChangeActions.REVERT, | 
 |     ChangeActions.STOP_EDIT, | 
 |     QUICK_APPROVE_ACTION.key, | 
 |     RevisionActions.REBASE, | 
 |     RevisionActions.SUBMIT, | 
 |   ]); | 
 |  | 
 |   const AWAIT_CHANGE_ATTEMPTS = 5; | 
 |   const AWAIT_CHANGE_TIMEOUT_MS = 1000; | 
 |  | 
 |   Polymer({ | 
 |     is: 'gr-change-actions', | 
 |  | 
 |     /** | 
 |      * Fired when the change should be reloaded. | 
 |      * | 
 |      * @event reload-change | 
 |      */ | 
 |  | 
 |     /** | 
 |      * Fired when an action is tapped. | 
 |      * | 
 |      * @event <action key>-tap | 
 |      */ | 
 |  | 
 |     /** | 
 |      * Fires to show an alert when a send is attempted on the non-latest patch. | 
 |      * | 
 |      * @event show-alert | 
 |      */ | 
 |  | 
 |     /** | 
 |      * Fires when a change action fails. | 
 |      * | 
 |      * @event show-error | 
 |      */ | 
 |  | 
 |     properties: { | 
 |       /** | 
 |        * @type {{ | 
 |        *    _number: number, | 
 |        *    branch: string, | 
 |        *    id: string, | 
 |        *    project: string, | 
 |        *    subject: string, | 
 |        *  }} | 
 |        */ | 
 |       change: Object, | 
 |       actions: { | 
 |         type: Object, | 
 |         value() { return {}; }, | 
 |       }, | 
 |       primaryActionKeys: { | 
 |         type: Array, | 
 |         value() { | 
 |           return [ | 
 |             RevisionActions.SUBMIT, | 
 |           ]; | 
 |         }, | 
 |       }, | 
 |       disableEdit: { | 
 |         type: Boolean, | 
 |         value: false, | 
 |       }, | 
 |       _hasKnownChainState: { | 
 |         type: Boolean, | 
 |         value: false, | 
 |       }, | 
 |       _hideQuickApproveAction: { | 
 |         type: Boolean, | 
 |         value: false, | 
 |       }, | 
 |       changeNum: String, | 
 |       changeStatus: String, | 
 |       commitNum: String, | 
 |       hasParent: { | 
 |         type: Boolean, | 
 |         observer: '_computeChainState', | 
 |       }, | 
 |       latestPatchNum: String, | 
 |       commitMessage: { | 
 |         type: String, | 
 |         value: '', | 
 |       }, | 
 |       /** @type {?} */ | 
 |       revisionActions: { | 
 |         type: Object, | 
 |         value() { return {}; }, | 
 |       }, | 
 |       privateByDefault: String, | 
 |  | 
 |       _loading: { | 
 |         type: Boolean, | 
 |         value: true, | 
 |       }, | 
 |       _actionLoadingMessage: { | 
 |         type: String, | 
 |         value: '', | 
 |       }, | 
 |       _allActionValues: { | 
 |         type: Array, | 
 |         computed: '_computeAllActions(actions.*, revisionActions.*,' + | 
 |             'primaryActionKeys.*, _additionalActions.*, change, ' + | 
 |             '_actionPriorityOverrides.*)', | 
 |       }, | 
 |       _topLevelActions: { | 
 |         type: Array, | 
 |         computed: '_computeTopLevelActions(_allActionValues.*, ' + | 
 |             '_hiddenActions.*, _overflowActions.*)', | 
 |         observer: '_filterPrimaryActions', | 
 |       }, | 
 |       _topLevelPrimaryActions: Array, | 
 |       _topLevelSecondaryActions: Array, | 
 |       _menuActions: { | 
 |         type: Array, | 
 |         computed: '_computeMenuActions(_allActionValues.*, _hiddenActions.*, ' + | 
 |             '_overflowActions.*)', | 
 |       }, | 
 |       _overflowActions: { | 
 |         type: Array, | 
 |         value() { | 
 |           const value = [ | 
 |             { | 
 |               type: ActionType.CHANGE, | 
 |               key: ChangeActions.WIP, | 
 |             }, | 
 |             { | 
 |               type: ActionType.CHANGE, | 
 |               key: ChangeActions.DELETE, | 
 |             }, | 
 |             { | 
 |               type: ActionType.REVISION, | 
 |               key: RevisionActions.CHERRYPICK, | 
 |             }, | 
 |             { | 
 |               type: ActionType.CHANGE, | 
 |               key: ChangeActions.MOVE, | 
 |             }, | 
 |             { | 
 |               type: ActionType.REVISION, | 
 |               key: RevisionActions.DOWNLOAD, | 
 |             }, | 
 |             { | 
 |               type: ActionType.CHANGE, | 
 |               key: ChangeActions.IGNORE, | 
 |             }, | 
 |             { | 
 |               type: ActionType.CHANGE, | 
 |               key: ChangeActions.UNIGNORE, | 
 |             }, | 
 |             { | 
 |               type: ActionType.CHANGE, | 
 |               key: ChangeActions.REVIEWED, | 
 |             }, | 
 |             { | 
 |               type: ActionType.CHANGE, | 
 |               key: ChangeActions.UNREVIEWED, | 
 |             }, | 
 |             { | 
 |               type: ActionType.CHANGE, | 
 |               key: ChangeActions.PRIVATE, | 
 |             }, | 
 |             { | 
 |               type: ActionType.CHANGE, | 
 |               key: ChangeActions.PRIVATE_DELETE, | 
 |             }, | 
 |             { | 
 |               type: ActionType.CHANGE, | 
 |               key: ChangeActions.FOLLOW_UP, | 
 |             }, | 
 |           ]; | 
 |           return value; | 
 |         }, | 
 |       }, | 
 |       _actionPriorityOverrides: { | 
 |         type: Array, | 
 |         value() { return []; }, | 
 |       }, | 
 |       _additionalActions: { | 
 |         type: Array, | 
 |         value() { return []; }, | 
 |       }, | 
 |       _hiddenActions: { | 
 |         type: Array, | 
 |         value() { return []; }, | 
 |       }, | 
 |       _disabledMenuActions: { | 
 |         type: Array, | 
 |         value() { return []; }, | 
 |       }, | 
 |       // editPatchsetLoaded == "does the current selected patch range have | 
 |       // 'edit' as one of either basePatchNum or patchNum". | 
 |       editPatchsetLoaded: { | 
 |         type: Boolean, | 
 |         value: false, | 
 |       }, | 
 |       // editMode == "is edit mode enabled in the file list". | 
 |       editMode: { | 
 |         type: Boolean, | 
 |         value: false, | 
 |       }, | 
 |       editBasedOnCurrentPatchSet: { | 
 |         type: Boolean, | 
 |         value: true, | 
 |       }, | 
 |     }, | 
 |  | 
 |     ActionType, | 
 |     ChangeActions, | 
 |     RevisionActions, | 
 |  | 
 |     behaviors: [ | 
 |       Gerrit.PatchSetBehavior, | 
 |       Gerrit.RESTClientBehavior, | 
 |     ], | 
 |  | 
 |     observers: [ | 
 |       '_actionsChanged(actions.*, revisionActions.*, _additionalActions.*)', | 
 |       '_changeChanged(change)', | 
 |       '_editStatusChanged(editMode, editPatchsetLoaded, ' + | 
 |           'editBasedOnCurrentPatchSet, disableEdit, actions.*, change.*)', | 
 |     ], | 
 |  | 
 |     listeners: { | 
 |       'fullscreen-overlay-opened': '_handleHideBackgroundContent', | 
 |       'fullscreen-overlay-closed': '_handleShowBackgroundContent', | 
 |     }, | 
 |  | 
 |     ready() { | 
 |       this.$.jsAPI.addElement(this.$.jsAPI.Element.CHANGE_ACTIONS, this); | 
 |       this._handleLoadingComplete(); | 
 |     }, | 
 |  | 
 |     reload() { | 
 |       if (!this.changeNum || !this.latestPatchNum) { | 
 |         return Promise.resolve(); | 
 |       } | 
 |  | 
 |       this._loading = true; | 
 |       return this._getRevisionActions().then(revisionActions => { | 
 |         if (!revisionActions) { return; } | 
 |  | 
 |         this.revisionActions = revisionActions; | 
 |         this._handleLoadingComplete(); | 
 |       }).catch(err => { | 
 |         this.fire('show-alert', {message: ERR_REVISION_ACTIONS}); | 
 |         this._loading = false; | 
 |         throw err; | 
 |       }); | 
 |     }, | 
 |  | 
 |     _handleLoadingComplete() { | 
 |       Gerrit.awaitPluginsLoaded().then(() => this._loading = false); | 
 |     }, | 
 |  | 
 |     _changeChanged() { | 
 |       this.reload(); | 
 |     }, | 
 |  | 
 |     addActionButton(type, label) { | 
 |       if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { | 
 |         throw Error(`Invalid action type: ${type}`); | 
 |       } | 
 |       const action = { | 
 |         enabled: true, | 
 |         label, | 
 |         __type: type, | 
 |         __key: ADDITIONAL_ACTION_KEY_PREFIX + | 
 |             Math.random().toString(36).substr(2), | 
 |       }; | 
 |       this.push('_additionalActions', action); | 
 |       return action.__key; | 
 |     }, | 
 |  | 
 |     removeActionButton(key) { | 
 |       const idx = this._indexOfActionButtonWithKey(key); | 
 |       if (idx === -1) { | 
 |         return; | 
 |       } | 
 |       this.splice('_additionalActions', idx, 1); | 
 |     }, | 
 |  | 
 |     setActionButtonProp(key, prop, value) { | 
 |       this.set([ | 
 |         '_additionalActions', | 
 |         this._indexOfActionButtonWithKey(key), | 
 |         prop, | 
 |       ], value); | 
 |     }, | 
 |  | 
 |     setActionOverflow(type, key, overflow) { | 
 |       if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { | 
 |         throw Error(`Invalid action type given: ${type}`); | 
 |       } | 
 |       const index = this._getActionOverflowIndex(type, key); | 
 |       const action = { | 
 |         type, | 
 |         key, | 
 |         overflow, | 
 |       }; | 
 |       if (!overflow && index !== -1) { | 
 |         this.splice('_overflowActions', index, 1); | 
 |       } else if (overflow) { | 
 |         this.push('_overflowActions', action); | 
 |       } | 
 |     }, | 
 |  | 
 |     setActionPriority(type, key, priority) { | 
 |       if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { | 
 |         throw Error(`Invalid action type given: ${type}`); | 
 |       } | 
 |       const index = this._actionPriorityOverrides.findIndex(action => { | 
 |         return action.type === type && action.key === key; | 
 |       }); | 
 |       const action = { | 
 |         type, | 
 |         key, | 
 |         priority, | 
 |       }; | 
 |       if (index !== -1) { | 
 |         this.set('_actionPriorityOverrides', index, action); | 
 |       } else { | 
 |         this.push('_actionPriorityOverrides', action); | 
 |       } | 
 |     }, | 
 |  | 
 |     setActionHidden(type, key, hidden) { | 
 |       if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { | 
 |         throw Error(`Invalid action type given: ${type}`); | 
 |       } | 
 |  | 
 |       const idx = this._hiddenActions.indexOf(key); | 
 |       if (hidden && idx === -1) { | 
 |         this.push('_hiddenActions', key); | 
 |       } else if (!hidden && idx !== -1) { | 
 |         this.splice('_hiddenActions', idx, 1); | 
 |       } | 
 |     }, | 
 |  | 
 |     getActionDetails(action) { | 
 |       if (this.revisionActions[action]) { | 
 |         return this.revisionActions[action]; | 
 |       } else if (this.actions[action]) { | 
 |         return this.actions[action]; | 
 |       } | 
 |     }, | 
 |  | 
 |     _indexOfActionButtonWithKey(key) { | 
 |       for (let i = 0; i < this._additionalActions.length; i++) { | 
 |         if (this._additionalActions[i].__key === key) { | 
 |           return i; | 
 |         } | 
 |       } | 
 |       return -1; | 
 |     }, | 
 |  | 
 |     _getRevisionActions() { | 
 |       return this.$.restAPI.getChangeRevisionActions(this.changeNum, | 
 |           this.latestPatchNum); | 
 |     }, | 
 |  | 
 |     _shouldHideActions(actions, loading) { | 
 |       return loading || !actions || !actions.base || !actions.base.length; | 
 |     }, | 
 |  | 
 |     _keyCount(changeRecord) { | 
 |       return Object.keys((changeRecord && changeRecord.base) || {}).length; | 
 |     }, | 
 |  | 
 |     _actionsChanged(actionsChangeRecord, revisionActionsChangeRecord, | 
 |         additionalActionsChangeRecord) { | 
 |       const additionalActions = (additionalActionsChangeRecord && | 
 |           additionalActionsChangeRecord.base) || []; | 
 |       this.hidden = this._keyCount(actionsChangeRecord) === 0 && | 
 |           this._keyCount(revisionActionsChangeRecord) === 0 && | 
 |               additionalActions.length === 0; | 
 |       this._actionLoadingMessage = ''; | 
 |       this._disabledMenuActions = []; | 
 |  | 
 |       const revisionActions = revisionActionsChangeRecord.base || {}; | 
 |       if (Object.keys(revisionActions).length !== 0) { | 
 |         if (!revisionActions.download) { | 
 |           this.set('revisionActions.download', DOWNLOAD_ACTION); | 
 |         } | 
 |       } | 
 |     }, | 
 |  | 
 |       /** | 
 |        * @param {string=} actionName | 
 |        */ | 
 |     _deleteAndNotify(actionName) { | 
 |       if (this.actions[actionName]) { | 
 |         delete this.actions[actionName]; | 
 |         this.notifyPath('actions.' + actionName); | 
 |       } | 
 |     }, | 
 |  | 
 |     _editStatusChanged(editMode, editPatchsetLoaded, | 
 |         editBasedOnCurrentPatchSet, disableEdit) { | 
 |       if (disableEdit) { | 
 |         this._deleteAndNotify('publishEdit'); | 
 |         this._deleteAndNotify('rebaseEdit'); | 
 |         this._deleteAndNotify('deleteEdit'); | 
 |         this._deleteAndNotify('stopEdit'); | 
 |         this._deleteAndNotify('edit'); | 
 |         return; | 
 |       } | 
 |       if (editPatchsetLoaded) { | 
 |         // Only show actions that mutate an edit if an actual edit patch set | 
 |         // is loaded. | 
 |         if (this.changeIsOpen(this.change.status)) { | 
 |           if (editBasedOnCurrentPatchSet) { | 
 |             if (!this.actions.publishEdit) { | 
 |               this.set('actions.publishEdit', PUBLISH_EDIT); | 
 |             } | 
 |             this._deleteAndNotify('rebaseEdit'); | 
 |           } else { | 
 |             if (!this.actions.rebaseEdit) { | 
 |               this.set('actions.rebaseEdit', REBASE_EDIT); | 
 |             } | 
 |             this._deleteAndNotify('publishEdit'); | 
 |           } | 
 |         } | 
 |         if (!this.actions.deleteEdit) { | 
 |           this.set('actions.deleteEdit', DELETE_EDIT); | 
 |         } | 
 |       } else { | 
 |         this._deleteAndNotify('publishEdit'); | 
 |         this._deleteAndNotify('rebaseEdit'); | 
 |         this._deleteAndNotify('deleteEdit'); | 
 |       } | 
 |  | 
 |       if (this.changeIsOpen(this.change.status)) { | 
 |         // Only show edit button if there is no edit patchset loaded and the | 
 |         // file list is not in edit mode. | 
 |         if (editPatchsetLoaded || editMode) { | 
 |           this._deleteAndNotify('edit'); | 
 |         } else { | 
 |           if (!this.actions.edit) { this.set('actions.edit', EDIT); } | 
 |         } | 
 |         // Only show STOP_EDIT if edit mode is enabled, but no edit patch set | 
 |         // is loaded. | 
 |         if (editMode && !editPatchsetLoaded) { | 
 |           if (!this.actions.stopEdit) { | 
 |             this.set('actions.stopEdit', STOP_EDIT); | 
 |           } | 
 |         } else { | 
 |           this._deleteAndNotify('stopEdit'); | 
 |         } | 
 |       } else { | 
 |         // Remove edit button. | 
 |         this._deleteAndNotify('edit'); | 
 |       } | 
 |     }, | 
 |  | 
 |     _getValuesFor(obj) { | 
 |       return Object.keys(obj).map(key => { | 
 |         return obj[key]; | 
 |       }); | 
 |     }, | 
 |  | 
 |     _getLabelStatus(label) { | 
 |       if (label.approved) { | 
 |         return LabelStatus.OK; | 
 |       } else if (label.rejected) { | 
 |         return LabelStatus.REJECT; | 
 |       } else if (label.optional) { | 
 |         return LabelStatus.OPTIONAL; | 
 |       } else { | 
 |         return LabelStatus.NEED; | 
 |       } | 
 |     }, | 
 |  | 
 |     /** | 
 |      * Get highest score for last missing permitted label for current change. | 
 |      * Returns null if no labels permitted or more than one label missing. | 
 |      * | 
 |      * @return {{label: string, score: string}|null} | 
 |      */ | 
 |     _getTopMissingApproval() { | 
 |       if (!this.change || | 
 |           !this.change.labels || | 
 |           !this.change.permitted_labels) { | 
 |         return null; | 
 |       } | 
 |       let result; | 
 |       for (const label in this.change.labels) { | 
 |         if (!(label in this.change.permitted_labels)) { | 
 |           continue; | 
 |         } | 
 |         if (this.change.permitted_labels[label].length === 0) { | 
 |           continue; | 
 |         } | 
 |         const status = this._getLabelStatus(this.change.labels[label]); | 
 |         if (status === LabelStatus.NEED) { | 
 |           if (result) { | 
 |             // More than one label is missing, so it's unclear which to quick | 
 |             // approve, return null; | 
 |             return null; | 
 |           } | 
 |           result = label; | 
 |         } else if (status === LabelStatus.REJECT || | 
 |             status === LabelStatus.IMPOSSIBLE) { | 
 |           return null; | 
 |         } | 
 |       } | 
 |       if (result) { | 
 |         const score = this.change.permitted_labels[result].slice(-1)[0]; | 
 |         const maxScore = | 
 |             Object.keys(this.change.labels[result].values).slice(-1)[0]; | 
 |         if (score === maxScore) { | 
 |           // Allow quick approve only for maximal score. | 
 |           return { | 
 |             label: result, | 
 |             score, | 
 |           }; | 
 |         } | 
 |       } | 
 |       return null; | 
 |     }, | 
 |  | 
 |     hideQuickApproveAction() { | 
 |       this._topLevelSecondaryActions = | 
 |         this._topLevelSecondaryActions.filter(sa => { | 
 |           return sa.key !== QUICK_APPROVE_ACTION.key; | 
 |         }); | 
 |       this._hideQuickApproveAction = true; | 
 |     }, | 
 |  | 
 |     _getQuickApproveAction() { | 
 |       if (this._hideQuickApproveAction) { | 
 |         return null; | 
 |       } | 
 |       const approval = this._getTopMissingApproval(); | 
 |       if (!approval) { | 
 |         return null; | 
 |       } | 
 |       const action = Object.assign({}, QUICK_APPROVE_ACTION); | 
 |       action.label = approval.label + approval.score; | 
 |       const review = { | 
 |         drafts: 'PUBLISH_ALL_REVISIONS', | 
 |         labels: {}, | 
 |       }; | 
 |       review.labels[approval.label] = approval.score; | 
 |       action.payload = review; | 
 |       return action; | 
 |     }, | 
 |  | 
 |     _getActionValues(actionsChangeRecord, primariesChangeRecord, | 
 |         additionalActionsChangeRecord, type) { | 
 |       if (!actionsChangeRecord || !primariesChangeRecord) { return []; } | 
 |  | 
 |       const actions = actionsChangeRecord.base || {}; | 
 |       const primaryActionKeys = primariesChangeRecord.base || []; | 
 |       const result = []; | 
 |       const values = this._getValuesFor( | 
 |           type === ActionType.CHANGE ? ChangeActions : RevisionActions); | 
 |       const pluginActions = []; | 
 |       Object.keys(actions).forEach(a => { | 
 |         actions[a].__key = a; | 
 |         actions[a].__type = type; | 
 |         actions[a].__primary = primaryActionKeys.includes(a); | 
 |         // Plugin actions always contain ~ in the key. | 
 |         if (a.indexOf('~') !== -1) { | 
 |           this._populateActionUrl(actions[a]); | 
 |           pluginActions.push(actions[a]); | 
 |           // Add server-side provided plugin actions to overflow menu. | 
 |           this._overflowActions.push({ | 
 |             type, | 
 |             key: a, | 
 |           }); | 
 |           return; | 
 |         } else if (!values.includes(a)) { | 
 |           return; | 
 |         } | 
 |         actions[a].label = this._getActionLabel(actions[a]); | 
 |  | 
 |         // Triggers a re-render by ensuring object inequality. | 
 |         result.push(Object.assign({}, actions[a])); | 
 |       }); | 
 |  | 
 |       let additionalActions = (additionalActionsChangeRecord && | 
 |       additionalActionsChangeRecord.base) || []; | 
 |       additionalActions = additionalActions.filter(a => { | 
 |         return a.__type === type; | 
 |       }).map(a => { | 
 |         a.__primary = primaryActionKeys.includes(a.__key); | 
 |         // Triggers a re-render by ensuring object inequality. | 
 |         return Object.assign({}, a); | 
 |       }); | 
 |       return result.concat(additionalActions).concat(pluginActions); | 
 |     }, | 
 |  | 
 |     _populateActionUrl(action) { | 
 |       const patchNum = | 
 |             action.__type === ActionType.REVISION ? this.latestPatchNum : null; | 
 |       this.$.restAPI.getChangeActionURL( | 
 |           this.changeNum, patchNum, '/' + action.__key) | 
 |           .then(url => action.__url = url); | 
 |     }, | 
 |  | 
 |     /** | 
 |      * Given a change action, return a display label that uses the appropriate | 
 |      * casing or includes explanatory details. | 
 |      */ | 
 |     _getActionLabel(action) { | 
 |       if (action.label === 'Delete') { | 
 |         // This label is common within change and revision actions. Make it more | 
 |         // explicit to the user. | 
 |         return 'Delete change'; | 
 |       } else if (action.label === 'WIP') { | 
 |         return 'Mark as work in progress'; | 
 |       } | 
 |       // Otherwise, just map the name to sentence case. | 
 |       return this._toSentenceCase(action.label); | 
 |     }, | 
 |  | 
 |     /** | 
 |      * Capitalize the first letter and lowecase all others. | 
 |      * @param {string} s | 
 |      * @return {string} | 
 |      */ | 
 |     _toSentenceCase(s) { | 
 |       if (!s.length) { return ''; } | 
 |       return s[0].toUpperCase() + s.slice(1).toLowerCase(); | 
 |     }, | 
 |  | 
 |     _computeLoadingLabel(action) { | 
 |       return ActionLoadingLabels[action] || 'Working...'; | 
 |     }, | 
 |  | 
 |     _canSubmitChange() { | 
 |       return this.$.jsAPI.canSubmitChange(this.change, | 
 |           this._getRevision(this.change, this.latestPatchNum)); | 
 |     }, | 
 |  | 
 |     _getRevision(change, patchNum) { | 
 |       for (const rev of Object.values(change.revisions)) { | 
 |         if (this.patchNumEquals(rev._number, patchNum)) { | 
 |           return rev; | 
 |         } | 
 |       } | 
 |       return null; | 
 |     }, | 
 |  | 
 |     _modifyRevertMsg() { | 
 |       return this.$.jsAPI.modifyRevertMsg(this.change, | 
 |           this.$.confirmRevertDialog.message, this.commitMessage); | 
 |     }, | 
 |  | 
 |     showRevertDialog() { | 
 |       this.$.confirmRevertDialog.populateRevertMessage( | 
 |           this.commitMessage, this.change.current_revision); | 
 |       this.$.confirmRevertDialog.message = this._modifyRevertMsg(); | 
 |       this._showActionDialog(this.$.confirmRevertDialog); | 
 |     }, | 
 |  | 
 |     _handleActionTap(e) { | 
 |       e.preventDefault(); | 
 |       let el = Polymer.dom(e).localTarget; | 
 |       while (el.is !== 'gr-button') { | 
 |         if (!el.parentElement) { return; } | 
 |         el = el.parentElement; | 
 |       } | 
 |  | 
 |       const key = el.getAttribute('data-action-key'); | 
 |       if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) || | 
 |           key.indexOf('~') !== -1) { | 
 |         this.fire(`${key}-tap`, {node: el}); | 
 |         return; | 
 |       } | 
 |       const type = el.getAttribute('data-action-type'); | 
 |       this._handleAction(type, key); | 
 |     }, | 
 |  | 
 |     _handleOveflowItemTap(e) { | 
 |       e.preventDefault(); | 
 |       const el = Polymer.dom(e).localTarget; | 
 |       const key = e.detail.action.__key; | 
 |       if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) || | 
 |           key.indexOf('~') !== -1) { | 
 |         this.fire(`${key}-tap`, {node: el}); | 
 |         return; | 
 |       } | 
 |       this._handleAction(e.detail.action.__type, e.detail.action.__key); | 
 |     }, | 
 |  | 
 |     _handleAction(type, key) { | 
 |       this.$.reporting.reportInteraction(`${type}-${key}`); | 
 |       switch (type) { | 
 |         case ActionType.REVISION: | 
 |           this._handleRevisionAction(key); | 
 |           break; | 
 |         case ActionType.CHANGE: | 
 |           this._handleChangeAction(key); | 
 |           break; | 
 |         default: | 
 |           this._fireAction(this._prependSlash(key), this.actions[key], false); | 
 |       } | 
 |     }, | 
 |  | 
 |     _handleChangeAction(key) { | 
 |       let action; | 
 |       switch (key) { | 
 |         case ChangeActions.REVERT: | 
 |           this.showRevertDialog(); | 
 |           break; | 
 |         case ChangeActions.ABANDON: | 
 |           this._showActionDialog(this.$.confirmAbandonDialog); | 
 |           break; | 
 |         case QUICK_APPROVE_ACTION.key: | 
 |           action = this._allActionValues.find(o => { | 
 |             return o.key === key; | 
 |           }); | 
 |           this._fireAction( | 
 |               this._prependSlash(key), action, true, action.payload); | 
 |           break; | 
 |         case ChangeActions.EDIT: | 
 |           this._handleEditTap(); | 
 |           break; | 
 |         case ChangeActions.STOP_EDIT: | 
 |           this._handleStopEditTap(); | 
 |           break; | 
 |         case ChangeActions.DELETE: | 
 |           this._handleDeleteTap(); | 
 |           break; | 
 |         case ChangeActions.DELETE_EDIT: | 
 |           this._handleDeleteEditTap(); | 
 |           break; | 
 |         case ChangeActions.FOLLOW_UP: | 
 |           this._handleFollowUpTap(); | 
 |           break; | 
 |         case ChangeActions.WIP: | 
 |           this._handleWipTap(); | 
 |           break; | 
 |         case ChangeActions.MOVE: | 
 |           this._handleMoveTap(); | 
 |           break; | 
 |         case ChangeActions.PUBLISH_EDIT: | 
 |           this._handlePublishEditTap(); | 
 |           break; | 
 |         case ChangeActions.REBASE_EDIT: | 
 |           this._handleRebaseEditTap(); | 
 |           break; | 
 |         default: | 
 |           this._fireAction(this._prependSlash(key), this.actions[key], false); | 
 |       } | 
 |     }, | 
 |  | 
 |     _handleRevisionAction(key) { | 
 |       switch (key) { | 
 |         case RevisionActions.REBASE: | 
 |           this._showActionDialog(this.$.confirmRebase); | 
 |           this.$.confirmRebase.fetchRecentChanges(); | 
 |           break; | 
 |         case RevisionActions.CHERRYPICK: | 
 |           this._handleCherrypickTap(); | 
 |           break; | 
 |         case RevisionActions.DOWNLOAD: | 
 |           this._handleDownloadTap(); | 
 |           break; | 
 |         case RevisionActions.SUBMIT: | 
 |           if (!this._canSubmitChange()) { return; } | 
 |           this._showActionDialog(this.$.confirmSubmitDialog); | 
 |           break; | 
 |         default: | 
 |           this._fireAction(this._prependSlash(key), | 
 |               this.revisionActions[key], true); | 
 |       } | 
 |     }, | 
 |  | 
 |     _prependSlash(key) { | 
 |       return key === '/' ? key : `/${key}`; | 
 |     }, | 
 |  | 
 |     /** | 
 |      * _hasKnownChainState set to true true if hasParent is defined (can be | 
 |      * either true or false). set to false otherwise. | 
 |      */ | 
 |     _computeChainState(hasParent) { | 
 |       this._hasKnownChainState = true; | 
 |     }, | 
 |  | 
 |     _calculateDisabled(action, hasKnownChainState) { | 
 |       if (action.__key === 'rebase' && hasKnownChainState === false) { | 
 |         return true; | 
 |       } | 
 |       return !action.enabled; | 
 |     }, | 
 |  | 
 |     _handleConfirmDialogCancel() { | 
 |       this._hideAllDialogs(); | 
 |     }, | 
 |  | 
 |     _hideAllDialogs() { | 
 |       const dialogEls = | 
 |           Polymer.dom(this.root).querySelectorAll('.confirmDialog'); | 
 |       for (const dialogEl of dialogEls) { dialogEl.hidden = true; } | 
 |       this.$.overlay.close(); | 
 |     }, | 
 |  | 
 |     _handleRebaseConfirm(e) { | 
 |       const el = this.$.confirmRebase; | 
 |       const payload = {base: e.detail.base}; | 
 |       this.$.overlay.close(); | 
 |       el.hidden = true; | 
 |       this._fireAction('/rebase', this.revisionActions.rebase, true, payload); | 
 |     }, | 
 |  | 
 |     _handleCherrypickConfirm() { | 
 |       const el = this.$.confirmCherrypick; | 
 |       if (!el.branch) { | 
 |         // TODO(davido): Fix error handling | 
 |         this.fire('show-alert', {message: ERR_BRANCH_EMPTY}); | 
 |         return; | 
 |       } | 
 |       if (!el.message) { | 
 |         this.fire('show-alert', {message: ERR_COMMIT_EMPTY}); | 
 |         return; | 
 |       } | 
 |       this.$.overlay.close(); | 
 |       el.hidden = true; | 
 |       this._fireAction( | 
 |           '/cherrypick', | 
 |           this.revisionActions.cherrypick, | 
 |           true, | 
 |           { | 
 |             destination: el.branch, | 
 |             message: el.message, | 
 |           } | 
 |       ); | 
 |     }, | 
 |  | 
 |     _handleMoveConfirm() { | 
 |       const el = this.$.confirmMove; | 
 |       if (!el.branch) { | 
 |         this.fire('show-alert', {message: ERR_BRANCH_EMPTY}); | 
 |         return; | 
 |       } | 
 |       this.$.overlay.close(); | 
 |       el.hidden = true; | 
 |       this._fireAction( | 
 |           '/move', | 
 |           this.actions.move, | 
 |           false, | 
 |           { | 
 |             destination_branch: el.branch, | 
 |             message: el.message, | 
 |           } | 
 |       ); | 
 |     }, | 
 |  | 
 |     _handleRevertDialogConfirm() { | 
 |       const el = this.$.confirmRevertDialog; | 
 |       this.$.overlay.close(); | 
 |       el.hidden = true; | 
 |       this._fireAction('/revert', this.actions.revert, false, | 
 |           {message: el.message}); | 
 |     }, | 
 |  | 
 |     _handleAbandonDialogConfirm() { | 
 |       const el = this.$.confirmAbandonDialog; | 
 |       this.$.overlay.close(); | 
 |       el.hidden = true; | 
 |       this._fireAction('/abandon', this.actions.abandon, false, | 
 |           {message: el.message}); | 
 |     }, | 
 |  | 
 |     _handleCreateFollowUpChange() { | 
 |       this.$.createFollowUpChange.handleCreateChange(); | 
 |       this._handleCloseCreateFollowUpChange(); | 
 |     }, | 
 |  | 
 |     _handleCloseCreateFollowUpChange() { | 
 |       this.$.overlay.close(); | 
 |     }, | 
 |  | 
 |     _handleDeleteConfirm() { | 
 |       this._fireAction('/', this.actions[ChangeActions.DELETE], false); | 
 |     }, | 
 |  | 
 |     _handleDeleteEditConfirm() { | 
 |       this._hideAllDialogs(); | 
 |  | 
 |       this._fireAction('/edit', this.actions.deleteEdit, false); | 
 |     }, | 
 |  | 
 |     _handleSubmitConfirm() { | 
 |       if (!this._canSubmitChange()) { return; } | 
 |       this._hideAllDialogs(); | 
 |       this._fireAction('/submit', this.revisionActions.submit, true); | 
 |     }, | 
 |  | 
 |     _getActionOverflowIndex(type, key) { | 
 |       return this._overflowActions.findIndex(action => { | 
 |         return action.type === type && action.key === key; | 
 |       }); | 
 |     }, | 
 |  | 
 |     _setLoadingOnButtonWithKey(type, key) { | 
 |       this._actionLoadingMessage = this._computeLoadingLabel(key); | 
 |  | 
 |       // If the action appears in the overflow menu. | 
 |       if (this._getActionOverflowIndex(type, key) !== -1) { | 
 |         this.push('_disabledMenuActions', key === '/' ? 'delete' : key); | 
 |         return function() { | 
 |           this._actionLoadingMessage = ''; | 
 |           this._disabledMenuActions = []; | 
 |         }.bind(this); | 
 |       } | 
 |  | 
 |       // Otherwise it's a top-level action. | 
 |       const buttonEl = this.$$(`[data-action-key="${key}"]`); | 
 |       buttonEl.setAttribute('loading', true); | 
 |       buttonEl.disabled = true; | 
 |       return function() { | 
 |         this._actionLoadingMessage = ''; | 
 |         buttonEl.removeAttribute('loading'); | 
 |         buttonEl.disabled = false; | 
 |       }.bind(this); | 
 |     }, | 
 |  | 
 |     /** | 
 |      * @param {string} endpoint | 
 |      * @param {!Object|undefined} action | 
 |      * @param {boolean} revAction | 
 |      * @param {!Object|string=} opt_payload | 
 |      */ | 
 |     _fireAction(endpoint, action, revAction, opt_payload) { | 
 |       const cleanupFn = | 
 |           this._setLoadingOnButtonWithKey(action.__type, action.__key); | 
 |       this._send(action.method, opt_payload, endpoint, revAction, cleanupFn) | 
 |           .then(this._handleResponse.bind(this, action)); | 
 |     }, | 
 |  | 
 |     _showActionDialog(dialog) { | 
 |       this._hideAllDialogs(); | 
 |  | 
 |       dialog.hidden = false; | 
 |       this.$.overlay.open().then(() => { | 
 |         if (dialog.resetFocus) { | 
 |           dialog.resetFocus(); | 
 |         } | 
 |       }); | 
 |     }, | 
 |  | 
 |     // TODO(rmistry): Redo this after | 
 |     // https://bugs.chromium.org/p/gerrit/issues/detail?id=4671 is resolved. | 
 |     _setLabelValuesOnRevert(newChangeId) { | 
 |       const labels = this.$.jsAPI.getLabelValuesPostRevert(this.change); | 
 |       if (!labels) { return Promise.resolve(); } | 
 |       return this.$.restAPI.saveChangeReview(newChangeId, 'current', {labels}); | 
 |     }, | 
 |  | 
 |     _handleResponse(action, response) { | 
 |       if (!response) { return; } | 
 |       return this.$.restAPI.getResponseObject(response).then(obj => { | 
 |         switch (action.__key) { | 
 |           case ChangeActions.REVERT: | 
 |             this._waitForChangeReachable(obj._number) | 
 |                 .then(() => this._setLabelValuesOnRevert(obj._number)) | 
 |                 .then(() => { | 
 |                   Gerrit.Nav.navigateToChange(obj); | 
 |                 }); | 
 |             break; | 
 |           case RevisionActions.CHERRYPICK: | 
 |             this._waitForChangeReachable(obj._number).then(() => { | 
 |               Gerrit.Nav.navigateToChange(obj); | 
 |             }); | 
 |             break; | 
 |           case ChangeActions.DELETE: | 
 |             if (action.__type === ActionType.CHANGE) { | 
 |               page.show('/'); | 
 |             } | 
 |             break; | 
 |           case ChangeActions.WIP: | 
 |           case ChangeActions.DELETE_EDIT: | 
 |           case ChangeActions.PUBLISH_EDIT: | 
 |           case ChangeActions.REBASE_EDIT: | 
 |             page.show(this.changePath(this.changeNum)); | 
 |             break; | 
 |           default: | 
 |             this.dispatchEvent(new CustomEvent('reload-change', | 
 |                 {detail: {action: action.__key}, bubbles: false})); | 
 |             break; | 
 |         } | 
 |       }); | 
 |     }, | 
 |  | 
 |     _handleResponseError(response) { | 
 |       return response.text().then(errText => { | 
 |         this.fire('show-error', | 
 |             {message: `Could not perform action: ${errText}`}); | 
 |         if (!errText.startsWith('Change is already up to date')) { | 
 |           throw Error(errText); | 
 |         } | 
 |       }); | 
 |     }, | 
 |  | 
 |     /** | 
 |      * @param {string} method | 
 |      * @param {string|!Object|undefined} payload | 
 |      * @param {string} actionEndpoint | 
 |      * @param {boolean} revisionAction | 
 |      * @param {?Function} cleanupFn | 
 |      * @param {?Function=} opt_errorFn | 
 |      */ | 
 |     _send(method, payload, actionEndpoint, revisionAction, cleanupFn, | 
 |         opt_errorFn) { | 
 |       const handleError = response => { | 
 |         cleanupFn.call(this); | 
 |         this._handleResponseError(response); | 
 |       }; | 
 |  | 
 |       return this.fetchChangeUpdates(this.change, this.$.restAPI) | 
 |           .then(result => { | 
 |             if (!result.isLatest) { | 
 |               this.fire('show-alert', { | 
 |                 message: 'Cannot set label: a newer patch has been ' + | 
 |                     'uploaded to this change.', | 
 |                 action: 'Reload', | 
 |                 callback: () => { | 
 |                   // Load the current change without any patch range. | 
 |                   Gerrit.Nav.navigateToChange(this.change); | 
 |                 }, | 
 |               }); | 
 |  | 
 |               // Because this is not a network error, call the cleanup function | 
 |               // but not the error handler. | 
 |               cleanupFn(); | 
 |  | 
 |               return Promise.resolve(); | 
 |             } | 
 |             const patchNum = revisionAction ? this.latestPatchNum : null; | 
 |             return this.$.restAPI.executeChangeAction(this.changeNum, method, | 
 |                 actionEndpoint, patchNum, payload, handleError) | 
 |                 .then(response => { | 
 |                   cleanupFn.call(this); | 
 |                   return response; | 
 |                 }); | 
 |           }); | 
 |     }, | 
 |  | 
 |     _handleAbandonTap() { | 
 |       this._showActionDialog(this.$.confirmAbandonDialog); | 
 |     }, | 
 |  | 
 |     _handleCherrypickTap() { | 
 |       this.$.confirmCherrypick.branch = ''; | 
 |       this._showActionDialog(this.$.confirmCherrypick); | 
 |     }, | 
 |  | 
 |     _handleMoveTap() { | 
 |       this.$.confirmMove.branch = ''; | 
 |       this.$.confirmMove.message = ''; | 
 |       this._showActionDialog(this.$.confirmMove); | 
 |     }, | 
 |  | 
 |     _handleDownloadTap() { | 
 |       this.fire('download-tap', null, {bubbles: false}); | 
 |     }, | 
 |  | 
 |     _handleDeleteTap() { | 
 |       this._showActionDialog(this.$.confirmDeleteDialog); | 
 |     }, | 
 |  | 
 |     _handleDeleteEditTap() { | 
 |       this._showActionDialog(this.$.confirmDeleteEditDialog); | 
 |     }, | 
 |  | 
 |     _handleFollowUpTap() { | 
 |       this._showActionDialog(this.$.createFollowUpDialog); | 
 |     }, | 
 |  | 
 |     _handleWipTap() { | 
 |       this._fireAction('/wip', this.actions.wip, false); | 
 |     }, | 
 |  | 
 |     _handlePublishEditTap() { | 
 |       this._fireAction('/edit:publish', this.actions.publishEdit, false); | 
 |     }, | 
 |  | 
 |     _handleRebaseEditTap() { | 
 |       this._fireAction('/edit:rebase', this.actions.rebaseEdit, false); | 
 |     }, | 
 |  | 
 |     _handleHideBackgroundContent() { | 
 |       this.$.mainContent.classList.add('overlayOpen'); | 
 |     }, | 
 |  | 
 |     _handleShowBackgroundContent() { | 
 |       this.$.mainContent.classList.remove('overlayOpen'); | 
 |     }, | 
 |  | 
 |     /** | 
 |      * Merge sources of change actions into a single ordered array of action | 
 |      * values. | 
 |      * @param {!Array} changeActionsRecord | 
 |      * @param {!Array} revisionActionsRecord | 
 |      * @param {!Array} primariesRecord | 
 |      * @param {!Array} additionalActionsRecord | 
 |      * @param {!Object} change The change object. | 
 |      * @return {!Array} | 
 |      */ | 
 |     _computeAllActions(changeActionsRecord, revisionActionsRecord, | 
 |         primariesRecord, additionalActionsRecord, change) { | 
 |       const revisionActionValues = this._getActionValues(revisionActionsRecord, | 
 |           primariesRecord, additionalActionsRecord, ActionType.REVISION); | 
 |       const changeActionValues = this._getActionValues(changeActionsRecord, | 
 |           primariesRecord, additionalActionsRecord, ActionType.CHANGE); | 
 |       const quickApprove = this._getQuickApproveAction(); | 
 |       if (quickApprove) { | 
 |         changeActionValues.unshift(quickApprove); | 
 |       } | 
 |  | 
 |       return revisionActionValues | 
 |           .concat(changeActionValues) | 
 |           .sort(this._actionComparator.bind(this)) | 
 |           .map(action => { | 
 |             if (ACTIONS_WITH_ICONS.has(action.__key)) { | 
 |               action.icon = action.__key; | 
 |             } | 
 |             return action; | 
 |           }); | 
 |     }, | 
 |  | 
 |     _getActionPriority(action) { | 
 |       if (action.__type && action.__key) { | 
 |         const overrideAction = this._actionPriorityOverrides.find(i => { | 
 |           return i.type === action.__type && i.key === action.__key; | 
 |         }); | 
 |  | 
 |         if (overrideAction !== undefined) { | 
 |           return overrideAction.priority; | 
 |         } | 
 |       } | 
 |       if (action.__key === 'review') { | 
 |         return ActionPriority.REVIEW; | 
 |       } else if (action.__primary) { | 
 |         return ActionPriority.PRIMARY; | 
 |       } else if (action.__type === ActionType.CHANGE) { | 
 |         return ActionPriority.CHANGE; | 
 |       } else if (action.__type === ActionType.REVISION) { | 
 |         return ActionPriority.REVISION; | 
 |       } | 
 |       return ActionPriority.DEFAULT; | 
 |     }, | 
 |  | 
 |     /** | 
 |      * Sort comparator to define the order of change actions. | 
 |      */ | 
 |     _actionComparator(actionA, actionB) { | 
 |       const priorityDelta = this._getActionPriority(actionA) - | 
 |           this._getActionPriority(actionB); | 
 |       // Sort by the button label if same priority. | 
 |       if (priorityDelta === 0) { | 
 |         return actionA.label > actionB.label ? 1 : -1; | 
 |       } else { | 
 |         return priorityDelta; | 
 |       } | 
 |     }, | 
 |  | 
 |     _computeTopLevelActions(actionRecord, hiddenActionsRecord) { | 
 |       const hiddenActions = hiddenActionsRecord.base || []; | 
 |       return actionRecord.base.filter(a => { | 
 |         const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1; | 
 |         return !(overflow || hiddenActions.includes(a.__key)); | 
 |       }); | 
 |     }, | 
 |  | 
 |     _filterPrimaryActions(_topLevelActions) { | 
 |       this._topLevelPrimaryActions = _topLevelActions.filter(action => | 
 |           action.__primary); | 
 |       this._topLevelSecondaryActions = _topLevelActions.filter(action => | 
 |           !action.__primary); | 
 |     }, | 
 |  | 
 |     _computeMenuActions(actionRecord, hiddenActionsRecord) { | 
 |       const hiddenActions = hiddenActionsRecord.base || []; | 
 |       return actionRecord.base.filter(a => { | 
 |         const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1; | 
 |         return overflow && !hiddenActions.includes(a.__key); | 
 |       }).map(action => { | 
 |         let key = action.__key; | 
 |         if (key === '/') { key = 'delete'; } | 
 |         return { | 
 |           name: action.label, | 
 |           id: `${key}-${action.__type}`, | 
 |           action, | 
 |           tooltip: action.title, | 
 |         }; | 
 |       }); | 
 |     }, | 
 |  | 
 |     /** | 
 |      * Occasionally, a change created by a change action is not yet knwon to the | 
 |      * API for a brief time. Wait for the given change number to be recognized. | 
 |      * | 
 |      * Returns a promise that resolves with true if a request is recognized, or | 
 |      * false if the change was never recognized after all attempts. | 
 |      * | 
 |      * @param  {number} changeNum | 
 |      * @return {Promise<boolean>} | 
 |      */ | 
 |     _waitForChangeReachable(changeNum) { | 
 |       let attempsRemaining = AWAIT_CHANGE_ATTEMPTS; | 
 |       return new Promise(resolve => { | 
 |         const check = () => { | 
 |           attempsRemaining--; | 
 |           // Pass a no-op error handler to avoid the "not found" error toast. | 
 |           this.$.restAPI.getChange(changeNum, () => {}).then(response => { | 
 |             // If the response is 404, the response will be undefined. | 
 |             if (response) { | 
 |               resolve(true); | 
 |               return; | 
 |             } | 
 |  | 
 |             if (attempsRemaining) { | 
 |               this.async(check, AWAIT_CHANGE_TIMEOUT_MS); | 
 |             } else { | 
 |               resolve(false); | 
 |             } | 
 |           }); | 
 |         }; | 
 |         check(); | 
 |       }); | 
 |     }, | 
 |  | 
 |     _handleEditTap() { | 
 |       this.dispatchEvent(new CustomEvent('edit-tap', {bubbles: false})); | 
 |     }, | 
 |  | 
 |     _handleStopEditTap() { | 
 |       this.dispatchEvent(new CustomEvent('stop-edit-tap', {bubbles: false})); | 
 |     }, | 
 |  | 
 |     _computeHasTooltip(title) { | 
 |       return !!title; | 
 |     }, | 
 |  | 
 |     _computeHasIcon(action) { | 
 |       return action.icon ? '' : 'hidden'; | 
 |     }, | 
 |   }); | 
 | })(); |