Files
gerrit/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
Paladox none 44b3a7c433 Migrate from Polymer fire to Gerrit.FireBehavior
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
2019-08-06 17:29:43 +00:00

1486 lines
43 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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 cant be empty.';
const ERR_COMMIT_EMPTY = 'The commit message cant be empty.';
const ERR_REVISION_ACTIONS = 'Couldnt 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';
},
});
})();