blob: 50ec33977af3c647d715029256cef3b17d7a83d1 [file] [log] [blame]
Dave Borowitz8cdc76b2018-03-26 10:04:27 -04001/**
2 * @license
Ben Rohlfs94fcbbc2022-05-27 10:45:03 +02003 * Copyright 2016 Google LLC
4 * SPDX-License-Identifier: Apache-2.0
Dave Borowitz8cdc76b2018-03-26 10:04:27 -04005 */
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02006import '../../admin/gr-create-change-dialog/gr-create-change-dialog';
7import '../../shared/gr-button/gr-button';
8import '../../shared/gr-dialog/gr-dialog';
9import '../../shared/gr-dropdown/gr-dropdown';
Chris Poucet1c713862022-07-25 13:12:24 +020010import '../../shared/gr-icon/gr-icon';
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +020011import '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog';
12import '../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog';
13import '../gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog';
14import '../gr-confirm-move-dialog/gr-confirm-move-dialog';
15import '../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog';
16import '../gr-confirm-revert-dialog/gr-confirm-revert-dialog';
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +020017import '../gr-confirm-submit-dialog/gr-confirm-submit-dialog';
18import '../../../styles/shared-styles';
Ben Rohlfs54934de2022-09-22 12:44:33 +020019import {navigationToken} from '../../core/gr-navigation/gr-navigation';
Chris Poucetc6e880b2021-11-15 19:57:06 +010020import {getAppContext} from '../../../services/app-context';
Paladox noned22af552023-04-10 15:31:00 +000021import {
22 CURRENT,
23 hasEditBasedOnCurrentPatchSet,
24} from '../../../utils/patch-set-util';
Dmitrii Filippov3bf68892020-07-12 00:19:10 +020025import {
26 changeIsOpen,
Frank Borden481e5f92020-11-30 19:59:09 -080027 isOwner,
Dmitrii Filippov3bf68892020-07-12 00:19:10 +020028 listChangesOptionsToHex,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +020029} from '../../../utils/change-util';
30import {
31 ChangeStatus,
32 DraftsAction,
33 HttpMethod,
34 NotifyType,
35} from '../../../constants/constants';
Ben Rohlfs4e913532022-10-24 12:31:58 +020036import {TargetElement} from '../../../api/plugin';
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +020037import {
Frank Borden481e5f92020-11-30 19:59:09 -080038 AccountInfo,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +020039 ActionInfo,
40 ActionNameToActionInfoMap,
41 BranchName,
Kamil Musind3f64972022-12-27 18:01:48 +010042 ChangeActionDialog,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +020043 ChangeInfo,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +020044 CherryPickInput,
Milutin Kristofic7c84f7e2024-04-02 21:41:58 +020045 CommentThread,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +020046 CommitId,
47 InheritedBooleanInfo,
48 isDetailedLabelInfo,
49 isQuickLabelInfo,
50 LabelInfo,
Chris Poucetfceaf782023-08-16 15:11:04 +020051 ListChangesOption,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +020052 NumericChangeId,
Paladox nonef4ddeed2023-02-22 19:30:47 +000053 PatchSetNumber,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +020054 RequestPayload,
55 RevertSubmissionInfo,
56 ReviewInput,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +020057} from '../../../types/common';
58import {GrConfirmAbandonDialog} from '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog';
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +020059import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
60import {GrCreateChangeDialog} from '../../admin/gr-create-change-dialog/gr-create-change-dialog';
61import {GrConfirmSubmitDialog} from '../gr-confirm-submit-dialog/gr-confirm-submit-dialog';
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +020062import {
63 ConfirmRevertEventDetail,
64 GrConfirmRevertDialog,
65 RevertType,
66} from '../gr-confirm-revert-dialog/gr-confirm-revert-dialog';
67import {GrConfirmMoveDialog} from '../gr-confirm-move-dialog/gr-confirm-move-dialog';
68import {GrConfirmCherrypickDialog} from '../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog';
69import {GrConfirmCherrypickConflictDialog} from '../gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog';
70import {
71 ConfirmRebaseEventDetail,
72 GrConfirmRebaseDialog,
73} from '../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog';
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +020074import {GrButton} from '../../shared/gr-button/gr-button';
75import {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +020076 GrChangeActionsElement,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +020077 UIActionInfo,
78} from '../../shared/gr-js-api-interface/gr-change-actions-js-api';
paladox32b861a2022-04-25 16:45:06 +010079import {
80 fire,
81 fireAlert,
Ben Rohlfs6bb90532023-02-17 18:55:56 +010082 fireError,
Ben Rohlfs44f01042023-02-18 13:27:57 +010083 fireNoBubbleNoCompose,
paladox32b861a2022-04-25 16:45:06 +010084} from '../../../utils/event-util';
Frank Borden481e5f92020-11-30 19:59:09 -080085import {
Frank Borden481e5f92020-11-30 19:59:09 -080086 getApprovalInfo,
87 getVotingRange,
Milutin Kristoficbd445802021-10-20 21:00:21 +020088 StandardLabels,
Frank Borden481e5f92020-11-30 19:59:09 -080089} from '../../../utils/label-util';
Ben Rohlfsa7ab9502021-02-15 17:45:45 +010090import {
91 ActionPriority,
92 ActionType,
93 ChangeActions,
94 PrimaryActionKey,
95 RevisionActions,
96} from '../../../api/change-actions';
97import {ErrorCallback} from '../../../api/rest';
Dhruv Srivastavae68d45a2021-04-07 11:50:00 +020098import {GrDropdown} from '../../shared/gr-dropdown/gr-dropdown';
paladox32b861a2022-04-25 16:45:06 +010099import {resolve} from '../../../models/dependency';
Chris Poucetbf65b8f2022-01-18 21:18:12 +0000100import {changeModelToken} from '../../../models/change/change-model';
paladox32b861a2022-04-25 16:45:06 +0100101import {sharedStyles} from '../../../styles/shared-styles';
102import {LitElement, PropertyValues, css, html, nothing} from 'lit';
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200103import {customElement, query, state} from 'lit/decorators.js';
Frank Borden42c1a452022-08-11 16:27:20 +0200104import {ifDefined} from 'lit/directives/if-defined.js';
Ben Rohlfsba440822023-04-11 18:08:03 +0200105import {assertIsDefined, queryAll, uuid} from '../../../utils/common-util';
Milutin Kristofic174c3432022-09-16 12:33:28 +0200106import {Interaction} from '../../../constants/reporting';
Ben Rohlfse9051fe2022-09-16 11:08:40 +0200107import {rootUrl} from '../../../utils/url-util';
Ben Rohlfs77c489a2022-09-21 14:25:56 +0200108import {createSearchUrl} from '../../../models/views/search';
Ben Rohlfsaa533902022-09-22 09:07:12 +0200109import {createChangeUrl} from '../../../models/views/change';
Chris Poucet20f09582022-10-24 23:29:27 +0200110import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
Ben Rohlfs4e913532022-10-24 12:31:58 +0200111import {ShowRevisionActionsDetail} from '../../shared/gr-js-api-interface/gr-js-api-types';
Dhruv Srivastava38e0b162022-10-24 14:22:17 +0200112import {whenVisible} from '../../../utils/dom-util';
Chris Poucete3d66862022-10-26 11:19:50 +0200113import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
Dhruv Srivastava84575c92022-10-26 19:13:59 +0200114import {modalStyles} from '../../../styles/gr-modal-styles';
Paladox nonef4ddeed2023-02-22 19:30:47 +0000115import {subscribe} from '../../lit/subscription-controller';
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200116import {userModelToken} from '../../../models/user/user-model';
117import {ParsedChangeInfo} from '../../../types/types';
118import {configModelToken} from '../../../models/config/config-model';
Kamil Musin1d8a1a32024-02-29 14:49:41 +0100119import {readJSONResponsePayload} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
Milutin Kristofic7c84f7e2024-04-02 21:41:58 +0200120import {commentsModelToken} from '../../../models/comments/comments-model';
121import {when} from 'lit/directives/when.js';
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100122
123const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
124const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
125const ERR_REVISION_ACTIONS = 'Couldn’t load revision actions.';
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200126
Milutin Kristoficb2237d62021-12-09 20:29:55 +0100127export enum LabelStatus {
Viktar Donich07130562016-12-06 11:21:39 -0800128 /**
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100129 * This label provides what is necessary for submission.
Viktar Donich07130562016-12-06 11:21:39 -0800130 */
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200131 OK = 'OK',
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100132 /**
133 * This label prevents the change from being submitted.
134 */
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200135 REJECT = 'REJECT',
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100136 /**
137 * The label may be set, but it's neither necessary for submission
138 * nor does it block submission if set.
139 */
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200140 MAY = 'MAY',
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100141 /**
142 * The label is required for submission, but has not been satisfied.
143 */
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200144 NEED = 'NEED',
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100145 /**
146 * The label is required for submission, but is impossible to complete.
147 * The likely cause is access has not been granted correctly by the
148 * project owner or site administrator.
149 */
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200150 IMPOSSIBLE = 'IMPOSSIBLE',
151 OPTIONAL = 'OPTIONAL',
152}
Viktar Donich07130562016-12-06 11:21:39 -0800153
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200154const ActionLoadingLabels: {[actionKey: string]: string} = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100155 abandon: 'Abandoning...',
156 cherrypick: 'Cherry-picking...',
157 delete: 'Deleting...',
158 move: 'Moving..',
159 rebase: 'Rebasing...',
160 restore: 'Restoring...',
161 revert: 'Reverting...',
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100162 submit: 'Submitting...',
163};
Andrew Bonventre1508cac2016-04-02 21:37:15 -0400164
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100165const ADDITIONAL_ACTION_KEY_PREFIX = '__additionalAction_';
Andrew Bonventrecd73e962016-06-22 17:52:56 -0400166
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200167interface QuickApproveUIActionInfo extends UIActionInfo {
168 key: string;
169 payload?: RequestPayload;
170}
171
172const QUICK_APPROVE_ACTION: QuickApproveUIActionInfo = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100173 __key: 'review',
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200174 __type: ActionType.CHANGE,
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100175 enabled: true,
176 key: 'review',
177 label: 'Quick approve',
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200178 method: HttpMethod.POST,
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100179};
Viktar Donich50e2c002016-11-11 14:41:24 -0800180
Dhruv Srivastava25ca8ea2021-03-31 21:37:30 +0200181function isQuickApproveAction(
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200182 action: UIActionInfo
183): action is QuickApproveUIActionInfo {
184 return (action as QuickApproveUIActionInfo).key === QUICK_APPROVE_ACTION.key;
185}
Viktar Donich7f412ae2017-04-18 16:08:13 -0700186
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200187const DOWNLOAD_ACTION: UIActionInfo = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100188 enabled: true,
189 label: 'Download patch',
190 title: 'Open download dialog',
191 __key: 'download',
192 __primary: false,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200193 __type: ActionType.REVISION,
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100194};
Kasper Nilssonf6c0b502017-04-25 10:45:28 +0200195
Dhruv Srivastava4dcb5432021-04-26 16:45:07 +0200196const INCLUDED_IN_ACTION: UIActionInfo = {
197 enabled: true,
198 label: 'Included In',
199 title: 'Open Included In dialog',
200 __key: 'includedIn',
201 __primary: false,
202 __type: ActionType.CHANGE,
203};
204
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200205const REBASE_EDIT: UIActionInfo = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100206 enabled: true,
207 label: 'Rebase edit',
208 title: 'Rebase change edit',
209 __key: 'rebaseEdit',
210 __primary: false,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200211 __type: ActionType.CHANGE,
212 method: HttpMethod.POST,
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100213};
Paladox nonea95b3682017-09-18 17:25:29 +0000214
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200215const PUBLISH_EDIT: UIActionInfo = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100216 enabled: true,
217 label: 'Publish edit',
218 title: 'Publish change edit',
219 __key: 'publishEdit',
220 __primary: false,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200221 __type: ActionType.CHANGE,
222 method: HttpMethod.POST,
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100223};
Paladox none68b75ba2017-09-13 21:23:07 +0000224
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200225const DELETE_EDIT: UIActionInfo = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100226 enabled: true,
227 label: 'Delete edit',
228 title: 'Delete change edit',
229 __key: 'deleteEdit',
230 __primary: false,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200231 __type: ActionType.CHANGE,
232 method: HttpMethod.DELETE,
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100233};
Paladox none68b75ba2017-09-13 21:23:07 +0000234
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200235const EDIT: UIActionInfo = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100236 enabled: true,
237 label: 'Edit',
238 title: 'Edit this change',
239 __key: 'edit',
240 __primary: false,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200241 __type: ActionType.CHANGE,
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100242};
Kasper Nilsson7d6fb7b2018-01-16 12:45:45 -0800243
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200244const STOP_EDIT: UIActionInfo = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100245 enabled: true,
246 label: 'Stop editing',
247 title: 'Stop editing this change',
248 __key: 'stopEdit',
249 __primary: false,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200250 __type: ActionType.CHANGE,
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100251};
Kasper Nilsson59d66d82018-01-23 17:46:47 -0800252
Chris Poucet1cd6394d2022-07-12 16:43:28 +0200253// Set of keys that have icons.
254const ACTIONS_WITH_ICONS = new Map<
255 string,
Chris Pouceta6b629c2022-08-12 16:25:48 +0200256 Pick<UIActionInfo, 'filled' | 'icon'>
Chris Poucet1cd6394d2022-07-12 16:43:28 +0200257>([
258 [ChangeActions.ABANDON, {icon: 'block'}],
259 [ChangeActions.DELETE_EDIT, {icon: 'delete', filled: true}],
260 [ChangeActions.EDIT, {icon: 'edit', filled: true}],
261 [ChangeActions.PUBLISH_EDIT, {icon: 'publish', filled: true}],
262 [ChangeActions.READY, {icon: 'visibility', filled: true}],
Chris Pouceta6b629c2022-08-12 16:25:48 +0200263 [ChangeActions.REBASE_EDIT, {icon: 'rebase_edit'}],
264 [RevisionActions.REBASE, {icon: 'rebase'}],
Chris Poucet1cd6394d2022-07-12 16:43:28 +0200265 [ChangeActions.RESTORE, {icon: 'history'}],
266 [ChangeActions.REVERT, {icon: 'undo'}],
267 [ChangeActions.STOP_EDIT, {icon: 'stop', filled: true}],
268 [QUICK_APPROVE_ACTION.key, {icon: 'check'}],
269 [RevisionActions.SUBMIT, {icon: 'done_all'}],
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100270]);
Kasper Nilsson59b65952018-05-14 17:44:10 -0700271
Dhruv Srivastavabc329e92020-10-30 09:02:15 +0100272const EDIT_ACTIONS: Set<string> = new Set([
273 ChangeActions.DELETE_EDIT,
274 ChangeActions.EDIT,
275 ChangeActions.PUBLISH_EDIT,
276 ChangeActions.REBASE_EDIT,
277 ChangeActions.STOP_EDIT,
278]);
279
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100280const AWAIT_CHANGE_ATTEMPTS = 5;
281const AWAIT_CHANGE_TIMEOUT_MS = 1000;
Wyatt Allen15ed72f2017-08-10 09:29:20 -0700282
David Ostrovsky954e5082021-05-05 12:28:13 +0200283const SKIP_ACTION_KEYS: string[] = [
Han-Wen Nienhuys64080a52021-08-10 13:03:41 +0200284 // REVIEWED/UNREVIEWED is made obsolete by AttentionSet. Once the
285 // backend stops supporting (UN)REVIEWED, we can remove these.
Ben Rohlfs50768ee2020-06-29 09:51:32 +0200286 ChangeActions.REVIEWED,
287 ChangeActions.UNREVIEWED,
Han-Wen Nienhuys64080a52021-08-10 13:03:41 +0200288 // REVERT_SUBMISSION is folded into the dialog for REVERT.
Dhruv Srivastava4f2f9382021-05-14 08:33:45 +0000289 ChangeActions.REVERT_SUBMISSION,
Ben Rohlfs50768ee2020-06-29 09:51:32 +0200290];
291
Dhruv Srivastavae68d45a2021-04-07 11:50:00 +0200292export function assertUIActionInfo(action?: ActionInfo): UIActionInfo {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200293 // TODO(TS): Remove this function. The gr-change-actions adds properties
294 // to existing ActionInfo objects instead of creating a new objects. This
295 // function checks, that 'action' has all property required by UIActionInfo.
296 // In the future, we should avoid updates of an existing ActionInfos and
297 // instead create a new object to make code cleaner. However, at the current
298 // state this is unsafe, because other code can expect these properties to be
299 // set in ActionInfo.
300 if (!action) {
301 throw new Error('action is undefined');
302 }
303 const result = action as UIActionInfo;
304 if (result.__key === undefined || result.__type === undefined) {
305 throw new Error('action is not an UIActionInfo');
306 }
307 return result;
308}
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100309
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200310interface MenuAction {
311 name: string;
312 id: string;
313 action: UIActionInfo;
314 tooltip?: string;
315}
316
317interface OverflowAction {
318 type: ActionType;
319 key: string;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200320}
321
322interface ActionPriorityOverride {
323 type: ActionType.CHANGE | ActionType.REVISION;
324 key: string;
325 priority: ActionPriority;
326}
327
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200328@customElement('gr-change-actions')
Frank Borden6988bdf2021-04-07 14:42:00 +0200329export class GrChangeActions
paladox32b861a2022-04-25 16:45:06 +0100330 extends LitElement
Chris Poucetcaeea1b2021-08-19 22:12:56 +0000331 implements GrChangeActionsElement
332{
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100333 /**
334 * Fired when the change should be reloaded.
335 *
Dhruv Srivastava8449e772020-07-22 11:57:29 +0200336 * @event reload
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100337 */
Dhruv Srivastava535e85d2020-02-13 19:15:20 +0100338
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100339 /**
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100340 * Fired when an action is tapped.
341 *
342 * @event custom-tap - naming pattern: <action key>-tap
Tao Zhou9a076812019-12-17 09:59:28 +0100343 */
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100344
paladox32b861a2022-04-25 16:45:06 +0100345 @query('#mainContent') mainContent?: Element;
346
Dhruv Srivastava38e0b162022-10-24 14:22:17 +0200347 @query('#actionsModal') actionsModal?: HTMLDialogElement;
paladox32b861a2022-04-25 16:45:06 +0100348
349 @query('#confirmRebase') confirmRebase?: GrConfirmRebaseDialog;
350
351 @query('#confirmCherrypick') confirmCherrypick?: GrConfirmCherrypickDialog;
352
353 @query('#confirmCherrypickConflict')
354 confirmCherrypickConflict?: GrConfirmCherrypickConflictDialog;
355
356 @query('#confirmMove') confirmMove?: GrConfirmMoveDialog;
357
358 @query('#confirmRevertDialog') confirmRevertDialog?: GrConfirmRevertDialog;
359
360 @query('#confirmAbandonDialog') confirmAbandonDialog?: GrConfirmAbandonDialog;
361
362 @query('#confirmSubmitDialog') confirmSubmitDialog?: GrConfirmSubmitDialog;
363
364 @query('#createFollowUpDialog') createFollowUpDialog?: GrDialog;
365
366 @query('#createFollowUpChange') createFollowUpChange?: GrCreateChangeDialog;
367
368 @query('#confirmDeleteDialog') confirmDeleteDialog?: GrDialog;
369
370 @query('#confirmDeleteEditDialog') confirmDeleteEditDialog?: GrDialog;
371
Milutin Kristofic7c84f7e2024-04-02 21:41:58 +0200372 @query('#confirmPublishEditDialog') confirmPublishEditDialog?: GrDialog;
373
paladox32b861a2022-04-25 16:45:06 +0100374 @query('#moreActions') moreActions?: GrDropdown;
375
376 @query('#secondaryActions') secondaryActions?: HTMLElement;
377
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200378 @state() change?: ParsedChangeInfo;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200379
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200380 @state() actions: ActionNameToActionInfoMap = {};
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200381
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200382 @state() primaryActionKeys: PrimaryActionKey[] = [
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200383 ChangeActions.READY,
384 RevisionActions.SUBMIT,
385 ];
386
paladox32b861a2022-04-25 16:45:06 +0100387 @state() _hideQuickApproveAction = false;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200388
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200389 @state() account?: AccountInfo;
Frank Borden481e5f92020-11-30 19:59:09 -0800390
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200391 @state() changeNum?: NumericChangeId;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200392
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200393 @state() changeStatus?: ChangeStatus;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200394
Ben Rohlfs2add82f2024-02-26 19:23:01 +0100395 @state() mergeable?: boolean;
396
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200397 @state() commitNum?: CommitId;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200398
Paladox nonef4ddeed2023-02-22 19:30:47 +0000399 @state() latestPatchNum?: PatchSetNumber;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200400
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200401 @state() commitMessage = '';
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200402
Ben Rohlfs5550f452024-02-26 19:12:11 +0100403 // The unfiltered result of calling `restApiService.getChangeRevisionActions()`.
404 // The DOWNLOAD action is also added to it in `actionsChanged()`.
Ben Rohlfs4af8c1d2024-02-29 11:41:35 +0100405 @state() revisionActions?: ActionNameToActionInfoMap;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200406
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200407 @state() privateByDefault?: InheritedBooleanInfo;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200408
paladox32b861a2022-04-25 16:45:06 +0100409 @state() actionLoadingMessage = '';
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200410
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200411 @state() inProgressActionKeys = new Set<string>();
Frank Borden10a69252022-12-08 17:28:49 +0100412
paladox32b861a2022-04-25 16:45:06 +0100413 @state() allActionValues: UIActionInfo[] = [];
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200414
paladox32b861a2022-04-25 16:45:06 +0100415 @state() topLevelActions?: UIActionInfo[];
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200416
paladox32b861a2022-04-25 16:45:06 +0100417 @state() topLevelPrimaryActions?: UIActionInfo[];
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200418
paladox32b861a2022-04-25 16:45:06 +0100419 @state() topLevelSecondaryActions?: UIActionInfo[];
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200420
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200421 @state() menuActions?: MenuAction[];
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200422
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200423 @state() overflowActions: OverflowAction[] = [
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200424 {
425 type: ActionType.CHANGE,
426 key: ChangeActions.WIP,
427 },
428 {
429 type: ActionType.CHANGE,
430 key: ChangeActions.DELETE,
431 },
432 {
433 type: ActionType.REVISION,
434 key: RevisionActions.CHERRYPICK,
435 },
436 {
437 type: ActionType.CHANGE,
438 key: ChangeActions.MOVE,
439 },
440 {
441 type: ActionType.REVISION,
442 key: RevisionActions.DOWNLOAD,
443 },
444 {
445 type: ActionType.CHANGE,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200446 key: ChangeActions.REVIEWED,
447 },
448 {
449 type: ActionType.CHANGE,
450 key: ChangeActions.UNREVIEWED,
451 },
452 {
453 type: ActionType.CHANGE,
454 key: ChangeActions.PRIVATE,
455 },
456 {
457 type: ActionType.CHANGE,
458 key: ChangeActions.PRIVATE_DELETE,
459 },
460 {
461 type: ActionType.CHANGE,
462 key: ChangeActions.FOLLOW_UP,
463 },
Dhruv Srivastava4dcb5432021-04-26 16:45:07 +0200464 {
465 type: ActionType.CHANGE,
466 key: ChangeActions.INCLUDED_IN,
467 },
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200468 ];
469
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200470 @state() actionPriorityOverrides: ActionPriorityOverride[] = [];
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200471
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200472 @state() additionalActions: UIActionInfo[] = [];
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200473
paladox32b861a2022-04-25 16:45:06 +0100474 @state() hiddenActions: string[] = [];
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200475
paladox32b861a2022-04-25 16:45:06 +0100476 @state() disabledMenuActions: string[] = [];
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200477
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200478 @state() editPatchsetLoaded = false;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200479
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200480 @state() editMode = false;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200481
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200482 @state() editBasedOnCurrentPatchSet = true;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200483
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200484 @state() loggedIn = false;
Paladox none7e2f9f32022-02-21 09:31:04 +0000485
Ben Rohlfs5550f452024-02-26 19:12:11 +0100486 @state() pluginsLoaded = false;
487
Milutin Kristofic7c84f7e2024-04-02 21:41:58 +0200488 @state() threadsWithSuggestions?: CommentThread[];
489
Chris Poucetc6e880b2021-11-15 19:57:06 +0100490 private readonly restApiService = getAppContext().restApiService;
Ben Rohlfs43935a42020-12-01 19:14:09 +0100491
Chris Poucete3d66862022-10-26 11:19:50 +0200492 private readonly reporting = getAppContext().reportingService;
493
494 private readonly getPluginLoader = resolve(this, pluginLoaderToken);
495
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200496 private readonly getUserModel = resolve(this, userModelToken);
497
498 private readonly getConfigModel = resolve(this, configModelToken);
499
Chris Poucete3d66862022-10-26 11:19:50 +0200500 private readonly getChangeModel = resolve(this, changeModelToken);
501
Chris Poucet20f09582022-10-24 23:29:27 +0200502 private readonly getStorage = resolve(this, storageServiceToken);
paladoxa80ea7c2022-02-19 17:42:15 +0000503
Ben Rohlfs77c489a2022-09-21 14:25:56 +0200504 private readonly getNavigation = resolve(this, navigationToken);
505
Milutin Kristofic7c84f7e2024-04-02 21:41:58 +0200506 private readonly getCommentsModel = resolve(this, commentsModelToken);
507
Ben Rohlfsf7f1e8e2021-03-12 14:36:40 +0100508 constructor() {
509 super();
Paladox nonef4ddeed2023-02-22 19:30:47 +0000510 subscribe(
511 this,
512 () => this.getChangeModel().latestPatchNum$,
513 x => (this.latestPatchNum = x)
514 );
Paladox noned22af552023-04-10 15:31:00 +0000515 subscribe(
516 this,
517 () => this.getChangeModel().patchsets$,
518 x => (this.editBasedOnCurrentPatchSet = hasEditBasedOnCurrentPatchSet(x))
519 );
520 subscribe(
521 this,
522 () => this.getChangeModel().patchNum$,
523 x => (this.editPatchsetLoaded = x === 'edit')
524 );
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200525 subscribe(
526 this,
527 () => this.getChangeModel().changeNum$,
528 x => (this.changeNum = x)
529 );
530 subscribe(
531 this,
532 () => this.getChangeModel().change$,
533 x => (this.change = x)
534 );
535 subscribe(
536 this,
537 () => this.getChangeModel().status$,
538 x => (this.changeStatus = x)
539 );
540 subscribe(
541 this,
Ben Rohlfs2add82f2024-02-26 19:23:01 +0100542 () => this.getChangeModel().mergeable$,
543 x => (this.mergeable = x)
544 );
545 subscribe(
546 this,
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200547 () => this.getChangeModel().editMode$,
548 x => (this.editMode = x)
549 );
550 subscribe(
551 this,
552 () => this.getChangeModel().revision$,
553 rev => (this.commitNum = rev?.commit?.commit)
554 );
555 subscribe(
556 this,
557 () => this.getChangeModel().latestRevision$,
558 rev => (this.commitMessage = rev?.commit?.message ?? '')
559 );
560 subscribe(
561 this,
562 () => this.getUserModel().account$,
563 x => (this.account = x)
564 );
565 subscribe(
566 this,
567 () => this.getUserModel().loggedIn$,
568 x => (this.loggedIn = x)
569 );
570 subscribe(
571 this,
Ben Rohlfs5550f452024-02-26 19:12:11 +0100572 () => this.getPluginLoader().pluginsModel.pluginsLoaded$,
573 x => (this.pluginsLoaded = x)
574 );
575 subscribe(
576 this,
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200577 () => this.getConfigModel().repoConfig$,
578 config => (this.privateByDefault = config?.private_by_default)
579 );
Milutin Kristofic7c84f7e2024-04-02 21:41:58 +0200580 subscribe(
581 this,
582 () => this.getCommentsModel().threadsWithSuggestions$,
583 x => (this.threadsWithSuggestions = x)
584 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100585 }
Kasper Nilsson2d9610c2018-08-23 13:18:11 -0700586
paladox32b861a2022-04-25 16:45:06 +0100587 override connectedCallback() {
588 super.connectedCallback();
Chris Poucete3d66862022-10-26 11:19:50 +0200589 this.getPluginLoader().jsApiService.addElement(
590 TargetElement.CHANGE_ACTIONS,
591 this
592 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100593 }
594
paladox32b861a2022-04-25 16:45:06 +0100595 static override get styles() {
596 return [
597 sharedStyles,
Dhruv Srivastava84575c92022-10-26 19:13:59 +0200598 modalStyles,
paladox32b861a2022-04-25 16:45:06 +0100599 css`
600 :host {
601 display: flex;
602 font-family: var(--font-family);
603 }
604 #actionLoadingMessage,
605 #mainContent,
606 section {
607 display: flex;
608 }
609 #actionLoadingMessage,
610 gr-button,
611 gr-dropdown {
612 /* px because don't have the same font size */
613 margin-left: 8px;
614 }
615 gr-button {
616 display: block;
617 }
618 #actionLoadingMessage {
619 align-items: center;
620 color: var(--deemphasized-text-color);
621 }
622 #confirmSubmitDialog .changeSubject {
623 margin: var(--spacing-l);
624 text-align: center;
625 }
Chris Poucet1c713862022-07-25 13:12:24 +0200626 gr-icon {
paladox32b861a2022-04-25 16:45:06 +0100627 color: inherit;
628 margin-right: var(--spacing-xs);
629 }
Chris Poucet1c713862022-07-25 13:12:24 +0200630 #moreActions gr-icon {
paladox32b861a2022-04-25 16:45:06 +0100631 margin: 0;
632 }
633 #moreMessage,
634 .hidden {
635 display: none;
636 }
Milutin Kristofic7c84f7e2024-04-02 21:41:58 +0200637 .info {
638 background-color: var(--info-background);
639 padding: var(--spacing-l) var(--spacing-xl);
640 margin-bottom: var(--spacing-l);
641 }
642 .info gr-icon {
643 color: var(--selected-foreground);
644 margin-right: var(--spacing-xl);
645 }
paladox32b861a2022-04-25 16:45:06 +0100646 @media screen and (max-width: 50em) {
647 #mainContent {
648 flex-wrap: wrap;
649 }
650 gr-button {
651 --gr-button-padding: var(--spacing-m);
652 white-space: nowrap;
653 }
654 gr-button,
655 gr-dropdown {
656 margin: 0;
657 }
658 #actionLoadingMessage {
659 margin: var(--spacing-m);
660 text-align: center;
661 }
662 #moreMessage {
663 display: inline;
664 }
665 }
666 `,
667 ];
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100668 }
669
paladox32b861a2022-04-25 16:45:06 +0100670 override render() {
671 if (!this.change) return nothing;
672 return html`
673 <div id="mainContent">
674 <span id="actionLoadingMessage" ?hidden=${!this.actionLoadingMessage}>
675 ${this.actionLoadingMessage}
676 </span>
677 <section
678 id="primaryActions"
Ben Rohlfs5550f452024-02-26 19:12:11 +0100679 ?hidden=${this.isLoading() ||
paladox32b861a2022-04-25 16:45:06 +0100680 !this.topLevelActions ||
681 !this.topLevelActions.length}
682 >
683 ${this.topLevelPrimaryActions?.map(action =>
Chris Poucet1cd6394d2022-07-12 16:43:28 +0200684 this.renderUIAction(action)
paladox32b861a2022-04-25 16:45:06 +0100685 )}
686 </section>
687 <section
688 id="secondaryActions"
Ben Rohlfs5550f452024-02-26 19:12:11 +0100689 ?hidden=${this.isLoading() ||
paladox32b861a2022-04-25 16:45:06 +0100690 !this.topLevelActions ||
691 !this.topLevelActions.length}
692 >
693 ${this.topLevelSecondaryActions?.map(action =>
Chris Poucet1cd6394d2022-07-12 16:43:28 +0200694 this.renderUIAction(action)
paladox32b861a2022-04-25 16:45:06 +0100695 )}
696 </section>
Ben Rohlfs5550f452024-02-26 19:12:11 +0100697 <gr-button ?hidden=${!this.isLoading()}>Loading actions...</gr-button>
paladox32b861a2022-04-25 16:45:06 +0100698 <gr-dropdown
699 id="moreActions"
700 link
701 .verticalOffset=${32}
702 .horizontalAlign=${'right'}
703 @tap-item=${this.handleOverflowItemTap}
Ben Rohlfs5550f452024-02-26 19:12:11 +0100704 ?hidden=${this.isLoading() ||
paladox32b861a2022-04-25 16:45:06 +0100705 !this.menuActions ||
706 !this.menuActions.length}
707 .disabledIds=${this.disabledMenuActions}
708 .items=${this.menuActions}
709 >
Chris Poucet1c713862022-07-25 13:12:24 +0200710 <gr-icon icon="more_vert" aria-labelledby="moreMessage"></gr-icon>
paladox32b861a2022-04-25 16:45:06 +0100711 <span id="moreMessage">More</span>
712 </gr-dropdown>
713 </div>
Dhruv Srivastava38e0b162022-10-24 14:22:17 +0200714 <dialog id="actionsModal" tabindex="-1">
paladox32b861a2022-04-25 16:45:06 +0100715 <gr-confirm-rebase-dialog
716 id="confirmRebase"
717 class="confirmDialog"
Ben Rohlfs5b3c6552023-02-18 13:02:46 +0100718 @confirm-rebase=${this.handleRebaseConfirm}
paladox32b861a2022-04-25 16:45:06 +0100719 @cancel=${this.handleConfirmDialogCancel}
Frank Borden10a69252022-12-08 17:28:49 +0100720 .disableActions=${this.inProgressActionKeys.has(
721 RevisionActions.REBASE
722 )}
paladox32b861a2022-04-25 16:45:06 +0100723 .branch=${this.change?.branch}
Ben Rohlfs4af8c1d2024-02-29 11:41:35 +0100724 .rebaseOnCurrent=${!!this.revisionActions?.rebase?.enabled}
paladox32b861a2022-04-25 16:45:06 +0100725 ></gr-confirm-rebase-dialog>
726 <gr-confirm-cherrypick-dialog
727 id="confirmCherrypick"
728 class="confirmDialog"
729 .changeStatus=${this.changeStatus}
730 .commitMessage=${this.commitMessage}
731 .commitNum=${this.commitNum}
732 @confirm=${this.handleCherrypickConfirm}
733 @cancel=${this.handleConfirmDialogCancel}
734 .project=${this.change?.project}
735 ></gr-confirm-cherrypick-dialog>
736 <gr-confirm-cherrypick-conflict-dialog
737 id="confirmCherrypickConflict"
738 class="confirmDialog"
739 @confirm=${this.handleCherrypickConflictConfirm}
740 @cancel=${this.handleConfirmDialogCancel}
741 ></gr-confirm-cherrypick-conflict-dialog>
742 <gr-confirm-move-dialog
743 id="confirmMove"
744 class="confirmDialog"
745 @confirm=${this.handleMoveConfirm}
746 @cancel=${this.handleConfirmDialogCancel}
747 .project=${this.change?.project}
748 ></gr-confirm-move-dialog>
749 <gr-confirm-revert-dialog
750 id="confirmRevertDialog"
751 class="confirmDialog"
Ben Rohlfs5b3c6552023-02-18 13:02:46 +0100752 @confirm-revert=${this.handleRevertDialogConfirm}
paladox32b861a2022-04-25 16:45:06 +0100753 @cancel=${this.handleConfirmDialogCancel}
754 ></gr-confirm-revert-dialog>
755 <gr-confirm-abandon-dialog
756 id="confirmAbandonDialog"
757 class="confirmDialog"
758 @confirm=${this.handleAbandonDialogConfirm}
759 @cancel=${this.handleConfirmDialogCancel}
760 ></gr-confirm-abandon-dialog>
761 <gr-confirm-submit-dialog
762 id="confirmSubmitDialog"
763 class="confirmDialog"
Ben Rohlfs4af8c1d2024-02-29 11:41:35 +0100764 .action=${this.revisionActions?.submit}
paladox32b861a2022-04-25 16:45:06 +0100765 @cancel=${this.handleConfirmDialogCancel}
766 @confirm=${this.handleSubmitConfirm}
767 ></gr-confirm-submit-dialog>
768 <gr-dialog
769 id="createFollowUpDialog"
770 class="confirmDialog"
771 confirm-label="Create"
772 @confirm=${this.handleCreateFollowUpChange}
773 @cancel=${this.handleCloseCreateFollowUpChange}
774 >
775 <div class="header" slot="header">Create Follow-Up Change</div>
776 <div class="main" slot="main">
777 <gr-create-change-dialog
778 id="createFollowUpChange"
779 .branch=${this.change?.branch}
780 .baseChange=${this.change?.id}
781 .repoName=${this.change?.project}
782 .privateByDefault=${this.privateByDefault}
783 ></gr-create-change-dialog>
784 </div>
785 </gr-dialog>
786 <gr-dialog
787 id="confirmDeleteDialog"
788 class="confirmDialog"
789 confirm-label="Delete"
790 confirm-on-enter=""
791 @cancel=${this.handleConfirmDialogCancel}
792 @confirm=${this.handleDeleteConfirm}
793 >
794 <div class="header" slot="header">Delete Change</div>
795 <div class="main" slot="main">
796 Do you really want to delete the change?
797 </div>
798 </gr-dialog>
799 <gr-dialog
800 id="confirmDeleteEditDialog"
801 class="confirmDialog"
802 confirm-label="Delete"
803 confirm-on-enter=""
804 @cancel=${this.handleConfirmDialogCancel}
805 @confirm=${this.handleDeleteEditConfirm}
806 >
807 <div class="header" slot="header">Delete Change Edit</div>
808 <div class="main" slot="main">
809 Do you really want to delete the edit?
810 </div>
811 </gr-dialog>
Milutin Kristofic7c84f7e2024-04-02 21:41:58 +0200812 <gr-dialog
813 id="confirmPublishEditDialog"
814 class="confirmDialog"
815 confirm-label="Publish"
816 confirm-on-enter=""
817 @cancel=${this.handleConfirmDialogCancel}
818 @confirm=${this.handlePublishEditConfirm}
819 >
820 <div class="header" slot="header">Publish Change Edit</div>
821 <div class="main" slot="main">
822 ${when(
823 this.numberOfThreadsWithSuggestions() > 0,
824 () => html`<p class="info">
825 <gr-icon id="icon" icon="info" small></gr-icon>
826 Heads Up! ${this.numberOfThreadsWithSuggestions()} comments have
827 suggestions you can apply before publishing
828 </p>`
829 )}
830 Do you really want to publish the edit?
831 </div>
832 </gr-dialog>
Dhruv Srivastava38e0b162022-10-24 14:22:17 +0200833 </dialog>
paladox32b861a2022-04-25 16:45:06 +0100834 `;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100835 }
836
Chris Poucet1cd6394d2022-07-12 16:43:28 +0200837 private renderUIAction(action: UIActionInfo) {
paladox32b861a2022-04-25 16:45:06 +0100838 return html`
839 <gr-tooltip-content
840 title=${ifDefined(action.title)}
841 .hasTooltip=${!!action.title}
842 ?position-below=${true}
843 >
844 <gr-button
845 link
846 class=${action.__key}
847 data-action-key=${action.__key}
848 data-label=${action.label}
849 ?disabled=${this.calculateDisabled(action)}
850 @click=${(e: MouseEvent) =>
851 this.handleActionTap(e, action.__key, action.__type)}
852 >
Chris Poucet1cd6394d2022-07-12 16:43:28 +0200853 ${this.renderUIActionIcon(action)} ${action.label}
paladox32b861a2022-04-25 16:45:06 +0100854 </gr-button>
855 </gr-tooltip-content>
856 `;
857 }
858
Chris Poucet1cd6394d2022-07-12 16:43:28 +0200859 private renderUIActionIcon(action: UIActionInfo) {
860 if (!action.icon) return nothing;
Chris Pouceta6b629c2022-08-12 16:25:48 +0200861 return html`
862 <gr-icon icon=${action.icon} ?filled=${action.filled}></gr-icon>
863 `;
paladox32b861a2022-04-25 16:45:06 +0100864 }
865
866 override willUpdate(changedProperties: PropertyValues) {
paladox32b861a2022-04-25 16:45:06 +0100867 if (changedProperties.has('change')) {
868 this.reload();
Milutin Kristofic7e0312a2022-06-17 15:58:26 +0200869 this.actions = this.change?.actions ?? {};
paladox32b861a2022-04-25 16:45:06 +0100870 }
871
Chris Poucet9369a9f2022-05-09 17:15:44 +0200872 this.editStatusChanged();
paladox32b861a2022-04-25 16:45:06 +0100873
Chris Poucet9369a9f2022-05-09 17:15:44 +0200874 this.actionsChanged();
875 this.allActionValues = this.computeAllActions();
876 this.topLevelActions = this.allActionValues.filter(a => {
877 if (this.hiddenActions.includes(a.__key)) return false;
878 if (this.editMode) return EDIT_ACTIONS.has(a.__key);
Ben Rohlfs73294302024-02-26 17:07:40 +0100879 return !this.isOverflowAction(a.__type, a.__key);
Chris Poucet9369a9f2022-05-09 17:15:44 +0200880 });
881 this.topLevelPrimaryActions = this.topLevelActions.filter(
882 action => action.__primary
883 );
884 this.topLevelSecondaryActions = this.topLevelActions.filter(
885 action => !action.__primary
886 );
887 this.menuActions = this.computeMenuActions();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100888 }
889
890 reload() {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200891 if (!this.changeNum || !this.latestPatchNum || !this.change) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100892 return Promise.resolve();
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100893 }
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200894 const change = this.change;
Ben Rohlfs4af8c1d2024-02-29 11:41:35 +0100895 this.revisionActions = undefined;
Ben Rohlfs43935a42020-12-01 19:14:09 +0100896 return this.restApiService
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200897 .getChangeRevisionActions(this.changeNum, this.latestPatchNum)
898 .then(revisionActions => {
Ben Rohlfs4af8c1d2024-02-29 11:41:35 +0100899 this.revisionActions = revisionActions ?? {};
paladox32b861a2022-04-25 16:45:06 +0100900 this.sendShowRevisionActions({
Ben Rohlfsf56f71c2023-04-27 17:20:43 +0200901 change: change as ChangeInfo,
Ben Rohlfs4af8c1d2024-02-29 11:41:35 +0100902 revisionActions: this.revisionActions,
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100903 });
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200904 })
905 .catch(err => {
Milutin Kristofic860fe4d2020-11-23 16:13:45 +0100906 fireAlert(this, ERR_REVISION_ACTIONS);
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200907 throw err;
908 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100909 }
Andrew Bonventre1508cac2016-04-02 21:37:15 -0400910
Ben Rohlfs5550f452024-02-26 19:12:11 +0100911 private isLoading() {
912 return (
913 !this.pluginsLoaded ||
914 !this.change ||
Ben Rohlfs2add82f2024-02-26 19:23:01 +0100915 this.mergeable === undefined ||
Ben Rohlfs4af8c1d2024-02-29 11:41:35 +0100916 this.revisionActions === undefined
Ben Rohlfs5550f452024-02-26 19:12:11 +0100917 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100918 }
Viktar Donich935b4e92018-03-26 13:13:00 -0700919
paladox32b861a2022-04-25 16:45:06 +0100920 // private but used in test
Ben Rohlfs4e913532022-10-24 12:31:58 +0200921 sendShowRevisionActions(detail: ShowRevisionActionsDetail) {
Chris Poucete3d66862022-10-26 11:19:50 +0200922 this.getPluginLoader().jsApiService.handleShowRevisionActions(detail);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100923 }
Tao Zhou2943bec2020-03-09 09:29:35 +0100924
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200925 addActionButton(type: ActionType, label: string) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100926 if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
927 throw Error(`Invalid action type: ${type}`);
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100928 }
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200929 const action: UIActionInfo = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100930 enabled: true,
931 label,
932 __type: type,
Ben Rohlfsba440822023-04-11 18:08:03 +0200933 __key: ADDITIONAL_ACTION_KEY_PREFIX + uuid(),
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100934 };
paladox32b861a2022-04-25 16:45:06 +0100935 this.additionalActions.push(action);
936 this.requestUpdate('additionalActions');
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100937 return action.__key;
938 }
Andrew Bonventrecd73e962016-06-22 17:52:56 -0400939
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200940 removeActionButton(key: string) {
paladox32b861a2022-04-25 16:45:06 +0100941 const idx = this.indexOfActionButtonWithKey(key);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100942 if (idx === -1) {
943 return;
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100944 }
paladox32b861a2022-04-25 16:45:06 +0100945 this.additionalActions.splice(idx, 1);
946 this.requestUpdate('additionalActions');
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100947 }
Andrew Bonventrecd73e962016-06-22 17:52:56 -0400948
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200949 setActionButtonProp<T extends keyof UIActionInfo>(
950 key: string,
951 prop: T,
952 value: UIActionInfo[T]
953 ) {
paladox32b861a2022-04-25 16:45:06 +0100954 this.additionalActions[this.indexOfActionButtonWithKey(key)][prop] = value;
955 this.requestUpdate('additionalActions');
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100956 }
Andrew Bonventrea6eb9432016-06-28 16:37:41 -0400957
Ben Rohlfs73294302024-02-26 17:07:40 +0100958 // TODO: Rename to toggleOverflow().
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200959 setActionOverflow(type: ActionType, key: string, overflow: boolean) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100960 if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
961 throw Error(`Invalid action type given: ${type}`);
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100962 }
Ben Rohlfs73294302024-02-26 17:07:40 +0100963 const isCurrentlyOverflow = this.isOverflowAction(type, key);
964 if (overflow === isCurrentlyOverflow) {
965 return;
966 }
967
968 // remove from overflowActions
969 if (!overflow) {
970 this.overflowActions = this.overflowActions.filter(
971 action => action.type !== type || action.key !== key
972 );
973 }
974 // add to overflowActions
975 if (overflow) {
976 this.overflowActions = [...this.overflowActions, {type, key}];
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100977 }
978 }
979
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200980 setActionPriority(
981 type: ActionType.CHANGE | ActionType.REVISION,
982 key: string,
983 priority: ActionPriority
984 ) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100985 if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
986 throw Error(`Invalid action type given: ${type}`);
987 }
paladox32b861a2022-04-25 16:45:06 +0100988 const index = this.actionPriorityOverrides.findIndex(
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +0200989 action => action.type === type && action.key === key
990 );
991 const action: ActionPriorityOverride = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100992 type,
993 key,
994 priority,
995 };
996 if (index !== -1) {
paladox32b861a2022-04-25 16:45:06 +0100997 this.actionPriorityOverrides[index] = action;
998 this.requestUpdate('actionPriorityOverrides');
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100999 } else {
paladox32b861a2022-04-25 16:45:06 +01001000 this.actionPriorityOverrides.push(action);
1001 this.requestUpdate('actionPriorityOverrides');
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001002 }
1003 }
1004
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001005 setActionHidden(
1006 type: ActionType.CHANGE | ActionType.REVISION,
1007 key: string,
1008 hidden: boolean
1009 ) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001010 if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
1011 throw Error(`Invalid action type given: ${type}`);
1012 }
1013
paladox32b861a2022-04-25 16:45:06 +01001014 const idx = this.hiddenActions.indexOf(key);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001015 if (hidden && idx === -1) {
paladox32b861a2022-04-25 16:45:06 +01001016 this.hiddenActions.push(key);
1017 this.requestUpdate('hiddenActions');
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001018 } else if (!hidden && idx !== -1) {
paladox32b861a2022-04-25 16:45:06 +01001019 this.hiddenActions.splice(idx, 1);
1020 this.requestUpdate('hiddenActions');
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001021 }
1022 }
1023
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001024 getActionDetails(actionName: string) {
Ben Rohlfs4af8c1d2024-02-29 11:41:35 +01001025 if (this.revisionActions?.[actionName]) {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001026 return this.revisionActions[actionName];
Ben Rohlfs4af8c1d2024-02-29 11:41:35 +01001027 } else if (this.actions?.[actionName]) {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001028 return this.actions[actionName];
1029 } else {
1030 return undefined;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001031 }
1032 }
1033
paladox32b861a2022-04-25 16:45:06 +01001034 private indexOfActionButtonWithKey(key: string) {
1035 for (let i = 0; i < this.additionalActions.length; i++) {
1036 if (this.additionalActions[i].__key === key) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001037 return i;
1038 }
1039 }
1040 return -1;
1041 }
1042
Chris Poucet9369a9f2022-05-09 17:15:44 +02001043 private actionsChanged() {
paladox32b861a2022-04-25 16:45:06 +01001044 this.actionLoadingMessage = '';
1045 this.disabledMenuActions = [];
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001046
Ben Rohlfs4af8c1d2024-02-29 11:41:35 +01001047 if (this.revisionActions && !this.revisionActions.download) {
Ben Rohlfs2eb8d522023-08-21 14:59:15 +02001048 this.revisionActions = {
1049 ...this.revisionActions,
1050 download: DOWNLOAD_ACTION,
1051 };
1052 fire(this, 'revision-actions-changed', {
1053 value: this.revisionActions,
1054 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001055 }
paladox32b861a2022-04-25 16:45:06 +01001056 if (
Chris Poucet9369a9f2022-05-09 17:15:44 +02001057 !this.actions.includedIn &&
paladox32b861a2022-04-25 16:45:06 +01001058 this.change?.status === ChangeStatus.MERGED
1059 ) {
1060 this.actions = {...this.actions, includedIn: INCLUDED_IN_ACTION};
Dhruv Srivastava4dcb5432021-04-26 16:45:07 +02001061 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001062 }
1063
Chris Poucet9369a9f2022-05-09 17:15:44 +02001064 private editStatusChanged() {
Ben Rohlfsf56f71c2023-04-27 17:20:43 +02001065 if (!this.change || !this.loggedIn) return;
Chris Poucet9369a9f2022-05-09 17:15:44 +02001066 if (this.editPatchsetLoaded) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001067 // Only show actions that mutate an edit if an actual edit patch set
1068 // is loaded.
Chris Poucet9369a9f2022-05-09 17:15:44 +02001069 if (changeIsOpen(this.change)) {
1070 if (this.editBasedOnCurrentPatchSet) {
1071 if (!this.actions.publishEdit) {
paladox32b861a2022-04-25 16:45:06 +01001072 this.actions = {...this.actions, publishEdit: PUBLISH_EDIT};
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001073 }
Chris Poucet9369a9f2022-05-09 17:15:44 +02001074 delete this.actions.rebaseEdit;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001075 } else {
Chris Poucet9369a9f2022-05-09 17:15:44 +02001076 if (!this.actions.rebaseEdit) {
paladox32b861a2022-04-25 16:45:06 +01001077 this.actions = {...this.actions, rebaseEdit: REBASE_EDIT};
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001078 }
Chris Poucet9369a9f2022-05-09 17:15:44 +02001079 delete this.actions.publishEdit;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001080 }
1081 }
Chris Poucet9369a9f2022-05-09 17:15:44 +02001082 if (!this.actions.deleteEdit) {
paladox32b861a2022-04-25 16:45:06 +01001083 this.actions = {...this.actions, deleteEdit: DELETE_EDIT};
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001084 }
1085 } else {
Chris Poucet9369a9f2022-05-09 17:15:44 +02001086 delete this.actions.rebaseEdit;
1087 delete this.actions.publishEdit;
1088 delete this.actions.deleteEdit;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001089 }
1090
Chris Poucet9369a9f2022-05-09 17:15:44 +02001091 if (changeIsOpen(this.change)) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001092 // Only show edit button if there is no edit patchset loaded and the
1093 // file list is not in edit mode.
Chris Poucet9369a9f2022-05-09 17:15:44 +02001094 if (this.editPatchsetLoaded || this.editMode) {
1095 delete this.actions.edit;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001096 } else {
Chris Poucet9369a9f2022-05-09 17:15:44 +02001097 if (!this.actions.edit) {
paladox32b861a2022-04-25 16:45:06 +01001098 this.actions = {...this.actions, edit: EDIT};
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001099 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001100 }
1101 // Only show STOP_EDIT if edit mode is enabled, but no edit patch set
1102 // is loaded.
Chris Poucet9369a9f2022-05-09 17:15:44 +02001103 if (this.editMode && !this.editPatchsetLoaded) {
1104 if (!this.actions.stopEdit) {
paladox32b861a2022-04-25 16:45:06 +01001105 this.actions = {...this.actions, stopEdit: STOP_EDIT};
Milutin Kristoficf0b380e2021-01-26 11:55:12 +01001106 fireAlert(this, 'Change is in edit mode');
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001107 }
1108 } else {
Chris Poucet9369a9f2022-05-09 17:15:44 +02001109 delete this.actions.stopEdit;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001110 }
1111 } else {
1112 // Remove edit button.
Chris Poucet9369a9f2022-05-09 17:15:44 +02001113 delete this.actions.edit;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001114 }
paladox32b861a2022-04-25 16:45:06 +01001115 }
1116
1117 private getValuesFor<T>(obj: {[key: string]: T}): T[] {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001118 return Object.keys(obj).map(key => obj[key]);
1119 }
1120
paladox32b861a2022-04-25 16:45:06 +01001121 private getLabelStatus(label: LabelInfo): LabelStatus {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001122 if (isQuickLabelInfo(label)) {
1123 if (label.approved) {
1124 return LabelStatus.OK;
1125 } else if (label.rejected) {
1126 return LabelStatus.REJECT;
1127 }
1128 }
1129 if (label.optional) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001130 return LabelStatus.OPTIONAL;
1131 } else {
1132 return LabelStatus.NEED;
1133 }
1134 }
1135
1136 /**
1137 * Get highest score for last missing permitted label for current change.
1138 * Returns null if no labels permitted or more than one label missing.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001139 */
paladox32b861a2022-04-25 16:45:06 +01001140 private getTopMissingApproval() {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001141 if (!this.change || !this.change.labels || !this.change.permitted_labels) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001142 return null;
1143 }
Ben Rohlfsf56f71c2023-04-27 17:20:43 +02001144 if (this.change?.status === ChangeStatus.MERGED) {
Frank Borden99c15d72020-12-10 17:30:29 -08001145 return null;
1146 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001147 let result;
Ben Rohlfs7b71b112021-02-12 10:36:08 +01001148 for (const [label, labelInfo] of Object.entries(this.change.labels)) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001149 if (!(label in this.change.permitted_labels)) {
1150 continue;
1151 }
1152 if (this.change.permitted_labels[label].length === 0) {
1153 continue;
1154 }
paladox32b861a2022-04-25 16:45:06 +01001155 const status = this.getLabelStatus(labelInfo);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001156 if (status === LabelStatus.NEED) {
1157 if (result) {
Dhruv Srivastava25ca8ea2021-03-31 21:37:30 +02001158 // More than one label is missing, so check if Code Review can be
1159 // given
1160 result = null;
1161 break;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001162 }
1163 result = label;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001164 } else if (
1165 status === LabelStatus.REJECT ||
1166 status === LabelStatus.IMPOSSIBLE
1167 ) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001168 return null;
1169 }
1170 }
Frank Bordena42cf772020-11-23 21:10:09 -08001171 // Allow the user to use quick approve to vote the max score on code review
Frank Borden481e5f92020-11-30 19:59:09 -08001172 // even if it is already granted by someone else. Does not apply if the
1173 // user owns the change or has already granted the max score themselves.
Milutin Kristoficbd445802021-10-20 21:00:21 +02001174 const codeReviewLabel = this.change.labels[StandardLabels.CODE_REVIEW];
1175 const codeReviewPermittedValues =
1176 this.change.permitted_labels[StandardLabels.CODE_REVIEW];
Frank Bordena42cf772020-11-23 21:10:09 -08001177 if (
1178 !result &&
Frank Borden481e5f92020-11-30 19:59:09 -08001179 codeReviewLabel &&
1180 codeReviewPermittedValues &&
1181 this.account?._account_id &&
1182 isDetailedLabelInfo(codeReviewLabel) &&
Frank Borden481e5f92020-11-30 19:59:09 -08001183 !isOwner(this.change, this.account) &&
1184 getApprovalInfo(codeReviewLabel, this.account)?.value !==
1185 getVotingRange(codeReviewLabel)?.max
Frank Bordena42cf772020-11-23 21:10:09 -08001186 ) {
Milutin Kristoficbd445802021-10-20 21:00:21 +02001187 result = StandardLabels.CODE_REVIEW;
Frank Bordena42cf772020-11-23 21:10:09 -08001188 }
1189
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001190 if (result) {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001191 const labelInfo = this.change.labels[result];
1192 if (!isDetailedLabelInfo(labelInfo)) {
1193 return null;
1194 }
Frank Borden481e5f92020-11-30 19:59:09 -08001195 const permittedValues = this.change.permitted_labels[result];
1196 const usersMaxPermittedScore =
1197 permittedValues[permittedValues.length - 1];
1198 const maxScoreForLabel = getVotingRange(labelInfo)?.max;
1199 if (Number(usersMaxPermittedScore) === maxScoreForLabel) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001200 // Allow quick approve only for maximal score.
1201 return {
1202 label: result,
Frank Borden481e5f92020-11-30 19:59:09 -08001203 score: usersMaxPermittedScore,
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001204 };
1205 }
1206 }
1207 return null;
1208 }
1209
1210 hideQuickApproveAction() {
paladox32b861a2022-04-25 16:45:06 +01001211 if (!this.topLevelSecondaryActions) {
1212 throw new Error('topLevelSecondaryActions must be set');
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001213 }
paladox32b861a2022-04-25 16:45:06 +01001214 this.topLevelSecondaryActions = this.topLevelSecondaryActions.filter(
Dhruv Srivastava25ca8ea2021-03-31 21:37:30 +02001215 sa => !isQuickApproveAction(sa)
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001216 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001217 this._hideQuickApproveAction = true;
1218 }
1219
paladox32b861a2022-04-25 16:45:06 +01001220 private getQuickApproveAction(): QuickApproveUIActionInfo | null {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001221 if (this._hideQuickApproveAction) {
1222 return null;
1223 }
paladox32b861a2022-04-25 16:45:06 +01001224 const approval = this.getTopMissingApproval();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001225 if (!approval) {
1226 return null;
1227 }
Tao Zhou4cd35cb2020-07-22 11:28:22 +02001228 const action = {...QUICK_APPROVE_ACTION};
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001229 action.label = approval.label + approval.score;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001230
1231 const score = Number(approval.score);
1232 if (isNaN(score)) {
1233 return null;
1234 }
1235
1236 const review: ReviewInput = {
1237 drafts: DraftsAction.PUBLISH_ALL_REVISIONS,
1238 labels: {
1239 [approval.label]: score,
1240 },
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001241 };
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001242 action.payload = review;
1243 return action;
1244 }
1245
paladox32b861a2022-04-25 16:45:06 +01001246 private getActionValues(
Ben Rohlfs4af8c1d2024-02-29 11:41:35 +01001247 actionsChange: ActionNameToActionInfoMap | undefined,
paladox32b861a2022-04-25 16:45:06 +01001248 primariesChange: PrimaryActionKey[],
1249 additionalActionsChange: UIActionInfo[],
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001250 type: ActionType
1251 ): UIActionInfo[] {
paladox32b861a2022-04-25 16:45:06 +01001252 if (!actionsChange || !primariesChange) {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001253 return [];
1254 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001255
paladox32b861a2022-04-25 16:45:06 +01001256 const actions = actionsChange;
1257 const primaryActionKeys = primariesChange;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001258 const result: UIActionInfo[] = [];
1259 const values: Array<ChangeActions | RevisionActions> =
1260 type === ActionType.CHANGE
paladox32b861a2022-04-25 16:45:06 +01001261 ? this.getValuesFor(ChangeActions)
1262 : this.getValuesFor(RevisionActions);
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001263
1264 const pluginActions: UIActionInfo[] = [];
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001265 Object.keys(actions).forEach(a => {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001266 const action: UIActionInfo = actions[a] as UIActionInfo;
1267 action.__key = a;
1268 action.__type = type;
1269 action.__primary = primaryActionKeys.includes(a as PrimaryActionKey);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001270 // Plugin actions always contain ~ in the key.
1271 if (a.indexOf('~') !== -1) {
paladox32b861a2022-04-25 16:45:06 +01001272 this.populateActionUrl(action);
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001273 pluginActions.push(action);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001274 // Add server-side provided plugin actions to overflow menu.
paladox32b861a2022-04-25 16:45:06 +01001275 this.overflowActions.push({
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001276 type,
1277 key: a,
1278 });
paladox32b861a2022-04-25 16:45:06 +01001279 this.requestUpdate('overflowActions');
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001280 return;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001281 } else if (!values.includes(a as PrimaryActionKey)) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001282 return;
1283 }
paladox32b861a2022-04-25 16:45:06 +01001284 action.label = this.getActionLabel(action);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001285
1286 // Triggers a re-render by ensuring object inequality.
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001287 result.push({...action});
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001288 });
1289
paladox32b861a2022-04-25 16:45:06 +01001290 let additionalActions = additionalActionsChange;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001291 additionalActions = additionalActions
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001292 .filter(a => a.__type === type)
1293 .map(a => {
1294 a.__primary = primaryActionKeys.includes(a.__key as PrimaryActionKey);
1295 // Triggers a re-render by ensuring object inequality.
1296 return {...a};
1297 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001298 return result.concat(additionalActions).concat(pluginActions);
1299 }
1300
paladox32b861a2022-04-25 16:45:06 +01001301 private populateActionUrl(action: UIActionInfo) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001302 const patchNum =
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001303 action.__type === ActionType.REVISION ? this.latestPatchNum : undefined;
1304 if (!this.changeNum) {
1305 return;
1306 }
Ben Rohlfs43935a42020-12-01 19:14:09 +01001307 this.restApiService
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001308 .getChangeActionURL(this.changeNum, patchNum, '/' + action.__key)
1309 .then(url => (action.__url = url));
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001310 }
1311
1312 /**
1313 * Given a change action, return a display label that uses the appropriate
1314 * casing or includes explanatory details.
1315 */
paladox32b861a2022-04-25 16:45:06 +01001316 private getActionLabel(action: UIActionInfo) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001317 if (action.label === 'Delete') {
1318 // This label is common within change and revision actions. Make it more
1319 // explicit to the user.
1320 return 'Delete change';
1321 } else if (action.label === 'WIP') {
1322 return 'Mark as work in progress';
1323 }
1324 // Otherwise, just map the name to sentence case.
paladox32b861a2022-04-25 16:45:06 +01001325 return this.toSentenceCase(action.label);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001326 }
1327
1328 /**
Frank Borden5bf43b62022-06-24 16:29:49 +02001329 * Capitalize the first letter and lowercase all others.
paladox32b861a2022-04-25 16:45:06 +01001330 *
1331 * private but used in test
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001332 */
paladox32b861a2022-04-25 16:45:06 +01001333 toSentenceCase(s: string) {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001334 if (!s.length) {
1335 return '';
1336 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001337 return s[0].toUpperCase() + s.slice(1).toLowerCase();
1338 }
1339
paladox32b861a2022-04-25 16:45:06 +01001340 private computeLoadingLabel(action: string) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001341 return ActionLoadingLabels[action] || 'Working...';
1342 }
1343
paladox32b861a2022-04-25 16:45:06 +01001344 // private but used in test
1345 canSubmitChange() {
Ben Rohlfsf56f71c2023-04-27 17:20:43 +02001346 if (!this.change) return false;
1347 const change = this.change as ChangeInfo;
1348 const revision = this.getRevision(change, this.latestPatchNum);
Chris Poucete3d66862022-10-26 11:19:50 +02001349 return this.getPluginLoader().jsApiService.canSubmitChange(
Ben Rohlfsf56f71c2023-04-27 17:20:43 +02001350 change,
1351 revision
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001352 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001353 }
1354
paladox32b861a2022-04-25 16:45:06 +01001355 // private but used in test
Ben Rohlfsf56f71c2023-04-27 17:20:43 +02001356 getRevision(change: ChangeInfo, patchNum?: PatchSetNumber) {
1357 for (const rev of Object.values(change.revisions ?? {})) {
Dhruv Srivastavad1da4582021-01-11 16:34:19 +01001358 if (rev._number === patchNum) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001359 return rev;
1360 }
1361 }
1362 return null;
1363 }
1364
1365 showRevertDialog() {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001366 const change = this.change;
1367 if (!change) return;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001368 const query = `submissionid: "${change.submission_id}"`;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001369 /* A chromium plugin expects that the modifyRevertMsg hook will only
1370 be called after the revert button is pressed, hence we populate the
1371 revert dialog after revert button is pressed. */
Ben Rohlfs43935a42020-12-01 19:14:09 +01001372 this.restApiService.getChanges(0, query).then(changes => {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001373 if (!changes) {
Kamil Musinf0ece022022-10-14 15:22:44 +02001374 this.reporting.error(
1375 'Change Actions',
1376 new Error('getChanges returns undefined')
1377 );
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001378 return;
1379 }
paladox32b861a2022-04-25 16:45:06 +01001380 assertIsDefined(this.confirmRevertDialog, 'confirmRevertDialog');
Kamil Musin808b4f22022-12-08 17:43:11 +01001381 this.confirmRevertDialog.populate(
1382 change,
1383 this.commitMessage,
1384 changes.length
1385 );
paladox32b861a2022-04-25 16:45:06 +01001386 this.showActionDialog(this.confirmRevertDialog);
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001387 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001388 }
1389
Joerg Zieren79b448c2021-08-12 16:36:37 +02001390 showSubmitDialog() {
paladox32b861a2022-04-25 16:45:06 +01001391 if (!this.canSubmitChange()) {
Joerg Zieren79b448c2021-08-12 16:36:37 +02001392 return;
1393 }
paladox32b861a2022-04-25 16:45:06 +01001394 assertIsDefined(this.confirmSubmitDialog, 'confirmSubmitDialog');
1395 this.showActionDialog(this.confirmSubmitDialog);
Joerg Zieren79b448c2021-08-12 16:36:37 +02001396 }
1397
paladox32b861a2022-04-25 16:45:06 +01001398 private handleActionTap(e: MouseEvent, key: string, type: string) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001399 e.preventDefault();
paladox32b861a2022-04-25 16:45:06 +01001400 let el = e.target as Element;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001401 while (el.tagName.toLowerCase() !== 'gr-button') {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001402 if (!el.parentElement) {
1403 return;
1404 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001405 el = el.parentElement;
1406 }
1407
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001408 if (
1409 key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) ||
1410 key.indexOf('~') !== -1
1411 ) {
1412 this.dispatchEvent(
1413 new CustomEvent(`${key}-tap`, {
1414 detail: {node: el},
1415 composed: true,
1416 bubbles: true,
1417 })
1418 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001419 return;
1420 }
paladox32b861a2022-04-25 16:45:06 +01001421 this.handleAction(type as ActionType, key);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001422 }
1423
paladox32b861a2022-04-25 16:45:06 +01001424 private handleOverflowItemTap(e: CustomEvent<MenuAction>) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001425 e.preventDefault();
Paladox none999b73a92022-06-25 16:04:43 +00001426 const el = e.target as Element;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001427 const key = e.detail.action.__key;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001428 if (
1429 key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) ||
1430 key.indexOf('~') !== -1
1431 ) {
1432 this.dispatchEvent(
1433 new CustomEvent(`${key}-tap`, {
1434 detail: {node: el},
1435 composed: true,
1436 bubbles: true,
1437 })
1438 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001439 return;
1440 }
paladox32b861a2022-04-25 16:45:06 +01001441 this.handleAction(e.detail.action.__type, e.detail.action.__key);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001442 }
1443
paladox32b861a2022-04-25 16:45:06 +01001444 // private but used in test
1445 handleAction(type: ActionType, key: string) {
Milutin Kristoficda88b332020-03-24 10:19:12 +01001446 this.reporting.reportInteraction(`${type}-${key}`);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001447 switch (type) {
1448 case ActionType.REVISION:
paladox32b861a2022-04-25 16:45:06 +01001449 this.handleRevisionAction(key);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001450 break;
1451 case ActionType.CHANGE:
paladox32b861a2022-04-25 16:45:06 +01001452 this.handleChangeAction(key);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001453 break;
1454 default:
paladox32b861a2022-04-25 16:45:06 +01001455 this.fireAction(
1456 this.prependSlash(key),
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001457 assertUIActionInfo(this.actions[key]),
1458 false
1459 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001460 }
1461 }
1462
paladox32b861a2022-04-25 16:45:06 +01001463 // private but used in test
1464 handleChangeAction(key: string) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001465 switch (key) {
1466 case ChangeActions.REVERT:
1467 this.showRevertDialog();
1468 break;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001469 case ChangeActions.ABANDON:
paladox32b861a2022-04-25 16:45:06 +01001470 assertIsDefined(this.confirmAbandonDialog, 'confirmAbandonDialog');
1471 this.showActionDialog(this.confirmAbandonDialog);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001472 break;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001473 case QUICK_APPROVE_ACTION.key: {
paladox32b861a2022-04-25 16:45:06 +01001474 const action = this.allActionValues.find(isQuickApproveAction);
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001475 if (!action) {
1476 return;
1477 }
paladox32b861a2022-04-25 16:45:06 +01001478 this.fireAction(this.prependSlash(key), action, true, action.payload);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001479 break;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001480 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001481 case ChangeActions.EDIT:
paladox32b861a2022-04-25 16:45:06 +01001482 this.handleEditTap();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001483 break;
1484 case ChangeActions.STOP_EDIT:
paladox32b861a2022-04-25 16:45:06 +01001485 this.handleStopEditTap();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001486 break;
1487 case ChangeActions.DELETE:
paladox32b861a2022-04-25 16:45:06 +01001488 this.handleDeleteTap();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001489 break;
1490 case ChangeActions.DELETE_EDIT:
paladox32b861a2022-04-25 16:45:06 +01001491 this.handleDeleteEditTap();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001492 break;
1493 case ChangeActions.FOLLOW_UP:
paladox32b861a2022-04-25 16:45:06 +01001494 this.handleFollowUpTap();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001495 break;
1496 case ChangeActions.WIP:
paladox32b861a2022-04-25 16:45:06 +01001497 this.handleWipTap();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001498 break;
1499 case ChangeActions.MOVE:
paladox32b861a2022-04-25 16:45:06 +01001500 this.handleMoveTap();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001501 break;
1502 case ChangeActions.PUBLISH_EDIT:
paladox32b861a2022-04-25 16:45:06 +01001503 this.handlePublishEditTap();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001504 break;
1505 case ChangeActions.REBASE_EDIT:
paladox32b861a2022-04-25 16:45:06 +01001506 this.handleRebaseEditTap();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001507 break;
Dhruv Srivastava4dcb5432021-04-26 16:45:07 +02001508 case ChangeActions.INCLUDED_IN:
paladox32b861a2022-04-25 16:45:06 +01001509 this.handleIncludedInTap();
Dhruv Srivastava4dcb5432021-04-26 16:45:07 +02001510 break;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001511 default:
paladox32b861a2022-04-25 16:45:06 +01001512 this.fireAction(
1513 this.prependSlash(key),
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001514 assertUIActionInfo(this.actions[key]),
1515 false
1516 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001517 }
1518 }
1519
paladox32b861a2022-04-25 16:45:06 +01001520 private handleRevisionAction(key: string) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001521 switch (key) {
1522 case RevisionActions.REBASE:
paladox32b861a2022-04-25 16:45:06 +01001523 assertIsDefined(this.confirmRebase, 'confirmRebase');
1524 this.showActionDialog(this.confirmRebase);
1525 this.confirmRebase.fetchRecentChanges();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001526 break;
1527 case RevisionActions.CHERRYPICK:
paladox32b861a2022-04-25 16:45:06 +01001528 this.handleCherrypickTap();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001529 break;
1530 case RevisionActions.DOWNLOAD:
paladox32b861a2022-04-25 16:45:06 +01001531 this.handleDownloadTap();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001532 break;
1533 case RevisionActions.SUBMIT:
paladox32b861a2022-04-25 16:45:06 +01001534 if (!this.canSubmitChange()) {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001535 return;
1536 }
paladox32b861a2022-04-25 16:45:06 +01001537 assertIsDefined(this.confirmSubmitDialog, 'confirmSubmitDialog');
1538 this.showActionDialog(this.confirmSubmitDialog);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001539 break;
1540 default:
paladox32b861a2022-04-25 16:45:06 +01001541 this.fireAction(
1542 this.prependSlash(key),
Ben Rohlfs4af8c1d2024-02-29 11:41:35 +01001543 assertUIActionInfo(this.revisionActions?.[key]),
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001544 true
1545 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001546 }
1547 }
1548
paladox32b861a2022-04-25 16:45:06 +01001549 private prependSlash(key: string) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001550 return key === '/' ? key : `/${key}`;
1551 }
1552
Ben Rohlfs42500872023-02-27 19:16:38 +01001553 private calculateDisabled(action: UIActionInfo) {
1554 // TODO(b/270972983): Remove this special casing once the backend is more
1555 // aggressive about setting`enabled:true`.
1556 if (action.__key === 'rebase') return false;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001557 return !action.enabled;
1558 }
1559
paladox32b861a2022-04-25 16:45:06 +01001560 private handleConfirmDialogCancel() {
1561 this.hideAllDialogs();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001562 }
1563
paladox32b861a2022-04-25 16:45:06 +01001564 private hideAllDialogs() {
1565 assertIsDefined(this.confirmSubmitDialog, 'confirmSubmitDialog');
1566 const dialogEls = queryAll(this, '.confirmDialog');
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001567 for (const dialogEl of dialogEls) {
1568 (dialogEl as HTMLElement).hidden = true;
1569 }
Dhruv Srivastava38e0b162022-10-24 14:22:17 +02001570 assertIsDefined(this.actionsModal, 'actionsModal');
1571 this.actionsModal.close();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001572 }
1573
paladox32b861a2022-04-25 16:45:06 +01001574 // private but used in test
1575 handleRebaseConfirm(e: CustomEvent<ConfirmRebaseEventDetail>) {
1576 assertIsDefined(this.confirmRebase, 'confirmRebase');
Dhruv Srivastava38e0b162022-10-24 14:22:17 +02001577 assertIsDefined(this.actionsModal, 'actionsModal');
Milutin Kristofic842c3132022-08-25 21:00:57 +02001578 const payload = {
1579 base: e.detail.base,
1580 allow_conflicts: e.detail.allowConflicts,
Milutin Kristofic0ff6b2c2023-03-01 15:02:07 +01001581 on_behalf_of_uploader: e.detail.onBehalfOfUploader,
Kaushik Lingarkar2c68bf72023-06-06 15:20:01 -07001582 committer_email: e.detail.committerEmail,
Milutin Kristofic842c3132022-08-25 21:00:57 +02001583 };
Milutin Kristofic12252142023-01-11 15:13:45 +01001584 const rebaseChain = !!e.detail.rebaseChain;
paladox32b861a2022-04-25 16:45:06 +01001585 this.fireAction(
Milutin Kristofic12252142023-01-11 15:13:45 +01001586 rebaseChain ? '/rebase:chain' : '/rebase',
Ben Rohlfs4af8c1d2024-02-29 11:41:35 +01001587 assertUIActionInfo(this.revisionActions?.rebase),
Milutin Kristofic12252142023-01-11 15:13:45 +01001588 rebaseChain ? false : true,
Milutin Kristofic174c3432022-09-16 12:33:28 +02001589 payload,
Milutin Kristofic0ff6b2c2023-03-01 15:02:07 +01001590 {
1591 allow_conflicts: payload.allow_conflicts,
1592 on_behalf_of_uploader: payload.on_behalf_of_uploader,
1593 }
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001594 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001595 }
1596
paladox32b861a2022-04-25 16:45:06 +01001597 // private but used in test
1598 handleCherrypickConfirm() {
1599 this.handleCherryPickRestApi(false);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001600 }
1601
paladox32b861a2022-04-25 16:45:06 +01001602 // private but used in test
1603 handleCherrypickConflictConfirm() {
1604 this.handleCherryPickRestApi(true);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001605 }
1606
paladox32b861a2022-04-25 16:45:06 +01001607 private handleCherryPickRestApi(conflicts: boolean) {
1608 assertIsDefined(this.confirmCherrypick, 'confirmCherrypick');
Dhruv Srivastava38e0b162022-10-24 14:22:17 +02001609 assertIsDefined(this.actionsModal, 'actionsModal');
paladox32b861a2022-04-25 16:45:06 +01001610 const el = this.confirmCherrypick;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001611 if (!el.branch) {
Milutin Kristofic860fe4d2020-11-23 16:13:45 +01001612 fireAlert(this, ERR_BRANCH_EMPTY);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001613 return;
1614 }
1615 if (!el.message) {
Milutin Kristofic860fe4d2020-11-23 16:13:45 +01001616 fireAlert(this, ERR_COMMIT_EMPTY);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001617 return;
1618 }
Dhruv Srivastava38e0b162022-10-24 14:22:17 +02001619 this.actionsModal.close();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001620 el.hidden = true;
paladox32b861a2022-04-25 16:45:06 +01001621 this.fireAction(
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001622 '/cherrypick',
Ben Rohlfs4af8c1d2024-02-29 11:41:35 +01001623 assertUIActionInfo(this.revisionActions?.cherrypick),
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001624 true,
1625 {
1626 destination: el.branch,
1627 base: el.baseCommit ? el.baseCommit : null,
1628 message: el.message,
1629 allow_conflicts: conflicts,
Yash Chaturvedi13bf88e2023-09-28 08:48:32 +00001630 committer_email: el.committerEmail ? el.committerEmail : null,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001631 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001632 );
1633 }
1634
paladox32b861a2022-04-25 16:45:06 +01001635 // private but used in test
1636 handleMoveConfirm() {
1637 assertIsDefined(this.confirmMove, 'confirmMove');
Dhruv Srivastava38e0b162022-10-24 14:22:17 +02001638 assertIsDefined(this.actionsModal, 'actionsModal');
paladox32b861a2022-04-25 16:45:06 +01001639 const el = this.confirmMove;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001640 if (!el.branch) {
Milutin Kristofic860fe4d2020-11-23 16:13:45 +01001641 fireAlert(this, ERR_BRANCH_EMPTY);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001642 return;
1643 }
Dhruv Srivastava38e0b162022-10-24 14:22:17 +02001644 this.actionsModal.close();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001645 el.hidden = true;
paladox32b861a2022-04-25 16:45:06 +01001646 this.fireAction('/move', assertUIActionInfo(this.actions.move), false, {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001647 destination_branch: el.branch,
1648 message: el.message,
1649 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001650 }
1651
paladox32b861a2022-04-25 16:45:06 +01001652 private handleRevertDialogConfirm(e: CustomEvent<ConfirmRevertEventDetail>) {
1653 assertIsDefined(this.confirmRevertDialog, 'confirmRevertDialog');
Dhruv Srivastava38e0b162022-10-24 14:22:17 +02001654 assertIsDefined(this.actionsModal, 'actionsModal');
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001655 const revertType = e.detail.revertType;
1656 const message = e.detail.message;
paladox32b861a2022-04-25 16:45:06 +01001657 const el = this.confirmRevertDialog;
Dhruv Srivastava38e0b162022-10-24 14:22:17 +02001658 this.actionsModal.close();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001659 el.hidden = true;
1660 switch (revertType) {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001661 case RevertType.REVERT_SINGLE_CHANGE:
paladox32b861a2022-04-25 16:45:06 +01001662 this.fireAction(
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001663 '/revert',
1664 assertUIActionInfo(this.actions.revert),
1665 false,
Frank Bordenbd998112022-11-22 14:48:52 +00001666 {message}
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001667 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001668 break;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001669 case RevertType.REVERT_SUBMISSION:
Dhruv Srivastavae4de13b2021-05-14 10:17:52 +02001670 // TODO(dhruvsri): replace with this.actions.revert_submission once
1671 // BE starts sending it again
paladox32b861a2022-04-25 16:45:06 +01001672 this.fireAction(
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001673 '/revert_submission',
Dhruv Srivastavae4de13b2021-05-14 10:17:52 +02001674 {__key: 'revert_submission', method: HttpMethod.POST} as UIActionInfo,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001675 false,
Frank Bordenbd998112022-11-22 14:48:52 +00001676 {message}
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001677 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001678 break;
1679 default:
Kamil Musinf0ece022022-10-14 15:22:44 +02001680 this.reporting.error(
1681 'Change Actions',
1682 new Error('invalid revert type')
1683 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001684 }
1685 }
1686
paladox32b861a2022-04-25 16:45:06 +01001687 // private but used in test
1688 handleAbandonDialogConfirm() {
1689 assertIsDefined(this.confirmAbandonDialog, 'confirmAbandonDialog');
Dhruv Srivastava38e0b162022-10-24 14:22:17 +02001690 assertIsDefined(this.actionsModal, 'actionsModal');
paladox32b861a2022-04-25 16:45:06 +01001691 const el = this.confirmAbandonDialog;
Dhruv Srivastava38e0b162022-10-24 14:22:17 +02001692 this.actionsModal.close();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001693 el.hidden = true;
paladox32b861a2022-04-25 16:45:06 +01001694 this.fireAction(
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001695 '/abandon',
1696 assertUIActionInfo(this.actions.abandon),
1697 false,
1698 {
1699 message: el.message,
1700 }
1701 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001702 }
1703
paladox32b861a2022-04-25 16:45:06 +01001704 private handleCreateFollowUpChange() {
1705 assertIsDefined(this.createFollowUpChange, 'createFollowUpChange');
1706 this.createFollowUpChange.handleCreateChange();
1707 this.handleCloseCreateFollowUpChange();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001708 }
1709
paladox32b861a2022-04-25 16:45:06 +01001710 private handleCloseCreateFollowUpChange() {
Dhruv Srivastava38e0b162022-10-24 14:22:17 +02001711 assertIsDefined(this.actionsModal, 'actionsModal');
1712 this.actionsModal.close();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001713 }
1714
paladox32b861a2022-04-25 16:45:06 +01001715 private handleDeleteConfirm() {
1716 this.hideAllDialogs();
1717 this.fireAction(
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001718 '/',
1719 assertUIActionInfo(this.actions[ChangeActions.DELETE]),
1720 false
1721 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001722 }
1723
paladox32b861a2022-04-25 16:45:06 +01001724 private handleDeleteEditConfirm() {
1725 this.hideAllDialogs();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001726
paladoxa80ea7c2022-02-19 17:42:15 +00001727 // We need to make sure that all cached version of a change
1728 // edit are deleted.
Chris Poucet20f09582022-10-24 23:29:27 +02001729 this.getStorage().eraseEditableContentItemsForChangeEdit(this.changeNum);
paladoxa80ea7c2022-02-19 17:42:15 +00001730
paladox32b861a2022-04-25 16:45:06 +01001731 this.fireAction(
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001732 '/edit',
1733 assertUIActionInfo(this.actions.deleteEdit),
1734 false
1735 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001736 }
1737
Milutin Kristofic7c84f7e2024-04-02 21:41:58 +02001738 private handlePublishEditConfirm() {
1739 this.hideAllDialogs();
1740
1741 if (!this.actions.publishEdit) return;
1742
1743 // We need to make sure that all cached version of a change
1744 // edit are deleted.
1745 this.getStorage().eraseEditableContentItemsForChangeEdit(this.changeNum);
1746
1747 this.fireAction(
1748 '/edit:publish',
1749 assertUIActionInfo(this.actions.publishEdit),
1750 false,
1751 {notify: NotifyType.NONE}
1752 );
1753 }
1754
paladox32b861a2022-04-25 16:45:06 +01001755 // private but used in test
1756 handleSubmitConfirm() {
1757 if (!this.canSubmitChange()) {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001758 return;
1759 }
paladox32b861a2022-04-25 16:45:06 +01001760 this.hideAllDialogs();
1761 this.fireAction(
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001762 '/submit',
Ben Rohlfs4af8c1d2024-02-29 11:41:35 +01001763 assertUIActionInfo(this.revisionActions?.submit),
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001764 true
1765 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001766 }
1767
Ben Rohlfs73294302024-02-26 17:07:40 +01001768 private isOverflowAction(type: string, key: string) {
1769 return this.overflowActions.some(
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001770 action => action.type === type && action.key === key
1771 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001772 }
1773
paladox32b861a2022-04-25 16:45:06 +01001774 // private but used in test
Frank Borden10a69252022-12-08 17:28:49 +01001775 setLoadingOnButtonWithKey(action: UIActionInfo) {
1776 const key = action.__key;
1777 this.inProgressActionKeys.add(key);
paladox32b861a2022-04-25 16:45:06 +01001778 this.actionLoadingMessage = this.computeLoadingLabel(key);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001779 let buttonKey = key;
1780 // TODO(dhruvsri): clean this up later
1781 // If key is revert-submission, then button key should be 'revert'
1782 if (buttonKey === ChangeActions.REVERT_SUBMISSION) {
1783 // Revert submission button no longer exists
1784 buttonKey = ChangeActions.REVERT;
1785 }
1786
Ben Rohlfs73294302024-02-26 17:07:40 +01001787 if (this.isOverflowAction(action.__type, buttonKey)) {
paladox32b861a2022-04-25 16:45:06 +01001788 this.disabledMenuActions.push(buttonKey === '/' ? 'delete' : buttonKey);
1789 this.requestUpdate('disabledMenuActions');
Tao Zhou38b3e0d2020-09-09 17:02:21 +02001790 return () => {
Frank Borden10a69252022-12-08 17:28:49 +01001791 this.inProgressActionKeys.delete(key);
paladox32b861a2022-04-25 16:45:06 +01001792 this.actionLoadingMessage = '';
1793 this.disabledMenuActions = [];
Frank Borden10a69252022-12-08 17:28:49 +01001794 this.requestUpdate();
Tao Zhou38b3e0d2020-09-09 17:02:21 +02001795 };
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001796 }
1797
1798 // Otherwise it's a top-level action.
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001799 const buttonEl = this.shadowRoot!.querySelector(
1800 `[data-action-key="${buttonKey}"]`
1801 ) as GrButton;
1802 if (!buttonEl) {
1803 throw new Error(`Can't find button by data-action-key '${buttonKey}'`);
1804 }
1805 buttonEl.setAttribute('loading', 'true');
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001806 buttonEl.disabled = true;
Tao Zhou38b3e0d2020-09-09 17:02:21 +02001807 return () => {
Frank Borden10a69252022-12-08 17:28:49 +01001808 this.inProgressActionKeys.delete(action.__key);
paladox32b861a2022-04-25 16:45:06 +01001809 this.actionLoadingMessage = '';
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001810 buttonEl.removeAttribute('loading');
1811 buttonEl.disabled = false;
Frank Borden10a69252022-12-08 17:28:49 +01001812 this.requestUpdate();
Tao Zhou38b3e0d2020-09-09 17:02:21 +02001813 };
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001814 }
1815
paladox32b861a2022-04-25 16:45:06 +01001816 // private but used in test
1817 fireAction(
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001818 endpoint: string,
1819 action: UIActionInfo,
1820 revAction: boolean,
Milutin Kristofic174c3432022-09-16 12:33:28 +02001821 payload?: RequestPayload,
1822 toReport?: Object
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001823 ) {
Frank Borden10a69252022-12-08 17:28:49 +01001824 const cleanupFn = this.setLoadingOnButtonWithKey(action);
Milutin Kristofic174c3432022-09-16 12:33:28 +02001825 this.reporting.reportInteraction(Interaction.CHANGE_ACTION_FIRED, {
1826 endpoint,
1827 toReport,
1828 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001829
paladox32b861a2022-04-25 16:45:06 +01001830 this.send(
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001831 action.method,
1832 payload,
1833 endpoint,
1834 revAction,
1835 cleanupFn,
1836 action
paladox32b861a2022-04-25 16:45:06 +01001837 ).then(res => this.handleResponse(action, res));
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001838 }
1839
paladox32b861a2022-04-25 16:45:06 +01001840 // private but used in test
1841 showActionDialog(dialog: ChangeActionDialog) {
1842 this.hideAllDialogs();
Dhruv Srivastavac48d6942021-02-08 13:42:57 +01001843 if (dialog.init) dialog.init();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001844 dialog.hidden = false;
Dhruv Srivastava38e0b162022-10-24 14:22:17 +02001845 assertIsDefined(this.actionsModal, 'actionsModal');
Ben Rohlfsf56f71c2023-04-27 17:20:43 +02001846 if (this.actionsModal.isConnected) this.actionsModal.showModal();
Dhruv Srivastava38e0b162022-10-24 14:22:17 +02001847 whenVisible(dialog, () => {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001848 if (dialog.resetFocus) {
1849 dialog.resetFocus();
1850 }
1851 });
1852 }
1853
1854 // TODO(rmistry): Redo this after
Edwin Kempin11da19d2023-06-19 14:17:00 +00001855 // https://issues.gerritcodereview.com/issues/40004936 is resolved.
paladox32b861a2022-04-25 16:45:06 +01001856 // private but used in test
1857 setReviewOnRevert(newChangeId: NumericChangeId) {
Chris Poucete3d66862022-10-26 11:19:50 +02001858 const review = this.getPluginLoader().jsApiService.getReviewPostRevert(
Ben Rohlfsf56f71c2023-04-27 17:20:43 +02001859 this.change as ChangeInfo
Chris Poucete3d66862022-10-26 11:19:50 +02001860 );
Edward Lesmesa63a1cd2021-01-25 11:51:20 -08001861 if (!review) {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001862 return Promise.resolve(undefined);
1863 }
Edward Lesmesa63a1cd2021-01-25 11:51:20 -08001864 return this.restApiService.saveChangeReview(newChangeId, CURRENT, review);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001865 }
1866
paladox32b861a2022-04-25 16:45:06 +01001867 // private but used in test
Kamil Musin1d8a1a32024-02-29 14:49:41 +01001868 async handleResponse(action: UIActionInfo, response: Response | undefined) {
1869 if (!response?.ok) {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001870 return;
1871 }
Kamil Musin3fb732f2023-08-25 15:48:09 +02001872 switch (action.__key) {
1873 case ChangeActions.REVERT: {
Kamil Musin1d8a1a32024-02-29 14:49:41 +01001874 const revertChangeInfo = (await readJSONResponsePayload(response))
1875 .parsed as unknown as ChangeInfo;
Kamil Musin48677cb2024-02-27 17:06:12 +01001876 this.restApiService.addRepoNameToCache(
Kamil Musin3fb732f2023-08-25 15:48:09 +02001877 revertChangeInfo._number,
1878 revertChangeInfo.project
1879 );
1880 const reachable = await this.waitForChangeReachable(
1881 revertChangeInfo._number
1882 );
1883 if (!reachable) return;
1884 await this.setReviewOnRevert(revertChangeInfo._number);
1885 this.getNavigation().setUrl(
1886 createChangeUrl({change: revertChangeInfo})
1887 );
1888 break;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001889 }
Kamil Musin3fb732f2023-08-25 15:48:09 +02001890 case RevisionActions.CHERRYPICK: {
Kamil Musin1d8a1a32024-02-29 14:49:41 +01001891 const cherrypickChangeInfo = (await readJSONResponsePayload(response))
1892 .parsed as unknown as ChangeInfo;
Kamil Musin48677cb2024-02-27 17:06:12 +01001893 this.restApiService.addRepoNameToCache(
Kamil Musin3fb732f2023-08-25 15:48:09 +02001894 cherrypickChangeInfo._number,
1895 cherrypickChangeInfo.project
1896 );
1897 const reachable = this.waitForChangeReachable(
1898 cherrypickChangeInfo._number
1899 );
1900 if (!reachable) return;
1901 this.getNavigation().setUrl(
1902 createChangeUrl({change: cherrypickChangeInfo})
1903 );
1904 break;
1905 }
1906 case ChangeActions.DELETE:
1907 if (action.__type === ActionType.CHANGE) {
1908 this.getNavigation().setUrl(rootUrl());
1909 }
1910 break;
1911 case ChangeActions.WIP:
1912 case ChangeActions.DELETE_EDIT:
1913 case ChangeActions.PUBLISH_EDIT:
1914 case ChangeActions.REBASE_EDIT:
1915 case ChangeActions.REBASE:
1916 case ChangeActions.SUBMIT:
1917 // Hide rebase dialog only if the action succeeds
1918 this.actionsModal?.close();
1919 this.hideAllDialogs();
1920 this.getChangeModel().navigateToChangeResetReload();
1921 break;
1922 case ChangeActions.REVERT_SUBMISSION: {
Kamil Musin1d8a1a32024-02-29 14:49:41 +01001923 const revertSubmistionInfo = (await readJSONResponsePayload(response))
1924 .parsed as unknown as RevertSubmissionInfo;
Kamil Musin3fb732f2023-08-25 15:48:09 +02001925 if (
1926 !revertSubmistionInfo.revert_changes ||
1927 !revertSubmistionInfo.revert_changes.length
1928 )
1929 return;
1930 /* If there is only 1 change then gerrit will automatically
1931 redirect to that change */
1932 const topic = revertSubmistionInfo.revert_changes[0].topic;
1933 this.getNavigation().setUrl(createSearchUrl({topic}));
1934 break;
1935 }
1936 default:
1937 this.getChangeModel().navigateToChangeResetReload();
1938 break;
1939 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001940 }
1941
paladox32b861a2022-04-25 16:45:06 +01001942 // private but used in test
1943 handleResponseError(
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001944 action: UIActionInfo,
1945 response: Response | undefined | null,
1946 body?: RequestPayload
1947 ) {
1948 if (!response) {
1949 return Promise.resolve(() => {
Ben Rohlfs6bb90532023-02-17 18:55:56 +01001950 fireError(this, `Could not perform action '${action.__key}'`);
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001951 });
1952 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001953 if (action && action.__key === RevisionActions.CHERRYPICK) {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001954 if (
1955 response.status === 409 &&
1956 body &&
1957 !(body as CherryPickInput).allow_conflicts
1958 ) {
paladox32b861a2022-04-25 16:45:06 +01001959 assertIsDefined(
1960 this.confirmCherrypickConflict,
1961 'confirmCherrypickConflict'
1962 );
1963 this.showActionDialog(this.confirmCherrypickConflict);
Ben Rohlfs885e0e72022-02-25 12:51:30 +01001964 return;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001965 }
1966 }
1967 return response.text().then(errText => {
Ben Rohlfs6bb90532023-02-17 18:55:56 +01001968 fireError(this, `Could not perform action: ${errText}`);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001969 if (!errText.startsWith('Change is already up to date')) {
1970 throw Error(errText);
1971 }
1972 });
1973 }
1974
paladox32b861a2022-04-25 16:45:06 +01001975 // private but used in test
1976 send(
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001977 method: HttpMethod | undefined,
1978 payload: RequestPayload | undefined,
1979 actionEndpoint: string,
1980 revisionAction: boolean,
1981 cleanupFn: () => void,
1982 action: UIActionInfo
Kamil Musin1d8a1a32024-02-29 14:49:41 +01001983 ): Promise<Response | undefined> {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001984 const handleError: ErrorCallback = response => {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001985 cleanupFn.call(this);
paladox32b861a2022-04-25 16:45:06 +01001986 this.handleResponseError(action, response, payload);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001987 };
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02001988 const change = this.change;
1989 const changeNum = this.changeNum;
1990 if (!change || !changeNum) {
1991 return Promise.reject(
1992 new Error('Properties change and changeNum must be set.')
1993 );
1994 }
Chris Poucetbf65b8f2022-01-18 21:18:12 +00001995 return this.getChangeModel()
1996 .fetchChangeUpdates(change)
1997 .then(result => {
1998 if (!result.isLatest) {
Ben Rohlfsa36fa832023-02-18 14:55:02 +01001999 fire(this, 'show-alert', {
Ben Rohlfs6bb90532023-02-17 18:55:56 +01002000 message:
2001 'Cannot set label: a newer patch has been ' +
2002 'uploaded to this change.',
2003 action: 'Reload',
Ben Rohlfsd49e8332023-04-20 22:40:04 +02002004 callback: () => this.getChangeModel().navigateToChangeResetReload(),
Ben Rohlfs6bb90532023-02-17 18:55:56 +01002005 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002006
Chris Poucetbf65b8f2022-01-18 21:18:12 +00002007 // Because this is not a network error, call the cleanup function
2008 // but not the error handler.
2009 cleanupFn();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002010
Chris Poucetbf65b8f2022-01-18 21:18:12 +00002011 return Promise.resolve(undefined);
2012 }
2013 const patchNum = revisionAction ? this.latestPatchNum : undefined;
2014 return this.restApiService
2015 .executeChangeAction(
2016 changeNum,
2017 method,
2018 actionEndpoint,
2019 patchNum,
2020 payload,
2021 handleError
2022 )
2023 .then(response => {
2024 cleanupFn.call(this);
2025 return response;
2026 });
2027 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002028 }
2029
paladox32b861a2022-04-25 16:45:06 +01002030 // private but used in test
Chris Poucetc55c7db2023-07-21 11:08:06 +02002031 async handleCherrypickTap() {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002032 if (!this.change) {
2033 throw new Error('The change property must be set');
2034 }
paladox32b861a2022-04-25 16:45:06 +01002035 assertIsDefined(this.confirmCherrypick, 'confirmCherrypick');
2036 this.confirmCherrypick.branch = '' as BranchName;
Chris Poucetc55c7db2023-07-21 11:08:06 +02002037 const changes = await this.getCherryPickChanges();
2038 if (!changes.length) return;
2039 this.confirmCherrypick.updateChanges(changes);
2040 this.showActionDialog(this.confirmCherrypick);
2041 }
2042
2043 private async getCherryPickChanges() {
2044 if (!this.change) return [];
2045 if (!this.change.topic) return [this.change];
Dhruv Srivastavabab00cf2020-03-19 12:11:50 +01002046 const query = `topic: "${this.change.topic}"`;
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002047 const options = listChangesOptionsToHex(
2048 ListChangesOption.MESSAGES,
2049 ListChangesOption.ALL_REVISIONS
2050 );
Chris Poucetc55c7db2023-07-21 11:08:06 +02002051 return this.restApiService
Ben Rohlfs43935a42020-12-01 19:14:09 +01002052 .getChanges(0, query, undefined, options)
2053 .then(changes => {
2054 if (!changes) {
Kamil Musinf0ece022022-10-14 15:22:44 +02002055 this.reporting.error(
2056 'Change Actions',
2057 new Error('getChanges returns undefined')
2058 );
Chris Poucetc55c7db2023-07-21 11:08:06 +02002059 return [];
Ben Rohlfs43935a42020-12-01 19:14:09 +01002060 }
Chris Poucetc55c7db2023-07-21 11:08:06 +02002061 return changes;
Ben Rohlfs43935a42020-12-01 19:14:09 +01002062 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002063 }
2064
paladox32b861a2022-04-25 16:45:06 +01002065 // private but used in test
2066 handleMoveTap() {
2067 assertIsDefined(this.confirmMove, 'confirmMove');
2068 this.confirmMove.branch = '' as BranchName;
2069 this.confirmMove.message = '';
2070 this.showActionDialog(this.confirmMove);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002071 }
2072
paladox32b861a2022-04-25 16:45:06 +01002073 // private but used in test
2074 handleDownloadTap() {
Ben Rohlfs44f01042023-02-18 13:27:57 +01002075 fire(this, 'download-tap', {});
Dhruv Srivastava4dcb5432021-04-26 16:45:07 +02002076 }
2077
paladox32b861a2022-04-25 16:45:06 +01002078 // private but used in test
2079 handleIncludedInTap() {
Ben Rohlfs44f01042023-02-18 13:27:57 +01002080 fire(this, 'included-tap', {});
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002081 }
2082
paladox32b861a2022-04-25 16:45:06 +01002083 // private but used in test
2084 handleDeleteTap() {
2085 assertIsDefined(this.confirmDeleteDialog, 'confirmDeleteDialog');
2086 this.showActionDialog(this.confirmDeleteDialog);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002087 }
2088
paladox32b861a2022-04-25 16:45:06 +01002089 // private but used in test
2090 handleDeleteEditTap() {
2091 assertIsDefined(this.confirmDeleteEditDialog, 'confirmDeleteEditDialog');
2092 this.showActionDialog(this.confirmDeleteEditDialog);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002093 }
2094
paladox32b861a2022-04-25 16:45:06 +01002095 private handleFollowUpTap() {
2096 assertIsDefined(this.createFollowUpDialog, 'createFollowUpDialog');
2097 this.showActionDialog(this.createFollowUpDialog);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002098 }
2099
paladox32b861a2022-04-25 16:45:06 +01002100 private handleWipTap() {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002101 if (!this.actions.wip) {
2102 return;
2103 }
paladox32b861a2022-04-25 16:45:06 +01002104 this.fireAction('/wip', assertUIActionInfo(this.actions.wip), false);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002105 }
2106
paladox32b861a2022-04-25 16:45:06 +01002107 private handlePublishEditTap() {
Milutin Kristofic7c84f7e2024-04-02 21:41:58 +02002108 assertIsDefined(this.confirmPublishEditDialog, 'confirmPublishEditDialog');
2109 this.showActionDialog(this.confirmPublishEditDialog);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002110 }
2111
paladox32b861a2022-04-25 16:45:06 +01002112 private handleRebaseEditTap() {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002113 if (!this.actions.rebaseEdit) {
2114 return;
2115 }
paladox32b861a2022-04-25 16:45:06 +01002116 this.fireAction(
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002117 '/edit:rebase',
2118 assertUIActionInfo(this.actions.rebaseEdit),
2119 false
2120 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002121 }
2122
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002123 /**
2124 * Merge sources of change actions into a single ordered array of action
2125 * values.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002126 */
Chris Poucet9369a9f2022-05-09 17:15:44 +02002127 private computeAllActions(): UIActionInfo[] {
Chris Poucet9369a9f2022-05-09 17:15:44 +02002128 if (this.change === undefined) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002129 return [];
2130 }
2131
paladox32b861a2022-04-25 16:45:06 +01002132 const revisionActionValues = this.getActionValues(
Chris Poucet9369a9f2022-05-09 17:15:44 +02002133 this.revisionActions,
2134 this.primaryActionKeys,
2135 this.additionalActions,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002136 ActionType.REVISION
2137 );
paladox32b861a2022-04-25 16:45:06 +01002138 const changeActionValues = this.getActionValues(
Chris Poucet9369a9f2022-05-09 17:15:44 +02002139 this.actions,
2140 this.primaryActionKeys,
2141 this.additionalActions,
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002142 ActionType.CHANGE
2143 );
paladox32b861a2022-04-25 16:45:06 +01002144 const quickApprove = this.getQuickApproveAction();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002145 if (quickApprove) {
2146 changeActionValues.unshift(quickApprove);
2147 }
2148
2149 return revisionActionValues
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002150 .concat(changeActionValues)
paladox32b861a2022-04-25 16:45:06 +01002151 .sort((a, b) => this.actionComparator(a, b))
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002152 .map(action => {
Chris Poucet1cd6394d2022-07-12 16:43:28 +02002153 return {
2154 ...action,
2155 ...(ACTIONS_WITH_ICONS.get(action.__key) ?? {}),
2156 };
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002157 })
paladox32b861a2022-04-25 16:45:06 +01002158 .filter(action => !this.shouldSkipAction(action));
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002159 }
2160
paladox32b861a2022-04-25 16:45:06 +01002161 private getActionPriority(action: UIActionInfo) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002162 if (action.__type && action.__key) {
paladox32b861a2022-04-25 16:45:06 +01002163 const overrideAction = this.actionPriorityOverrides.find(
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002164 i => i.type === action.__type && i.key === action.__key
2165 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002166
2167 if (overrideAction !== undefined) {
2168 return overrideAction.priority;
2169 }
2170 }
2171 if (action.__key === 'review') {
2172 return ActionPriority.REVIEW;
2173 } else if (action.__primary) {
2174 return ActionPriority.PRIMARY;
2175 } else if (action.__type === ActionType.CHANGE) {
2176 return ActionPriority.CHANGE;
2177 } else if (action.__type === ActionType.REVISION) {
2178 return ActionPriority.REVISION;
2179 }
2180 return ActionPriority.DEFAULT;
2181 }
2182
2183 /**
2184 * Sort comparator to define the order of change actions.
paladox32b861a2022-04-25 16:45:06 +01002185 *
2186 * private but used in test
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002187 */
paladox32b861a2022-04-25 16:45:06 +01002188 actionComparator(actionA: UIActionInfo, actionB: UIActionInfo) {
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002189 const priorityDelta =
paladox32b861a2022-04-25 16:45:06 +01002190 this.getActionPriority(actionA) - this.getActionPriority(actionB);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002191 // Sort by the button label if same priority.
2192 if (priorityDelta === 0) {
2193 return actionA.label > actionB.label ? 1 : -1;
2194 } else {
2195 return priorityDelta;
2196 }
2197 }
2198
paladox32b861a2022-04-25 16:45:06 +01002199 private shouldSkipAction(action: UIActionInfo) {
David Ostrovsky954e5082021-05-05 12:28:13 +02002200 return SKIP_ACTION_KEYS.includes(action.__key);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002201 }
2202
Chris Poucet9369a9f2022-05-09 17:15:44 +02002203 private computeMenuActions(): MenuAction[] {
2204 return this.allActionValues
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002205 .filter(a => {
Ben Rohlfs73294302024-02-26 17:07:40 +01002206 const overflow = this.isOverflowAction(a.__type, a.__key);
Chris Poucet9369a9f2022-05-09 17:15:44 +02002207 return overflow && !this.hiddenActions.includes(a.__key);
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002208 })
2209 .map(action => {
2210 let key = action.__key;
2211 if (key === '/') {
2212 key = 'delete';
2213 }
2214 return {
2215 name: action.label,
2216 id: `${key}-${action.__type}`,
2217 action,
2218 tooltip: action.title,
2219 };
2220 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002221 }
2222
2223 /**
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002224 * Occasionally, a change created by a change action is not yet known to the
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002225 * API for a brief time. Wait for the given change number to be recognized.
2226 *
2227 * Returns a promise that resolves with true if a request is recognized, or
2228 * false if the change was never recognized after all attempts.
2229 *
paladox32b861a2022-04-25 16:45:06 +01002230 * private but used in test
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002231 */
Kamil Musin3fb732f2023-08-25 15:48:09 +02002232 waitForChangeReachable(changeNum: NumericChangeId): Promise<boolean> {
David Ostrovskyf91f9662021-02-22 19:34:37 +01002233 let attemptsRemaining = AWAIT_CHANGE_ATTEMPTS;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002234 return new Promise(resolve => {
2235 const check = () => {
David Ostrovskyf91f9662021-02-22 19:34:37 +01002236 attemptsRemaining--;
Kamil Musin3fb732f2023-08-25 15:48:09 +02002237 // Pass a no-op error handler to avoid the "not found" error toast,
2238 // unless it's the last attempt
Ben Rohlfs43935a42020-12-01 19:14:09 +01002239 this.restApiService
Kamil Musin3fb732f2023-08-25 15:48:09 +02002240 .getChange(changeNum, attemptsRemaining !== 0 ? () => {} : undefined)
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002241 .then(response => {
2242 // If the response is 404, the response will be undefined.
2243 if (response) {
2244 resolve(true);
2245 return;
2246 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002247
David Ostrovskyf91f9662021-02-22 19:34:37 +01002248 if (attemptsRemaining) {
Ben Rohlfs6b078932021-03-10 15:20:03 +01002249 setTimeout(check, AWAIT_CHANGE_TIMEOUT_MS);
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002250 } else {
2251 resolve(false);
2252 }
2253 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002254 };
2255 check();
2256 });
2257 }
2258
paladox32b861a2022-04-25 16:45:06 +01002259 private handleEditTap() {
Ben Rohlfs44f01042023-02-18 13:27:57 +01002260 fireNoBubbleNoCompose(this, 'edit-tap', {});
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002261 }
2262
paladox32b861a2022-04-25 16:45:06 +01002263 private handleStopEditTap() {
Ben Rohlfs44f01042023-02-18 13:27:57 +01002264 fireNoBubbleNoCompose(this, 'stop-edit-tap', {});
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002265 }
Milutin Kristofic7c84f7e2024-04-02 21:41:58 +02002266
2267 private numberOfThreadsWithSuggestions() {
2268 if (!this.threadsWithSuggestions) return 0;
2269 return this.threadsWithSuggestions.length;
2270 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01002271}
2272
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002273declare global {
paladox32b861a2022-04-25 16:45:06 +01002274 interface HTMLElementEventMap {
Ben Rohlfs5b3c6552023-02-18 13:02:46 +01002275 'download-tap': CustomEvent<{}>;
2276 'edit-tap': CustomEvent<{}>;
2277 'included-tap': CustomEvent<{}>;
paladox32b861a2022-04-25 16:45:06 +01002278 'revision-actions-changed': CustomEvent<{value: ActionNameToActionInfoMap}>;
Ben Rohlfs5b3c6552023-02-18 13:02:46 +01002279 'stop-edit-tap': CustomEvent<{}>;
paladox32b861a2022-04-25 16:45:06 +01002280 }
Dmitrii Filippov353bfbf2020-09-30 11:40:14 +02002281 interface HTMLElementTagNameMap {
2282 'gr-change-actions': GrChangeActions;
2283 }
2284}