commit 353bfbfca9a190f8c5d68ab8e2796bc669747553 Author: Dmitrii Filippov Date: Wed Sep 30 11:40:14 2020 +0200 Convert files to typescript The change converts the following files to typescript: * elements/change/gr-change-actions/gr-change-actions.ts Change-Id: I5912e36a59e075e0adae956413af3a4bddf046e5 diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts index fba4b53..a9024ef 100644 --- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts +++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts @@ -14,109 +14,135 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import '../../admin/gr-create-change-dialog/gr-create-change-dialog.js'; -import '../../shared/gr-button/gr-button.js'; -import '../../shared/gr-dialog/gr-dialog.js'; -import '../../shared/gr-dropdown/gr-dropdown.js'; -import '../../shared/gr-icons/gr-icons.js'; -import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; -import '../../shared/gr-overlay/gr-overlay.js'; -import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; -import '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js'; -import '../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js'; -import '../gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js'; -import '../gr-confirm-move-dialog/gr-confirm-move-dialog.js'; -import '../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js'; -import '../gr-confirm-revert-dialog/gr-confirm-revert-dialog.js'; -import '../gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js'; -import '../gr-confirm-submit-dialog/gr-confirm-submit-dialog.js'; -import '../../../styles/shared-styles.js'; -import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; -import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; -import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; -import {PolymerElement} from '@polymer/polymer/polymer-element.js'; -import {htmlTemplate} from './gr-change-actions_html.js'; -import {GerritNav} from '../../core/gr-navigation/gr-navigation.js'; -import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js'; -import {appContext} from '../../../services/app-context.js'; +import '../../admin/gr-create-change-dialog/gr-create-change-dialog'; +import '../../shared/gr-button/gr-button'; +import '../../shared/gr-dialog/gr-dialog'; +import '../../shared/gr-dropdown/gr-dropdown'; +import '../../shared/gr-icons/gr-icons'; +import '../../shared/gr-js-api-interface/gr-js-api-interface'; +import '../../shared/gr-overlay/gr-overlay'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface'; +import '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog'; +import '../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog'; +import '../gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog'; +import '../gr-confirm-move-dialog/gr-confirm-move-dialog'; +import '../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog'; +import '../gr-confirm-revert-dialog/gr-confirm-revert-dialog'; +import '../gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog'; +import '../gr-confirm-submit-dialog/gr-confirm-submit-dialog'; +import '../../../styles/shared-styles'; +import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin'; +import {PolymerElement} from '@polymer/polymer/polymer-element'; +import {htmlTemplate} from './gr-change-actions_html'; +import {GerritNav} from '../../core/gr-navigation/gr-navigation'; +import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader'; +import {appContext} from '../../../services/app-context'; import { fetchChangeUpdates, patchNumEquals, -} from '../../../utils/patch-set-util.js'; +} from '../../../utils/patch-set-util'; import { changeIsOpen, ListChangesOption, listChangesOptionsToHex, -} from '../../../utils/change-util.js'; -import {NotifyType} from '../../../constants/constants.js'; -import {TargetElement, EventType} from '../../plugins/gr-plugin-types.js'; +} from '../../../utils/change-util'; +import { + ChangeStatus, + DraftsAction, + HttpMethod, + NotifyType, +} from '../../../constants/constants'; +import {EventType, TargetElement} from '../../plugins/gr-plugin-types'; +import {customElement, observe, property} from '@polymer/decorators'; +import {GrJsApiInterface} from '../../shared/gr-js-api-interface/gr-js-api-interface-element'; +import { + ActionPriority, + ActionType, + ErrorCallback, + RestApiService, +} from '../../../services/services/gr-rest-api/gr-rest-api'; +import { + ActionInfo, + ActionNameToActionInfoMap, + BranchName, + ChangeInfo, + ChangeViewChangeInfo, + CherryPickInput, + CommitId, + InheritedBooleanInfo, + isDetailedLabelInfo, + isQuickLabelInfo, + LabelInfo, + NumericChangeId, + PatchSetNum, + PropertyType, + RequestPayload, + RevertSubmissionInfo, + ReviewInput, + ServerInfo, +} from '../../../types/common'; +import {GrConfirmAbandonDialog} from '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog'; +import {GrOverlay} from '../../shared/gr-overlay/gr-overlay'; +import {GrDialog} from '../../shared/gr-dialog/gr-dialog'; +import {GrCreateChangeDialog} from '../../admin/gr-create-change-dialog/gr-create-change-dialog'; +import {GrConfirmSubmitDialog} from '../gr-confirm-submit-dialog/gr-confirm-submit-dialog'; +import {GrConfirmRevertSubmissionDialog} from '../gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog'; +import { + ConfirmRevertEventDetail, + GrConfirmRevertDialog, + RevertType, +} from '../gr-confirm-revert-dialog/gr-confirm-revert-dialog'; +import {GrConfirmMoveDialog} from '../gr-confirm-move-dialog/gr-confirm-move-dialog'; +import {GrConfirmCherrypickDialog} from '../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog'; +import {GrConfirmCherrypickConflictDialog} from '../gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog'; +import { + ConfirmRebaseEventDetail, + GrConfirmRebaseDialog, +} from '../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog'; +import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces'; +import {GrButton} from '../../shared/gr-button/gr-button'; +import { + ChangeActions, + GrChangeActionsElement, + PrimaryActionKey, + RevisionActions, + UIActionInfo, +} from '../../shared/gr-js-api-interface/gr-change-actions-js-api'; 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 = { + +enum LabelStatus { /** * This label provides what is necessary for submission. */ - OK: 'OK', + OK = 'OK', /** * This label prevents the change from being submitted. */ - REJECT: 'REJECT', + REJECT = 'REJECT', /** * The label may be set, but it's neither necessary for submission * nor does it block submission if set. */ - MAY: 'MAY', + MAY = 'MAY', /** * The label is required for submission, but has not been satisfied. */ - NEED: 'NEED', + 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', -}; - -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: 'rebase', - REBASE_EDIT: 'rebaseEdit', - READY: 'ready', - RESTORE: 'restore', - REVERT: 'revert', - REVERT_SUBMISSION: 'revert_submission', - REVIEWED: 'reviewed', - STOP_EDIT: 'stopEdit', - SUBMIT: 'submit', - UNIGNORE: 'unignore', - UNREVIEWED: 'unreviewed', - WIP: 'wip', -}; - -const RevisionActions = { - CHERRYPICK: 'cherrypick', - REBASE: 'rebase', - SUBMIT: 'submit', - DOWNLOAD: 'download', -}; + IMPOSSIBLE = 'IMPOSSIBLE', + OPTIONAL = 'OPTIONAL', +} -const ActionLoadingLabels = { +const ActionLoadingLabels: {[actionKey: string]: string} = { abandon: 'Abandoning...', cherrypick: 'Cherry-picking...', delete: 'Deleting...', @@ -128,85 +154,83 @@ const ActionLoadingLabels = { submit: 'Submitting...', }; -const ActionType = { - CHANGE: 'change', - REVISION: 'revision', -}; - const ADDITIONAL_ACTION_KEY_PREFIX = '__additionalAction_'; -const QUICK_APPROVE_ACTION = { +interface QuickApproveUIActionInfo extends UIActionInfo { + key: string; + payload?: RequestPayload; +} + +const QUICK_APPROVE_ACTION: QuickApproveUIActionInfo = { __key: 'review', - __type: 'change', + __type: ActionType.CHANGE, enabled: true, key: 'review', label: 'Quick approve', - method: 'POST', + method: HttpMethod.POST, }; -const ActionPriority = { - CHANGE: 2, - DEFAULT: 0, - PRIMARY: 3, - REVIEW: -3, - REVISION: 1, -}; +function isQuckApproveAction( + action: UIActionInfo +): action is QuickApproveUIActionInfo { + return (action as QuickApproveUIActionInfo).key === QUICK_APPROVE_ACTION.key; +} -const DOWNLOAD_ACTION = { +const DOWNLOAD_ACTION: UIActionInfo = { enabled: true, label: 'Download patch', title: 'Open download dialog', __key: 'download', __primary: false, - __type: 'revision', + __type: ActionType.REVISION, }; -const REBASE_EDIT = { +const REBASE_EDIT: UIActionInfo = { enabled: true, label: 'Rebase edit', title: 'Rebase change edit', __key: 'rebaseEdit', __primary: false, - __type: 'change', - method: 'POST', + __type: ActionType.CHANGE, + method: HttpMethod.POST, }; -const PUBLISH_EDIT = { +const PUBLISH_EDIT: UIActionInfo = { enabled: true, label: 'Publish edit', title: 'Publish change edit', __key: 'publishEdit', __primary: false, - __type: 'change', - method: 'POST', + __type: ActionType.CHANGE, + method: HttpMethod.POST, }; -const DELETE_EDIT = { +const DELETE_EDIT: UIActionInfo = { enabled: true, label: 'Delete edit', title: 'Delete change edit', __key: 'deleteEdit', __primary: false, - __type: 'change', - method: 'DELETE', + __type: ActionType.CHANGE, + method: HttpMethod.DELETE, }; -const EDIT = { +const EDIT: UIActionInfo = { enabled: true, label: 'Edit', title: 'Edit this change', __key: 'edit', __primary: false, - __type: 'change', + __type: ActionType.CHANGE, }; -const STOP_EDIT = { +const STOP_EDIT: UIActionInfo = { enabled: true, label: 'Stop editing', title: 'Stop editing this change', __key: 'stopEdit', __primary: false, - __type: 'change', + __type: ActionType.CHANGE, }; // Set of keys that have icons. As more icons are added to gr-icons.html, this @@ -230,11 +254,6 @@ const ACTIONS_WITH_ICONS = new Set([ const AWAIT_CHANGE_ATTEMPTS = 5; const AWAIT_CHANGE_TIMEOUT_MS = 1000; -const REVERT_TYPES = { - REVERT_SINGLE_CHANGE: 1, - REVERT_SUBMISSION: 2, -}; - /* Revert submission is skipped as the normal revert dialog will now show the user a choice between reverting single change or an entire submission. Hence, a second button is not needed. @@ -246,14 +265,76 @@ const SKIP_ACTION_KEYS_ATTENTION_SET = [ ChangeActions.UNREVIEWED, ]; -/** - * @extends PolymerElement - */ -class GrChangeActions extends GestureEventListeners( - LegacyElementMixin(PolymerElement)) { - static get template() { return htmlTemplate; } +function assertUIActionInfo(action?: ActionInfo): UIActionInfo { + // TODO(TS): Remove this function. The gr-change-actions adds properties + // to existing ActionInfo objects instead of creating a new objects. This + // function checks, that 'action' has all property required by UIActionInfo. + // In the future, we should avoid updates of an existing ActionInfos and + // instead create a new object to make code cleaner. However, at the current + // state this is unsafe, because other code can expect these properties to be + // set in ActionInfo. + if (!action) { + throw new Error('action is undefined'); + } + const result = action as UIActionInfo; + if (result.__key === undefined || result.__type === undefined) { + throw new Error('action is not an UIActionInfo'); + } + return result; +} + +interface MenuAction { + name: string; + id: string; + action: UIActionInfo; + tooltip?: string; +} + +interface OverflowAction { + type: ActionType; + key: string; + overflow?: boolean; +} + +interface ActionPriorityOverride { + type: ActionType.CHANGE | ActionType.REVISION; + key: string; + priority: ActionPriority; +} + +interface ChangeActionDialog extends HTMLElement { + resetFocus?(): void; +} + +export interface GrChangeActions { + $: { + jsAPI: GrJsApiInterface; + restAPI: RestApiService & Element; + mainContent: Element; + overlay: GrOverlay; + confirmRebase: GrConfirmRebaseDialog; + confirmCherrypick: GrConfirmCherrypickDialog; + confirmCherrypickConflict: GrConfirmCherrypickConflictDialog; + confirmMove: GrConfirmMoveDialog; + confirmRevertDialog: GrConfirmRevertDialog; + confirmRevertSubmissionDialog: GrConfirmRevertSubmissionDialog; + confirmAbandonDialog: GrConfirmAbandonDialog; + confirmSubmitDialog: GrConfirmSubmitDialog; + createFollowUpDialog: GrDialog; + createFollowUpChange: GrCreateChangeDialog; + confirmDeleteDialog: GrDialog; + confirmDeleteEditDialog: GrDialog; + }; +} + +@customElement('gr-change-actions') +export class GrChangeActions + extends GestureEventListeners(LegacyElementMixin(PolymerElement)) + implements GrChangeActionsElement { + static get template() { + return htmlTemplate; + } - static get is() { return 'gr-change-actions'; } /** * Fired when the change should be reloaded. * @@ -278,219 +359,191 @@ class GrChangeActions extends GestureEventListeners( * @event show-error */ - constructor() { - super(); - this.ActionType = ActionType; - this.ChangeActions = ChangeActions; - this.RevisionActions = RevisionActions; - this.reporting = appContext.reportingService; - } - - static get properties() { - return { - /** - * @type {{ - * _number: number, - * branch: string, - * id: string, - * project: string, - * subject: string, - * }} - */ - change: Object, - actions: { - type: Object, - value() { return {}; }, - }, - primaryActionKeys: { - type: Array, - value() { - return [ - ChangeActions.READY, - 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, - notify: true, - 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, + // TODO(TS): Ensure that ActionType, ChangeActions and RevisionActions + // properties are replaced with enums everywhere and remove them from + // the GrChangeActions class + ActionType = ActionType; - _loading: { - type: Boolean, - value: true, - }, - _actionLoadingMessage: { - type: String, - value: '', - }, - _allActionValues: { - type: Array, - computed: '_computeAllActions(actions.*, revisionActions.*,' + - 'primaryActionKeys.*, _additionalActions.*, change, ' + - '_config, _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, - }, - _config: Object, - }; - } + ChangeActions = ChangeActions; - static get observers() { - return [ - '_actionsChanged(actions.*, revisionActions.*, _additionalActions.*)', - '_changeChanged(change)', - '_editStatusChanged(editMode, editPatchsetLoaded, ' + - 'editBasedOnCurrentPatchSet, disableEdit, actions.*, change.*)', - ]; - } + RevisionActions = RevisionActions; + + reporting = appContext.reportingService; + + @property({type: Object}) + change?: ChangeViewChangeInfo; + + @property({type: Object}) + actions: ActionNameToActionInfoMap = {}; + + @property({type: Array}) + primaryActionKeys: PrimaryActionKey[] = [ + ChangeActions.READY, + RevisionActions.SUBMIT, + ]; + + @property({type: Boolean}) + disableEdit = false; + + @property({type: Boolean}) + _hasKnownChainState = false; + + @property({type: Boolean}) + _hideQuickApproveAction = false; + + @property({type: String}) + changeNum?: NumericChangeId; + + @property({type: String}) + changeStatus?: ChangeStatus; + + @property({type: String}) + commitNum?: CommitId; + + @property({type: Boolean, observer: '_computeChainState'}) + hasParent?: boolean; + + @property({type: String}) + latestPatchNum?: PatchSetNum; + + @property({type: String}) + commitMessage = ''; + + @property({type: Object, notify: true}) + revisionActions: ActionNameToActionInfoMap = {}; + + @property({type: Object, computed: '_getSubmitAction(revisionActions)'}) + _revisionSubmitAction?: ActionInfo | null; + + @property({type: Object, computed: '_getRebaseAction(revisionActions)'}) + _revisionRebaseAction?: ActionInfo | null; + + @property({type: String}) + privateByDefault?: InheritedBooleanInfo; + + @property({type: Boolean}) + _loading = true; + + @property({type: String}) + _actionLoadingMessage = ''; + + @property({ + type: Array, + computed: + '_computeAllActions(actions.*, revisionActions.*,' + + 'primaryActionKeys.*, _additionalActions.*, change, ' + + '_config, _actionPriorityOverrides.*)', + }) + _allActionValues: UIActionInfo[] = []; // _computeAllActions always returns an array + + @property({ + type: Array, + computed: + '_computeTopLevelActions(_allActionValues.*, ' + + '_hiddenActions.*, _overflowActions.*)', + observer: '_filterPrimaryActions', + }) + _topLevelActions?: UIActionInfo[]; + + @property({type: Array}) + _topLevelPrimaryActions?: UIActionInfo[]; + + @property({type: Array}) + _topLevelSecondaryActions?: UIActionInfo[]; + + @property({ + type: Array, + computed: + '_computeMenuActions(_allActionValues.*, ' + + '_hiddenActions.*, _overflowActions.*)', + }) + _menuActions?: MenuAction[]; + + @property({type: Array}) + _overflowActions: OverflowAction[] = [ + { + 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, + }, + ]; + + @property({type: Array}) + _actionPriorityOverrides: ActionPriorityOverride[] = []; + + @property({type: Array}) + _additionalActions: UIActionInfo[] = []; + + @property({type: Array}) + _hiddenActions: string[] = []; + + @property({type: Array}) + _disabledMenuActions: string[] = []; + + @property({type: Boolean}) + editPatchsetLoaded = false; + + @property({type: Boolean}) + editMode = false; + + @property({type: Boolean}) + editBasedOnCurrentPatchSet = true; + + @property({type: Object}) + _config?: ServerInfo; /** @override */ created() { super.created(); - this.addEventListener('fullscreen-overlay-opened', - () => this._handleHideBackgroundContent()); - this.addEventListener('fullscreen-overlay-closed', - () => this._handleShowBackgroundContent()); + this.addEventListener('fullscreen-overlay-opened', () => + this._handleHideBackgroundContent() + ); + this.addEventListener('fullscreen-overlay-closed', () => + this._handleShowBackgroundContent() + ); } /** @override */ @@ -503,86 +556,97 @@ class GrChangeActions extends GestureEventListeners( this._handleLoadingComplete(); } - _getSubmitAction(revisionActions) { - return this._getRevisionAction(revisionActions, 'submit', null); + _getSubmitAction(revisionActions: ActionNameToActionInfoMap) { + return this._getRevisionAction(revisionActions, 'submit'); } - _getRebaseAction(revisionActions) { - return this._getRevisionAction(revisionActions, 'rebase', null); + _getRebaseAction(revisionActions: ActionNameToActionInfoMap) { + return this._getRevisionAction(revisionActions, 'rebase'); } - _getRevisionAction(revisionActions, actionName, emptyActionValue) { + _getRevisionAction( + revisionActions: ActionNameToActionInfoMap, + actionName: string + ) { 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 null; } return revisionActions[actionName]; } reload() { - if (!this.changeNum || !this.latestPatchNum) { + if (!this.changeNum || !this.latestPatchNum || !this.change) { return Promise.resolve(); } + const change = this.change; this._loading = true; - return this._getRevisionActions() - .then(revisionActions => { - if (!revisionActions) { return; } - - this.revisionActions = revisionActions; - this._sendShowRevisionActions({ - change: this.change, - revisionActions, - }); - this._handleLoadingComplete(); - }) - .catch(err => { - this.dispatchEvent(new CustomEvent('show-alert', { - detail: {message: ERR_REVISION_ACTIONS}, - composed: true, bubbles: true, - })); - this._loading = false; - throw err; + return this.$.restAPI + .getChangeRevisionActions(this.changeNum, this.latestPatchNum) + .then(revisionActions => { + if (!revisionActions) { + return; + } + + this.revisionActions = revisionActions; + this._sendShowRevisionActions({ + change, + revisionActions, }); + this._handleLoadingComplete(); + }) + .catch(err => { + this.dispatchEvent( + new CustomEvent('show-alert', { + detail: {message: ERR_REVISION_ACTIONS}, + composed: true, + bubbles: true, + }) + ); + this._loading = false; + throw err; + }); } _handleLoadingComplete() { - getPluginLoader().awaitPluginsLoaded() - .then(() => this._loading = false); + getPluginLoader() + .awaitPluginsLoaded() + .then(() => (this._loading = false)); } - _sendShowRevisionActions(detail) { - this.$.jsAPI.handleEvent( - EventType.SHOW_REVISION_ACTIONS, - detail - ); + _sendShowRevisionActions(detail: { + change: ChangeInfo; + revisionActions: ActionNameToActionInfoMap; + }) { + this.$.jsAPI.handleEvent(EventType.SHOW_REVISION_ACTIONS, detail); } + @observe('change') _changeChanged() { this.reload(); } - addActionButton(type, label) { + addActionButton(type: ActionType, label: string) { if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { throw Error(`Invalid action type: ${type}`); } - const action = { + const action: UIActionInfo = { enabled: true, label, __type: type, - __key: ADDITIONAL_ACTION_KEY_PREFIX + - Math.random().toString(36) - .substr(2), + __key: + ADDITIONAL_ACTION_KEY_PREFIX + Math.random().toString(36).substr(2), }; this.push('_additionalActions', action); return action.__key; } - removeActionButton(key) { + removeActionButton(key: string) { const idx = this._indexOfActionButtonWithKey(key); if (idx === -1) { return; @@ -590,20 +654,23 @@ class GrChangeActions extends GestureEventListeners( this.splice('_additionalActions', idx, 1); } - setActionButtonProp(key, prop, value) { - this.set([ - '_additionalActions', - this._indexOfActionButtonWithKey(key), - prop, - ], value); + setActionButtonProp( + key: string, + prop: T, + value: UIActionInfo[T] + ) { + this.set( + ['_additionalActions', this._indexOfActionButtonWithKey(key), prop], + value + ); } - setActionOverflow(type, key, overflow) { + setActionOverflow(type: ActionType, key: string, overflow: boolean) { if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { throw Error(`Invalid action type given: ${type}`); } const index = this._getActionOverflowIndex(type, key); - const action = { + const action: OverflowAction = { type, key, overflow, @@ -615,13 +682,18 @@ class GrChangeActions extends GestureEventListeners( } } - setActionPriority(type, key, priority) { + setActionPriority( + type: ActionType.CHANGE | ActionType.REVISION, + key: string, + priority: ActionPriority + ) { if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { throw Error(`Invalid action type given: ${type}`); } - const index = this._actionPriorityOverrides - .findIndex(action => action.type === type && action.key === key); - const action = { + const index = this._actionPriorityOverrides.findIndex( + action => action.type === type && action.key === key + ); + const action: ActionPriorityOverride = { type, key, priority, @@ -633,7 +705,11 @@ class GrChangeActions extends GestureEventListeners( } } - setActionHidden(type, key, hidden) { + setActionHidden( + type: ActionType.CHANGE | ActionType.REVISION, + key: string, + hidden: boolean + ) { if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { throw Error(`Invalid action type given: ${type}`); } @@ -646,15 +722,17 @@ class GrChangeActions extends GestureEventListeners( } } - getActionDetails(action) { - if (this.revisionActions[action]) { - return this.revisionActions[action]; - } else if (this.actions[action]) { - return this.actions[action]; + getActionDetails(actionName: string) { + if (this.revisionActions[actionName]) { + return this.revisionActions[actionName]; + } else if (this.actions[actionName]) { + return this.actions[actionName]; + } else { + return undefined; } } - _indexOfActionButtonWithKey(key) { + _indexOfActionButtonWithKey(key: string) { for (let i = 0; i < this._additionalActions.length; i++) { if (this._additionalActions[i].__key === key) { return i; @@ -663,35 +741,54 @@ class GrChangeActions extends GestureEventListeners( return -1; } - _getRevisionActions() { - return this.$.restAPI.getChangeRevisionActions(this.changeNum, - this.latestPatchNum); - } - - _shouldHideActions(actions, loading) { + _shouldHideActions( + actions?: PolymerDeepPropertyChange, + loading?: boolean + ) { return loading || !actions || !actions.base || !actions.base.length; } - _keyCount(changeRecord) { - return Object.keys((changeRecord && changeRecord.base) || {}).length; - } - - _actionsChanged(actionsChangeRecord, revisionActionsChangeRecord, - additionalActionsChangeRecord) { + _keyCount( + changeRecord?: PolymerDeepPropertyChange< + ActionNameToActionInfoMap, + ActionNameToActionInfoMap + > + ) { + return Object.keys(changeRecord?.base || {}).length; + } + + @observe('actions.*', 'revisionActions.*', '_additionalActions.*') + _actionsChanged( + actionsChangeRecord?: PolymerDeepPropertyChange< + ActionNameToActionInfoMap, + ActionNameToActionInfoMap + >, + revisionActionsChangeRecord?: PolymerDeepPropertyChange< + ActionNameToActionInfoMap, + ActionNameToActionInfoMap + >, + additionalActionsChangeRecord?: PolymerDeepPropertyChange< + UIActionInfo[], + UIActionInfo[] + > + ) { // Polymer 2: check for undefined - if ([ - actionsChangeRecord, - revisionActionsChangeRecord, - additionalActionsChangeRecord, - ].includes(undefined)) { + if ( + actionsChangeRecord === undefined || + revisionActionsChangeRecord === undefined || + additionalActionsChangeRecord === undefined + ) { return; } - const additionalActions = (additionalActionsChangeRecord && - additionalActionsChangeRecord.base) || []; - this.hidden = this._keyCount(actionsChangeRecord) === 0 && - this._keyCount(revisionActionsChangeRecord) === 0 && - additionalActions.length === 0; + const additionalActions = + (additionalActionsChangeRecord && additionalActionsChangeRecord.base) || + []; + this.hidden = + this._keyCount(actionsChangeRecord) === 0 && + this._keyCount(revisionActionsChangeRecord) === 0 && + additionalActions.length === 0; + this._actionLoadingMessage = ''; this._actionLoadingMessage = ''; this._disabledMenuActions = []; @@ -703,10 +800,7 @@ class GrChangeActions extends GestureEventListeners( } } - /** - * @param {string=} actionName - */ - _deleteAndNotify(actionName) { + _deleteAndNotify(actionName: string) { if (this.actions && this.actions[actionName]) { delete this.actions[actionName]; // We assign a fake value of 'false' to support Polymer 2 @@ -715,17 +809,28 @@ class GrChangeActions extends GestureEventListeners( } } - _editStatusChanged(editMode, editPatchsetLoaded, - editBasedOnCurrentPatchSet, disableEdit) { - // Polymer 2: check for undefined - if ([ - editMode, - editBasedOnCurrentPatchSet, - disableEdit, - ].includes(undefined)) { + @observe( + 'editMode', + 'editPatchsetLoaded', + 'editBasedOnCurrentPatchSet', + 'disableEdit', + 'actions.*', + 'change.*' + ) + _editStatusChanged( + editMode: boolean, + editPatchsetLoaded: boolean, + editBasedOnCurrentPatchSet: boolean, + disableEdit: boolean, + actionsChangeRecord?: PolymerDeepPropertyChange< + ActionNameToActionInfoMap, + ActionNameToActionInfoMap + >, + changeChangeRecord?: PolymerDeepPropertyChange + ) { + if (actionsChangeRecord === undefined || changeChangeRecord === undefined) { return; } - if (disableEdit) { this._deleteAndNotify('publishEdit'); this._deleteAndNotify('rebaseEdit'); @@ -734,23 +839,25 @@ class GrChangeActions extends GestureEventListeners( this._deleteAndNotify('edit'); return; } - if (this.actions && editPatchsetLoaded) { + const actions = actionsChangeRecord.base; + const change = changeChangeRecord.base; + if (actions && editPatchsetLoaded) { // Only show actions that mutate an edit if an actual edit patch set // is loaded. - if (changeIsOpen(this.change)) { + if (changeIsOpen(change)) { if (editBasedOnCurrentPatchSet) { - if (!this.actions.publishEdit) { + if (!actions.publishEdit) { this.set('actions.publishEdit', PUBLISH_EDIT); } this._deleteAndNotify('rebaseEdit'); } else { - if (!this.actions.rebaseEdit) { + if (!actions.rebaseEdit) { this.set('actions.rebaseEdit', REBASE_EDIT); } this._deleteAndNotify('publishEdit'); } } - if (!this.actions.deleteEdit) { + if (!actions.deleteEdit) { this.set('actions.deleteEdit', DELETE_EDIT); } } else { @@ -759,18 +866,20 @@ class GrChangeActions extends GestureEventListeners( this._deleteAndNotify('deleteEdit'); } - if (this.actions && changeIsOpen(this.change)) { + if (actions && changeIsOpen(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); } + if (!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) { + if (!actions.stopEdit) { this.set('actions.stopEdit', STOP_EDIT); } } else { @@ -782,16 +891,19 @@ class GrChangeActions extends GestureEventListeners( } } - _getValuesFor(obj) { + _getValuesFor(obj: {[key: string]: T}): T[] { return Object.keys(obj).map(key => obj[key]); } - _getLabelStatus(label) { - if (label.approved) { - return LabelStatus.OK; - } else if (label.rejected) { - return LabelStatus.REJECT; - } else if (label.optional) { + _getLabelStatus(label: LabelInfo): LabelStatus { + if (isQuickLabelInfo(label)) { + if (label.approved) { + return LabelStatus.OK; + } else if (label.rejected) { + return LabelStatus.REJECT; + } + } + if (label.optional) { return LabelStatus.OPTIONAL; } else { return LabelStatus.NEED; @@ -801,13 +913,9 @@ class GrChangeActions extends GestureEventListeners( /** * 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) { + if (!this.change || !this.change.labels || !this.change.permitted_labels) { return null; } let result; @@ -826,15 +934,20 @@ class GrChangeActions extends GestureEventListeners( return null; } result = label; - } else if (status === LabelStatus.REJECT || - status === LabelStatus.IMPOSSIBLE) { + } 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]; + const labelInfo = this.change.labels[result]; + if (!isDetailedLabelInfo(labelInfo)) { + return null; + } + const maxScore = Object.keys(labelInfo.values).slice(-1)[0]; if (score === maxScore) { // Allow quick approve only for maximal score. return { @@ -847,13 +960,16 @@ class GrChangeActions extends GestureEventListeners( } hideQuickApproveAction() { - this._topLevelSecondaryActions = - this._topLevelSecondaryActions - .filter(sa => sa.key !== QUICK_APPROVE_ACTION.key); + if (!this._topLevelSecondaryActions) { + throw new Error('_topLevelSecondaryActions must be set'); + } + this._topLevelSecondaryActions = this._topLevelSecondaryActions.filter( + sa => !isQuckApproveAction(sa) + ); this._hideQuickApproveAction = true; } - _getQuickApproveAction() { + _getQuickApproveAction(): QuickApproveUIActionInfo | null { if (this._hideQuickApproveAction) { return null; } @@ -863,73 +979,103 @@ class GrChangeActions extends GestureEventListeners( } const action = {...QUICK_APPROVE_ACTION}; action.label = approval.label + approval.score; - const review = { - drafts: 'PUBLISH_ALL_REVISIONS', - labels: {}, + + const score = Number(approval.score); + if (isNaN(score)) { + return null; + } + + const review: ReviewInput = { + drafts: DraftsAction.PUBLISH_ALL_REVISIONS, + labels: { + [approval.label]: score, + }, }; - review.labels[approval.label] = approval.score; action.payload = review; return action; } - _getActionValues(actionsChangeRecord, primariesChangeRecord, - additionalActionsChangeRecord, type) { - if (!actionsChangeRecord || !primariesChangeRecord) { return []; } + _getActionValues( + actionsChangeRecord: PolymerDeepPropertyChange< + ActionNameToActionInfoMap, + ActionNameToActionInfoMap + >, + primariesChangeRecord: PolymerDeepPropertyChange< + PrimaryActionKey[], + PrimaryActionKey[] + >, + additionalActionsChangeRecord: PolymerDeepPropertyChange< + UIActionInfo[], + UIActionInfo[] + >, + type: ActionType + ): UIActionInfo[] { + 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 = []; + const result: UIActionInfo[] = []; + const values: Array = + type === ActionType.CHANGE + ? this._getValuesFor(ChangeActions) + : this._getValuesFor(RevisionActions); + + const pluginActions: UIActionInfo[] = []; Object.keys(actions).forEach(a => { - actions[a].__key = a; - actions[a].__type = type; - actions[a].__primary = primaryActionKeys.includes(a); + const action: UIActionInfo = actions[a] as UIActionInfo; + action.__key = a; + action.__type = type; + action.__primary = primaryActionKeys.includes(a as PrimaryActionKey); // Plugin actions always contain ~ in the key. if (a.indexOf('~') !== -1) { - this._populateActionUrl(actions[a]); - pluginActions.push(actions[a]); + this._populateActionUrl(action); + pluginActions.push(action); // Add server-side provided plugin actions to overflow menu. this._overflowActions.push({ type, key: a, }); return; - } else if (!values.includes(a)) { + } else if (!values.includes(a as PrimaryActionKey)) { return; } - actions[a].label = this._getActionLabel(actions[a]); + action.label = this._getActionLabel(action); // Triggers a re-render by ensuring object inequality. - result.push({...actions[a]}); + result.push({...action}); }); - let additionalActions = (additionalActionsChangeRecord && - additionalActionsChangeRecord.base) || []; + let additionalActions = + (additionalActionsChangeRecord && additionalActionsChangeRecord.base) || + []; additionalActions = additionalActions - .filter(a => a.__type === type) - .map(a => { - a.__primary = primaryActionKeys.includes(a.__key); - // Triggers a re-render by ensuring object inequality. - return {...a}; - }); + .filter(a => a.__type === type) + .map(a => { + a.__primary = primaryActionKeys.includes(a.__key as PrimaryActionKey); + // Triggers a re-render by ensuring object inequality. + return {...a}; + }); return result.concat(additionalActions).concat(pluginActions); } - _populateActionUrl(action) { + _populateActionUrl(action: UIActionInfo) { const patchNum = - action.__type === ActionType.REVISION ? this.latestPatchNum : null; - this.$.restAPI.getChangeActionURL( - this.changeNum, patchNum, '/' + action.__key) - .then(url => action.__url = url); + action.__type === ActionType.REVISION ? this.latestPatchNum : undefined; + if (!this.changeNum) { + return; + } + 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) { + _getActionLabel(action: UIActionInfo) { if (action.label === 'Delete') { // This label is common within change and revision actions. Make it more // explicit to the user. @@ -943,25 +1089,29 @@ class GrChangeActions extends GestureEventListeners( /** * Capitalize the first letter and lowecase all others. - * - * @param {string} s - * @return {string} */ - _toSentenceCase(s) { - if (!s.length) { return ''; } + _toSentenceCase(s: string) { + if (!s.length) { + return ''; + } return s[0].toUpperCase() + s.slice(1).toLowerCase(); } - _computeLoadingLabel(action) { + _computeLoadingLabel(action: string) { return ActionLoadingLabels[action] || 'Working...'; } _canSubmitChange() { - return this.$.jsAPI.canSubmitChange(this.change, - this._getRevision(this.change, this.latestPatchNum)); + if (!this.change) { + return false; + } + return this.$.jsAPI.canSubmitChange( + this.change, + this._getRevision(this.change, this.latestPatchNum) + ); } - _getRevision(change, patchNum) { + _getRevision(change: ChangeViewChangeInfo, patchNum?: PatchSetNum) { for (const rev of Object.values(change.revisions)) { if (patchNumEquals(rev._number, patchNum)) { return rev; @@ -971,66 +1121,92 @@ class GrChangeActions extends GestureEventListeners( } showRevertDialog() { + const change = this.change; + if (!change) return; // The search is still broken if there is a " in the topic. - const query = `submissionid: "${this.change.submission_id}"`; + const query = `submissionid: "${change.submission_id}"`; /* A chromium plugin expects that the modifyRevertMsg hook will only be called after the revert button is pressed, hence we populate the revert dialog after revert button is pressed. */ - this.$.restAPI.getChanges('', query) - .then(changes => { - this.$.confirmRevertDialog.populate(this.change, - this.commitMessage, changes); - this._showActionDialog(this.$.confirmRevertDialog); - }); + this.$.restAPI.getChanges(0, query).then(changes => { + if (!changes) { + console.error('changes is undefined'); + return; + } + this.$.confirmRevertDialog.populate(change, this.commitMessage, changes); + this._showActionDialog(this.$.confirmRevertDialog); + }); } showRevertSubmissionDialog() { - const query = 'submissionid:' + this.change.submission_id; - this.$.restAPI.getChanges('', query) - .then(changes => { - this.$.confirmRevertSubmissionDialog. - _populateRevertSubmissionMessage(this.change, changes); - this._showActionDialog(this.$.confirmRevertSubmissionDialog); - }); + const change = this.change; + if (!change) return; + const query = `submissionid:${change.submission_id}`; + this.$.restAPI.getChanges(0, query).then(changes => { + if (!changes) { + console.error('changes is undefined'); + return; + } + this.$.confirmRevertSubmissionDialog._populateRevertSubmissionMessage( + change, + changes + ); + this._showActionDialog(this.$.confirmRevertSubmissionDialog); + }); } - _handleActionTap(e) { + _handleActionTap(e: MouseEvent) { e.preventDefault(); - let el = dom(e).localTarget; + let el = (dom(e) as EventApi).localTarget as Element; while (el.tagName.toLowerCase() !== 'gr-button') { - if (!el.parentElement) { return; } + 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.dispatchEvent(new CustomEvent(`${key}-tap`, { - detail: {node: el}, - composed: true, bubbles: true, - })); + if (!key) { + throw new Error("Button doesn't have data-action-key attribute"); + } + if ( + key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) || + key.indexOf('~') !== -1 + ) { + this.dispatchEvent( + new CustomEvent(`${key}-tap`, { + detail: {node: el}, + composed: true, + bubbles: true, + }) + ); return; } - const type = el.getAttribute('data-action-type'); + const type = el.getAttribute('data-action-type') as ActionType; this._handleAction(type, key); } - _handleOverflowItemTap(e) { + _handleOverflowItemTap(e: CustomEvent) { e.preventDefault(); - const el = dom(e).localTarget; + const el = (dom(e) as EventApi).localTarget as Element; const key = e.detail.action.__key; - if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) || - key.indexOf('~') !== -1) { - this.dispatchEvent(new CustomEvent(`${key}-tap`, { - detail: {node: el}, - composed: true, bubbles: true, - })); + if ( + key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) || + key.indexOf('~') !== -1 + ) { + this.dispatchEvent( + new CustomEvent(`${key}-tap`, { + detail: {node: el}, + composed: true, + bubbles: true, + }) + ); return; } this._handleAction(e.detail.action.__type, e.detail.action.__key); } - _handleAction(type, key) { + _handleAction(type: ActionType, key: string) { this.reporting.reportInteraction(`${type}-${key}`); switch (type) { case ActionType.REVISION: @@ -1040,12 +1216,15 @@ class GrChangeActions extends GestureEventListeners( this._handleChangeAction(key); break; default: - this._fireAction(this._prependSlash(key), this.actions[key], false); + this._fireAction( + this._prependSlash(key), + assertUIActionInfo(this.actions[key]), + false + ); } } - _handleChangeAction(key) { - let action; + _handleChangeAction(key: string) { switch (key) { case ChangeActions.REVERT: this.showRevertDialog(); @@ -1056,11 +1235,14 @@ class GrChangeActions extends GestureEventListeners( case ChangeActions.ABANDON: this._showActionDialog(this.$.confirmAbandonDialog); break; - case QUICK_APPROVE_ACTION.key: - action = this._allActionValues.find(o => o.key === key); - this._fireAction( - this._prependSlash(key), action, true, action.payload); + case QUICK_APPROVE_ACTION.key: { + const action = this._allActionValues.find(isQuckApproveAction); + if (!action) { + return; + } + this._fireAction(this._prependSlash(key), action, true, action.payload); break; + } case ChangeActions.EDIT: this._handleEditTap(); break; @@ -1089,11 +1271,15 @@ class GrChangeActions extends GestureEventListeners( this._handleRebaseEditTap(); break; default: - this._fireAction(this._prependSlash(key), this.actions[key], false); + this._fireAction( + this._prependSlash(key), + assertUIActionInfo(this.actions[key]), + false + ); } } - _handleRevisionAction(key) { + _handleRevisionAction(key: string) { switch (key) { case RevisionActions.REBASE: this._showActionDialog(this.$.confirmRebase); @@ -1106,16 +1292,21 @@ class GrChangeActions extends GestureEventListeners( this._handleDownloadTap(); break; case RevisionActions.SUBMIT: - if (!this._canSubmitChange()) { return; } + if (!this._canSubmitChange()) { + return; + } this._showActionDialog(this.$.confirmSubmitDialog); break; default: - this._fireAction(this._prependSlash(key), - this.revisionActions[key], true); + this._fireAction( + this._prependSlash(key), + assertUIActionInfo(this.revisionActions[key]), + true + ); } } - _prependSlash(key) { + _prependSlash(key: string) { return key === '/' ? key : `/${key}`; } @@ -1123,11 +1314,11 @@ class GrChangeActions extends GestureEventListeners( * _hasKnownChainState set to true true if hasParent is defined (can be * either true or false). set to false otherwise. */ - _computeChainState(hasParent) { + _computeChainState() { this._hasKnownChainState = true; } - _calculateDisabled(action, hasKnownChainState) { + _calculateDisabled(action: UIActionInfo, hasKnownChainState: boolean) { if (action.__key === 'rebase') { // Rebase button is only disabled when change has no parent(s). return hasKnownChainState === false; @@ -1140,18 +1331,24 @@ class GrChangeActions extends GestureEventListeners( } _hideAllDialogs() { - const dialogEls = - this.root.querySelectorAll('.confirmDialog'); - for (const dialogEl of dialogEls) { dialogEl.hidden = true; } + const dialogEls = this.root!.querySelectorAll('.confirmDialog'); + for (const dialogEl of dialogEls) { + (dialogEl as HTMLElement).hidden = true; + } this.$.overlay.close(); } - _handleRebaseConfirm(e) { + _handleRebaseConfirm(e: CustomEvent) { const el = this.$.confirmRebase; const payload = {base: e.detail.base}; this.$.overlay.close(); el.hidden = true; - this._fireAction('/rebase', this.revisionActions.rebase, true, payload); + this._fireAction( + '/rebase', + assertUIActionInfo(this.revisionActions.rebase), + true, + payload + ); } _handleCherrypickConfirm() { @@ -1162,73 +1359,85 @@ class GrChangeActions extends GestureEventListeners( this._handleCherryPickRestApi(true); } - _handleCherryPickRestApi(conflicts) { + _handleCherryPickRestApi(conflicts: boolean) { const el = this.$.confirmCherrypick; if (!el.branch) { - this.dispatchEvent(new CustomEvent('show-alert', { - detail: {message: ERR_BRANCH_EMPTY}, - composed: true, bubbles: true, - })); + this.dispatchEvent( + new CustomEvent('show-alert', { + detail: {message: ERR_BRANCH_EMPTY}, + composed: true, + bubbles: true, + }) + ); return; } if (!el.message) { - this.dispatchEvent(new CustomEvent('show-alert', { - detail: {message: ERR_COMMIT_EMPTY}, - composed: true, bubbles: true, - })); + this.dispatchEvent( + new CustomEvent('show-alert', { + detail: {message: ERR_COMMIT_EMPTY}, + composed: true, + bubbles: true, + }) + ); 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, - } + '/cherrypick', + assertUIActionInfo(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.dispatchEvent(new CustomEvent('show-alert', { - detail: {message: ERR_BRANCH_EMPTY}, - composed: true, bubbles: true, - })); + this.dispatchEvent( + new CustomEvent('show-alert', { + detail: {message: ERR_BRANCH_EMPTY}, + composed: true, + bubbles: true, + }) + ); return; } this.$.overlay.close(); el.hidden = true; - this._fireAction( - '/move', - this.actions.move, - false, - { - destination_branch: el.branch, - message: el.message, - } - ); + this._fireAction('/move', assertUIActionInfo(this.actions.move), false, { + destination_branch: el.branch, + message: el.message, + }); } - _handleRevertDialogConfirm(e) { + _handleRevertDialogConfirm(e: CustomEvent) { const revertType = e.detail.revertType; const message = e.detail.message; const el = this.$.confirmRevertDialog; this.$.overlay.close(); el.hidden = true; switch (revertType) { - case REVERT_TYPES.REVERT_SINGLE_CHANGE: - this._fireAction('/revert', this.actions.revert, false, - {message}); + case RevertType.REVERT_SINGLE_CHANGE: + this._fireAction( + '/revert', + assertUIActionInfo(this.actions.revert), + false, + {message} + ); break; - case REVERT_TYPES.REVERT_SUBMISSION: - this._fireAction('/revert_submission', this.actions.revert_submission, - false, {message}); + case RevertType.REVERT_SUBMISSION: + this._fireAction( + '/revert_submission', + assertUIActionInfo(this.actions.revert_submission), + false, + {message} + ); break; default: console.error('invalid revert type'); @@ -1239,16 +1448,26 @@ class GrChangeActions extends GestureEventListeners( const el = this.$.confirmRevertSubmissionDialog; this.$.overlay.close(); el.hidden = true; - this._fireAction('/revert_submission', this.actions.revert_submission, - false, {message: el.message}); + this._fireAction( + '/revert_submission', + assertUIActionInfo(this.actions.revert_submission), + 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}); + this._fireAction( + '/abandon', + assertUIActionInfo(this.actions.abandon), + false, + { + message: el.message, + } + ); } _handleCreateFollowUpChange() { @@ -1261,27 +1480,42 @@ class GrChangeActions extends GestureEventListeners( } _handleDeleteConfirm() { - this._fireAction('/', this.actions[ChangeActions.DELETE], false); + this._fireAction( + '/', + assertUIActionInfo(this.actions[ChangeActions.DELETE]), + false + ); } _handleDeleteEditConfirm() { this._hideAllDialogs(); - this._fireAction('/edit', this.actions.deleteEdit, false); + this._fireAction( + '/edit', + assertUIActionInfo(this.actions.deleteEdit), + false + ); } _handleSubmitConfirm() { - if (!this._canSubmitChange()) { return; } + if (!this._canSubmitChange()) { + return; + } this._hideAllDialogs(); - this._fireAction('/submit', this.revisionActions.submit, true); + this._fireAction( + '/submit', + assertUIActionInfo(this.revisionActions.submit), + true + ); } - _getActionOverflowIndex(type, key) { - return this._overflowActions - .findIndex(action => action.type === type && action.key === key); + _getActionOverflowIndex(type: string, key: string) { + return this._overflowActions.findIndex( + action => action.type === type && action.key === key + ); } - _setLoadingOnButtonWithKey(type, key) { + _setLoadingOnButtonWithKey(type: string, key: string) { this._actionLoadingMessage = this._computeLoadingLabel(key); let buttonKey = key; // TODO(dhruvsri): clean this up later @@ -1293,8 +1527,10 @@ class GrChangeActions extends GestureEventListeners( // If the action appears in the overflow menu. if (this._getActionOverflowIndex(type, buttonKey) !== -1) { - this.push('_disabledMenuActions', buttonKey === '/' ? 'delete' : - buttonKey); + this.push( + '_disabledMenuActions', + buttonKey === '/' ? 'delete' : buttonKey + ); return () => { this._actionLoadingMessage = ''; this._disabledMenuActions = []; @@ -1302,9 +1538,13 @@ class GrChangeActions extends GestureEventListeners( } // Otherwise it's a top-level action. - const buttonEl = this.shadowRoot - .querySelector(`[data-action-key="${buttonKey}"]`); - buttonEl.setAttribute('loading', true); + const buttonEl = this.shadowRoot!.querySelector( + `[data-action-key="${buttonKey}"]` + ) as GrButton; + if (!buttonEl) { + throw new Error(`Can't find button by data-action-key '${buttonKey}'`); + } + buttonEl.setAttribute('loading', 'true'); buttonEl.disabled = true; return () => { this._actionLoadingMessage = ''; @@ -1313,21 +1553,28 @@ class GrChangeActions extends GestureEventListeners( }; } - /** - * @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); + _fireAction( + endpoint: string, + action: UIActionInfo, + revAction: boolean, + payload?: RequestPayload + ) { + const cleanupFn = this._setLoadingOnButtonWithKey( + action.__type, + action.__key + ); - this._send(action.method, opt_payload, endpoint, revAction, cleanupFn, - action).then(res => this._handleResponse(action, res)); + this._send( + action.method, + payload, + endpoint, + revAction, + cleanupFn, + action + ).then(res => this._handleResponse(action, res)); } - _showActionDialog(dialog) { + _showActionDialog(dialog: ChangeActionDialog) { this._hideAllDialogs(); dialog.hidden = false; @@ -1340,28 +1587,38 @@ class GrChangeActions extends GestureEventListeners( // TODO(rmistry): Redo this after // https://bugs.chromium.org/p/gerrit/issues/detail?id=4671 is resolved. - _setLabelValuesOnRevert(newChangeId) { + _setLabelValuesOnRevert(newChangeId: NumericChangeId) { const labels = this.$.jsAPI.getLabelValuesPostRevert(this.change); - if (!labels) { return Promise.resolve(); } + if (!labels) { + return Promise.resolve(undefined); + } return this.$.restAPI.saveChangeReview(newChangeId, 'current', {labels}); } - _handleResponse(action, response) { - if (!response) { return; } + _handleResponse(action: UIActionInfo, response?: 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(() => { - GerritNav.navigateToChange(obj); - }); + case ChangeActions.REVERT: { + const revertChangeInfo: ChangeInfo = (obj as unknown) as ChangeInfo; + this._waitForChangeReachable(revertChangeInfo._number) + .then(() => this._setLabelValuesOnRevert(revertChangeInfo._number)) + .then(() => { + GerritNav.navigateToChange(revertChangeInfo); + }); break; - case RevisionActions.CHERRYPICK: - this._waitForChangeReachable(obj._number).then(() => { - GerritNav.navigateToChange(obj); - }); + } + case RevisionActions.CHERRYPICK: { + const cherrypickChangeInfo: ChangeInfo = (obj as unknown) as ChangeInfo; + this._waitForChangeReachable(cherrypickChangeInfo._number).then( + () => { + GerritNav.navigateToChange(cherrypickChangeInfo); + } + ); break; + } case ChangeActions.DELETE: if (action.__type === ActionType.CHANGE) { GerritNav.navigateToRelativeUrl(GerritNav.getUrlForRoot()); @@ -1373,27 +1630,36 @@ class GrChangeActions extends GestureEventListeners( case ChangeActions.REBASE_EDIT: case ChangeActions.REBASE: case ChangeActions.SUBMIT: - this.dispatchEvent(new CustomEvent('reload', - { - detail: {clearPatchset: true}, - bubbles: false, - composed: true, - })); + this.dispatchEvent( + new CustomEvent('reload', { + detail: {clearPatchset: true}, + bubbles: false, + composed: true, + }) + ); break; - case ChangeActions.REVERT_SUBMISSION: - if (!obj.revert_changes || !obj.revert_changes.length) return; + case ChangeActions.REVERT_SUBMISSION: { + const revertSubmistionInfo = (obj as unknown) as RevertSubmissionInfo; + if ( + !revertSubmistionInfo.revert_changes || + !revertSubmistionInfo.revert_changes.length + ) + return; /* If there is only 1 change then gerrit will automatically redirect to that change */ - GerritNav.navigateToSearchQuery('topic: ' + - obj.revert_changes[0].topic); + GerritNav.navigateToSearchQuery( + `topic: ${revertSubmistionInfo.revert_changes[0].topic}` + ); break; + } default: - this.dispatchEvent(new CustomEvent('reload', - { - detail: {action: action.__key, clearPatchset: true}, - bubbles: false, - composed: true, - })); + this.dispatchEvent( + new CustomEvent('reload', { + detail: {action: action.__key, clearPatchset: true}, + bubbles: false, + composed: true, + }) + ); break; } }); @@ -1403,72 +1669,109 @@ class GrChangeActions extends GestureEventListeners( this._hideAllDialogs(); } - _handleResponseError(action, response, body) { + _handleResponseError( + action: UIActionInfo, + response: Response | undefined | null, + body?: RequestPayload + ) { + if (!response) { + return Promise.resolve(() => { + this.dispatchEvent( + new CustomEvent('show-error', { + detail: {message: `Could not perform action '${action.__key}'`}, + composed: true, + bubbles: true, + }) + ); + }); + } if (action && action.__key === RevisionActions.CHERRYPICK) { - if (response && response.status === 409 && - body && !body.allow_conflicts) { - return this._showActionDialog( - this.$.confirmCherrypickConflict); + if ( + response.status === 409 && + body && + !(body as CherryPickInput).allow_conflicts + ) { + return this._showActionDialog(this.$.confirmCherrypickConflict); } } return response.text().then(errText => { - this.dispatchEvent(new CustomEvent('show-error', { - detail: {message: `Could not perform action: ${errText}`}, - composed: true, bubbles: true, - })); + this.dispatchEvent( + new CustomEvent('show-error', { + detail: {message: `Could not perform action: ${errText}`}, + composed: true, + bubbles: true, + }) + ); 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 => { + _send( + method: HttpMethod | undefined, + payload: RequestPayload | undefined, + actionEndpoint: string, + revisionAction: boolean, + cleanupFn: () => void, + action: UIActionInfo + ): Promise { + const handleError: ErrorCallback = response => { cleanupFn.call(this); this._handleResponseError(action, response, payload); }; - return fetchChangeUpdates(this.change, this.$.restAPI) - .then(result => { - if (!result.isLatest) { - this.dispatchEvent(new CustomEvent('show-alert', { - detail: { - message: 'Cannot set label: a newer patch has been ' + - 'uploaded to this change.', - action: 'Reload', - callback: () => { - this.dispatchEvent(new CustomEvent('reload', - { - detail: {clearPatchset: true}, - bubbles: false, - composed: true, - })); - }, + const change = this.change; + const changeNum = this.changeNum; + if (!change || !changeNum) { + return Promise.reject( + new Error('Properties change and changeNum must be set.') + ); + } + return fetchChangeUpdates(change, this.$.restAPI).then(result => { + if (!result.isLatest) { + this.dispatchEvent( + new CustomEvent('show-alert', { + detail: { + message: + 'Cannot set label: a newer patch has been ' + + 'uploaded to this change.', + action: 'Reload', + callback: () => { + this.dispatchEvent( + new CustomEvent('reload', { + detail: {clearPatchset: true}, + bubbles: false, + composed: true, + }) + ); }, - composed: true, bubbles: true, - })); + }, + composed: true, + bubbles: true, + }) + ); - // Because this is not a network error, call the cleanup function - // but not the error handler. - cleanupFn(); + // 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; - }); + return Promise.resolve(undefined); + } + const patchNum = revisionAction ? this.latestPatchNum : undefined; + return this.$.restAPI + .executeChangeAction( + changeNum, + method, + actionEndpoint, + patchNum, + payload, + handleError + ) + .then(response => { + cleanupFn.call(this); + return response; }); + }); } _handleAbandonTap() { @@ -1476,28 +1779,38 @@ class GrChangeActions extends GestureEventListeners( } _handleCherrypickTap() { - this.$.confirmCherrypick.branch = ''; + if (!this.change) { + throw new Error('The change property must be set'); + } + this.$.confirmCherrypick.branch = '' as BranchName; const query = `topic: "${this.change.topic}"`; - const options = - listChangesOptionsToHex(ListChangesOption.MESSAGES, - ListChangesOption.ALL_REVISIONS); - this.$.restAPI.getChanges('', query, undefined, options) - .then(changes => { - this.$.confirmCherrypick.updateChanges(changes); - this._showActionDialog(this.$.confirmCherrypick); - }); + const options = listChangesOptionsToHex( + ListChangesOption.MESSAGES, + ListChangesOption.ALL_REVISIONS + ); + this.$.restAPI.getChanges(0, query, undefined, options).then(changes => { + if (!changes) { + console.error('getChanges returns undefined'); + return; + } + this.$.confirmCherrypick.updateChanges(changes); + this._showActionDialog(this.$.confirmCherrypick); + }); } _handleMoveTap() { - this.$.confirmMove.branch = ''; + this.$.confirmMove.branch = '' as BranchName; this.$.confirmMove.message = ''; this._showActionDialog(this.$.confirmMove); } _handleDownloadTap() { - this.dispatchEvent(new CustomEvent('download-tap', { - composed: true, bubbles: false, - })); + this.dispatchEvent( + new CustomEvent('download-tap', { + composed: true, + bubbles: false, + }) + ); } _handleDeleteTap() { @@ -1513,17 +1826,35 @@ class GrChangeActions extends GestureEventListeners( } _handleWipTap() { - this._fireAction('/wip', this.actions.wip, false); + if (!this.actions.wip) { + return; + } + this._fireAction('/wip', assertUIActionInfo(this.actions.wip), false); } _handlePublishEditTap() { // Type of payload is PublishChangeEditInput. const payload = {notify: NotifyType.NONE}; - this._fireAction('/edit:publish', this.actions.publishEdit, false, payload); + if (!this.actions.publishEdit) { + return; + } + this._fireAction( + '/edit:publish', + assertUIActionInfo(this.actions.publishEdit), + false, + payload + ); } _handleRebaseEditTap() { - this._fireAction('/edit:rebase', this.actions.rebaseEdit, false); + if (!this.actions.rebaseEdit) { + return; + } + this._fireAction( + '/edit:rebase', + assertUIActionInfo(this.actions.rebaseEdit), + false + ); } _handleHideBackgroundContent() { @@ -1537,59 +1868,80 @@ class GrChangeActions extends GestureEventListeners( /** * 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. - * @param {!Object} config server configuration info - * @return {!Array} */ - _computeAllActions(changeActionsRecord, revisionActionsRecord, - primariesRecord, additionalActionsRecord, change, config) { + _computeAllActions( + changeActionsRecord: PolymerDeepPropertyChange< + ActionNameToActionInfoMap, + ActionNameToActionInfoMap + >, + revisionActionsRecord: PolymerDeepPropertyChange< + ActionNameToActionInfoMap, + ActionNameToActionInfoMap + >, + primariesRecord: PolymerDeepPropertyChange< + PrimaryActionKey[], + PrimaryActionKey[] + >, + additionalActionsRecord: PolymerDeepPropertyChange< + UIActionInfo[], + UIActionInfo[] + >, + change?: ChangeInfo, + config?: ServerInfo + ): UIActionInfo[] { // Polymer 2: check for undefined - if ([ - changeActionsRecord, - revisionActionsRecord, - primariesRecord, - additionalActionsRecord, - change, - ].includes(undefined)) { + if ( + [ + changeActionsRecord, + revisionActionsRecord, + primariesRecord, + additionalActionsRecord, + change, + ].includes(undefined) + ) { return []; } - const revisionActionValues = this._getActionValues(revisionActionsRecord, - primariesRecord, additionalActionsRecord, ActionType.REVISION); - const changeActionValues = this._getActionValues(changeActionsRecord, - primariesRecord, additionalActionsRecord, ActionType.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((a, b) => this._actionComparator(a, b)) - .map(action => { - if (ACTIONS_WITH_ICONS.has(action.__key)) { - action.icon = action.__key; - } - // TODO(brohlfs): Temporary hack until change 269573 is live in all - // backends. - if (action.__key === ChangeActions.READY) { - action.label = 'Mark as Active'; - } - // End of hack - return action; - }) - .filter(action => !this._shouldSkipAction(action, config)); + .concat(changeActionValues) + .sort((a, b) => this._actionComparator(a, b)) + .map(action => { + if (ACTIONS_WITH_ICONS.has(action.__key)) { + action.icon = action.__key; + } + // TODO(brohlfs): Temporary hack until change 269573 is live in all + // backends. + if (action.__key === ChangeActions.READY) { + action.label = 'Mark as Active'; + } + // End of hack + return action; + }) + .filter(action => !this._shouldSkipAction(action, config)); } - _getActionPriority(action) { + _getActionPriority(action: UIActionInfo) { if (action.__type && action.__key) { - const overrideAction = this._actionPriorityOverrides - .find(i => i.type === action.__type && i.key === action.__key); + const overrideAction = this._actionPriorityOverrides.find( + i => i.type === action.__type && i.key === action.__key + ); if (overrideAction !== undefined) { return overrideAction.priority; @@ -1610,9 +1962,9 @@ class GrChangeActions extends GestureEventListeners( /** * Sort comparator to define the order of change actions. */ - _actionComparator(actionA, actionB) { - const priorityDelta = this._getActionPriority(actionA) - - this._getActionPriority(actionB); + _actionComparator(actionA: UIActionInfo, actionB: UIActionInfo) { + 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; @@ -1621,17 +1973,20 @@ class GrChangeActions extends GestureEventListeners( } } - _shouldSkipAction(action, config) { - const skipActionKeys = [...SKIP_ACTION_KEYS]; - const isAttentionSetEnabled = !!config && !!config.change - && config.change.enable_attention_set; + _shouldSkipAction(action: UIActionInfo, config?: ServerInfo) { + const skipActionKeys: string[] = [...SKIP_ACTION_KEYS]; + const isAttentionSetEnabled = + !!config && !!config.change && config.change.enable_attention_set; if (isAttentionSetEnabled) { skipActionKeys.push(...SKIP_ACTION_KEYS_ATTENTION_SET); } return skipActionKeys.includes(action.__key); } - _computeTopLevelActions(actionRecord, hiddenActionsRecord) { + _computeTopLevelActions( + actionRecord: PolymerDeepPropertyChange, + hiddenActionsRecord: PolymerDeepPropertyChange + ): UIActionInfo[] { const hiddenActions = hiddenActionsRecord.base || []; return actionRecord.base.filter(a => { const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1; @@ -1639,31 +1994,42 @@ class GrChangeActions extends GestureEventListeners( }); } - _filterPrimaryActions(_topLevelActions) { - this._topLevelPrimaryActions = _topLevelActions.filter(action => - action.__primary); - this._topLevelSecondaryActions = _topLevelActions.filter(action => - !action.__primary); + _filterPrimaryActions(_topLevelActions: UIActionInfo[]) { + this._topLevelPrimaryActions = _topLevelActions.filter( + action => action.__primary + ); + this._topLevelSecondaryActions = _topLevelActions.filter( + action => !action.__primary + ); } - _computeMenuActions(actionRecord, hiddenActionsRecord) { + _computeMenuActions( + actionRecord: PolymerDeepPropertyChange, + hiddenActionsRecord: PolymerDeepPropertyChange + ): MenuAction[] { 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, - }; - }); + 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, + }; + }); } - _computeRebaseOnCurrent(revisionRebaseAction) { + _computeRebaseOnCurrent( + revisionRebaseAction: PropertyType + ) { if (revisionRebaseAction) { return !!revisionRebaseAction.enabled; } @@ -1671,34 +2037,34 @@ class GrChangeActions extends GestureEventListeners( } /** - * Occasionally, a change created by a change action is not yet knwon to the + * Occasionally, a change created by a change action is not yet known 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} */ - _waitForChangeReachable(changeNum) { + _waitForChangeReachable(changeNum: NumericChangeId) { 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); - } - }); + 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(); }); @@ -1712,13 +2078,17 @@ class GrChangeActions extends GestureEventListeners( this.dispatchEvent(new CustomEvent('stop-edit-tap', {bubbles: false})); } - _computeHasTooltip(title) { + _computeHasTooltip(title?: string) { return !!title; } - _computeHasIcon(action) { + _computeHasIcon(action: UIActionInfo) { return action.icon ? '' : 'hidden'; } } -customElements.define(GrChangeActions.is, GrChangeActions); +declare global { + interface HTMLElementTagNameMap { + 'gr-change-actions': GrChangeActions; + } +} diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js index 091e5a2..301f176 100644 --- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js +++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js @@ -153,19 +153,17 @@ suite('gr-change-actions tests', () => { }); }); - test('plugin change actions', done => { + test('plugin change actions', async () => { sinon.stub(element.$.restAPI, 'getChangeActionURL').returns( Promise.resolve('the-url')); element.actions = { 'plugin~action': {}, }; assert.isOk(element.actions['plugin~action']); - flush(() => { - assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith( - element.changeNum, null, '/plugin~action')); - assert.equal(element.actions['plugin~action'].__url, 'the-url'); - done(); - }); + await flush(); + assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith( + element.changeNum, undefined, '/plugin~action')); + assert.equal(element.actions['plugin~action'].__url, 'the-url'); }); test('not supported actions are filtered out', () => { @@ -813,6 +811,14 @@ suite('gr-change-actions tests', () => { setup(() => { fireActionStub = sinon.stub(element, '_fireAction'); sinon.stub(window, 'alert'); + element.actions = { + move: { + method: 'POST', + label: 'Move', + title: 'Move the change', + enabled: true, + }, + }; }); test('works', () => { @@ -1614,7 +1620,7 @@ suite('gr-change-actions tests', () => { assert.isTrue(fireActionStub.called); assert.isTrue(fireActionStub.calledWith('/review')); const payload = fireActionStub.lastCall.args[3]; - assert.deepEqual(payload.labels, {foo: '+1'}); + assert.deepEqual(payload.labels, {foo: 1}); }); test('not added when multiple labels are required', () => { @@ -1834,16 +1840,12 @@ suite('gr-change-actions tests', () => { 'navigateToChange').returns(Promise.resolve(true)); }); - test('change action', done => { - element - ._send('DELETE', payload, '/endpoint', false, cleanup) - .then(() => { - assert.isFalse(onShowError.called); - assert.isTrue(cleanup.calledOnce); - assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint', - null, payload)); - done(); - }); + test('change action', async () => { + await element._send('DELETE', payload, '/endpoint', false, cleanup); + assert.isFalse(onShowError.called); + assert.isTrue(cleanup.calledOnce); + assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint', + undefined, payload)); }); suite('show revert submission dialog', () => { @@ -1990,6 +1992,13 @@ suite('gr-change-actions tests', () => { test('_handleAction reports', () => { sinon.stub(element, '_fireAction'); + element.actions = { + key: { + __key: 'key', + __type: 'type', + }, + }; + const reportStub = sinon.stub(element.reporting, 'reportInteraction'); element._handleAction('type', 'key'); assert.isTrue(reportStub.called); diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts index ae9254a..e05bac0 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts +++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts @@ -31,6 +31,7 @@ import { BranchInfo, RepoName, BranchName, + CommitId, } from '../../../types/common'; import {ReportingService} from '../../../services/gr-reporting/gr-reporting'; import {customElement, property, observe} from '@polymer/decorators'; @@ -105,7 +106,7 @@ export class GrConfirmCherrypickDialog extends GestureEventListeners( commitMessage?: string; @property({type: String}) - commitNum?: string; + commitNum?: CommitId; @property({type: String}) message?: string; diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts index 00b9906..db0e1ff 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts +++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts @@ -37,6 +37,10 @@ interface RebaseChange { value: NumericChangeId; } +export interface ConfirmRebaseEventDetail { + base: string | null; +} + export interface GrConfirmRebaseDialog { $: { restAPI: RestApiService & Element; @@ -187,9 +191,10 @@ export class GrConfirmRebaseDialog extends GestureEventListeners( _handleConfirmTap(e: Event) { e.preventDefault(); e.stopPropagation(); - this.dispatchEvent( - new CustomEvent('confirm', {detail: {base: this._getSelectedBase()}}) - ); + const detail: ConfirmRebaseEventDetail = { + base: this._getSelectedBase(), + }; + this.dispatchEvent(new CustomEvent('confirm', {detail})); this._text = ''; } diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts index beaf0f8..5c0b19f 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts +++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts @@ -30,10 +30,15 @@ const ERR_COMMIT_NOT_FOUND = 'Unable to find the commit hash of this change.'; const CHANGE_SUBJECT_LIMIT = 50; // TODO(dhruvsri): clean up repeated definitions after moving to js modules -const REVERT_TYPES = { - REVERT_SINGLE_CHANGE: 1, - REVERT_SUBMISSION: 2, -}; +export enum RevertType { + REVERT_SINGLE_CHANGE = 1, + REVERT_SUBMISSION = 2, +} + +export interface ConfirmRevertEventDetail { + revertType: RevertType; + message?: string; +} export interface GrConfirmRevertDialog { $: { @@ -66,7 +71,7 @@ export class GrConfirmRevertDialog extends GestureEventListeners( _message?: string; @property({type: Number}) - _revertType = REVERT_TYPES.REVERT_SINGLE_CHANGE; + _revertType = RevertType.REVERT_SINGLE_CHANGE; @property({type: Boolean}) _showRevertSubmission = false; @@ -88,11 +93,11 @@ export class GrConfirmRevertDialog extends GestureEventListeners( _revertMessages: string[] = []; _computeIfSingleRevert(revertType: number) { - return revertType === REVERT_TYPES.REVERT_SINGLE_CHANGE; + return revertType === RevertType.REVERT_SINGLE_CHANGE; } _computeIfRevertSubmission(revertType: number) { - return revertType === REVERT_TYPES.REVERT_SUBMISSION; + return revertType === RevertType.REVERT_SUBMISSION; } _modifyRevertMsg(change: ChangeInfo, commitMessage: string, message: string) { @@ -135,7 +140,7 @@ export class GrConfirmRevertDialog extends GestureEventListeners( 'Reason for revert: \n'; // This is to give plugins a chance to update message this._message = this._modifyRevertMsg(change, commitMessage, message); - this._revertType = REVERT_TYPES.REVERT_SINGLE_CHANGE; + this._revertType = RevertType.REVERT_SINGLE_CHANGE; this._showRevertSubmission = false; this._revertMessages[this._revertType] = this._message; this._originalRevertMessages[this._revertType] = this._message; @@ -190,7 +195,7 @@ export class GrConfirmRevertDialog extends GestureEventListeners( message, commitMessage ); - this._revertType = REVERT_TYPES.REVERT_SUBMISSION; + this._revertType = RevertType.REVERT_SUBMISSION; this._revertMessages[this._revertType] = this._message; this._originalRevertMessages[this._revertType] = this._message; this._showRevertSubmission = true; @@ -199,17 +204,17 @@ export class GrConfirmRevertDialog extends GestureEventListeners( _handleRevertSingleChangeClicked() { this._showErrorMessage = false; if (this._message) - this._revertMessages[REVERT_TYPES.REVERT_SUBMISSION] = this._message; - this._message = this._revertMessages[REVERT_TYPES.REVERT_SINGLE_CHANGE]; - this._revertType = REVERT_TYPES.REVERT_SINGLE_CHANGE; + this._revertMessages[RevertType.REVERT_SUBMISSION] = this._message; + this._message = this._revertMessages[RevertType.REVERT_SINGLE_CHANGE]; + this._revertType = RevertType.REVERT_SINGLE_CHANGE; } _handleRevertSubmissionClicked() { this._showErrorMessage = false; - this._revertType = REVERT_TYPES.REVERT_SUBMISSION; + this._revertType = RevertType.REVERT_SUBMISSION; if (this._message) - this._revertMessages[REVERT_TYPES.REVERT_SINGLE_CHANGE] = this._message; - this._message = this._revertMessages[REVERT_TYPES.REVERT_SUBMISSION]; + this._revertMessages[RevertType.REVERT_SINGLE_CHANGE] = this._message; + this._message = this._revertMessages[RevertType.REVERT_SUBMISSION]; } _handleConfirmTap(e: MouseEvent) { @@ -219,9 +224,13 @@ export class GrConfirmRevertDialog extends GestureEventListeners( this._showErrorMessage = true; return; } + const detail: ConfirmRevertEventDetail = { + revertType: this._revertType, + message: this._message, + }; this.dispatchEvent( new CustomEvent('confirm', { - detail: {revertType: this._revertType, message: this._message}, + detail, composed: true, bubbles: false, }) diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts index 1fadce5..47bad2c 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts @@ -47,7 +47,7 @@ export interface GrDropdown { }; } -interface DropdownLink { +export interface DropdownLink { url?: string; name?: string; external?: boolean; diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts index 3a86700..a493a2e 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts @@ -15,50 +15,113 @@ * limitations under the License. */ import { - GrChangeActions, ActionType, ActionPriority, } from '../../../services/services/gr-rest-api/gr-rest-api'; import {JsApiService} from './gr-js-api-types'; import {TargetElement} from '../../plugins/gr-plugin-types'; +import {ActionInfo, RequireProperties} from '../../../types/common'; interface Plugin { getPluginName(): string; } -export class GrChangeActionsInterface { - private _el?: GrChangeActions; - // TODO(TS): define correct types when gr-change-actions is converted to ts +export enum 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 = 'rebase', + REBASE_EDIT = 'rebaseEdit', + READY = 'ready', + RESTORE = 'restore', + REVERT = 'revert', + REVERT_SUBMISSION = 'revert_submission', + REVIEWED = 'reviewed', + STOP_EDIT = 'stopEdit', + SUBMIT = 'submit', + UNIGNORE = 'unignore', + UNREVIEWED = 'unreviewed', + WIP = 'wip', +} + +export enum RevisionActions { + CHERRYPICK = 'cherrypick', + REBASE = 'rebase', + SUBMIT = 'submit', + DOWNLOAD = 'download', +} + +export type PrimaryActionKey = ChangeActions | RevisionActions; +export interface UIActionInfo extends RequireProperties { + __key: string; + __url?: string; + __primary?: boolean; + __type: ActionType; + icon?: string; +} + +// This interface is required to avoid circular dependencies between files; +export interface GrChangeActionsElement extends Element { RevisionActions?: Record; + ChangeActions: Record; + ActionType: Record; + primaryActionKeys: string[]; + push(propName: 'primaryActionKeys', value: string): void; + hideQuickApproveAction(): void; + setActionOverflow(type: ActionType, key: string, overflow: boolean): void; + setActionPriority( + type: ActionType, + key: string, + overflow: ActionPriority + ): void; + setActionHidden(type: ActionType, key: string, hidden: boolean): void; + addActionButton(type: ActionType, label: string): string; + removeActionButton(key: string): void; + setActionButtonProp( + key: string, + prop: T, + value: UIActionInfo[T] + ): void; + getActionDetails(actionName: string): ActionInfo | undefined; +} + +export class GrChangeActionsInterface { + private _el?: GrChangeActionsElement; + + RevisionActions = RevisionActions; - ChangeActions?: Record; + ChangeActions = ChangeActions; - ActionType?: Record; + ActionType = ActionType; - constructor(public plugin: Plugin, el?: GrChangeActions) { + constructor(public plugin: Plugin, el?: GrChangeActionsElement) { this.setEl(el); } /** * Set gr-change-actions element to a GrChangeActionsInterface instance. */ - private setEl(el?: GrChangeActions) { + private setEl(el?: GrChangeActionsElement) { if (!el) { console.warn('changeActions() is not ready'); return; } this._el = el; - this.RevisionActions = el.RevisionActions; - this.ChangeActions = el.ChangeActions; - this.ActionType = el.ActionType; } /** * Ensure GrChangeActionsInterface instance has access to gr-change-actions * element and retrieve if the interface was created before element. */ - private ensureEl(): GrChangeActions { + private ensureEl(): GrChangeActionsElement { if (!this._el) { const sharedApiElement = (document.createElement( 'gr-js-api-interface' @@ -66,13 +129,13 @@ export class GrChangeActionsInterface { this.setEl( (sharedApiElement.getElement( TargetElement.CHANGE_ACTIONS - ) as unknown) as GrChangeActions + ) as unknown) as GrChangeActionsElement ); } return this._el!; } - addPrimaryActionKey(key: string) { + addPrimaryActionKey(key: PrimaryActionKey) { const el = this.ensureEl(); if (el.primaryActionKeys.includes(key)) { return; @@ -130,7 +193,7 @@ export class GrChangeActionsInterface { this.ensureEl().setActionButtonProp(key, 'title', text); } - setEnabled(key: string, enabled: string) { + setEnabled(key: string, enabled: boolean) { this.ensureEl().setActionButtonProp(key, 'enabled', enabled); } diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts index 4fc6f9f..26bf50a 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts @@ -20,7 +20,11 @@ import {PolymerElement} from '@polymer/polymer/polymer-element'; import {getPluginLoader} from './gr-plugin-loader'; import {patchNumEquals} from '../../../utils/patch-set-util'; import {customElement} from '@polymer/decorators'; -import {ChangeInfo, RevisionInfo} from '../../../types/common'; +import { + ChangeInfo, + LabelNameToValuesMap, + RevisionInfo, +} from '../../../types/common'; import {GrAnnotationActionsInterface} from './gr-annotation-actions-js-api'; import {GrAdminApi} from '../../plugins/gr-admin-api/gr-admin-api'; import { @@ -87,7 +91,7 @@ export class GrJsApiInterface eventCallbacks[eventName].push(callback); } - canSubmitChange(change: ChangeInfo, revision: RevisionInfo) { + canSubmitChange(change: ChangeInfo, revision?: RevisionInfo | null) { const submitCallbacks = this._getEventCallbacks(EventType.SUBMIT_CHANGE); const cancelSubmit = submitCallbacks.some(callback => { try { @@ -299,8 +303,8 @@ export class GrJsApiInterface return links; } - getLabelValuesPostRevert(change: ChangeInfo) { - let labels = {}; + getLabelValuesPostRevert(change?: ChangeInfo): LabelNameToValuesMap { + let labels: LabelNameToValuesMap = {}; for (const cb of this._getEventCallbacks(EventType.POST_REVERT)) { try { labels = cb(change); diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts index b3f4987..ffdf710 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts @@ -15,13 +15,9 @@ * limitations under the License. */ -import { - RevisionInfo, - ChangeInfo, - RequestPayload, - ActionInfo, -} from '../../../types/common'; +import {RevisionInfo, ChangeInfo, RequestPayload} from '../../../types/common'; import {PluginApi} from '../../plugins/gr-plugin-types'; +import {UIActionInfo} from './gr-change-actions-js-api'; interface GrPopupInterface { close(): void; @@ -36,7 +32,7 @@ export class GrPluginActionContext { constructor( public readonly plugin: PluginApi, - public readonly action: ActionInfo, + public readonly action: UIActionInfo, public readonly change: ChangeInfo, public readonly revision: RevisionInfo ) {} diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts index 043293f..0625f67 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts @@ -45,7 +45,7 @@ import { import {RequestPayload} from '../../../types/common'; import {HttpMethod} from '../../../constants/constants'; import {JsApiService} from './gr-js-api-types'; -import {GrChangeActions} from '../../../services/services/gr-rest-api/gr-rest-api'; +import {GrChangeActions} from '../../change/gr-change-actions/gr-change-actions'; /** * Plugin-provided custom components can affect content in extension diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts index a10f931..9eec5c9 100644 --- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts +++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts @@ -33,10 +33,10 @@ import { ChangeInfo, AccountInfo, LabelInfo, - DetailedLabelInfo, - QuickLabelInfo, ApprovalInfo, AccountId, + isQuickLabelInfo, + isDetailedLabelInfo, } from '../../../types/common'; import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api'; import {GrButton} from '../gr-button/gr-button'; @@ -67,11 +67,6 @@ interface FormattedLabel { value: string; } -// type guard to check if label is QuickLabelInfo -function isQuickLabelInfo(label: LabelInfo): label is QuickLabelInfo { - return !(label as DetailedLabelInfo).values; -} - @customElement('gr-label-info') export class GrLabelInfo extends GestureEventListeners( LegacyElementMixin(PolymerElement) @@ -107,8 +102,11 @@ export class GrLabelInfo extends GestureEventListeners( if (!labelInfo || !account) { return result; } - if (isQuickLabelInfo(labelInfo)) { - if (labelInfo.rejected || labelInfo.approved) { + if (!isDetailedLabelInfo(labelInfo)) { + if ( + isQuickLabelInfo(labelInfo) && + (labelInfo.rejected || labelInfo.approved) + ) { const ok = labelInfo.approved || !labelInfo.rejected; return [ { @@ -128,7 +126,11 @@ export class GrLabelInfo extends GestureEventListeners( ); const votingRange = getVotingRange(labelInfo); for (const label of votes) { - if (label.value && label.value !== labelInfo.default_value) { + if ( + label.value && + (!isQuickLabelInfo(labelInfo) || + label.value !== labelInfo.default_value) + ) { let labelClassName; let labelValPrefix = ''; if (label.value > 0) { @@ -226,8 +228,8 @@ export class GrLabelInfo extends GestureEventListeners( _computeValueTooltip(labelInfo: LabelInfo, score: string) { if ( !labelInfo || - isQuickLabelInfo(labelInfo) || - !labelInfo.values?.[score] + !isDetailedLabelInfo(labelInfo) || + !labelInfo.values[score] ) { return ''; } @@ -238,19 +240,25 @@ export class GrLabelInfo extends GestureEventListeners( * This method also listens change.labels.* in * order to trigger computation when a label is removed from the change. */ - _computeShowPlaceholder(labelInfo: LabelInfo) { + _computeShowPlaceholder(labelInfo?: LabelInfo) { + if (!labelInfo) { + return ''; + } if ( - labelInfo && + !isDetailedLabelInfo(labelInfo) && isQuickLabelInfo(labelInfo) && (labelInfo.rejected || labelInfo.approved) ) { return 'hidden'; } - // TODO(TS): might replace with hasOwnProperty instead - if (labelInfo && (labelInfo as DetailedLabelInfo).all) { - for (const label of (labelInfo as DetailedLabelInfo).all || []) { - if (label.value && label.value !== labelInfo.default_value) { + if (isDetailedLabelInfo(labelInfo) && labelInfo.all) { + for (const label of labelInfo.all) { + if ( + label.value && + (!isQuickLabelInfo(labelInfo) || + label.value !== labelInfo.default_value) + ) { return 'hidden'; } } diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.js b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.js index be7878b..3a2cc39 100644 --- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.js +++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.js @@ -204,25 +204,32 @@ suite('gr-label-info tests', () => { }); test('placeholder', () => { + const values = { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', + }; element.labelInfo = {}; assert.isFalse(isHidden(element.shadowRoot .querySelector('.placeholder'))); - element.labelInfo = {all: []}; + element.labelInfo = {all: [], values}; assert.isFalse(isHidden(element.shadowRoot .querySelector('.placeholder'))); - element.labelInfo = {all: [{value: 1}]}; + element.labelInfo = {all: [{value: 1}], values}; assert.isTrue(isHidden(element.shadowRoot .querySelector('.placeholder'))); element.labelInfo = {rejected: []}; assert.isTrue(isHidden(element.shadowRoot .querySelector('.placeholder'))); - element.labelInfo = {values: [], rejected: [], all: [{value: 1}]}; + element.labelInfo = {values: [], rejected: [], all: [{value: 1}, values]}; assert.isTrue(isHidden(element.shadowRoot .querySelector('.placeholder'))); element.labelInfo = {approved: []}; assert.isTrue(isHidden(element.shadowRoot .querySelector('.placeholder'))); - element.labelInfo = {values: [], approved: [], all: [{value: 1}]}; + element.labelInfo = {values: [], approved: [], all: [{value: 1}, values]}; assert.isTrue(isHidden(element.shadowRoot .querySelector('.placeholder'))); }); diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts index 5b9f360..7f2f018 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts @@ -134,6 +134,8 @@ import { FilePathToDiffInfoMap, ChangeViewChangeInfo, BlameInfo, + ActionNameToActionInfoMap, + RevisionId, } from '../../../types/common'; import { CancelConditionCallback, @@ -194,7 +196,7 @@ interface SendChangeRequestBase { endpoint: string; anonymizedEndpoint?: string; changeNum: NumericChangeId; - method: HttpMethod; + method: HttpMethod | undefined; errFn?: ErrorCallback; headers?: Record; contentType?: string; @@ -1374,10 +1376,12 @@ export class GrRestApiInterface getChangeActionURL( changeNum: NumericChangeId, - patchNum: PatchSetNum | undefined, + revisionId: RevisionId | undefined, endpoint: string ): Promise { - return this._changeBaseURL(changeNum, patchNum).then(url => url + endpoint); + return this._changeBaseURL(changeNum, revisionId).then( + url => url + endpoint + ); } getChangeDetail( @@ -1601,14 +1605,19 @@ export class GrRestApiInterface return this.getChangeFiles(changeNum, patchRange); } - getChangeRevisionActions(changeNum: NumericChangeId, patchNum: PatchSetNum) { + getChangeRevisionActions( + changeNum: NumericChangeId, + patchNum: PatchSetNum + ): Promise { const req: FetchChangeJSON = { changeNum, endpoint: '/actions', patchNum, reportEndpointAsIs: true, }; - return this._getChangeURLAndFetch(req); + return this._getChangeURLAndFetch(req) as Promise< + ActionNameToActionInfoMap | undefined + >; } getChangeSuggestedReviewers( @@ -2765,7 +2774,7 @@ export class GrRestApiInterface _getDiffCommentsFetchURL( changeNum: NumericChangeId, endpoint: string, - patchNum?: PatchSetNum + patchNum?: RevisionId ) { return this._changeBaseURL(changeNum, patchNum).then(url => url + endpoint); } @@ -2901,7 +2910,7 @@ export class GrRestApiInterface getB64FileContents( changeId: NumericChangeId, - patchNum: PatchSetNum, + patchNum: RevisionId, path: string, parentIndex?: number ) { @@ -2974,7 +2983,7 @@ export class GrRestApiInterface _changeBaseURL( changeNum: NumericChangeId, - patchNum?: PatchSetNum, + revisionId?: RevisionId, project?: RepoName ): Promise { // TODO(kaspern): For full slicer migration, app should warn with a call @@ -2987,8 +2996,8 @@ export class GrRestApiInterface let url = `/changes/${encodeURIComponent( project as RepoName )}~${changeNum}`; - if (patchNum) { - url += `/revisions/${patchNum}`; + if (revisionId) { + url += `/revisions/${revisionId}`; } return url; }); @@ -3279,7 +3288,7 @@ export class GrRestApiInterface * Given a changeNum, gets the change. */ getChange( - changeNum: NumericChangeId, + changeNum: ChangeId | NumericChangeId, errFn: ErrorCallback ): Promise { // Cannot use _changeBaseURL, as this function is used by _projectLookup. @@ -3416,7 +3425,7 @@ export class GrRestApiInterface executeChangeAction( changeNum: NumericChangeId, - method: HttpMethod, + method: HttpMethod | undefined, endpoint: string, patchNum?: PatchSetNum, payload?: RequestPayload @@ -3424,7 +3433,7 @@ export class GrRestApiInterface executeChangeAction( changeNum: NumericChangeId, - method: HttpMethod, + method: HttpMethod | undefined, endpoint: string, patchNum: PatchSetNum | undefined, payload: RequestPayload | undefined, @@ -3436,7 +3445,7 @@ export class GrRestApiInterface */ executeChangeAction( changeNum: NumericChangeId, - method: HttpMethod, + method: HttpMethod | undefined, endpoint: string, patchNum?: PatchSetNum, payload?: RequestPayload, diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts index 6354aab..eee48d2 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts @@ -168,7 +168,7 @@ export type FetchParams = { }; interface SendRequestBase { - method: HttpMethod; + method: HttpMethod | undefined; body?: RequestPayload; contentType?: string; headers?: Record; diff --git a/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts index 10715cf..0438ab1 100644 --- a/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts +++ b/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts @@ -22,7 +22,6 @@ import { NumericChangeId, ServerInfo, ProjectInfo, - ActionInfo, AccountCapabilityInfo, SuggestedReviewerInfo, GroupNameToGroupInfoMap, @@ -91,6 +90,8 @@ import { BlameInfo, PatchRange, ImagesForDiff, + ActionNameToActionInfoMap, + RevisionId, } from '../../../types/common'; import {ParsedChangeInfo} from '../../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser'; import {HttpMethod, IgnoreWhitespaceType} from '../../../constants/constants'; @@ -121,27 +122,6 @@ export enum ActionPriority { REVISION = 1, } -// TODO(TS) remove interface when GrChangeActions is converted to typescript -export interface GrChangeActions extends Element { - RevisionActions?: Record; - ChangeActions: Record; - ActionType: Record; - primaryActionKeys: string[]; - push(propName: 'primaryActionKeys', value: string): void; - hideQuickApproveAction(): void; - setActionOverflow(type: ActionType, key: string, overflow: boolean): void; - setActionPriority( - type: ActionType, - key: string, - overflow: ActionPriority - ): void; - setActionHidden(type: ActionType, key: string, hidden: boolean): void; - addActionButton(type: ActionType, label: string): string; - removeActionButton(key: string): void; - setActionButtonProp(key: string, prop: string, value: string): void; - getActionDetails(actionName: string): ActionInfo; -} - export interface GetDiffCommentsOutput { baseComments: CommentInfo[]; comments: CommentInfo[]; @@ -214,7 +194,7 @@ export interface RestApiService { ): Promise; executeChangeAction( changeNum: NumericChangeId, - method: HttpMethod, + method: HttpMethod | undefined, endpoint: string, patchNum?: PatchSetNum, payload?: RequestPayload, @@ -234,6 +214,11 @@ export interface RestApiService { opt_cancelCondition?: Function ): Promise; + getChange( + changeNum: ChangeId | NumericChangeId, + errFn: ErrorCallback + ): Promise; + savePreferences(prefs: PreferencesInput): Promise; getDiffPreferences(): Promise; @@ -399,19 +384,19 @@ export interface RestApiService { ): Promise; saveChangeReview( - changeNum: NumericChangeId, - patchNum: PatchSetNum, + changeNum: ChangeId | NumericChangeId, + patchNum: RevisionId, review: ReviewInput ): Promise; saveChangeReview( - changeNum: NumericChangeId, - patchNum: PatchSetNum, + changeNum: ChangeId | NumericChangeId, + patchNum: RevisionId, review: ReviewInput, errFn: ErrorCallback ): Promise; saveChangeReview( - changeNum: NumericChangeId, - patchNum: PatchSetNum, + changeNum: ChangeId | NumericChangeId, + patchNum: RevisionId, review: ReviewInput, errFn?: ErrorCallback ): Promise; @@ -421,6 +406,12 @@ export interface RestApiService { downloadCommands?: boolean ): Promise; + getChangeActionURL( + changeNum: NumericChangeId, + patchNum: PatchSetNum | undefined, + endpoint: string + ): Promise; + createChange( project: RepoName, branch: BranchName, @@ -761,4 +752,9 @@ export interface RestApiService { diff: DiffInfo, patchRange: PatchRange ): Promise; + + getChangeRevisionActions( + changeNum: NumericChangeId, + patchNum: PatchSetNum + ): Promise; } diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts index e71f09b..199bf7c 100644 --- a/polygerrit-ui/app/types/common.ts +++ b/polygerrit-ui/app/types/common.ts @@ -43,6 +43,7 @@ import { DraftsAction, NotifyType, } from '../constants/constants'; +import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces'; export type BrandType = T & {[__brand in BrandName]: never}; @@ -53,6 +54,13 @@ export type BrandType = T & export type RequireProperties = Omit & Required>; +export type PropertyType = ReturnType<() => T[K]>; + +export type ElementPropertyDeepChange< + T, + K extends keyof T +> = PolymerDeepPropertyChange, PropertyType>; + /** * Type alias for parsed json object to make code cleaner */ @@ -80,6 +88,10 @@ export type ReviewInputTag = BrandType; export type RobotId = BrandType; export type RobotRunId = BrandType; +// RevisionId '0' is the same as 'current'. However, we want to avoid '0' +// in our code, so it is not added here as a possible value. +export type RevisionId = 'current' | CommitId | PatchSetNum; + // The UUID of the suggested fix. export type FixId = BrandType; export type EmailAddress = BrandType; @@ -149,7 +161,10 @@ export type LabelValueToDescriptionMap = {[labelValue: string]: string}; * corresponding to the current patch set. * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#label-info */ -export type LabelInfo = QuickLabelInfo | DetailedLabelInfo; +export type LabelInfo = + | QuickLabelInfo + | DetailedLabelInfo + | (QuickLabelInfo & DetailedLabelInfo); interface LabelCommonInfo { optional?: boolean; // not set if false @@ -177,9 +192,24 @@ export interface DetailedLabelInfo extends LabelCommonInfo { default_value?: number; } +export function isQuickLabelInfo( + l: LabelInfo +): l is QuickLabelInfo | (QuickLabelInfo & DetailedLabelInfo) { + const quickLabelInfo = l as QuickLabelInfo; + return ( + quickLabelInfo.approved !== undefined || + quickLabelInfo.rejected !== undefined || + quickLabelInfo.recommended !== undefined || + quickLabelInfo.disliked !== undefined || + quickLabelInfo.blocking !== undefined || + quickLabelInfo.blocking !== undefined || + quickLabelInfo.value !== undefined + ); +} + export function isDetailedLabelInfo( label: LabelInfo -): label is DetailedLabelInfo { +): label is DetailedLabelInfo | (QuickLabelInfo & DetailedLabelInfo) { return !!(label as DetailedLabelInfo).values; } @@ -225,9 +255,10 @@ export interface ChangeInfo { deletions: number; // Number of deleted lines total_comment_count?: number; unresolved_comment_count?: number; + // TODO(TS): Use changed_id everywhere in code instead of (legacy) _number _number: NumericChangeId; owner: AccountInfo; - actions?: ActionInfo[]; + actions?: ActionNameToActionInfoMap; requirements?: Requirement[]; labels?: LabelNameToInfoMap; permitted_labels?: LabelNameToValueMap; @@ -420,14 +451,23 @@ export interface MembersInput { * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#action-info */ export interface ActionInfo { - __key?: string; - __url?: string; method?: HttpMethod; // Most actions use POST, PUT or DELETE to cause state changes. label?: string; // Short title to display to a user describing the action title?: string; // Longer text to display describing the action enabled?: boolean; // not set if false } +export interface ActionNameToActionInfoMap { + [actionType: string]: ActionInfo | undefined; + // List of actions explicitly used in code: + wip?: ActionInfo; + publishEdit?: ActionInfo; + rebaseEdit?: ActionInfo; + deleteEdit?: ActionInfo; + edit?: ActionInfo; + stopEdit?: ActionInfo; +} + /** * The Requirement entity contains information about a requirement relative to * a change. @@ -2156,3 +2196,28 @@ export interface SubmittedTogetherInfo { changes: ChangeInfo[]; non_visible_changes: number; } + +/** + * The RevertSubmissionInfo entity describes the revert changes. + * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revert-submission-info + */ +export interface RevertSubmissionInfo { + revert_changes: ChangeInfo[]; +} + +/** + * The CherryPickInput entity contains information for cherry-picking a change to a new branch. + * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#cherrypick-input + */ +export interface CherryPickInput { + message?: string; + destination: BranchName; + base?: CommitId; + parent?: number; + notify?: NotifyType; + notify_details: RecipientTypeToNotifyInfoMap; + keep_reviewers?: boolean; + allow_conflicts?: boolean; + topic?: TopicName; + allow_empty?: boolean; +}