
Polymer 2 deprecates the 'fire' method for legacy elements. So let's use gerrit's core fire method which is almost exactly the same. Change-Id: I59aebd29a89d26d9cb39e63e9a41afa9756b942f
1486 lines
43 KiB
JavaScript
1486 lines
43 KiB
JavaScript
/**
|
||
* @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',
|
||
_legacyUndefinedCheck: true,
|
||
|
||
/**
|
||
* 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 {}; },
|
||
},
|
||
// If property binds directly to [[revisionActions.submit]] it is not
|
||
// updated when revisionActions doesn't contain submit action.
|
||
/** @type {?} */
|
||
_revisionSubmitAction: {
|
||
type: Object,
|
||
computed: '_getSubmitAction(revisionActions)',
|
||
},
|
||
// If property binds directly to [[revisionActions.rebase]] it is not
|
||
// updated when revisionActions doesn't contain rebase action.
|
||
/** @type {?} */
|
||
_revisionRebaseAction: {
|
||
type: Object,
|
||
computed: '_getRebaseAction(revisionActions)',
|
||
},
|
||
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.FireBehavior,
|
||
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();
|
||
},
|
||
|
||
_getSubmitAction(revisionActions) {
|
||
return this._getRevisionAction(revisionActions, 'submit', null);
|
||
},
|
||
|
||
_getRebaseAction(revisionActions) {
|
||
return this._getRevisionAction(revisionActions, 'rebase',
|
||
{rebaseOnCurrent: null}
|
||
);
|
||
},
|
||
|
||
_getRevisionAction(revisionActions, actionName, emptyActionValue) {
|
||
if (!revisionActions) {
|
||
return undefined;
|
||
}
|
||
if (revisionActions[actionName] === undefined) {
|
||
// Return null to fire an event when reveisionActions was loaded
|
||
// but doesn't contain actionName. undefined doesn't fire an event
|
||
return emptyActionValue;
|
||
}
|
||
return revisionActions[actionName];
|
||
},
|
||
|
||
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 && this.actions[actionName]) {
|
||
delete this.actions[actionName];
|
||
// We assign a fake value of 'false' to support Polymer 2
|
||
// see https://github.com/Polymer/polymer/issues/2631
|
||
this.notifyPath('actions.' + actionName, false);
|
||
}
|
||
},
|
||
|
||
_editStatusChanged(editMode, editPatchsetLoaded,
|
||
editBasedOnCurrentPatchSet, disableEdit) {
|
||
if (disableEdit) {
|
||
this._deleteAndNotify('publishEdit');
|
||
this._deleteAndNotify('rebaseEdit');
|
||
this._deleteAndNotify('deleteEdit');
|
||
this._deleteAndNotify('stopEdit');
|
||
this._deleteAndNotify('edit');
|
||
return;
|
||
}
|
||
if (this.actions && editPatchsetLoaded) {
|
||
// Only show actions that mutate an edit if an actual edit patch set
|
||
// is loaded.
|
||
if (this.changeIsOpen(this.change)) {
|
||
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.actions && this.changeIsOpen(this.change)) {
|
||
// 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() {
|
||
this._handleCherryPickRestApi(false);
|
||
},
|
||
|
||
_handleCherrypickConflictConfirm() {
|
||
this._handleCherryPickRestApi(true);
|
||
},
|
||
|
||
_handleCherryPickRestApi(conflicts) {
|
||
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,
|
||
base: el.baseCommit ? el.baseCommit : null,
|
||
message: el.message,
|
||
allow_conflicts: conflicts,
|
||
}
|
||
);
|
||
},
|
||
|
||
_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,
|
||
action).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) {
|
||
Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getUrlForRoot());
|
||
}
|
||
break;
|
||
case ChangeActions.WIP:
|
||
case ChangeActions.DELETE_EDIT:
|
||
case ChangeActions.PUBLISH_EDIT:
|
||
case ChangeActions.REBASE_EDIT:
|
||
Gerrit.Nav.navigateToChange(this.change);
|
||
break;
|
||
default:
|
||
this.dispatchEvent(new CustomEvent('reload-change',
|
||
{detail: {action: action.__key}, bubbles: false}));
|
||
break;
|
||
}
|
||
});
|
||
},
|
||
|
||
_handleResponseError(action, response, body) {
|
||
if (action && action.__key === RevisionActions.CHERRYPICK) {
|
||
if (response && response.status === 409 &&
|
||
body && !body.allow_conflicts) {
|
||
return this._showActionDialog(
|
||
this.$.confirmCherrypickConflict);
|
||
}
|
||
}
|
||
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 {!Object|undefined} action
|
||
*/
|
||
_send(method, payload, actionEndpoint, revisionAction, cleanupFn, action) {
|
||
const handleError = response => {
|
||
cleanupFn.call(this);
|
||
this._handleResponseError(action, response, payload);
|
||
};
|
||
|
||
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';
|
||
},
|
||
});
|
||
})();
|