blob: 7499082b650703c5bc9fae5719a9e85598bd8fb9 [file] [log] [blame]
Dave Borowitz8cdc76b2018-03-26 10:04:27 -04001/**
2 * @license
Ben Rohlfs94fcbbc2022-05-27 10:45:03 +02003 * Copyright 2015 Google LLC
4 * SPDX-License-Identifier: Apache-2.0
Dave Borowitz8cdc76b2018-03-26 10:04:27 -04005 */
Milutin Kristoficafae0052020-09-17 10:38:08 +02006import '../../../styles/shared-styles';
7import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
8import '../../plugins/gr-endpoint-param/gr-endpoint-param';
9import '../gr-button/gr-button';
10import '../gr-dialog/gr-dialog';
Milutin Kristoficafae0052020-09-17 10:38:08 +020011import '../gr-formatted-text/gr-formatted-text';
Chris Poucet1c713862022-07-25 13:12:24 +020012import '../gr-icon/gr-icon';
Milutin Kristoficafae0052020-09-17 10:38:08 +020013import '../gr-textarea/gr-textarea';
14import '../gr-tooltip-content/gr-tooltip-content';
15import '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
16import '../gr-account-label/gr-account-label';
Chris Poucetc6e880b2021-11-15 19:57:06 +010017import {getAppContext} from '../../../services/app-context';
Milutin Kristofic1d219672022-06-21 14:57:25 +020018import {css, html, LitElement, nothing, PropertyValues} from 'lit';
Frank Borden42c1a452022-08-11 16:27:20 +020019import {customElement, property, query, state} from 'lit/decorators.js';
Milutin Kristofice9dbbe92023-05-17 21:21:28 +020020import {provide, resolve} from '../../../models/dependency';
Milutin Kristoficafae0052020-09-17 10:38:08 +020021import {GrTextarea} from '../gr-textarea/gr-textarea';
Milutin Kristoficafae0052020-09-17 10:38:08 +020022import {
Milutin Kristoficafae0052020-09-17 10:38:08 +020023 AccountDetailInfo,
Dhruv Srivastava65edec82023-02-28 19:37:59 +010024 DraftInfo,
Ben Rohlfs4401b232021-10-21 13:51:59 +020025 NumericChangeId,
Dhruv Srivastava0287bf92020-09-11 16:56:38 +020026 RepoName,
Ben Rohlfs05750b92021-10-29 08:23:08 +020027 RobotCommentInfo,
Dhruv Srivastava4e27dc42023-03-01 10:49:49 +010028 Comment,
Dhruv Srivastava4e27dc42023-03-01 10:49:49 +010029 isRobot,
Ben Rohlfs1efb1a72023-04-12 23:25:31 +020030 isSaving,
31 isError,
Ben Rohlfs1efb1a72023-04-12 23:25:31 +020032 isDraft,
Ben Rohlfs610bb4f2023-04-17 12:34:35 +020033 isNew,
Chris Poucet77226982023-08-10 18:10:04 +020034 CommentInput,
Milutin Kristoficafae0052020-09-17 10:38:08 +020035} from '../../../types/common';
Milutin Kristoficafae0052020-09-17 10:38:08 +020036import {GrConfirmDeleteCommentDialog} from '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
Ben Rohlfs1d487062020-09-26 11:26:03 +020037import {
Chris Poucet77226982023-08-10 18:10:04 +020038 convertToCommentInput,
Ben Rohlfs23843882022-08-04 18:06:27 +020039 createUserFixSuggestion,
Milutin Kristofic1d219672022-06-21 14:57:25 +020040 getContentInCommentRange,
Ben Rohlfs23843882022-08-04 18:06:27 +020041 getUserSuggestion,
Milutin Kristofic1d219672022-06-21 14:57:25 +020042 hasUserSuggestion,
Ben Rohlfsba440822023-04-11 18:08:03 +020043 id,
Ben Rohlfs23843882022-08-04 18:06:27 +020044 NEWLINE_PATTERN,
Milutin Kristofic1d219672022-06-21 14:57:25 +020045 USER_SUGGESTION_START_PATTERN,
Ben Rohlfs31825d82020-10-02 18:08:04 +020046} from '../../../utils/comment-util';
Ben Rohlfs05750b92021-10-29 08:23:08 +020047import {
48 OpenFixPreviewEventDetail,
Ben Rohlfs23843882022-08-04 18:06:27 +020049 ReplyToCommentEventDetail,
Ben Rohlfs05750b92021-10-29 08:23:08 +020050 ValueChangedEvent,
51} from '../../../types/events';
Ben Rohlfs44f01042023-02-18 13:27:57 +010052import {fire} from '../../../utils/event-util';
Ben Rohlfs23843882022-08-04 18:06:27 +020053import {assertIsDefined, assert} from '../../../utils/common-util';
Dhruv Srivastavaa49dffd2022-10-20 14:04:37 +020054import {Key, Modifier, whenVisible} from '../../../utils/dom-util';
Chris Poucetdae98bf2022-01-05 15:23:45 +010055import {commentsModelToken} from '../../../models/comments/comments-model';
Ben Rohlfs05750b92021-10-29 08:23:08 +020056import {sharedStyles} from '../../../styles/shared-styles';
57import {subscribe} from '../../lit/subscription-controller';
58import {ShortcutController} from '../../lit/shortcut-controller';
Frank Borden42c1a452022-08-11 16:27:20 +020059import {classMap} from 'lit/directives/class-map.js';
Ben Rohlfsb9956102023-05-12 17:07:06 +020060import {FILE, LineNumber} from '../../../api/diff';
Milutin Kristofic1d219672022-06-21 14:57:25 +020061import {CommentSide, SpecialFilePath} from '../../../constants/constants';
Ben Rohlfs2e237552021-11-24 10:34:28 +010062import {Subject} from 'rxjs';
63import {debounceTime} from 'rxjs/operators';
Chris Poucetbf65b8f2022-01-18 21:18:12 +000064import {changeModelToken} from '../../../models/change/change-model';
Milutin Kristofic1d219672022-06-21 14:57:25 +020065import {isBase64FileContent} from '../../../api/rest-api';
Ben Rohlfsb91a6a42023-01-13 09:29:31 +010066import {createDiffUrl} from '../../../models/views/change';
Chris Poucetbb0cf832022-10-24 12:32:10 +020067import {userModelToken} from '../../../models/user/user-model';
Dhruv Srivastava4063d262022-11-09 18:46:29 +053068import {modalStyles} from '../../../styles/gr-modal-styles';
Milutin Kristofic734df552023-08-07 10:38:50 +020069import {KnownExperimentId} from '../../../services/flags/flags';
70import {pluginLoaderToken} from '../gr-js-api-interface/gr-plugin-loader';
Milutin Kristofice9dbbe92023-05-17 21:21:28 +020071import {
72 CommentModel,
73 commentModelToken,
74} from '../gr-comment-model/gr-comment-model';
Milutin Kristoficaa1c08b2023-09-06 10:34:16 +020075import {formStyles} from '../../../styles/form-styles';
Wyatt Allen846ac2f2018-05-14 12:59:23 -070076
Ben Rohlfs2e237552021-11-24 10:34:28 +010077// visible for testing
78export const AUTO_SAVE_DEBOUNCE_DELAY_MS = 2000;
79
Ben Rohlfs05750b92021-10-29 08:23:08 +020080declare global {
81 interface HTMLElementEventMap {
Dhruv Srivastavaee018e92022-08-31 11:37:46 +020082 'comment-editing-changed': CustomEvent<CommentEditingChangedDetail>;
Dhruv Srivastava463bb332022-08-31 13:00:49 +020083 'comment-unresolved-changed': ValueChangedEvent<boolean>;
84 'comment-text-changed': ValueChangedEvent<string>;
Ben Rohlfs05750b92021-10-29 08:23:08 +020085 'comment-anchor-tap': CustomEvent<CommentAnchorTapEventDetail>;
86 }
Milutin Kristoficafae0052020-09-17 10:38:08 +020087}
88
Ben Rohlfs05750b92021-10-29 08:23:08 +020089export interface CommentAnchorTapEventDetail {
90 number: LineNumber;
91 side?: CommentSide;
Milutin Kristoficafae0052020-09-17 10:38:08 +020092}
Dmitrii Filippov3f3c2052020-09-22 16:51:18 +020093
Dhruv Srivastavaee018e92022-08-31 11:37:46 +020094export interface CommentEditingChangedDetail {
95 editing: boolean;
96 path: string;
97}
98
Milutin Kristoficafae0052020-09-17 10:38:08 +020099@customElement('gr-comment')
Ben Rohlfs05750b92021-10-29 08:23:08 +0200100export class GrComment extends LitElement {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100101 /**
Ben Rohlfs23843882022-08-04 18:06:27 +0200102 * Fired when the parent thread component should create a reply.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100103 *
Ben Rohlfs23843882022-08-04 18:06:27 +0200104 * @event reply-to-comment
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100105 */
Kasper Nilssond43d2a72018-10-19 14:26:41 -0700106
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100107 /**
Ben Rohlfs23843882022-08-04 18:06:27 +0200108 * Fired when the open fix preview action is triggered.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100109 *
110 * @event open-fix-preview
Tao Zhou500437d2020-02-14 16:57:27 +0100111 */
Tao Zhou500437d2020-02-14 16:57:27 +0100112
113 /**
Tao Zhou31f3f102020-04-27 16:15:29 +0200114 * Fired when editing status changed.
115 *
116 * @event comment-editing-changed
117 */
118
119 /**
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100120 * Fired when the comment's timestamp is tapped.
121 *
122 * @event comment-anchor-tap
123 */
Andrew Bonventre28165262016-05-19 17:24:45 -0700124
Ben Rohlfs05750b92021-10-29 08:23:08 +0200125 @query('#editTextarea')
126 textarea?: GrTextarea;
Viktar Donich7ad28922016-05-23 15:24:05 -0700127
Ben Rohlfs05750b92021-10-29 08:23:08 +0200128 @query('#container')
129 container?: HTMLElement;
Dhruv Srivastava0287bf92020-09-11 16:56:38 +0200130
Ben Rohlfs05750b92021-10-29 08:23:08 +0200131 @query('#resolvedCheckbox')
132 resolvedCheckbox?: HTMLInputElement;
Kasper Nilssond43d2a72018-10-19 14:26:41 -0700133
Dhruv Srivastava4063d262022-11-09 18:46:29 +0530134 @query('#confirmDeleteModal')
135 confirmDeleteModal?: HTMLDialogElement;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200136
Kamil Musinc7d3f282022-12-29 13:27:55 +0100137 @query('#confirmDeleteCommentDialog')
138 confirmDeleteDialog?: GrConfirmDeleteCommentDialog;
139
Ben Rohlfs05750b92021-10-29 08:23:08 +0200140 @property({type: Object})
141 comment?: Comment;
142
143 // TODO: Move this out of gr-comment. gr-comment should not have a comments
144 // property. This is only used for hasHumanReply at the moment.
Milutin Kristoficafae0052020-09-17 10:38:08 +0200145 @property({type: Array})
Ben Rohlfs05750b92021-10-29 08:23:08 +0200146 comments?: Comment[];
Milutin Kristoficafae0052020-09-17 10:38:08 +0200147
148 /**
Ben Rohlfs05750b92021-10-29 08:23:08 +0200149 * Initial collapsed state of the comment.
Milutin Kristoficafae0052020-09-17 10:38:08 +0200150 */
Ben Rohlfs05750b92021-10-29 08:23:08 +0200151 @property({type: Boolean, attribute: 'initially-collapsed'})
152 initiallyCollapsed?: boolean;
153
154 /**
Dhruv Srivastavae75f6f72022-08-25 10:14:37 +0200155 * Hide the header for patchset level comments used in GrReplyDialog.
156 */
157 @property({type: Boolean, attribute: 'hide-header'})
158 hideHeader = false;
159
160 /**
Ben Rohlfs05750b92021-10-29 08:23:08 +0200161 * This is the *current* (internal) collapsed state of the comment. Do not set
162 * from the outside. Use `initiallyCollapsed` instead. This is just a
163 * reflected property such that css rules can be based on it.
164 */
165 @property({type: Boolean, reflect: true})
166 collapsed?: boolean;
167
168 @property({type: Boolean, attribute: 'robot-button-disabled'})
169 robotButtonDisabled = false;
170
Dhruv Srivastavaf007abc2022-09-06 11:18:57 +0200171 @property({type: String})
172 messagePlaceholder?: string;
173
Dhruv Srivastava4e5fd112022-08-25 12:01:22 +0200174 // GrReplyDialog requires the patchset level comment to always remain
175 // editable.
176 @property({type: Boolean, attribute: 'permanent-editing-mode'})
177 permanentEditingMode = false;
178
Chris Poucet77226982023-08-10 18:10:04 +0200179 // Whether to disable autosaving
180 @property({type: Boolean})
181 disableAutoSaving = false;
182
Ben Rohlfs2e237552021-11-24 10:34:28 +0100183 @state()
Ben Rohlfs607126f2021-12-07 08:21:52 +0100184 autoSaving?: Promise<DraftInfo>;
Ben Rohlfs2e237552021-11-24 10:34:28 +0100185
Ben Rohlfs05750b92021-10-29 08:23:08 +0200186 @state()
187 changeNum?: NumericChangeId;
188
189 @state()
190 editing = false;
191
192 @state()
Ben Rohlfs05750b92021-10-29 08:23:08 +0200193 repoName?: RepoName;
194
195 /* The 'dirty' state of the comment.message, which will be saved on demand. */
196 @state()
197 messageText = '';
198
199 /* The 'dirty' state of !comment.unresolved, which will be saved on demand. */
200 @state()
201 unresolved = true;
Milutin Kristoficafae0052020-09-17 10:38:08 +0200202
Ben Rohlfs05750b92021-10-29 08:23:08 +0200203 @property({type: Boolean, attribute: 'show-patchset'})
204 showPatchset = false;
Tao Zhou500437d2020-02-14 16:57:27 +0100205
Ben Rohlfs05750b92021-10-29 08:23:08 +0200206 @property({type: Boolean, attribute: 'show-ported-comment'})
Dhruv Srivastava0287bf92020-09-11 16:56:38 +0200207 showPortedComment = false;
208
Ben Rohlfs05750b92021-10-29 08:23:08 +0200209 @state()
210 account?: AccountDetailInfo;
211
212 @state()
213 isAdmin = false;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100214
Milutin Kristofic15bfb3e2022-08-16 20:01:33 +0200215 @state()
216 isOwner = false;
217
Chris Poucetc6e880b2021-11-15 19:57:06 +0100218 private readonly restApiService = getAppContext().restApiService;
Ben Rohlfs43935a42020-12-01 19:14:09 +0100219
Chris Poucetc6e880b2021-11-15 19:57:06 +0100220 private readonly reporting = getAppContext().reportingService;
Milutin Kristoficafae0052020-09-17 10:38:08 +0200221
Chris Poucetbf65b8f2022-01-18 21:18:12 +0000222 private readonly getChangeModel = resolve(this, changeModelToken);
Chris Poucet01422482021-11-30 19:43:28 +0100223
Chris Poucetbb0cf832022-10-24 12:32:10 +0200224 private readonly getCommentsModel = resolve(this, commentsModelToken);
Dhruv Srivastavadb2ab602021-06-24 15:20:29 +0200225
Chris Poucetbb0cf832022-10-24 12:32:10 +0200226 private readonly getUserModel = resolve(this, userModelToken);
Ben Rohlfsf92d3b52021-03-10 23:13:03 +0100227
Milutin Kristofic734df552023-08-07 10:38:50 +0200228 private readonly getPluginLoader = resolve(this, pluginLoaderToken);
229
230 private readonly flagsService = getAppContext().flagsService;
231
Ben Rohlfs05750b92021-10-29 08:23:08 +0200232 private readonly shortcuts = new ShortcutController(this);
Ben Rohlfsf92d3b52021-03-10 23:13:03 +0100233
Milutin Kristofice9dbbe92023-05-17 21:21:28 +0200234 private commentModel = new CommentModel(undefined);
235
Ben Rohlfs2e237552021-11-24 10:34:28 +0100236 /**
237 * This is triggered when the user types into the editing textarea. We then
238 * debounce it and call autoSave().
239 */
Frank Borden3801d7d2023-03-27 09:00:58 +0000240 private autoSaveTrigger$ = new Subject();
Ben Rohlfs2e237552021-11-24 10:34:28 +0100241
242 /**
243 * Set to the content of DraftInfo when entering editing mode.
244 * Only used for "Cancel".
245 */
246 private originalMessage = '';
247
248 /**
249 * Set to the content of DraftInfo when entering editing mode.
250 * Only used for "Cancel".
251 */
252 private originalUnresolved = false;
253
Ben Rohlfs05750b92021-10-29 08:23:08 +0200254 constructor() {
255 super();
Milutin Kristofice9dbbe92023-05-17 21:21:28 +0200256 provide(this, commentModelToken, () => this.commentModel);
Dhruv Srivastavae110a372022-09-08 12:18:33 +0200257 // Allow the shortcuts to bubble up so that GrReplyDialog can respond to
258 // them as well.
259 this.shortcuts.addLocal({key: Key.ESC}, () => this.handleEsc(), {
260 preventDefault: false,
261 });
Dhruv Srivastavaf43eee72022-09-14 11:03:01 +0200262 for (const modifier of [Modifier.CTRL_KEY, Modifier.META_KEY]) {
263 this.shortcuts.addLocal(
264 {key: Key.ENTER, modifiers: [modifier]},
265 () => {
266 this.save();
267 },
268 {preventDefault: false}
269 );
270 }
271 // For Ctrl+s add shorctut with preventDefault so that it does
272 // not bubble up to the browser
273 for (const modifier of [Modifier.CTRL_KEY, Modifier.META_KEY]) {
274 this.shortcuts.addLocal({key: 's', modifiers: [modifier]}, () => {
275 this.save();
276 });
Ben Rohlfsaadbdd12021-10-19 11:49:01 +0200277 }
Milutin Kristofic1ebae372022-11-22 20:35:38 +0100278 this.addEventListener('open-user-suggest-preview', e => {
279 this.handleShowFix(e.detail.code);
280 });
Ben Rohlfsb7082e12023-01-23 11:43:48 +0100281 this.messagePlaceholder = 'Mention others with @';
Chris Poucet0b961412022-01-05 16:24:50 +0100282 subscribe(
283 this,
Chris Poucetbb0cf832022-10-24 12:32:10 +0200284 () => this.getUserModel().account$,
Chris Poucet5ec77f02022-05-12 11:25:21 +0200285 x => (this.account = x)
286 );
287 subscribe(
288 this,
Chris Poucetbb0cf832022-10-24 12:32:10 +0200289 () => this.getUserModel().isAdmin$,
Chris Poucet5ec77f02022-05-12 11:25:21 +0200290 x => (this.isAdmin = x)
291 );
292
293 subscribe(
294 this,
295 () => this.getChangeModel().repo$,
296 x => (this.repoName = x)
297 );
298 subscribe(
299 this,
300 () => this.getChangeModel().changeNum$,
Chris Poucetbf65b8f2022-01-18 21:18:12 +0000301 x => (this.changeNum = x)
302 );
303 subscribe(
304 this,
Milutin Kristofic15bfb3e2022-08-16 20:01:33 +0200305 () => this.getChangeModel().isOwner$,
306 x => (this.isOwner = x)
307 );
308 subscribe(
309 this,
Chris Poucet5ec77f02022-05-12 11:25:21 +0200310 () =>
311 this.autoSaveTrigger$.pipe(debounceTime(AUTO_SAVE_DEBOUNCE_DELAY_MS)),
Chris Poucetbf65b8f2022-01-18 21:18:12 +0000312 () => {
313 this.autoSave();
314 }
315 );
Chris Poucet0b961412022-01-05 16:24:50 +0100316 }
317
Gerrit Code Review86b969c2021-08-19 14:33:41 +0000318 override disconnectedCallback() {
Ben Rohlfs05750b92021-10-29 08:23:08 +0200319 // Clean up emoji dropdown.
320 if (this.textarea) this.textarea.closeDropdown();
Ben Rohlfs5f520da2021-03-10 14:58:43 +0100321 super.disconnectedCallback();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100322 }
Andrew Bonventre78792e82016-03-04 17:48:22 -0500323
Ben Rohlfs05750b92021-10-29 08:23:08 +0200324 static override get styles() {
325 return [
Milutin Kristoficaa1c08b2023-09-06 10:34:16 +0200326 formStyles,
Ben Rohlfs05750b92021-10-29 08:23:08 +0200327 sharedStyles,
Dhruv Srivastava4063d262022-11-09 18:46:29 +0530328 modalStyles,
Ben Rohlfs05750b92021-10-29 08:23:08 +0200329 css`
330 :host {
331 display: block;
332 font-family: var(--font-family);
333 padding: var(--spacing-m);
334 }
335 :host([collapsed]) {
336 padding: var(--spacing-s) var(--spacing-m);
337 }
Ben Rohlfs1efb1a72023-04-12 23:25:31 +0200338 :host([error]) {
339 background-color: var(--error-background);
340 border-radius: var(--border-radius);
Ben Rohlfs05750b92021-10-29 08:23:08 +0200341 }
Ben Rohlfs05750b92021-10-29 08:23:08 +0200342 .header {
343 align-items: center;
344 cursor: pointer;
345 display: flex;
Dhruv Srivastavad8f61e72022-09-16 07:34:34 +0000346 padding-bottom: var(--spacing-m);
347 }
348 :host([collapsed]) .header {
349 padding-bottom: 0px;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200350 }
351 .headerLeft > span {
352 font-weight: var(--font-weight-bold);
353 }
354 .headerMiddle {
355 color: var(--deemphasized-text-color);
356 flex: 1;
357 overflow: hidden;
358 }
Ben Rohlfs05750b92021-10-29 08:23:08 +0200359 .draftTooltip {
Ben Rohlfsba361a42022-09-01 12:12:45 +0200360 font-weight: var(--font-weight-bold);
Ben Rohlfs05750b92021-10-29 08:23:08 +0200361 display: inline;
362 }
Ben Rohlfsba361a42022-09-01 12:12:45 +0200363 .draftTooltip gr-icon {
364 color: var(--info-foreground);
365 }
Ben Rohlfs05750b92021-10-29 08:23:08 +0200366 .date {
367 justify-content: flex-end;
368 text-align: right;
369 white-space: nowrap;
370 }
371 span.date {
372 color: var(--deemphasized-text-color);
373 }
374 span.date:hover {
375 text-decoration: underline;
376 }
377 .actions,
378 .robotActions {
379 display: flex;
380 justify-content: flex-end;
381 padding-top: 0;
382 }
383 .robotActions {
384 /* Better than the negative margin would be to remove the gr-button
385 * padding, but then we would also need to fix the buttons that are
386 * inserted by plugins. :-/ */
387 margin: 4px 0 -4px;
388 }
389 .action {
390 margin-left: var(--spacing-l);
391 }
392 .rightActions {
393 display: flex;
394 justify-content: flex-end;
395 }
396 .rightActions gr-button {
397 --gr-button-padding: 0 var(--spacing-s);
398 }
399 .editMessage {
400 display: block;
Dhruv Srivastava694e9372022-09-13 10:29:08 +0200401 margin-bottom: var(--spacing-m);
Ben Rohlfs05750b92021-10-29 08:23:08 +0200402 width: 100%;
403 }
404 .show-hide {
405 margin-left: var(--spacing-s);
406 }
407 .robotId {
408 color: var(--deemphasized-text-color);
409 margin-bottom: var(--spacing-m);
410 }
411 .robotRun {
412 margin-left: var(--spacing-m);
413 }
414 .robotRunLink {
415 margin-left: var(--spacing-m);
416 }
417 /* just for a11y */
418 input.show-hide {
419 display: none;
420 }
421 label.show-hide {
422 cursor: pointer;
423 display: block;
424 }
Chris Poucet1c713862022-07-25 13:12:24 +0200425 label.show-hide gr-icon {
Ben Rohlfs05750b92021-10-29 08:23:08 +0200426 vertical-align: top;
427 }
428 :host([collapsed]) #container .body {
429 padding-top: 0;
430 }
431 #container .collapsedContent {
432 display: block;
433 overflow: hidden;
434 padding-left: var(--spacing-m);
435 text-overflow: ellipsis;
436 white-space: nowrap;
437 }
438 .resolve,
439 .unresolved {
440 align-items: center;
441 display: flex;
442 flex: 1;
443 margin: 0;
444 }
445 .resolve label {
446 color: var(--comment-text-color);
447 }
448 gr-dialog .main {
449 display: flex;
450 flex-direction: column;
451 width: 100%;
452 }
453 #deleteBtn {
454 --gr-button-text-color: var(--deemphasized-text-color);
455 --gr-button-padding: 0;
456 }
457
458 /** Disable select for the caret and actions */
459 .actions,
460 .show-hide {
461 -webkit-user-select: none;
462 -moz-user-select: none;
463 -ms-user-select: none;
464 user-select: none;
465 }
466
Ben Rohlfs05750b92021-10-29 08:23:08 +0200467 .pointer {
468 cursor: pointer;
469 }
470 .patchset-text {
471 color: var(--deemphasized-text-color);
472 margin-left: var(--spacing-s);
473 }
474 .headerLeft gr-account-label {
475 --account-max-length: 130px;
476 width: 150px;
477 }
478 .headerLeft gr-account-label::part(gr-account-label-text) {
479 font-weight: var(--font-weight-bold);
480 }
481 .draft gr-account-label {
482 width: unset;
483 }
Frank Borden0c078842022-09-19 15:47:26 +0200484 .draft gr-formatted-text.message {
Frank Borden3b3a4c92022-09-28 14:14:00 +0200485 display: block;
Frank Borden0c078842022-09-19 15:47:26 +0200486 margin-bottom: var(--spacing-m);
487 }
Ben Rohlfs05750b92021-10-29 08:23:08 +0200488 .portedMessage {
489 margin: 0 var(--spacing-m);
490 }
491 .link-icon {
Chris Poucetc4142042022-06-28 17:51:50 +0200492 margin-left: var(--spacing-m);
Ben Rohlfs05750b92021-10-29 08:23:08 +0200493 cursor: pointer;
494 }
Milutin Kristofic80c3c7e2023-03-16 20:22:28 +0100495 .suggestEdit {
496 /** same height as header */
497 --margin: calc(0px - var(--spacing-s));
498 margin-right: var(--spacing-s);
499 }
500 .suggestEdit gr-icon {
501 color: inherit;
502 margin-right: var(--spacing-s);
503 }
Ben Rohlfs05750b92021-10-29 08:23:08 +0200504 `,
505 ];
Dhruv Srivastavacf70e792020-07-24 15:35:39 +0200506 }
507
Ben Rohlfs05750b92021-10-29 08:23:08 +0200508 override render() {
Ben Rohlfs1efb1a72023-04-12 23:25:31 +0200509 if (!this.comment) return;
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200510 this.toggleAttribute('saving', isSaving(this.comment));
511 this.toggleAttribute('error', isError(this.comment));
Ben Rohlfs1efb1a72023-04-12 23:25:31 +0200512 const classes = {
513 container: true,
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200514 draft: isDraft(this.comment),
Ben Rohlfs1efb1a72023-04-12 23:25:31 +0200515 };
Ben Rohlfs05750b92021-10-29 08:23:08 +0200516 return html`
Ben Rohlfs7a167842022-09-29 21:55:50 +0200517 <gr-endpoint-decorator name="comment">
518 <gr-endpoint-param name="comment" .value=${this.comment}>
519 </gr-endpoint-param>
520 <gr-endpoint-param name="editing" .value=${this.editing}>
521 </gr-endpoint-param>
Ben Rohlfs57c2c592022-10-25 12:49:11 +0200522 <gr-endpoint-param name="message" .value=${this.messageText}>
523 </gr-endpoint-param>
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200524 <gr-endpoint-param name="isDraft" .value=${isDraft(this.comment)}>
Ben Rohlfs57c2c592022-10-25 12:49:11 +0200525 </gr-endpoint-param>
Ben Rohlfs7a167842022-09-29 21:55:50 +0200526 <div id="container" class=${classMap(classes)}>
527 ${this.renderHeader()}
528 <div class="body">
529 ${this.renderRobotAuthor()} ${this.renderEditingTextarea()}
530 ${this.renderCommentMessage()}
531 <gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
532 ${this.renderHumanActions()} ${this.renderRobotActions()}
Ben Rohlfs7a167842022-09-29 21:55:50 +0200533 </div>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200534 </div>
Ben Rohlfs7a167842022-09-29 21:55:50 +0200535 </gr-endpoint-decorator>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200536 ${this.renderConfirmDialog()}
537 `;
538 }
539
Dhruv Srivastavae75f6f72022-08-25 10:14:37 +0200540 private renderHeader() {
541 if (this.hideHeader) return nothing;
542 return html`
543 <div
544 class="header"
545 id="header"
546 @click=${() => (this.collapsed = !this.collapsed)}
547 >
548 <div class="headerLeft">
549 ${this.renderAuthor()} ${this.renderPortedCommentMessage()}
550 ${this.renderDraftLabel()}
551 </div>
552 <div class="headerMiddle">${this.renderCollapsedContent()}</div>
Milutin Kristofic8238de52023-01-12 19:33:45 +0100553 ${this.renderSuggestEditButton()} ${this.renderRunDetails()}
554 ${this.renderDeleteButton()} ${this.renderPatchset()}
Ben Rohlfs0ed33592023-04-12 11:33:27 +0200555 ${this.renderSeparator()} ${this.renderDate()} ${this.renderToggle()}
Dhruv Srivastavae75f6f72022-08-25 10:14:37 +0200556 </div>
557 `;
558 }
559
Ben Rohlfs05750b92021-10-29 08:23:08 +0200560 private renderAuthor() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200561 if (isDraft(this.comment)) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200562 if (isRobot(this.comment)) {
563 const id = this.comment.robot_id;
564 return html`<span class="robotName">${id}</span>`;
565 }
Ben Rohlfs05750b92021-10-29 08:23:08 +0200566 return html`
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200567 <gr-account-label .account=${this.comment?.author ?? this.account}>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200568 </gr-account-label>
569 `;
570 }
571
572 private renderPortedCommentMessage() {
573 if (!this.showPortedComment) return;
574 if (!this.comment?.patch_set) return;
575 return html`
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200576 <a href=${this.getUrlForComment()}>
577 <span class="portedMessage" @click=${this.handlePortedMessageClick}>
Ben Rohlfs95796222021-12-01 16:39:42 +0100578 From patchset ${this.comment?.patch_set}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200579 </span>
580 </a>
581 `;
582 }
583
584 private renderDraftLabel() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200585 if (!isDraft(this.comment)) return;
Ben Rohlfsba361a42022-09-01 12:12:45 +0200586 let label = 'Draft';
Ben Rohlfs05750b92021-10-29 08:23:08 +0200587 let tooltip =
588 'This draft is only visible to you. ' +
589 "To publish drafts, click the 'Reply' or 'Start review' button " +
590 "at the top of the change or press the 'a' key.";
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200591 if (isError(this.comment)) {
Ben Rohlfs05750b92021-10-29 08:23:08 +0200592 label += ' (Failed to save)';
593 tooltip = 'Unable to save draft. Please try to save again.';
594 }
595 return html`
596 <gr-tooltip-content
597 class="draftTooltip"
598 has-tooltip
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200599 title=${tooltip}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200600 max-width="20em"
Ben Rohlfs05750b92021-10-29 08:23:08 +0200601 >
Ben Rohlfsba361a42022-09-01 12:12:45 +0200602 <gr-icon filled icon="rate_review"></gr-icon>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200603 <span class="draftLabel">${label}</span>
604 </gr-tooltip-content>
605 `;
606 }
607
608 private renderCollapsedContent() {
609 if (!this.collapsed) return;
610 return html`
611 <span class="collapsedContent">${this.comment?.message}</span>
612 `;
613 }
614
615 private renderRunDetails() {
616 if (!isRobot(this.comment)) return;
617 if (!this.comment?.url || this.collapsed) return;
618 return html`
619 <div class="runIdMessage message">
620 <div class="runIdInformation">
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200621 <a class="robotRunLink" href=${this.comment.url}>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200622 <span class="robotRun link">Run Details</span>
623 </a>
624 </div>
625 </div>
626 `;
627 }
628
629 /**
630 * Deleting a comment is an admin feature. It means more than just discarding
631 * a draft. It is an action applied to published comments.
632 */
633 private renderDeleteButton() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200634 if (!this.isAdmin || isDraft(this.comment) || isRobot(this.comment)) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200635 if (this.collapsed) return;
636 return html`
637 <gr-button
638 id="deleteBtn"
639 title="Delete Comment"
640 link
641 class="action delete"
Kamil Musin462428b2022-12-29 11:12:08 +0100642 @click=${(e: MouseEvent) => {
643 e.stopPropagation();
644 this.openDeleteCommentModal();
645 }}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200646 >
Chris Poucet1c713862022-07-25 13:12:24 +0200647 <gr-icon id="icon" icon="delete" filled></gr-icon>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200648 </gr-button>
649 `;
650 }
651
652 private renderPatchset() {
653 if (!this.showPatchset) return;
654 assertIsDefined(this.comment?.patch_set, 'comment.patch_set');
655 return html`
656 <span class="patchset-text"> Patchset ${this.comment.patch_set}</span>
657 `;
658 }
659
Ben Rohlfs0ed33592023-04-12 11:33:27 +0200660 private renderSeparator() {
661 // This should match the condition of `renderPatchset()`.
662 if (!this.showPatchset) return;
663 // This should match the condition of `renderDate()`.
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200664 if (this.collapsed) return;
Ben Rohlfs0ed33592023-04-12 11:33:27 +0200665 // Render separator, if both are present: patchset AND date.
666 return html`<span class="separator"></span>`;
667 }
668
Ben Rohlfs05750b92021-10-29 08:23:08 +0200669 private renderDate() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200670 if (this.collapsed) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200671 return html`
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200672 <span class="date" tabindex="0" @click=${this.handleAnchorClick}>
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200673 ${this.renderDateInner()}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200674 </span>
675 `;
676 }
677
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200678 private renderDateInner() {
679 if (isError(this.comment)) return 'Error';
Ben Rohlfsd98a04c2023-05-04 11:31:15 +0200680 if (isSaving(this.comment) && !this.autoSaving) return 'Saving';
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200681 if (isNew(this.comment)) return 'New';
682 return html`
683 <gr-date-formatter
684 withTooltip
685 .dateStr=${this.comment!.updated}
686 ></gr-date-formatter>
687 `;
688 }
689
Ben Rohlfs05750b92021-10-29 08:23:08 +0200690 private renderToggle() {
Chris Poucetb8c06392022-07-08 16:35:43 +0200691 const icon = this.collapsed ? 'expand_more' : 'expand_less';
Ben Rohlfs05750b92021-10-29 08:23:08 +0200692 const ariaLabel = this.collapsed ? 'Expand' : 'Collapse';
693 return html`
694 <div class="show-hide" tabindex="0">
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200695 <label class="show-hide" aria-label=${ariaLabel}>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200696 <input
697 type="checkbox"
698 class="show-hide"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200699 ?checked=${this.collapsed}
700 @change=${() => (this.collapsed = !this.collapsed)}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200701 />
Chris Poucet1c713862022-07-25 13:12:24 +0200702 <gr-icon icon=${icon} id="icon"></gr-icon>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200703 </label>
704 </div>
705 `;
706 }
707
708 private renderRobotAuthor() {
709 if (!isRobot(this.comment) || this.collapsed) return;
710 return html`<div class="robotId">${this.comment.author?.name}</div>`;
711 }
712
713 private renderEditingTextarea() {
714 if (!this.editing || this.collapsed) return;
715 return html`
716 <gr-textarea
717 id="editTextarea"
718 class="editMessage"
719 autocomplete="on"
720 code=""
Ben Rohlfs05750b92021-10-29 08:23:08 +0200721 rows="4"
Dhruv Srivastavaf007abc2022-09-06 11:18:57 +0200722 .placeholder=${this.messagePlaceholder}
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200723 text=${this.messageText}
724 @text-changed=${(e: ValueChangedEvent) => {
Ben Rohlfs05750b92021-10-29 08:23:08 +0200725 // TODO: This is causing a re-render of <gr-comment> on every key
726 // press. Try to avoid always setting `this.messageText` or at least
Ben Rohlfs2e237552021-11-24 10:34:28 +0100727 // debounce it. Most of the code can just inspect the current value
Ben Rohlfs05750b92021-10-29 08:23:08 +0200728 // of the textare instead of needing a dedicated property.
729 this.messageText = e.detail.value;
Ben Rohlfs2e237552021-11-24 10:34:28 +0100730 this.autoSaveTrigger$.next();
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200731 }}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200732 ></gr-textarea>
733 `;
734 }
735
Ben Rohlfs05750b92021-10-29 08:23:08 +0200736 private renderCommentMessage() {
737 if (this.collapsed || this.editing) return;
Frank Bordenf9a29992022-08-24 20:19:23 +0200738
Ben Rohlfs05750b92021-10-29 08:23:08 +0200739 return html`
740 <!--The "message" class is needed to ensure selectability from
741 gr-diff-selection.-->
742 <gr-formatted-text
743 class="message"
Frank Bordenabdd1872022-09-26 12:55:59 +0200744 .markdown=${true}
745 .content=${this.comment?.message ?? ''}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200746 ></gr-formatted-text>
747 `;
748 }
749
750 private renderCopyLinkIcon() {
751 // Only show the icon when the thread contains a published comment.
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200752 if (!this.comment?.in_reply_to && isDraft(this.comment)) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200753 return html`
Chris Poucet1c713862022-07-25 13:12:24 +0200754 <gr-icon
755 icon="link"
756 class="copy link-icon"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200757 @click=${this.handleCopyLink}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200758 title="Copy link to this comment"
Ben Rohlfs05750b92021-10-29 08:23:08 +0200759 role="button"
760 tabindex="0"
Chris Poucet1c713862022-07-25 13:12:24 +0200761 ></gr-icon>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200762 `;
763 }
764
765 private renderHumanActions() {
766 if (!this.account || isRobot(this.comment)) return;
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200767 if (this.collapsed || !isDraft(this.comment)) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200768 return html`
769 <div class="actions">
770 <div class="action resolve">
771 <label>
772 <input
773 type="checkbox"
774 id="resolvedCheckbox"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200775 ?checked=${!this.unresolved}
776 @change=${this.handleToggleResolved}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200777 />
778 Resolved
779 </label>
780 </div>
781 ${this.renderDraftActions()}
782 </div>
783 `;
784 }
785
786 private renderDraftActions() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200787 if (!isDraft(this.comment)) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200788 return html`
789 <div class="rightActions">
Milutin Kristofic1ebae372022-11-22 20:35:38 +0100790 ${this.renderDiscardButton()} ${this.renderEditButton()}
Milutin Kristofic734df552023-08-07 10:38:50 +0200791 ${this.renderGenerateSuggestEditButton()} ${this.renderCancelButton()}
792 ${this.renderSaveButton()} ${this.renderCopyLinkIcon()}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200793 </div>
794 `;
795 }
796
Milutin Kristofic1d219672022-06-21 14:57:25 +0200797 private renderSuggestEditButton() {
Dhruv Srivastava66a15632022-09-06 11:57:34 +0200798 if (
Milutin Kristofic8238de52023-01-12 19:33:45 +0100799 !this.editing ||
Dhruv Srivastava66a15632022-09-06 11:57:34 +0200800 this.permanentEditingMode ||
801 this.comment?.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS
802 ) {
803 return nothing;
804 }
Milutin Kristofic1d219672022-06-21 14:57:25 +0200805 assertIsDefined(this.comment, 'comment');
806 if (hasUserSuggestion(this.comment)) return nothing;
807 // TODO(milutin): remove this check once suggesting on commit message is
808 // fixed. Currently diff line doesn't match commit message line, because
809 // of metadata in diff, which aren't in content api request.
810 if (this.comment.path === SpecialFilePath.COMMIT_MESSAGE) return nothing;
Milutin Kristofic50394b12023-02-01 11:28:52 +0000811 if (this.isOwner) return nothing;
Milutin Kristofic7dec89b2022-09-13 12:11:35 +0200812 return html`<gr-button
813 link
814 class="action suggestEdit"
Milutin Kristofic80c3c7e2023-03-16 20:22:28 +0100815 title="This button copies the text to make a suggestion"
Milutin Kristofic7dec89b2022-09-13 12:11:35 +0200816 @click=${this.createSuggestEdit}
Milutin Kristofic80c3c7e2023-03-16 20:22:28 +0100817 ><gr-icon icon="edit" id="icon" filled></gr-icon> Suggest edit</gr-button
Milutin Kristofic1d219672022-06-21 14:57:25 +0200818 >`;
819 }
820
Ben Rohlfs05750b92021-10-29 08:23:08 +0200821 private renderDiscardButton() {
Dhruv Srivastava705eac92022-09-01 11:33:27 +0200822 if (this.editing || this.permanentEditingMode) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200823 return html`<gr-button
824 link
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200825 ?disabled=${isSaving(this.comment) && !this.autoSaving}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200826 class="action discard"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200827 @click=${this.discard}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200828 >Discard</gr-button
829 >`;
830 }
831
832 private renderEditButton() {
833 if (this.editing) return;
Ben Rohlfs1efb1a72023-04-12 23:25:31 +0200834 return html`<gr-button link class="action edit" @click=${this.edit}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200835 >Edit</gr-button
836 >`;
837 }
838
839 private renderCancelButton() {
Dhruv Srivastava705eac92022-09-01 11:33:27 +0200840 if (!this.editing || this.permanentEditingMode) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200841 return html`
842 <gr-button
843 link
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200844 ?disabled=${isSaving(this.comment) && !this.autoSaving}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200845 class="action cancel"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200846 @click=${this.cancel}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200847 >Cancel</gr-button
848 >
849 `;
850 }
851
852 private renderSaveButton() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200853 if (!this.editing) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200854 return html`
855 <gr-button
856 link
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200857 ?disabled=${this.isSaveDisabled()}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200858 class="action save"
Dhruv Srivastava705eac92022-09-01 11:33:27 +0200859 @click=${this.handleSaveButtonClicked}
Dhruv Srivastava00831e72022-09-05 08:20:20 +0200860 >${this.permanentEditingMode ? 'Preview' : 'Save'}</gr-button
Ben Rohlfs05750b92021-10-29 08:23:08 +0200861 >
862 `;
863 }
864
Milutin Kristofic734df552023-08-07 10:38:50 +0200865 // TODO(milutin): This is temporary solution for experimenting
866 private renderGenerateSuggestEditButton() {
867 if (!this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT)) {
868 return nothing;
869 }
870 return html`
871 <gr-button link class="action" @click=${this.generateSuggestEdit}
872 >Suggestion</gr-button
873 >
874 `;
875 }
876
877 // TODO(milutin): This is temporary solution for experimenting
878 private async generateSuggestEdit() {
879 const suggestionsPlugins =
880 this.getPluginLoader().pluginsModel.getState().suggestionsPlugins;
881 if (suggestionsPlugins.length === 0) return;
882 if (!this.changeNum || !this.comment?.patch_set || !this.comments?.[0].path)
883 return;
884 const suggestion = await suggestionsPlugins[0].provider.suggestCode({
885 prompt: this.messageText,
886 changeNumber: this.changeNum,
887 patchsetNumber: this.comment?.patch_set,
888 filePath: this.comments?.[0].path,
889 range: this.comments?.[0].range,
890 lineNumber: this.comments?.[0].line,
891 });
892 const replacement = suggestion.suggestions?.[0].replacement;
893 if (!replacement) return;
Milutin Kristoficb59aeeb2023-08-23 09:18:06 +0200894 const addNewLine = this.messageText.length !== 0;
895 this.messageText += `${
896 addNewLine ? '\n' : ''
897 }${'```\n'}${replacement}${'\n```'}`;
Milutin Kristofic734df552023-08-07 10:38:50 +0200898 }
899
Ben Rohlfs05750b92021-10-29 08:23:08 +0200900 private renderRobotActions() {
901 if (!this.account || !isRobot(this.comment)) return;
902 const endpoint = html`
903 <gr-endpoint-decorator name="robot-comment-controls">
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200904 <gr-endpoint-param name="comment" .value=${this.comment}>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200905 </gr-endpoint-param>
906 </gr-endpoint-decorator>
907 `;
908 return html`
909 <div class="robotActions">
910 ${this.renderCopyLinkIcon()} ${endpoint} ${this.renderShowFixButton()}
911 ${this.renderPleaseFixButton()}
912 </div>
913 `;
914 }
915
916 private renderShowFixButton() {
Kamil Musind4418632023-03-07 10:20:49 +0100917 const fix_suggestions = (this.comment as RobotCommentInfo)?.fix_suggestions;
918 if (!fix_suggestions || fix_suggestions.length === 0) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200919 return html`
920 <gr-button
921 link
922 secondary
923 class="action show-fix"
Kamil Musind4418632023-03-07 10:20:49 +0100924 @click=${() => this.handleShowFix()}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200925 >
926 Show Fix
927 </gr-button>
928 `;
929 }
930
931 private renderPleaseFixButton() {
932 if (this.hasHumanReply()) return;
933 return html`
934 <gr-button
935 link
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200936 ?disabled=${this.robotButtonDisabled}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200937 class="action fix"
Ben Rohlfs23843882022-08-04 18:06:27 +0200938 @click=${this.handlePleaseFix}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200939 >
940 Please Fix
941 </gr-button>
942 `;
943 }
944
945 private renderConfirmDialog() {
Ben Rohlfs05750b92021-10-29 08:23:08 +0200946 return html`
Dhruv Srivastava4063d262022-11-09 18:46:29 +0530947 <dialog id="confirmDeleteModal" tabindex="-1">
Ben Rohlfs05750b92021-10-29 08:23:08 +0200948 <gr-confirm-delete-comment-dialog
Kamil Musinc7d3f282022-12-29 13:27:55 +0100949 id="confirmDeleteCommentDialog"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200950 @confirm=${this.handleConfirmDeleteComment}
Dhruv Srivastava4063d262022-11-09 18:46:29 +0530951 @cancel=${this.closeDeleteCommentModal}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200952 >
953 </gr-confirm-delete-comment-dialog>
Dhruv Srivastava4063d262022-11-09 18:46:29 +0530954 </dialog>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200955 `;
956 }
957
958 private getUrlForComment() {
Ben Rohlfsba440822023-04-11 18:08:03 +0200959 if (!this.changeNum || !this.repoName || !this.comment?.id) return '';
Ben Rohlfs731738b2022-09-15 15:55:33 +0200960 return createDiffUrl({
961 changeNum: this.changeNum,
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200962 repo: this.repoName,
Ben Rohlfsba440822023-04-11 18:08:03 +0200963 commentId: this.comment.id,
Ben Rohlfs731738b2022-09-15 15:55:33 +0200964 });
Dhruv Srivastava0287bf92020-09-11 16:56:38 +0200965 }
966
Ben Rohlfs05750b92021-10-29 08:23:08 +0200967 private firstWillUpdateDone = false;
968
969 firstWillUpdate() {
970 if (this.firstWillUpdateDone) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200971 assertIsDefined(this.comment, 'comment');
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200972 this.firstWillUpdateDone = true;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200973 this.unresolved = this.comment.unresolved ?? true;
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200974 if (this.permanentEditingMode) {
975 this.edit();
976 }
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200977 if (isDraft(this.comment)) {
Ben Rohlfs05750b92021-10-29 08:23:08 +0200978 this.collapsed = false;
979 } else {
980 this.collapsed = !!this.initiallyCollapsed;
981 }
982 }
983
Dhruv Srivastavaa49dffd2022-10-20 14:04:37 +0200984 override updated(changed: PropertyValues) {
985 if (changed.has('editing')) {
Dhruv Srivastavae8b86392022-10-20 17:17:21 +0200986 if (this.editing && !this.permanentEditingMode) {
Ben Rohlfsba9329c2023-05-16 10:05:12 +0200987 // Note that this is a bit fragile, because we are relying on the
988 // comment to become visible soonish. If that does not happen, then we
989 // will be waiting indefinitely and grab focus at some point in the
990 // distant future.
Dhruv Srivastavaa49dffd2022-10-20 14:04:37 +0200991 whenVisible(this, () => this.textarea?.putCursorAtEnd());
992 }
993 }
Milutin Kristofice9dbbe92023-05-17 21:21:28 +0200994 if (changed.has('changeNum') || changed.has('comment')) {
995 if (
996 !this.flagsService.isEnabled(
997 KnownExperimentId.DIFF_FOR_USER_SUGGESTED_EDIT
998 ) ||
999 !this.changeNum ||
1000 !this.comment
1001 )
1002 return;
1003 (async () => {
1004 const commentedText = await this.getCommentedCode();
1005 this.commentModel.updateState({
1006 commentedText,
1007 });
1008 })();
1009 }
Dhruv Srivastavaa49dffd2022-10-20 14:04:37 +02001010 }
1011
Ben Rohlfs05750b92021-10-29 08:23:08 +02001012 override willUpdate(changed: PropertyValues) {
1013 this.firstWillUpdate();
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001014 if (changed.has('comment')) {
1015 if (isDraft(this.comment) && isError(this.comment)) {
1016 this.edit();
1017 }
Milutin Kristofice9dbbe92023-05-17 21:21:28 +02001018 if (this.comment) {
1019 this.commentModel.updateState({
1020 comment: this.comment,
1021 });
1022 }
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001023 }
Ben Rohlfs05750b92021-10-29 08:23:08 +02001024 if (changed.has('editing')) {
Chris Poucetafd0f7c2022-10-04 10:04:43 +00001025 this.onEditingChanged();
Ben Rohlfs05750b92021-10-29 08:23:08 +02001026 }
1027 if (changed.has('unresolved')) {
1028 // The <gr-comment-thread> component wants to change its color based on
1029 // the (dirty) unresolved state, so let's notify it about changes.
Dhruv Srivastava463bb332022-08-31 13:00:49 +02001030 fire(this, 'comment-unresolved-changed', {value: this.unresolved});
1031 }
1032 if (changed.has('messageText')) {
1033 // GrReplyDialog updates it's state when text inside patchset level
1034 // comment changes.
1035 fire(this, 'comment-text-changed', {value: this.messageText});
Ben Rohlfs05750b92021-10-29 08:23:08 +02001036 }
1037 }
1038
1039 private handlePortedMessageClick() {
Ben Rohlfsc1c6afd2021-02-18 13:13:22 +01001040 assertIsDefined(this.comment, 'comment');
Dhruv Srivastavac8df7602021-01-15 10:59:00 +01001041 this.reporting.reportInteraction('navigate-to-original-comment', {
1042 line: this.comment.line,
1043 range: this.comment.range,
1044 });
1045 }
1046
Ben Rohlfs05750b92021-10-29 08:23:08 +02001047 private handleCopyLink() {
Ben Rohlfs44f01042023-02-18 13:27:57 +01001048 fire(this, 'copy-comment-link', {});
Ben Rohlfs05750b92021-10-29 08:23:08 +02001049 }
1050
1051 /** Enter editing mode. */
Ben Rohlfsba9329c2023-05-16 10:05:12 +02001052 edit() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001053 assert(isDraft(this.comment), 'only drafts are editable');
Ben Rohlfs05750b92021-10-29 08:23:08 +02001054 if (this.editing) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +02001055 this.editing = true;
Ben Rohlfs05750b92021-10-29 08:23:08 +02001056 }
1057
1058 // TODO: Move this out of gr-comment. gr-comment should not have a comments
1059 // property.
1060 private hasHumanReply() {
1061 if (!this.comment || !this.comments) return false;
1062 return this.comments.some(
1063 c => c.in_reply_to && c.in_reply_to === this.comment?.id && !isRobot(c)
Milutin Kristoficafae0052020-09-17 10:38:08 +02001064 );
Ben Rohlfs05750b92021-10-29 08:23:08 +02001065 }
1066
1067 // private, but visible for testing
Milutin Kristofic1ebae372022-11-22 20:35:38 +01001068 async createFixPreview(
1069 replacement?: string
1070 ): Promise<OpenFixPreviewEventDetail> {
Ben Rohlfs05750b92021-10-29 08:23:08 +02001071 assertIsDefined(this.comment?.patch_set, 'comment.patch_set');
Ben Rohlfs23843882022-08-04 18:06:27 +02001072 assertIsDefined(this.comment?.path, 'comment.path');
1073
Milutin Kristofic1ebae372022-11-22 20:35:38 +01001074 if (hasUserSuggestion(this.comment) || replacement) {
1075 replacement = replacement ?? getUserSuggestion(this.comment);
Ben Rohlfs23843882022-08-04 18:06:27 +02001076 assert(!!replacement, 'malformed user suggestion');
1077 const line = await this.getCommentedCode();
1078
1079 return {
1080 fixSuggestions: createUserFixSuggestion(
1081 this.comment,
1082 line,
1083 replacement
1084 ),
1085 patchNum: this.comment.patch_set,
Milutin Kristoficeb8f6552023-02-09 22:17:35 +01001086 onCloseFixPreviewCallbacks: [
1087 fixApplied => {
1088 if (fixApplied) this.handleAppliedFix();
1089 },
1090 ],
Ben Rohlfs23843882022-08-04 18:06:27 +02001091 };
1092 }
1093 if (isRobot(this.comment) && this.comment.fix_suggestions.length > 0) {
1094 const id = this.comment.robot_id;
1095 return {
1096 fixSuggestions: this.comment.fix_suggestions.map(s => {
1097 return {
1098 ...s,
1099 description: `${id ?? ''} - ${s.description ?? ''}`,
1100 };
1101 }),
1102 patchNum: this.comment.patch_set,
Milutin Kristoficeb8f6552023-02-09 22:17:35 +01001103 onCloseFixPreviewCallbacks: [],
Ben Rohlfs23843882022-08-04 18:06:27 +02001104 };
1105 }
1106 throw new Error('unable to create preview fix event');
Ben Rohlfs05750b92021-10-29 08:23:08 +02001107 }
1108
Chris Poucetafd0f7c2022-10-04 10:04:43 +00001109 private onEditingChanged() {
1110 if (this.editing) {
1111 this.collapsed = false;
1112 this.messageText = this.comment?.message ?? '';
1113 this.unresolved = this.comment?.unresolved ?? true;
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001114 if (!isError(this.comment) && !isSaving(this.comment)) {
1115 this.originalMessage = this.messageText;
1116 this.originalUnresolved = this.unresolved;
1117 }
Chris Poucetafd0f7c2022-10-04 10:04:43 +00001118 }
1119
1120 // Parent components such as the reply dialog might be interested in whether
1121 // come of their child components are in editing mode.
1122 fire(this, 'comment-editing-changed', {
1123 editing: this.editing,
1124 path: this.comment?.path ?? '',
1125 });
Ben Rohlfs05750b92021-10-29 08:23:08 +02001126 }
1127
Ben Rohlfs05750b92021-10-29 08:23:08 +02001128 // private, but visible for testing
1129 isSaveDisabled() {
1130 assertIsDefined(this.comment, 'comment');
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001131 if (isSaving(this.comment) && !this.autoSaving) return true;
Ben Rohlfs2e237552021-11-24 10:34:28 +01001132 return !this.messageText?.trimEnd();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001133 }
1134
Dhruv Srivastavae8b86392022-10-20 17:17:21 +02001135 override focus() {
Ben Rohlfsba9329c2023-05-16 10:05:12 +02001136 // Note that this may not work as intended, because the textarea is not
1137 // rendered yet.
Dhruv Srivastavae8b86392022-10-20 17:17:21 +02001138 this.textarea?.focus();
1139 }
1140
Ben Rohlfs05750b92021-10-29 08:23:08 +02001141 private handleEsc() {
1142 // vim users don't like ESC to cancel/discard, so only do this when the
1143 // comment text is empty.
Ben Rohlfs2e237552021-11-24 10:34:28 +01001144 if (!this.messageText?.trimEnd()) this.cancel();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001145 }
1146
Ben Rohlfs05750b92021-10-29 08:23:08 +02001147 private handleAnchorClick() {
1148 assertIsDefined(this.comment, 'comment');
1149 fire(this, 'comment-anchor-tap', {
1150 number: this.comment.line || FILE,
1151 side: this.comment?.side,
Milutin Kristoficafae0052020-09-17 10:38:08 +02001152 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001153 }
1154
Dhruv Srivastava15950b452022-09-12 10:56:53 +02001155 private async handleSaveButtonClicked() {
1156 await this.save();
1157 if (this.permanentEditingMode) {
1158 this.editing = !this.editing;
1159 }
Dhruv Srivastava705eac92022-09-01 11:33:27 +02001160 }
1161
Ben Rohlfs23843882022-08-04 18:06:27 +02001162 private handlePleaseFix() {
1163 const message = this.comment?.message;
1164 assert(!!message, 'empty message');
1165 const quoted = message.replace(NEWLINE_PATTERN, '\n> ');
1166 const eventDetail: ReplyToCommentEventDetail = {
1167 content: `> ${quoted}\n\nPlease fix.`,
1168 userWantsToEdit: false,
1169 unresolved: true,
1170 };
Ben Rohlfs05750b92021-10-29 08:23:08 +02001171 // Handled by <gr-comment-thread>.
Ben Rohlfs23843882022-08-04 18:06:27 +02001172 fire(this, 'reply-to-comment', eventDetail);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001173 }
1174
Milutin Kristoficeb8f6552023-02-09 22:17:35 +01001175 private handleAppliedFix() {
1176 const message = this.comment?.message;
1177 assert(!!message, 'empty message');
1178 const eventDetail: ReplyToCommentEventDetail = {
1179 content: 'Fix applied.',
1180 userWantsToEdit: false,
1181 unresolved: false,
1182 };
1183 // Handled by <gr-comment-thread>.
1184 fire(this, 'reply-to-comment', eventDetail);
1185 }
1186
Milutin Kristofic1ebae372022-11-22 20:35:38 +01001187 private async handleShowFix(replacement?: string) {
Ben Rohlfs05750b92021-10-29 08:23:08 +02001188 // Handled top-level in the diff and change view components.
Milutin Kristofic1ebae372022-11-22 20:35:38 +01001189 fire(this, 'open-fix-preview', await this.createFixPreview(replacement));
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001190 }
1191
Milutin Kristofic8238de52023-01-12 19:33:45 +01001192 async createSuggestEdit(e: MouseEvent) {
1193 e.stopPropagation();
Ben Rohlfs23843882022-08-04 18:06:27 +02001194 const line = await this.getCommentedCode();
Milutin Kristofic7d557472023-08-21 11:23:36 +02001195 const addNewLine = this.messageText.length !== 0;
1196 this.messageText += `${
1197 addNewLine ? '\n' : ''
1198 }${USER_SUGGESTION_START_PATTERN}${line}${'\n```'}`;
Ben Rohlfs23843882022-08-04 18:06:27 +02001199 }
1200
1201 async getCommentedCode() {
Milutin Kristofic1d219672022-06-21 14:57:25 +02001202 assertIsDefined(this.comment, 'comment');
1203 assertIsDefined(this.changeNum, 'changeNum');
Ben Rohlfs23843882022-08-04 18:06:27 +02001204 // TODO(milutin): Show a toast while the file is being loaded.
1205 // TODO(milutin): This should be moved into a service/model.
Milutin Kristofic1d219672022-06-21 14:57:25 +02001206 const file = await this.restApiService.getFileContent(
1207 this.changeNum,
1208 this.comment.path!,
1209 this.comment.patch_set!
1210 );
Ben Rohlfs23843882022-08-04 18:06:27 +02001211 assert(
1212 !!file && isBase64FileContent(file) && !!file.content,
1213 'file content for comment not found'
1214 );
Milutin Kristofic1d219672022-06-21 14:57:25 +02001215 const line = getContentInCommentRange(file.content, this.comment);
Ben Rohlfs23843882022-08-04 18:06:27 +02001216 assert(!!line, 'file content for comment not found');
1217 return line;
Milutin Kristofic1d219672022-06-21 14:57:25 +02001218 }
1219
Ben Rohlfs05750b92021-10-29 08:23:08 +02001220 // private, but visible for testing
1221 cancel() {
1222 assertIsDefined(this.comment, 'comment');
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001223 assert(isDraft(this.comment), 'only drafts are editable');
Ben Rohlfs2e237552021-11-24 10:34:28 +01001224 this.messageText = this.originalMessage;
1225 this.unresolved = this.originalUnresolved;
1226 this.save();
Ben Rohlfs05750b92021-10-29 08:23:08 +02001227 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001228
Ben Rohlfs2e237552021-11-24 10:34:28 +01001229 async autoSave() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001230 if (isSaving(this.comment) || this.autoSaving) return;
Ben Rohlfs2e237552021-11-24 10:34:28 +01001231 if (!this.editing || !this.comment) return;
Chris Poucet77226982023-08-10 18:10:04 +02001232 if (this.disableAutoSaving) return;
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001233 assert(isDraft(this.comment), 'only drafts are editable');
Ben Rohlfs2e237552021-11-24 10:34:28 +01001234 const messageToSave = this.messageText.trimEnd();
1235 if (messageToSave === '') return;
1236 if (messageToSave === this.comment.message) return;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001237
Ben Rohlfs2e237552021-11-24 10:34:28 +01001238 try {
Ben Rohlfs1efb1a72023-04-12 23:25:31 +02001239 this.autoSaving = this.rawSave({showToast: false});
Ben Rohlfs2e237552021-11-24 10:34:28 +01001240 await this.autoSaving;
1241 } finally {
1242 this.autoSaving = undefined;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001243 }
Ben Rohlfs2e237552021-11-24 10:34:28 +01001244 }
1245
1246 async discard() {
1247 this.messageText = '';
1248 await this.save();
1249 }
1250
Chris Poucet77226982023-08-10 18:10:04 +02001251 convertToCommentInput(): CommentInput | undefined {
1252 if (!this.somethingToSave() || !this.comment) return;
1253 return convertToCommentInput({
1254 ...this.comment,
1255 message: this.messageText.trimEnd(),
1256 unresolved: this.unresolved,
1257 });
1258 }
1259
Ben Rohlfs2e237552021-11-24 10:34:28 +01001260 async save() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001261 assert(isDraft(this.comment), 'only drafts are editable');
Ben Rohlfs1efb1a72023-04-12 23:25:31 +02001262 // There is a minimal chance of `isSaving()` being false between iterations
1263 // of the below while loop. But this will be extremely rare and just lead
1264 // to a harmless assertion error. So let's not bother.
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001265 if (isSaving(this.comment) && !this.autoSaving) return;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001266
Ben Rohlfs1efb1a72023-04-12 23:25:31 +02001267 if (!this.permanentEditingMode) {
1268 this.editing = false;
1269 }
1270 if (this.autoSaving) {
1271 this.comment = await this.autoSaving;
1272 }
1273 // Depending on whether `messageToSave` is empty we treat this either as
1274 // a discard or a save action.
1275 const messageToSave = this.messageText.trimEnd();
1276 if (messageToSave === '') {
Ben Rohlfs0d9d0c32023-04-20 18:12:06 +02001277 if (!this.permanentEditingMode || this.somethingToSave()) {
1278 await this.getCommentsModel().discardDraft(id(this.comment));
1279 }
Ben Rohlfs1efb1a72023-04-12 23:25:31 +02001280 } else {
1281 // No need to make a backend call when nothing has changed.
1282 while (this.somethingToSave()) {
1283 this.comment = await this.rawSave({showToast: true});
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001284 if (isError(this.comment)) return;
Ben Rohlfs607126f2021-12-07 08:21:52 +01001285 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001286 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001287 }
1288
Ben Rohlfs1efb1a72023-04-12 23:25:31 +02001289 private somethingToSave() {
1290 if (!this.comment) return false;
1291 return (
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001292 isError(this.comment) ||
Ben Rohlfs1efb1a72023-04-12 23:25:31 +02001293 this.messageText.trimEnd() !== this.comment?.message ||
1294 this.unresolved !== this.comment.unresolved
1295 );
1296 }
1297
Ben Rohlfs2e237552021-11-24 10:34:28 +01001298 /** For sharing between save() and autoSave(). */
Ben Rohlfs1efb1a72023-04-12 23:25:31 +02001299 private rawSave(options: {showToast: boolean}) {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001300 assert(isDraft(this.comment), 'only drafts are editable');
1301 assert(!isSaving(this.comment), 'saving already in progress');
Chris Poucet6c6b54f2021-12-09 02:53:13 +01001302 return this.getCommentsModel().saveDraft(
Ben Rohlfs2e237552021-11-24 10:34:28 +01001303 {
1304 ...this.comment,
Ben Rohlfs1efb1a72023-04-12 23:25:31 +02001305 message: this.messageText.trimEnd(),
Ben Rohlfs2e237552021-11-24 10:34:28 +01001306 unresolved: this.unresolved,
1307 },
1308 options.showToast
1309 );
1310 }
1311
Ben Rohlfs05750b92021-10-29 08:23:08 +02001312 private handleToggleResolved() {
1313 this.unresolved = !this.unresolved;
Dhruv Srivastava73f9edc2021-12-02 11:23:27 +01001314 if (!this.editing) {
1315 // messageText is only assigned a value if the comment reaches editing
1316 // state, however it is possible that the user toggles the resolved state
1317 // without editing the comment in which case we assign the correct value
1318 // to messageText here
1319 this.messageText = this.comment?.message ?? '';
1320 this.save();
1321 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001322 }
1323
Kamil Musin9c8833a2022-12-29 12:05:08 +01001324 private openDeleteCommentModal() {
1325 this.confirmDeleteModal?.showModal();
Kamil Musinc7d3f282022-12-29 13:27:55 +01001326 whenVisible(this.confirmDeleteDialog!, () => {
1327 this.confirmDeleteDialog!.resetFocus();
1328 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001329 }
1330
Dhruv Srivastava4063d262022-11-09 18:46:29 +05301331 private closeDeleteCommentModal() {
Dhruv Srivastava4063d262022-11-09 18:46:29 +05301332 this.confirmDeleteModal?.close();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001333 }
1334
Ben Rohlfs05750b92021-10-29 08:23:08 +02001335 /**
1336 * Deleting a *published* comment is an admin feature. It means more than just
1337 * discarding a draft.
Ben Rohlfs05750b92021-10-29 08:23:08 +02001338 */
1339 // private, but visible for testing
Kamil Musind88622f2023-01-02 11:52:57 +01001340 async handleConfirmDeleteComment() {
Kamil Musinc7d3f282022-12-29 13:27:55 +01001341 if (!this.confirmDeleteDialog || !this.confirmDeleteDialog.message) {
Milutin Kristoficafae0052020-09-17 10:38:08 +02001342 throw new Error('missing confirm delete dialog');
1343 }
Ben Rohlfs05750b92021-10-29 08:23:08 +02001344 assertIsDefined(this.changeNum, 'changeNum');
1345 assertIsDefined(this.comment, 'comment');
Kamil Musind88622f2023-01-02 11:52:57 +01001346
1347 await this.getCommentsModel().deleteComment(
1348 this.changeNum,
1349 this.comment,
Kamil Musinc7d3f282022-12-29 13:27:55 +01001350 this.confirmDeleteDialog.message
Kamil Musind88622f2023-01-02 11:52:57 +01001351 );
1352 this.closeDeleteCommentModal();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001353 }
1354}
1355
Milutin Kristoficafae0052020-09-17 10:38:08 +02001356declare global {
1357 interface HTMLElementTagNameMap {
1358 'gr-comment': GrComment;
1359 }
Ben Rohlfs5b3c6552023-02-18 13:02:46 +01001360 interface HTMLElementEventMap {
1361 'copy-comment-link': CustomEvent<{}>;
1362 }
Milutin Kristoficafae0052020-09-17 10:38:08 +02001363}