diff options
Diffstat (limited to 'polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js')
-rw-r--r-- | polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js | 424 |
1 files changed, 323 insertions, 101 deletions
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js index e8c2d03815..3e5c4f760c 100644 --- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js +++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js @@ -1,16 +1,19 @@ -// 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. +/** + * @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'; @@ -52,6 +55,8 @@ ABANDON: 'abandon', DELETE: '/', DELETE_EDIT: 'deleteEdit', + EDIT: 'edit', + FOLLOW_UP: 'followup', IGNORE: 'ignore', MOVE: 'move', PRIVATE: 'private', @@ -61,6 +66,7 @@ RESTORE: 'restore', REVERT: 'revert', REVIEWED: 'reviewed', + STOP_EDIT: 'stopEdit', UNIGNORE: 'unignore', UNREVIEWED: 'unreviewed', WIP: 'wip', @@ -76,7 +82,7 @@ const ActionLoadingLabels = { abandon: 'Abandoning...', - cherrypick: 'Cherry-Picking...', + cherrypick: 'Cherry-picking...', delete: 'Deleting...', move: 'Moving..', rebase: 'Rebasing...', @@ -97,7 +103,7 @@ __type: 'change', enabled: true, key: 'review', - label: 'Quick Approve', + label: 'Quick approve', method: 'POST', }; @@ -120,7 +126,7 @@ const REBASE_EDIT = { enabled: true, - label: 'Rebase Edit', + label: 'Rebase edit', title: 'Rebase change edit', __key: 'rebaseEdit', __primary: false, @@ -130,7 +136,7 @@ const PUBLISH_EDIT = { enabled: true, - label: 'Publish Edit', + label: 'Publish edit', title: 'Publish change edit', __key: 'publishEdit', __primary: false, @@ -140,7 +146,7 @@ const DELETE_EDIT = { enabled: true, - label: 'Delete Edit', + label: 'Delete edit', title: 'Delete change edit', __key: 'deleteEdit', __primary: false, @@ -148,6 +154,40 @@ 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; @@ -172,8 +212,22 @@ * @event show-alert */ + /** + * Fires when a change action fails. + * + * @event show-error + */ + properties: { - /** @type {{ branch: string, project: string }} */ + /** + * @type {{ + * _number: number, + * branch: string, + * id: string, + * project: string, + * subject: string, + * }} + */ change: Object, actions: { type: Object, @@ -187,10 +241,18 @@ ]; }, }, + disableEdit: { + type: Boolean, + value: false, + }, _hasKnownChainState: { type: Boolean, value: false, }, + _hideQuickApproveAction: { + type: Boolean, + value: false, + }, changeNum: String, changeStatus: String, commitNum: String, @@ -198,7 +260,7 @@ type: Boolean, observer: '_computeChainState', }, - patchNum: String, + latestPatchNum: String, commitMessage: { type: String, value: '', @@ -208,6 +270,7 @@ type: Object, value() { return {}; }, }, + privateByDefault: String, _loading: { type: Boolean, @@ -227,7 +290,10 @@ type: Array, computed: '_computeTopLevelActions(_allActionValues.*, ' + '_hiddenActions.*, _overflowActions.*)', + observer: '_filterPrimaryActions', }, + _topLevelPrimaryActions: Array, + _topLevelSecondaryActions: Array, _menuActions: { type: Array, computed: '_computeMenuActions(_allActionValues.*, _hiddenActions.*, ' + @@ -281,6 +347,10 @@ type: ActionType.CHANGE, key: ChangeActions.PRIVATE_DELETE, }, + { + type: ActionType.CHANGE, + key: ChangeActions.FOLLOW_UP, + }, ]; return value; }, @@ -301,7 +371,14 @@ type: Array, value() { return []; }, }, - editLoaded: { + // 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, }, @@ -321,9 +398,10 @@ ], observers: [ - '_actionsChanged(actions.*, revisionActions.*, _additionalActions.*, ' + - 'editLoaded, editBasedOnCurrentPatchSet, change)', + '_actionsChanged(actions.*, revisionActions.*, _additionalActions.*)', '_changeChanged(change)', + '_editStatusChanged(editMode, editPatchsetLoaded, ' + + 'editBasedOnCurrentPatchSet, disableEdit, actions.*, change.*)', ], listeners: { @@ -333,11 +411,11 @@ ready() { this.$.jsAPI.addElement(this.$.jsAPI.Element.CHANGE_ACTIONS, this); - this._loading = false; + this._handleLoadingComplete(); }, reload() { - if (!this.changeNum || !this.patchNum) { + if (!this.changeNum || !this.latestPatchNum) { return Promise.resolve(); } @@ -346,7 +424,7 @@ if (!revisionActions) { return; } this.revisionActions = revisionActions; - this._loading = false; + this._handleLoadingComplete(); }).catch(err => { this.fire('show-alert', {message: ERR_REVISION_ACTIONS}); this._loading = false; @@ -354,6 +432,10 @@ }); }, + _handleLoadingComplete() { + Gerrit.awaitPluginsLoaded().then(() => this._loading = false); + }, + _changeChanged() { this.reload(); }, @@ -457,7 +539,7 @@ _getRevisionActions() { return this.$.restAPI.getChangeRevisionActions(this.changeNum, - this.patchNum); + this.latestPatchNum); }, _shouldHideActions(actions, loading) { @@ -469,8 +551,7 @@ }, _actionsChanged(actionsChangeRecord, revisionActionsChangeRecord, - additionalActionsChangeRecord, editLoaded, editBasedOnCurrentPatchSet, - change) { + additionalActionsChangeRecord) { const additionalActions = (additionalActionsChangeRecord && additionalActionsChangeRecord.base) || []; this.hidden = this._keyCount(actionsChangeRecord) === 0 && @@ -480,50 +561,78 @@ this._disabledMenuActions = []; const revisionActions = revisionActionsChangeRecord.base || {}; - if (Object.keys(revisionActions).length !== 0 && - !revisionActions.download) { - this.set('revisionActions.download', DOWNLOAD_ACTION); + if (Object.keys(revisionActions).length !== 0) { + if (!revisionActions.download) { + this.set('revisionActions.download', DOWNLOAD_ACTION); + } } + }, - const changeActions = actionsChangeRecord.base || {}; - if (Object.keys(changeActions).length !== 0) { - if (editLoaded) { - if (this.changeIsOpen(change.status)) { - if (editBasedOnCurrentPatchSet) { - if (!changeActions.publishEdit) { - this.set('actions.publishEdit', PUBLISH_EDIT); - } - if (changeActions.rebaseEdit) { - delete this.actions.rebaseEdit; - this.notifyPath('actions.rebaseEdit'); - } - } else { - if (!changeActions.rebasEdit) { - this.set('actions.rebaseEdit', REBASE_EDIT); - } - if (changeActions.publishEdit) { - delete this.actions.publishEdit; - this.notifyPath('actions.publishEdit'); - } + /** + * @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 (!changeActions.deleteEdit) { - this.set('actions.deleteEdit', DELETE_EDIT); - } + } + 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 (changeActions.publishEdit) { - delete this.actions.publishEdit; - this.notifyPath('actions.publishEdit'); - } - if (changeActions.rebaseEdit) { - delete this.actions.rebaseEdit; - this.notifyPath('actions.rebaseEdit'); - } - if (changeActions.deleteEdit) { - delete this.actions.deleteEdit; - this.notifyPath('actions.deleteEdit'); + 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'); } }, @@ -593,7 +702,18 @@ 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; @@ -636,13 +756,8 @@ } else if (!values.includes(a)) { return; } - if (actions[a].label === 'Delete') { - // This label is common within change and revision actions. Make it - // more explicit to the user. - if (type === ActionType.CHANGE) { - actions[a].label += ' Change'; - } - } + actions[a].label = this._getActionLabel(actions[a]); + // Triggers a re-render by ensuring object inequality. result.push(Object.assign({}, actions[a])); }); @@ -661,19 +776,45 @@ _populateActionUrl(action) { const patchNum = - action.__type === ActionType.REVISION ? this.patchNum : null; + 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.patchNum)); + this._getRevision(this.change, this.latestPatchNum)); }, _getRevision(change, patchNum) { @@ -699,7 +840,12 @@ _handleActionTap(e) { e.preventDefault(); - const el = Polymer.dom(e).localTarget; + 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) { @@ -723,6 +869,7 @@ }, _handleAction(type, key) { + this.$.reporting.reportInteraction(`${type}-${key}`); switch (type) { case ActionType.REVISION: this._handleRevisionAction(key); @@ -751,12 +898,21 @@ 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; @@ -778,6 +934,7 @@ switch (key) { case RevisionActions.REBASE: this._showActionDialog(this.$.confirmRebase); + this.$.confirmRebase.fetchRecentChanges(); break; case RevisionActions.CHERRYPICK: this._handleCherrypickTap(); @@ -786,10 +943,9 @@ this._handleDownloadTap(); break; case RevisionActions.SUBMIT: - if (!this._canSubmitChange()) { - return; - } - // eslint-disable-next-line no-fallthrough + if (!this._canSubmitChange()) { return; } + this._showActionDialog(this.$.confirmSubmitDialog); + break; default: this._fireAction(this._prependSlash(key), this.revisionActions[key], true); @@ -826,15 +982,23 @@ this.$.overlay.close(); }, - _handleRebaseConfirm() { + _handleRebaseConfirm(e) { const el = this.$.confirmRebase; - const payload = {base: el.base}; + 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 @@ -853,7 +1017,9 @@ true, { destination: el.branch, + base: el.baseCommit ? el.baseCommit : null, message: el.message, + allow_conflicts: conflicts, } ); }, @@ -893,6 +1059,15 @@ {message: el.message}); }, + _handleCreateFollowUpChange() { + this.$.createFollowUpChange.handleCreateChange(); + this._handleCloseCreateFollowUpChange(); + }, + + _handleCloseCreateFollowUpChange() { + this.$.overlay.close(); + }, + _handleDeleteConfirm() { this._fireAction('/', this.actions[ChangeActions.DELETE], false); }, @@ -903,6 +1078,12 @@ 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; @@ -941,8 +1122,9 @@ _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)); + + this._send(action.method, opt_payload, endpoint, revAction, cleanupFn, + action).then(this._handleResponse.bind(this, action)); }, _showActionDialog(dialog) { @@ -961,8 +1143,7 @@ _setLabelValuesOnRevert(newChangeId) { const labels = this.$.jsAPI.getLabelValuesPostRevert(this.change); if (!labels) { return Promise.resolve(); } - return this.$.restAPI.getChangeURLAndSend(newChangeId, - this.actions.revert.method, 'current', '/review', {labels}); + return this.$.restAPI.saveChangeReview(newChangeId, 'current', {labels}); }, _handleResponse(action, response) { @@ -983,14 +1164,14 @@ break; case ChangeActions.DELETE: if (action.__type === ActionType.CHANGE) { - page.show('/'); + Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getUrlForRoot()); } break; case ChangeActions.WIP: case ChangeActions.DELETE_EDIT: case ChangeActions.PUBLISH_EDIT: case ChangeActions.REBASE_EDIT: - page.show(this.changePath(this.changeNum)); + Gerrit.Nav.navigateToChange(this.change); break; default: this.dispatchEvent(new CustomEvent('reload-change', @@ -1000,9 +1181,16 @@ }); }, - _handleResponseError(response) { + _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-alert', + this.fire('show-error', {message: `Could not perform action: ${errText}`}); if (!errText.startsWith('Change is already up to date')) { throw Error(errText); @@ -1016,18 +1204,17 @@ * @param {string} actionEndpoint * @param {boolean} revisionAction * @param {?Function} cleanupFn - * @param {?Function=} opt_errorFn + * @param {!Object|undefined} action */ - _send(method, payload, actionEndpoint, revisionAction, cleanupFn, - opt_errorFn) { + _send(method, payload, actionEndpoint, revisionAction, cleanupFn, action) { const handleError = response => { cleanupFn.call(this); - this._handleResponseError(response); + this._handleResponseError(action, response, payload); }; - return this.fetchIsLatestKnown(this.change, this.$.restAPI) - .then(isLatest => { - if (!isLatest) { + 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.', @@ -1044,9 +1231,9 @@ return Promise.resolve(); } - const patchNum = revisionAction ? this.patchNum : null; - return this.$.restAPI.getChangeURLAndSend(this.changeNum, method, - patchNum, actionEndpoint, payload, handleError, this) + const patchNum = revisionAction ? this.latestPatchNum : null; + return this.$.restAPI.executeChangeAction(this.changeNum, method, + actionEndpoint, patchNum, payload, handleError) .then(response => { cleanupFn.call(this); return response; @@ -1081,6 +1268,10 @@ this._showActionDialog(this.$.confirmDeleteEditDialog); }, + _handleFollowUpTap() { + this._showActionDialog(this.$.createFollowUpDialog); + }, + _handleWipTap() { this._fireAction('/wip', this.actions.wip, false); }, @@ -1121,9 +1312,16 @@ if (quickApprove) { changeActionValues.unshift(quickApprove); } + return revisionActionValues .concat(changeActionValues) - .sort(this._actionComparator.bind(this)); + .sort(this._actionComparator.bind(this)) + .map(action => { + if (ACTIONS_WITH_ICONS.has(action.__key)) { + action.icon = action.__key; + } + return action; + }); }, _getActionPriority(action) { @@ -1170,6 +1368,13 @@ }); }, + _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 => { @@ -1182,6 +1387,7 @@ name: action.label, id: `${key}-${action.__type}`, action, + tooltip: action.title, }; }); }, @@ -1219,5 +1425,21 @@ 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'; + }, }); })(); |