blob: 644b0ae4d6bb9a44a969c4167f974917daeacc2b [file] [log] [blame]
Dave Borowitz8cdc76b2018-03-26 10:04:27 -04001/**
2 * @license
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01003 * Copyright (C) 2015 The Android Open Source Project
Dave Borowitz8cdc76b2018-03-26 10:04:27 -04004 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
Milutin Kristoficafae0052020-09-17 10:38:08 +020017import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
18import '../../../styles/shared-styles';
19import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
20import '../../plugins/gr-endpoint-param/gr-endpoint-param';
21import '../gr-button/gr-button';
22import '../gr-dialog/gr-dialog';
23import '../gr-date-formatter/gr-date-formatter';
24import '../gr-formatted-text/gr-formatted-text';
25import '../gr-icons/gr-icons';
26import '../gr-overlay/gr-overlay';
Milutin Kristoficafae0052020-09-17 10:38:08 +020027import '../gr-storage/gr-storage';
28import '../gr-textarea/gr-textarea';
29import '../gr-tooltip-content/gr-tooltip-content';
30import '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
31import '../gr-account-label/gr-account-label';
32import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
33import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
34import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
35import {PolymerElement} from '@polymer/polymer/polymer-element';
36import {htmlTemplate} from './gr-comment_html';
37import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
38import {getRootElement} from '../../../scripts/rootElement';
39import {appContext} from '../../../services/app-context';
Ben Rohlfs1d487062020-09-26 11:26:03 +020040import {customElement, observe, property} from '@polymer/decorators';
Dhruv Srivastava0287bf92020-09-11 16:56:38 +020041import {GerritNav} from '../../core/gr-navigation/gr-navigation';
Milutin Kristoficafae0052020-09-17 10:38:08 +020042import {GrTextarea} from '../gr-textarea/gr-textarea';
43import {GrStorage, StorageLocation} from '../gr-storage/gr-storage';
44import {GrOverlay} from '../gr-overlay/gr-overlay';
45import {
Milutin Kristoficafae0052020-09-17 10:38:08 +020046 AccountDetailInfo,
Dmitrii Filippov30ba5962020-09-29 18:41:45 +020047 NumericChangeId,
Ben Rohlfs1d487062020-09-26 11:26:03 +020048 ConfigInfo,
49 PatchSetNum,
Dhruv Srivastava0287bf92020-09-11 16:56:38 +020050 RepoName,
Milutin Kristoficafae0052020-09-17 10:38:08 +020051} from '../../../types/common';
52import {GrButton} from '../gr-button/gr-button';
53import {GrConfirmDeleteCommentDialog} from '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
54import {GrDialog} from '../gr-dialog/gr-dialog';
Ben Rohlfs1d487062020-09-26 11:26:03 +020055import {
56 isDraft,
57 UIComment,
58 UIDraft,
59 UIRobot,
Ben Rohlfs31825d82020-10-02 18:08:04 +020060} from '../../../utils/comment-util';
Dmitrii Filippov6a038002020-10-14 18:50:07 +020061import {OpenFixPreviewEventDetail} from '../../../types/events';
Milutin Kristofic860fe4d2020-11-23 16:13:45 +010062import {fireAlert} from '../../../utils/event-util';
Milutin Kristofic5b3f0872020-12-05 22:08:09 +010063import {pluralize} from '../../../utils/string-util';
Wyatt Allen494e7d42017-09-12 17:01:42 -070064
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010065const STORAGE_DEBOUNCE_INTERVAL = 400;
66const TOAST_DEBOUNCE_INTERVAL = 200;
Wyatt Allen7a4aa8c2016-05-18 12:37:53 -070067
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010068const SAVED_MESSAGE = 'All changes saved';
Dhruv Srivastava8b015a62020-07-09 17:45:25 +020069const UNSAVED_MESSAGE = 'Unable to save draft';
Wyatt Allen846ac2f2018-05-14 12:59:23 -070070
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010071const REPORT_CREATE_DRAFT = 'CreateDraftComment';
72const REPORT_UPDATE_DRAFT = 'UpdateDraftComment';
73const REPORT_DISCARD_DRAFT = 'DiscardDraftComment';
74
75const FILE = 'FILE';
76
Dhruv Srivastava8b015a62020-07-09 17:45:25 +020077export const __testOnly_UNSAVED_MESSAGE = UNSAVED_MESSAGE;
78
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010079/**
80 * All candidates tips to show, will pick randomly.
81 */
Milutin Kristoficafae0052020-09-17 10:38:08 +020082const RESPECTFUL_REVIEW_TIPS = [
Tao Zhoue5409852020-03-27 14:11:00 +010083 'Assume competence.',
84 'Provide rationale or context.',
85 'Consider how comments may be interpreted.',
86 'Avoid harsh language.',
87 'Make your comments specific and actionable.',
88 'When disagreeing, explain the advantage of your approach.',
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010089];
90
Milutin Kristoficafae0052020-09-17 10:38:08 +020091interface CommentOverlays {
92 confirmDelete?: GrOverlay | null;
93 confirmDiscard?: GrOverlay | null;
94}
95
96export interface GrComment {
97 $: {
Milutin Kristoficafae0052020-09-17 10:38:08 +020098 storage: GrStorage;
99 container: HTMLDivElement;
100 resolvedCheckbox: HTMLInputElement;
101 };
102}
Dmitrii Filippov3f3c2052020-09-22 16:51:18 +0200103
Milutin Kristoficafae0052020-09-17 10:38:08 +0200104@customElement('gr-comment')
105export class GrComment extends KeyboardShortcutMixin(
106 GestureEventListeners(LegacyElementMixin(PolymerElement))
107) {
108 static get template() {
109 return htmlTemplate;
110 }
111
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100112 /**
113 * Fired when the create fix comment action is triggered.
114 *
115 * @event create-fix-comment
116 */
Kasper Nilssond43d2a72018-10-19 14:26:41 -0700117
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100118 /**
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100119 * Fired when the show fix preview action is triggered.
120 *
121 * @event open-fix-preview
Tao Zhou500437d2020-02-14 16:57:27 +0100122 */
Tao Zhou500437d2020-02-14 16:57:27 +0100123
124 /**
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100125 * Fired when this comment is discarded.
126 *
127 * @event comment-discard
Tao Zhou9a076812019-12-17 09:59:28 +0100128 */
Andrew Bonventre78792e82016-03-04 17:48:22 -0500129
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100130 /**
131 * Fired when this comment is saved.
132 *
133 * @event comment-save
134 */
Julie Pan49d0a6e2019-10-07 07:37:02 -0700135
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100136 /**
137 * Fired when this comment is updated.
138 *
139 * @event comment-update
140 */
Andrew Bonventre78792e82016-03-04 17:48:22 -0500141
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100142 /**
Tao Zhou31f3f102020-04-27 16:15:29 +0200143 * Fired when editing status changed.
144 *
145 * @event comment-editing-changed
146 */
147
148 /**
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100149 * Fired when the comment's timestamp is tapped.
150 *
151 * @event comment-anchor-tap
152 */
Andrew Bonventre28165262016-05-19 17:24:45 -0700153
Milutin Kristoficafae0052020-09-17 10:38:08 +0200154 @property({type: Number})
Dmitrii Filippov30ba5962020-09-29 18:41:45 +0200155 changeNum?: NumericChangeId;
Viktar Donich7ad28922016-05-23 15:24:05 -0700156
Dhruv Srivastava0287bf92020-09-11 16:56:38 +0200157 @property({type: String})
158 projectName?: RepoName;
159
Milutin Kristoficafae0052020-09-17 10:38:08 +0200160 @property({type: Object, notify: true, observer: '_commentChanged'})
Dhruv Srivastava573ac7d2020-12-03 10:59:22 +0100161 comment?: UIComment;
Kasper Nilssond43d2a72018-10-19 14:26:41 -0700162
Milutin Kristoficafae0052020-09-17 10:38:08 +0200163 @property({type: Array})
Dhruv Srivastava573ac7d2020-12-03 10:59:22 +0100164 comments?: UIComment[];
Andrew Bonventre78792e82016-03-04 17:48:22 -0500165
Milutin Kristoficafae0052020-09-17 10:38:08 +0200166 @property({type: Boolean, reflectToAttribute: true})
167 isRobotComment = false;
Kasper Nilsson8d1ac7e2017-01-04 16:45:21 -0800168
Milutin Kristoficafae0052020-09-17 10:38:08 +0200169 @property({type: Boolean, reflectToAttribute: true})
170 disabled = false;
Wyatt Allen494e7d42017-09-12 17:01:42 -0700171
Milutin Kristoficafae0052020-09-17 10:38:08 +0200172 @property({type: Boolean, observer: '_draftChanged'})
173 draft = false;
Wyatt Allen5eab49a2017-11-02 18:17:52 -0700174
Milutin Kristoficafae0052020-09-17 10:38:08 +0200175 @property({type: Boolean, observer: '_editingChanged'})
176 editing = false;
Wyatt Allened225662018-04-05 10:48:03 -0700177
Milutin Kristoficafae0052020-09-17 10:38:08 +0200178 @property({type: Boolean, reflectToAttribute: true})
179 discarding = false;
180
181 @property({type: Boolean})
182 hasChildren?: boolean;
183
184 @property({type: String})
Milutin Kristofica6af5aa2020-09-23 09:08:14 +0200185 patchNum?: PatchSetNum;
Milutin Kristoficafae0052020-09-17 10:38:08 +0200186
187 @property({type: Boolean})
188 showActions?: boolean;
189
190 @property({type: Boolean})
191 _showHumanActions?: boolean;
192
193 @property({type: Boolean})
194 _showRobotActions?: boolean;
195
196 @property({
197 type: Boolean,
198 reflectToAttribute: true,
199 observer: '_toggleCollapseClass',
200 })
201 collapsed = true;
202
203 @property({type: Object})
204 projectConfig?: ConfigInfo;
205
206 @property({type: Boolean})
207 robotButtonDisabled?: boolean;
208
209 @property({type: Boolean})
210 _hasHumanReply?: boolean;
211
212 @property({type: Boolean})
213 _isAdmin = false;
214
215 @property({type: Object})
Dhruv Srivastavaf97b0552020-11-30 14:14:09 +0100216 // eslint-disable-next-line @typescript-eslint/no-explicit-any
Milutin Kristoficafae0052020-09-17 10:38:08 +0200217 _xhrPromise?: Promise<any>; // Used for testing.
218
219 @property({type: String, observer: '_messageTextChanged'})
220 _messageText = '';
221
222 @property({type: String})
Milutin Kristoficafae0052020-09-17 10:38:08 +0200223 side?: string;
224
225 @property({type: Boolean})
226 resolved?: boolean;
227
228 // Intentional to share the object across instances.
229 @property({type: Object})
230 _numPendingDraftRequests: {number: number} = {number: 0};
231
232 @property({type: Boolean})
233 _enableOverlay = false;
234
235 /**
236 * Property for storing references to overlay elements. When the overlays
237 * are moved to getRootElement() to be shown they are no-longer
238 * children, so they can't be queried along the tree, so they are stored
239 * here.
240 */
241 @property({type: Object})
242 _overlays: CommentOverlays = {};
243
244 @property({type: Boolean})
245 _showRespectfulTip = false;
246
247 @property({type: Boolean})
248 showPatchset = true;
249
250 @property({type: String})
251 _respectfulReviewTip?: string;
252
253 @property({type: Boolean})
254 _respectfulTipDismissed = false;
255
256 @property({type: Boolean})
257 _unableToSave = false;
258
259 @property({type: Object})
260 _selfAccount?: AccountDetailInfo;
Tao Zhou500437d2020-02-14 16:57:27 +0100261
Dhruv Srivastava0287bf92020-09-11 16:56:38 +0200262 @property({type: Boolean})
263 showPortedComment = false;
264
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100265 get keyBindings() {
266 return {
267 'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey',
Milutin Kristoficafae0052020-09-17 10:38:08 +0200268 esc: '_handleEsc',
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100269 };
270 }
271
Ben Rohlfs43935a42020-12-01 19:14:09 +0100272 private readonly restApiService = appContext.restApiService;
273
Milutin Kristoficafae0052020-09-17 10:38:08 +0200274 reporting = appContext.reportingService;
275
276 /** @override */
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100277 attached() {
278 super.attached();
Ben Rohlfs43935a42020-12-01 19:14:09 +0100279 this.restApiService.getAccount().then(account => {
Dhruv Srivastavacf70e792020-07-24 15:35:39 +0200280 this._selfAccount = account;
281 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100282 if (this.editing) {
283 this.collapsed = false;
284 } else if (this.comment) {
Milutin Kristoficafae0052020-09-17 10:38:08 +0200285 this.collapsed = !!this.comment.collapsed;
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100286 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100287 this._getIsAdmin().then(isAdmin => {
Milutin Kristoficafae0052020-09-17 10:38:08 +0200288 this._isAdmin = !!isAdmin;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100289 });
290 }
Andrew Bonventre78792e82016-03-04 17:48:22 -0500291
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100292 /** @override */
293 detached() {
294 super.detached();
295 this.cancelDebouncer('fire-update');
296 if (this.textarea) {
297 this.textarea.closeDropdown();
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100298 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100299 }
Andrew Bonventre78792e82016-03-04 17:48:22 -0500300
Ben Rohlfs1d487062020-09-26 11:26:03 +0200301 _getAuthor(comment: UIComment) {
Dhruv Srivastavacf70e792020-07-24 15:35:39 +0200302 return comment.author || this._selfAccount;
303 }
304
Dhruv Srivastava0287bf92020-09-11 16:56:38 +0200305 _getUrlForComment(comment: UIComment) {
306 if (!this.changeNum || !this.projectName) return '';
307 if (!comment.id) throw new Error('comment must have an id');
308 return GerritNav.getUrlForComment(
309 this.changeNum as NumericChangeId,
310 this.projectName,
311 comment.id
312 );
313 }
314
Dhruv Srivastavac8df7602021-01-15 10:59:00 +0100315 _handlePortedMessageClick() {
316 if (!this.comment) throw new Error('comment not set');
317 this.reporting.reportInteraction('navigate-to-original-comment', {
318 line: this.comment.line,
319 range: this.comment.range,
320 });
321 }
322
Milutin Kristoficafae0052020-09-17 10:38:08 +0200323 @observe('editing')
324 _onEditingChange(editing?: boolean) {
325 this.dispatchEvent(
326 new CustomEvent('comment-editing-changed', {
327 detail: !!editing,
328 bubbles: true,
329 composed: true,
330 })
331 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100332 if (!editing) return;
333 // visibility based on cache this will make sure we only and always show
334 // a tip once every Math.max(a day, period between creating comments)
Milutin Kristoficafae0052020-09-17 10:38:08 +0200335 const cachedVisibilityOfRespectfulTip = this.$.storage.getRespectfulTipVisibility();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100336 if (!cachedVisibilityOfRespectfulTip) {
337 // we still want to show the tip with a probability of 30%
338 if (this.getRandomNum(0, 3) >= 1) return;
339 this._showRespectfulTip = true;
340 const randomIdx = this.getRandomNum(0, RESPECTFUL_REVIEW_TIPS.length);
341 this._respectfulReviewTip = RESPECTFUL_REVIEW_TIPS[randomIdx];
Milutin Kristoficafae0052020-09-17 10:38:08 +0200342 this.reporting.reportInteraction('respectful-tip-appeared', {
343 tip: this._respectfulReviewTip,
344 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100345 // update cache
346 this.$.storage.setRespectfulTipVisibility();
347 }
348 }
349
350 /** Set as a separate method so easy to stub. */
Milutin Kristoficafae0052020-09-17 10:38:08 +0200351 getRandomNum(min: number, max: number) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100352 return Math.floor(Math.random() * (max - min) + min);
353 }
354
Milutin Kristoficafae0052020-09-17 10:38:08 +0200355 _computeVisibilityOfTip(showTip: boolean, tipDismissed: boolean) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100356 return showTip && !tipDismissed;
357 }
358
359 _dismissRespectfulTip() {
360 this._respectfulTipDismissed = true;
Milutin Kristoficafae0052020-09-17 10:38:08 +0200361 this.reporting.reportInteraction('respectful-tip-dismissed', {
362 tip: this._respectfulReviewTip,
363 });
Tao Zhoue5409852020-03-27 14:11:00 +0100364 // add a 14-day delay to the tip cache
365 this.$.storage.setRespectfulTipVisibility(/* delayDays= */ 14);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100366 }
367
368 _onRespectfulReadMoreClick() {
Milutin Kristoficda88b332020-03-24 10:19:12 +0100369 this.reporting.reportInteraction('respectful-read-more-clicked');
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100370 }
371
Milutin Kristoficafae0052020-09-17 10:38:08 +0200372 get textarea(): GrTextarea | null {
373 return this.shadowRoot?.querySelector('#editTextarea') as GrTextarea | null;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100374 }
375
376 get confirmDeleteOverlay() {
377 if (!this._overlays.confirmDelete) {
378 this._enableOverlay = true;
379 flush();
Milutin Kristoficafae0052020-09-17 10:38:08 +0200380 this._overlays.confirmDelete = this.shadowRoot?.querySelector(
381 '#confirmDeleteOverlay'
382 ) as GrOverlay | null;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100383 }
384 return this._overlays.confirmDelete;
385 }
386
387 get confirmDiscardOverlay() {
388 if (!this._overlays.confirmDiscard) {
389 this._enableOverlay = true;
390 flush();
Milutin Kristoficafae0052020-09-17 10:38:08 +0200391 this._overlays.confirmDiscard = this.shadowRoot?.querySelector(
392 '#confirmDiscardOverlay'
393 ) as GrOverlay | null;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100394 }
395 return this._overlays.confirmDiscard;
396 }
397
Milutin Kristoficafae0052020-09-17 10:38:08 +0200398 _computeShowHideIcon(collapsed: boolean) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100399 return collapsed ? 'gr-icons:expand-more' : 'gr-icons:expand-less';
400 }
401
Milutin Kristoficafae0052020-09-17 10:38:08 +0200402 _computeShowHideAriaLabel(collapsed: boolean) {
Tao Zhou2ccf4162020-05-25 15:04:34 +0200403 return collapsed ? 'Expand' : 'Collapse';
404 }
405
Milutin Kristoficafae0052020-09-17 10:38:08 +0200406 @observe('showActions', 'isRobotComment')
407 _calculateActionstoShow(showActions?: boolean, isRobotComment?: boolean) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100408 // Polymer 2: check for undefined
Dhruv Srivastava68943562020-06-26 12:46:44 +0200409 if ([showActions, isRobotComment].includes(undefined)) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100410 return;
Tao Zhou500437d2020-02-14 16:57:27 +0100411 }
412
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100413 this._showHumanActions = showActions && !isRobotComment;
414 this._showRobotActions = showActions && isRobotComment;
415 }
416
Milutin Kristoficafae0052020-09-17 10:38:08 +0200417 @observe('comment')
Ben Rohlfs1d487062020-09-26 11:26:03 +0200418 _isRobotComment(comment: UIRobot) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100419 this.isRobotComment = !!comment.robot_id;
420 }
421
422 isOnParent() {
423 return this.side === 'PARENT';
424 }
425
426 _getIsAdmin() {
Ben Rohlfs43935a42020-12-01 19:14:09 +0100427 return this.restApiService.getIsAdmin();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100428 }
429
Milutin Kristoficafae0052020-09-17 10:38:08 +0200430 _computeDraftTooltip(unableToSave: boolean) {
431 return unableToSave
432 ? 'Unable to save draft. Please try to save again.'
433 : "This draft is only visible to you. To publish drafts, click the 'Reply'" +
434 "or 'Start review' button at the top of the change or press the 'A' key.";
Dhruv Srivastava8b015a62020-07-09 17:45:25 +0200435 }
436
Milutin Kristoficafae0052020-09-17 10:38:08 +0200437 _computeDraftText(unableToSave: boolean) {
Dhruv Srivastava8b015a62020-07-09 17:45:25 +0200438 return 'DRAFT' + (unableToSave ? '(Failed to save)' : '');
439 }
440
Ben Rohlfs1d487062020-09-26 11:26:03 +0200441 save(opt_comment?: UIComment) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100442 let comment = opt_comment;
443 if (!comment) {
444 comment = this.comment;
Tao Zhou500437d2020-02-14 16:57:27 +0100445 }
446
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100447 this.set('comment.message', this._messageText);
448 this.editing = false;
449 this.disabled = true;
450
451 if (!this._messageText) {
452 return this._discardDraft();
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100453 }
Wyatt Allened225662018-04-05 10:48:03 -0700454
Milutin Kristoficafae0052020-09-17 10:38:08 +0200455 this._xhrPromise = this._saveDraft(comment)
456 .then(response => {
457 this.disabled = false;
458 if (!response.ok) {
459 return;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100460 }
Milutin Kristoficafae0052020-09-17 10:38:08 +0200461
462 this._eraseDraftComment();
Ben Rohlfs43935a42020-12-01 19:14:09 +0100463 return this.restApiService.getResponseObject(response).then(obj => {
Ben Rohlfs1d487062020-09-26 11:26:03 +0200464 const resComment = (obj as unknown) as UIDraft;
465 if (!isDraft(this.comment)) throw new Error('Can only save drafts.');
Milutin Kristoficafae0052020-09-17 10:38:08 +0200466 resComment.__draft = true;
467 // Maintain the ephemeral draft ID for identification by other
468 // elements.
Milutin Kristofica6af5aa2020-09-23 09:08:14 +0200469 if (this.comment?.__draftID) {
Milutin Kristoficafae0052020-09-17 10:38:08 +0200470 resComment.__draftID = this.comment.__draftID;
471 }
Milutin Kristoficafae0052020-09-17 10:38:08 +0200472 this.comment = resComment;
473 this._fireSave();
474 return obj;
Kasper Nilssona4c24de2017-05-16 13:50:27 -0700475 });
Milutin Kristoficafae0052020-09-17 10:38:08 +0200476 })
477 .catch(err => {
478 this.disabled = false;
479 throw err;
480 });
Wyatt Allend563a712017-10-26 14:11:24 -0700481
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100482 return this._xhrPromise;
483 }
484
485 _eraseDraftComment() {
486 // Prevents a race condition in which removing the draft comment occurs
487 // prior to it being saved.
488 this.cancelDebouncer('store');
489
Dhruv Srivastavadbddc1b2020-09-24 21:23:27 +0200490 if (!this.comment?.path) throw new Error('Cannot erase Draft Comment');
Milutin Kristofica6af5aa2020-09-23 09:08:14 +0200491 if (this.changeNum === undefined) {
492 throw new Error('undefined changeNum');
493 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100494 this.$.storage.eraseDraftComment({
495 changeNum: this.changeNum,
496 patchNum: this._getPatchNum(),
497 path: this.comment.path,
498 line: this.comment.line,
499 range: this.comment.range,
500 });
501 }
502
Ben Rohlfs1d487062020-09-26 11:26:03 +0200503 _commentChanged(comment: UIComment) {
Dhruv Srivastava573ac7d2020-12-03 10:59:22 +0100504 this.editing = isDraft(comment) && !!comment.__editing;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100505 this.resolved = !comment.unresolved;
Milutin Kristoficafae0052020-09-17 10:38:08 +0200506 if (this.editing) {
507 // It's a new draft/reply, notify.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100508 this._fireUpdate();
509 }
510 }
511
Milutin Kristoficafae0052020-09-17 10:38:08 +0200512 @observe('comment', 'comments.*')
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100513 _computeHasHumanReply() {
Milutin Kristofica6af5aa2020-09-23 09:08:14 +0200514 const comment = this.comment;
515 if (!comment || !this.comments) return;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100516 // hide please fix button for robot comment that has human reply
Milutin Kristoficafae0052020-09-17 10:38:08 +0200517 this._hasHumanReply = this.comments.some(
518 c =>
519 c.in_reply_to &&
Milutin Kristofica6af5aa2020-09-23 09:08:14 +0200520 c.in_reply_to === comment.id &&
Ben Rohlfs1d487062020-09-26 11:26:03 +0200521 !(c as UIRobot).robot_id
Milutin Kristoficafae0052020-09-17 10:38:08 +0200522 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100523 }
524
Dmitrii Filippov6a038002020-10-14 18:50:07 +0200525 _getEventPayload(): OpenFixPreviewEventDetail {
Dmitrii Filippov3f3c2052020-09-22 16:51:18 +0200526 return {comment: this.comment, patchNum: this.patchNum};
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100527 }
528
529 _fireSave() {
Milutin Kristoficafae0052020-09-17 10:38:08 +0200530 this.dispatchEvent(
531 new CustomEvent('comment-save', {
532 detail: this._getEventPayload(),
533 composed: true,
534 bubbles: true,
535 })
536 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100537 }
538
539 _fireUpdate() {
540 this.debounce('fire-update', () => {
Milutin Kristoficafae0052020-09-17 10:38:08 +0200541 this.dispatchEvent(
542 new CustomEvent('comment-update', {
543 detail: this._getEventPayload(),
544 composed: true,
545 bubbles: true,
546 })
547 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100548 });
549 }
550
Milutin Kristoficafae0052020-09-17 10:38:08 +0200551 _computeAccountLabelClass(draft: boolean) {
Dhruv Srivastavacf70e792020-07-24 15:35:39 +0200552 return draft ? 'draft' : '';
553 }
554
Milutin Kristoficafae0052020-09-17 10:38:08 +0200555 _draftChanged(draft: boolean) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100556 this.$.container.classList.toggle('draft', draft);
557 }
558
Milutin Kristoficafae0052020-09-17 10:38:08 +0200559 _editingChanged(editing?: boolean, previousValue?: boolean) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100560 // Polymer 2: observer fires when at least one property is defined.
561 // Do nothing to prevent comment.__editing being overwritten
562 // if previousValue is undefined
563 if (previousValue === undefined) return;
564
565 this.$.container.classList.toggle('editing', editing);
566 if (this.comment && this.comment.id) {
Milutin Kristoficafae0052020-09-17 10:38:08 +0200567 const cancelButton = this.shadowRoot?.querySelector(
568 '.cancel'
569 ) as GrButton | null;
Milutin Kristoficc218df52020-05-15 21:55:08 +0200570 if (cancelButton) {
571 cancelButton.hidden = !editing;
572 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100573 }
Dhruv Srivastava573ac7d2020-12-03 10:59:22 +0100574 if (isDraft(this.comment)) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100575 this.comment.__editing = this.editing;
576 }
Milutin Kristoficafae0052020-09-17 10:38:08 +0200577 if (!!editing !== !!previousValue) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100578 // To prevent event firing on comment creation.
579 this._fireUpdate();
580 }
581 if (editing) {
582 this.async(() => {
583 flush();
584 this.textarea && this.textarea.putCursorAtEnd();
585 }, 1);
586 }
587 }
588
Milutin Kristoficafae0052020-09-17 10:38:08 +0200589 _computeDeleteButtonClass(isAdmin: boolean, draft: boolean) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100590 return isAdmin && !draft ? 'showDeleteButtons' : '';
591 }
592
Milutin Kristofica6af5aa2020-09-23 09:08:14 +0200593 _computeSaveDisabled(
594 draft: string,
Ben Rohlfs1d487062020-09-26 11:26:03 +0200595 comment: UIComment | undefined,
Milutin Kristofica6af5aa2020-09-23 09:08:14 +0200596 resolved?: boolean
597 ) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100598 // If resolved state has changed and a msg exists, save should be enabled.
Milutin Kristoficafae0052020-09-17 10:38:08 +0200599 if (!comment || (comment.unresolved === resolved && draft)) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100600 return false;
601 }
602 return !draft || draft.trim() === '';
603 }
604
Milutin Kristoficafae0052020-09-17 10:38:08 +0200605 _handleSaveKey(e: Event) {
606 if (
607 !this._computeSaveDisabled(this._messageText, this.comment, this.resolved)
608 ) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100609 e.preventDefault();
610 this._handleSave(e);
611 }
612 }
613
Milutin Kristoficafae0052020-09-17 10:38:08 +0200614 _handleEsc(e: Event) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100615 if (!this._messageText.length) {
616 e.preventDefault();
617 this._handleCancel(e);
618 }
619 }
620
621 _handleToggleCollapsed() {
622 this.collapsed = !this.collapsed;
623 }
624
Milutin Kristoficafae0052020-09-17 10:38:08 +0200625 _toggleCollapseClass(collapsed: boolean) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100626 if (collapsed) {
627 this.$.container.classList.add('collapsed');
628 } else {
629 this.$.container.classList.remove('collapsed');
630 }
631 }
632
Milutin Kristoficafae0052020-09-17 10:38:08 +0200633 @observe('comment.message')
634 _commentMessageChanged(message: string) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100635 this._messageText = message || '';
636 }
637
Milutin Kristoficafae0052020-09-17 10:38:08 +0200638 _messageTextChanged(_: string, oldValue: string) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100639 if (!this.comment || (this.comment && this.comment.id)) {
640 return;
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100641 }
Andrew Bonventre78792e82016-03-04 17:48:22 -0500642
Milutin Kristofica6af5aa2020-09-23 09:08:14 +0200643 const patchNum = this.comment.patch_set
644 ? this.comment.patch_set
645 : this._getPatchNum();
Milutin Kristofic87f402b2020-09-23 17:46:40 +0200646 const {path, line, range} = this.comment;
Dhruv Srivastavadbddc1b2020-09-24 21:23:27 +0200647 if (path) {
Milutin Kristofic87f402b2020-09-23 17:46:40 +0200648 this.debounce(
649 'store',
650 () => {
651 const message = this._messageText;
652 if (this.changeNum === undefined) {
653 throw new Error('undefined changeNum');
654 }
655 const commentLocation: StorageLocation = {
656 changeNum: this.changeNum,
657 patchNum,
658 path,
659 line,
660 range,
661 };
Becky Siegelf3a98942016-12-01 10:55:14 -0800662
Milutin Kristofic87f402b2020-09-23 17:46:40 +0200663 if ((!message || !message.length) && oldValue) {
664 // If the draft has been modified to be empty, then erase the storage
665 // entry.
666 this.$.storage.eraseDraftComment(commentLocation);
667 } else {
668 this.$.storage.setDraftComment(commentLocation, message);
669 }
670 },
671 STORAGE_DEBOUNCE_INTERVAL
672 );
673 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100674 }
675
Milutin Kristoficafae0052020-09-17 10:38:08 +0200676 _handleAnchorClick(e: Event) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100677 e.preventDefault();
Dhruv Srivastavadbddc1b2020-09-24 21:23:27 +0200678 if (!this.comment) return;
Milutin Kristoficafae0052020-09-17 10:38:08 +0200679 this.dispatchEvent(
680 new CustomEvent('comment-anchor-tap', {
681 bubbles: true,
682 composed: true,
683 detail: {
684 number: this.comment.line || FILE,
685 side: this.side,
686 },
687 })
688 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100689 }
690
Milutin Kristoficafae0052020-09-17 10:38:08 +0200691 _handleEdit(e: Event) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100692 e.preventDefault();
Milutin Kristofic87f402b2020-09-23 17:46:40 +0200693 if (this.comment?.message) this._messageText = this.comment.message;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100694 this.editing = true;
Milutin Kristoficda88b332020-03-24 10:19:12 +0100695 this.reporting.recordDraftInteraction();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100696 }
697
Milutin Kristoficafae0052020-09-17 10:38:08 +0200698 _handleSave(e: Event) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100699 e.preventDefault();
700
701 // Ignore saves started while already saving.
702 if (this.disabled) {
703 return;
704 }
Milutin Kristofica6af5aa2020-09-23 09:08:14 +0200705 const timingLabel = this.comment?.id
Milutin Kristoficafae0052020-09-17 10:38:08 +0200706 ? REPORT_UPDATE_DRAFT
707 : REPORT_CREATE_DRAFT;
Milutin Kristoficda88b332020-03-24 10:19:12 +0100708 const timer = this.reporting.getTimer(timingLabel);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100709 this.set('comment.__editing', false);
Milutin Kristoficafae0052020-09-17 10:38:08 +0200710 return this.save().then(() => {
711 timer.end();
712 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100713 }
714
Milutin Kristoficafae0052020-09-17 10:38:08 +0200715 _handleCancel(e: Event) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100716 e.preventDefault();
717
Milutin Kristoficafae0052020-09-17 10:38:08 +0200718 if (
Milutin Kristofica6af5aa2020-09-23 09:08:14 +0200719 !this.comment?.message ||
Milutin Kristoficafae0052020-09-17 10:38:08 +0200720 this.comment.message.trim().length === 0 ||
721 !this.comment.id
722 ) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100723 this._fireDiscard();
724 return;
725 }
726 this._messageText = this.comment.message;
727 this.editing = false;
728 }
729
730 _fireDiscard() {
731 this.cancelDebouncer('fire-update');
Milutin Kristoficafae0052020-09-17 10:38:08 +0200732 this.dispatchEvent(
733 new CustomEvent('comment-discard', {
734 detail: this._getEventPayload(),
735 composed: true,
736 bubbles: true,
737 })
738 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100739 }
740
741 _handleFix() {
Milutin Kristoficafae0052020-09-17 10:38:08 +0200742 this.dispatchEvent(
743 new CustomEvent('create-fix-comment', {
744 bubbles: true,
745 composed: true,
746 detail: this._getEventPayload(),
747 })
748 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100749 }
750
751 _handleShowFix() {
Milutin Kristoficafae0052020-09-17 10:38:08 +0200752 this.dispatchEvent(
753 new CustomEvent('open-fix-preview', {
754 bubbles: true,
755 composed: true,
756 detail: this._getEventPayload(),
757 })
758 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100759 }
760
Ben Rohlfs1d487062020-09-26 11:26:03 +0200761 _hasNoFix(comment: UIComment) {
762 return !comment || !(comment as UIRobot).fix_suggestions;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100763 }
764
Milutin Kristoficafae0052020-09-17 10:38:08 +0200765 _handleDiscard(e: Event) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100766 e.preventDefault();
Milutin Kristoficda88b332020-03-24 10:19:12 +0100767 this.reporting.recordDraftInteraction();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100768
769 if (!this._messageText) {
770 this._discardDraft();
771 return;
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100772 }
Becky Siegel6bf4e4f2016-10-06 10:18:32 -0700773
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100774 this._openOverlay(this.confirmDiscardOverlay).then(() => {
Milutin Kristoficafae0052020-09-17 10:38:08 +0200775 const dialog = this.confirmDiscardOverlay?.querySelector(
776 '#confirmDiscardDialog'
777 ) as GrDialog | null;
778 if (dialog) dialog.resetFocus();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100779 });
780 }
781
Milutin Kristoficafae0052020-09-17 10:38:08 +0200782 _handleConfirmDiscard(e: Event) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100783 e.preventDefault();
Milutin Kristoficda88b332020-03-24 10:19:12 +0100784 const timer = this.reporting.getTimer(REPORT_DISCARD_DRAFT);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100785 this._closeConfirmDiscardOverlay();
Milutin Kristoficafae0052020-09-17 10:38:08 +0200786 return this._discardDraft().then(() => {
787 timer.end();
788 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100789 }
790
791 _discardDraft() {
Milutin Kristofica6af5aa2020-09-23 09:08:14 +0200792 if (!this.comment) return Promise.reject(new Error('undefined comment'));
Ben Rohlfs1d487062020-09-26 11:26:03 +0200793 if (!isDraft(this.comment)) {
Milutin Kristoficafae0052020-09-17 10:38:08 +0200794 return Promise.reject(new Error('Cannot discard a non-draft comment.'));
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100795 }
796 this.discarding = true;
797 this.editing = false;
798 this.disabled = true;
799 this._eraseDraftComment();
800
801 if (!this.comment.id) {
802 this.disabled = false;
803 this._fireDiscard();
Milutin Kristoficafae0052020-09-17 10:38:08 +0200804 return Promise.resolve();
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100805 }
Andrew Bonventrea12d3cd2016-05-23 19:03:11 -0400806
Milutin Kristoficafae0052020-09-17 10:38:08 +0200807 this._xhrPromise = this._deleteDraft(this.comment)
808 .then(response => {
809 this.disabled = false;
810 if (!response.ok) {
811 this.discarding = false;
812 }
Wyatt Allen7a4aa8c2016-05-18 12:37:53 -0700813
Milutin Kristoficafae0052020-09-17 10:38:08 +0200814 this._fireDiscard();
815 return response;
816 })
817 .catch(err => {
818 this.disabled = false;
819 throw err;
820 });
Wyatt Allen035c74f2016-05-23 13:53:10 -0700821
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100822 return this._xhrPromise;
823 }
824
825 _closeConfirmDiscardOverlay() {
826 this._closeOverlay(this.confirmDiscardOverlay);
827 }
828
Milutin Kristoficafae0052020-09-17 10:38:08 +0200829 _getSavingMessage(numPending: number, requestFailed?: boolean) {
Dhruv Srivastava8b015a62020-07-09 17:45:25 +0200830 if (requestFailed) {
831 return UNSAVED_MESSAGE;
832 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100833 if (numPending === 0) {
834 return SAVED_MESSAGE;
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100835 }
Milutin Kristofic5b3f0872020-12-05 22:08:09 +0100836 return `Saving ${pluralize(numPending, 'draft')}...`;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100837 }
Wyatt Allen7a4aa8c2016-05-18 12:37:53 -0700838
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100839 _showStartRequest() {
840 const numPending = ++this._numPendingDraftRequests.number;
841 this._updateRequestToast(numPending);
842 }
843
844 _showEndRequest() {
845 const numPending = --this._numPendingDraftRequests.number;
846 this._updateRequestToast(numPending);
847 }
848
849 _handleFailedDraftRequest() {
850 this._numPendingDraftRequests.number--;
851
852 // Cancel the debouncer so that error toasts from the error-manager will
853 // not be overridden.
854 this.cancelDebouncer('draft-toast');
Milutin Kristoficafae0052020-09-17 10:38:08 +0200855 this._updateRequestToast(
856 this._numPendingDraftRequests.number,
857 /* requestFailed=*/ true
858 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100859 }
860
Milutin Kristoficafae0052020-09-17 10:38:08 +0200861 _updateRequestToast(numPending: number, requestFailed?: boolean) {
Dhruv Srivastava8b015a62020-07-09 17:45:25 +0200862 const message = this._getSavingMessage(numPending, requestFailed);
Milutin Kristoficafae0052020-09-17 10:38:08 +0200863 this.debounce(
864 'draft-toast',
865 () => {
866 // Note: the event is fired on the body rather than this element because
867 // this element may not be attached by the time this executes, in which
868 // case the event would not bubble.
Milutin Kristofic860fe4d2020-11-23 16:13:45 +0100869 fireAlert(document.body, message);
Milutin Kristoficafae0052020-09-17 10:38:08 +0200870 },
871 TOAST_DEBOUNCE_INTERVAL
872 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100873 }
874
Dhruv Srivastava4d38dba2020-08-28 11:29:24 +0200875 _handleDraftFailure() {
876 this.$.container.classList.add('unableToSave');
877 this._unableToSave = true;
878 this._handleFailedDraftRequest();
879 }
880
Ben Rohlfs1d487062020-09-26 11:26:03 +0200881 _saveDraft(draft?: UIComment) {
Milutin Kristofica6af5aa2020-09-23 09:08:14 +0200882 if (!draft || this.changeNum === undefined || this.patchNum === undefined) {
883 throw new Error('undefined draft or changeNum or patchNum');
884 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100885 this._showStartRequest();
Ben Rohlfs43935a42020-12-01 19:14:09 +0100886 return this.restApiService
Milutin Kristoficafae0052020-09-17 10:38:08 +0200887 .saveDiffDraft(this.changeNum, this.patchNum, draft)
888 .then(result => {
889 if (result.ok) {
890 // remove
891 this._unableToSave = false;
892 this.$.container.classList.remove('unableToSave');
893 this._showEndRequest();
894 } else {
Dhruv Srivastava4d38dba2020-08-28 11:29:24 +0200895 this._handleDraftFailure();
Milutin Kristoficafae0052020-09-17 10:38:08 +0200896 }
897 return result;
898 })
899 .catch(err => {
900 this._handleDraftFailure();
901 throw err;
902 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100903 }
904
Ben Rohlfs1d487062020-09-26 11:26:03 +0200905 _deleteDraft(draft: UIComment) {
Milutin Kristofica6af5aa2020-09-23 09:08:14 +0200906 if (this.changeNum === undefined || this.patchNum === undefined) {
907 throw new Error('undefined changeNum or patchNum');
908 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100909 this._showStartRequest();
Ben Rohlfs739b1d22020-09-21 12:26:31 +0200910 if (!draft.id) throw new Error('Missing id in comment draft.');
Ben Rohlfs43935a42020-12-01 19:14:09 +0100911 return this.restApiService
Ben Rohlfs739b1d22020-09-21 12:26:31 +0200912 .deleteDiffDraft(this.changeNum, this.patchNum, {id: draft.id})
Milutin Kristoficafae0052020-09-17 10:38:08 +0200913 .then(result => {
914 if (result.ok) {
915 this._showEndRequest();
916 } else {
917 this._handleFailedDraftRequest();
918 }
919 return result;
920 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100921 }
922
Milutin Kristoficafae0052020-09-17 10:38:08 +0200923 _getPatchNum(): PatchSetNum {
Milutin Kristofica6af5aa2020-09-23 09:08:14 +0200924 const patchNum = this.isOnParent()
925 ? ('PARENT' as PatchSetNum)
926 : this.patchNum;
927 if (patchNum === undefined) throw new Error('patchNum undefined');
928 return patchNum;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100929 }
930
Milutin Kristoficafae0052020-09-17 10:38:08 +0200931 @observe('changeNum', 'patchNum', 'comment')
932 _loadLocalDraft(
933 changeNum: number,
934 patchNum?: PatchSetNum,
Ben Rohlfs1d487062020-09-26 11:26:03 +0200935 comment?: UIComment
Milutin Kristoficafae0052020-09-17 10:38:08 +0200936 ) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100937 // Polymer 2: check for undefined
Dhruv Srivastava68943562020-06-26 12:46:44 +0200938 if ([changeNum, patchNum, comment].includes(undefined)) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100939 return;
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100940 }
Andrew Bonventre78792e82016-03-04 17:48:22 -0500941
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100942 // Only apply local drafts to comments that haven't been saved
943 // remotely, and haven't been given a default message already.
Dhruv Srivastava2e9b6952020-12-03 09:39:28 +0100944 if (!comment || comment.id || comment.message || !comment.path) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100945 return;
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100946 }
Andrew Bonventre78792e82016-03-04 17:48:22 -0500947
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100948 const draft = this.$.storage.getDraftComment({
949 changeNum,
950 patchNum: this._getPatchNum(),
951 path: comment.path,
952 line: comment.line,
953 range: comment.range,
954 });
Wyatt Allenfb3733c2017-10-24 16:34:10 -0700955
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100956 if (draft) {
957 this.set('comment.message', draft.message);
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100958 }
959 }
960
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100961 _handleToggleResolved() {
Milutin Kristoficda88b332020-03-24 10:19:12 +0100962 this.reporting.recordDraftInteraction();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100963 this.resolved = !this.resolved;
964 // Modify payload instead of this.comment, as this.comment is passed from
965 // the parent by ref.
966 const payload = this._getEventPayload();
Milutin Kristofica6af5aa2020-09-23 09:08:14 +0200967 if (!payload.comment) {
968 throw new Error('comment not defined in payload');
969 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100970 payload.comment.unresolved = !this.$.resolvedCheckbox.checked;
Milutin Kristoficafae0052020-09-17 10:38:08 +0200971 this.dispatchEvent(
972 new CustomEvent('comment-update', {
973 detail: payload,
974 composed: true,
975 bubbles: true,
976 })
977 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100978 if (!this.editing) {
979 // Save the resolved state immediately.
980 this.save(payload.comment);
981 }
982 }
983
984 _handleCommentDelete() {
985 this._openOverlay(this.confirmDeleteOverlay);
986 }
987
988 _handleCancelDeleteComment() {
989 this._closeOverlay(this.confirmDeleteOverlay);
990 }
991
Milutin Kristoficafae0052020-09-17 10:38:08 +0200992 _openOverlay(overlay?: GrOverlay | null) {
993 if (!overlay) {
994 return Promise.reject(new Error('undefined overlay'));
995 }
Tao Zhou93a4ed72020-08-21 09:52:02 +0200996 getRootElement().appendChild(overlay);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100997 return overlay.open();
998 }
999
Ben Rohlfs1d487062020-09-26 11:26:03 +02001000 _computeHideRunDetails(comment: UIRobot, collapsed: boolean) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001001 if (!comment) return true;
1002 return !(comment.robot_id && comment.url && !collapsed);
1003 }
1004
Milutin Kristoficafae0052020-09-17 10:38:08 +02001005 _closeOverlay(overlay?: GrOverlay | null) {
1006 if (overlay) {
1007 getRootElement().removeChild(overlay);
1008 overlay.close();
1009 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001010 }
1011
1012 _handleConfirmDeleteComment() {
Milutin Kristoficafae0052020-09-17 10:38:08 +02001013 const dialog = this.confirmDeleteOverlay?.querySelector(
1014 '#confirmDeleteComment'
1015 ) as GrConfirmDeleteCommentDialog | null;
1016 if (!dialog || !dialog.message) {
1017 throw new Error('missing confirm delete dialog');
1018 }
Milutin Kristofica6af5aa2020-09-23 09:08:14 +02001019 if (
1020 !this.comment ||
Ben Rohlfs739b1d22020-09-21 12:26:31 +02001021 !this.comment.id ||
Milutin Kristofica6af5aa2020-09-23 09:08:14 +02001022 this.changeNum === undefined ||
1023 this.patchNum === undefined
1024 ) {
Ben Rohlfs739b1d22020-09-21 12:26:31 +02001025 throw new Error('undefined comment or id or changeNum or patchNum');
Milutin Kristofica6af5aa2020-09-23 09:08:14 +02001026 }
Ben Rohlfs43935a42020-12-01 19:14:09 +01001027 this.restApiService
Milutin Kristoficafae0052020-09-17 10:38:08 +02001028 .deleteComment(
1029 this.changeNum,
1030 this.patchNum,
1031 this.comment.id,
1032 dialog.message
1033 )
1034 .then(newComment => {
1035 this._handleCancelDeleteComment();
1036 this.comment = newComment;
1037 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001038 }
1039}
1040
Milutin Kristoficafae0052020-09-17 10:38:08 +02001041declare global {
1042 interface HTMLElementTagNameMap {
1043 'gr-comment': GrComment;
1044 }
1045}