blob: 10a134e13ae14568ae892c1d2e72eaf844f79df5 [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';
Milutin Kristofic490fa952023-09-12 20:17:36 +020017import '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
Chris Poucetc6e880b2021-11-15 19:57:06 +010018import {getAppContext} from '../../../services/app-context';
Milutin Kristofic1d219672022-06-21 14:57:25 +020019import {css, html, LitElement, nothing, PropertyValues} from 'lit';
Frank Borden42c1a452022-08-11 16:27:20 +020020import {customElement, property, query, state} from 'lit/decorators.js';
Milutin Kristofice9dbbe92023-05-17 21:21:28 +020021import {provide, resolve} from '../../../models/dependency';
Milutin Kristoficafae0052020-09-17 10:38:08 +020022import {GrTextarea} from '../gr-textarea/gr-textarea';
Milutin Kristoficafae0052020-09-17 10:38:08 +020023import {
Milutin Kristoficafae0052020-09-17 10:38:08 +020024 AccountDetailInfo,
Dhruv Srivastava65edec82023-02-28 19:37:59 +010025 DraftInfo,
Ben Rohlfs4401b232021-10-21 13:51:59 +020026 NumericChangeId,
Dhruv Srivastava0287bf92020-09-11 16:56:38 +020027 RepoName,
Ben Rohlfs05750b92021-10-29 08:23:08 +020028 RobotCommentInfo,
Dhruv Srivastava4e27dc42023-03-01 10:49:49 +010029 Comment,
Dhruv Srivastava4e27dc42023-03-01 10:49:49 +010030 isRobot,
Ben Rohlfs1efb1a72023-04-12 23:25:31 +020031 isSaving,
32 isError,
Ben Rohlfs1efb1a72023-04-12 23:25:31 +020033 isDraft,
Ben Rohlfs610bb4f2023-04-17 12:34:35 +020034 isNew,
Chris Poucet77226982023-08-10 18:10:04 +020035 CommentInput,
Milutin Kristoficafae0052020-09-17 10:38:08 +020036} from '../../../types/common';
Milutin Kristoficafae0052020-09-17 10:38:08 +020037import {GrConfirmDeleteCommentDialog} from '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
Ben Rohlfs1d487062020-09-26 11:26:03 +020038import {
Chris Poucet77226982023-08-10 18:10:04 +020039 convertToCommentInput,
Ben Rohlfs23843882022-08-04 18:06:27 +020040 createUserFixSuggestion,
Milutin Kristofic1d219672022-06-21 14:57:25 +020041 getContentInCommentRange,
Ben Rohlfs23843882022-08-04 18:06:27 +020042 getUserSuggestion,
Milutin Kristofic1d219672022-06-21 14:57:25 +020043 hasUserSuggestion,
Ben Rohlfsba440822023-04-11 18:08:03 +020044 id,
Ben Rohlfs23843882022-08-04 18:06:27 +020045 NEWLINE_PATTERN,
Milutin Kristofic1d219672022-06-21 14:57:25 +020046 USER_SUGGESTION_START_PATTERN,
Ben Rohlfs31825d82020-10-02 18:08:04 +020047} from '../../../utils/comment-util';
Ben Rohlfs05750b92021-10-29 08:23:08 +020048import {
49 OpenFixPreviewEventDetail,
Ben Rohlfs23843882022-08-04 18:06:27 +020050 ReplyToCommentEventDetail,
Ben Rohlfs05750b92021-10-29 08:23:08 +020051 ValueChangedEvent,
52} from '../../../types/events';
Ben Rohlfs44f01042023-02-18 13:27:57 +010053import {fire} from '../../../utils/event-util';
Milutin Kristofic96ac52e2023-09-18 10:28:58 +020054import {assertIsDefined, assert, uuid} from '../../../utils/common-util';
Dhruv Srivastavaa49dffd2022-10-20 14:04:37 +020055import {Key, Modifier, whenVisible} from '../../../utils/dom-util';
Chris Poucetdae98bf2022-01-05 15:23:45 +010056import {commentsModelToken} from '../../../models/comments/comments-model';
Ben Rohlfs05750b92021-10-29 08:23:08 +020057import {sharedStyles} from '../../../styles/shared-styles';
58import {subscribe} from '../../lit/subscription-controller';
59import {ShortcutController} from '../../lit/shortcut-controller';
Frank Borden42c1a452022-08-11 16:27:20 +020060import {classMap} from 'lit/directives/class-map.js';
Ben Rohlfsb9956102023-05-12 17:07:06 +020061import {FILE, LineNumber} from '../../../api/diff';
Milutin Kristofic1d219672022-06-21 14:57:25 +020062import {CommentSide, SpecialFilePath} from '../../../constants/constants';
Ben Rohlfs2e237552021-11-24 10:34:28 +010063import {Subject} from 'rxjs';
64import {debounceTime} from 'rxjs/operators';
Chris Poucetbf65b8f2022-01-18 21:18:12 +000065import {changeModelToken} from '../../../models/change/change-model';
Kamil Musin48ff12a2023-12-05 14:25:27 +010066import {ChangeInfo, isBase64FileContent} from '../../../api/rest-api';
Ben Rohlfsb91a6a42023-01-13 09:29:31 +010067import {createDiffUrl} from '../../../models/views/change';
Chris Poucetbb0cf832022-10-24 12:32:10 +020068import {userModelToken} from '../../../models/user/user-model';
Dhruv Srivastava4063d262022-11-09 18:46:29 +053069import {modalStyles} from '../../../styles/gr-modal-styles';
Milutin Kristofic734df552023-08-07 10:38:50 +020070import {KnownExperimentId} from '../../../services/flags/flags';
71import {pluginLoaderToken} from '../gr-js-api-interface/gr-plugin-loader';
Milutin Kristofice9dbbe92023-05-17 21:21:28 +020072import {
73 CommentModel,
74 commentModelToken,
75} from '../gr-comment-model/gr-comment-model';
Milutin Kristoficaa1c08b2023-09-06 10:34:16 +020076import {formStyles} from '../../../styles/form-styles';
Milutin Kristofic96ac52e2023-09-18 10:28:58 +020077import {Interaction} from '../../../constants/reporting';
Milutin Kristofica31d8942023-11-17 13:32:16 +010078import {Suggestion, SuggestionsProvider} from '../../../api/suggestions';
Milutin Kristofic74caea4b2023-11-14 20:10:56 +010079import {when} from 'lit/directives/when.js';
Milutin Kristofic5942c9d2023-11-15 20:34:42 +010080import {getDocUrl} from '../../../utils/url-util';
81import {configModelToken} from '../../../models/config/config-model';
Milutin Kristofica31d8942023-11-17 13:32:16 +010082import {getFileExtension} from '../../../utils/file-util';
Milutin Kristofic4d963732024-01-04 12:10:03 +010083import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
Wyatt Allen846ac2f2018-05-14 12:59:23 -070084
Ben Rohlfs2e237552021-11-24 10:34:28 +010085// visible for testing
86export const AUTO_SAVE_DEBOUNCE_DELAY_MS = 2000;
Milutin Kristoficfc5ccca2023-09-11 13:43:45 +020087export const GENERATE_SUGGESTION_DEBOUNCE_DELAY_MS = 1500;
Milutin Kristofic4d963732024-01-04 12:10:03 +010088export const ENABLE_GENERATE_SUGGESTION_STORAGE_KEY =
89 'enableGenerateSuggestionStorageKey';
Ben Rohlfs2e237552021-11-24 10:34:28 +010090
Ben Rohlfs05750b92021-10-29 08:23:08 +020091declare global {
92 interface HTMLElementEventMap {
Dhruv Srivastavaee018e92022-08-31 11:37:46 +020093 'comment-editing-changed': CustomEvent<CommentEditingChangedDetail>;
Dhruv Srivastava463bb332022-08-31 13:00:49 +020094 'comment-unresolved-changed': ValueChangedEvent<boolean>;
95 'comment-text-changed': ValueChangedEvent<string>;
Ben Rohlfs05750b92021-10-29 08:23:08 +020096 'comment-anchor-tap': CustomEvent<CommentAnchorTapEventDetail>;
97 }
Milutin Kristoficafae0052020-09-17 10:38:08 +020098}
99
Ben Rohlfs05750b92021-10-29 08:23:08 +0200100export interface CommentAnchorTapEventDetail {
101 number: LineNumber;
102 side?: CommentSide;
Milutin Kristoficafae0052020-09-17 10:38:08 +0200103}
Dmitrii Filippov3f3c2052020-09-22 16:51:18 +0200104
Dhruv Srivastavaee018e92022-08-31 11:37:46 +0200105export interface CommentEditingChangedDetail {
106 editing: boolean;
107 path: string;
108}
109
Milutin Kristoficafae0052020-09-17 10:38:08 +0200110@customElement('gr-comment')
Ben Rohlfs05750b92021-10-29 08:23:08 +0200111export class GrComment extends LitElement {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100112 /**
Ben Rohlfs23843882022-08-04 18:06:27 +0200113 * Fired when the parent thread component should create a reply.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100114 *
Ben Rohlfs23843882022-08-04 18:06:27 +0200115 * @event reply-to-comment
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100116 */
Kasper Nilssond43d2a72018-10-19 14:26:41 -0700117
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100118 /**
Ben Rohlfs23843882022-08-04 18:06:27 +0200119 * Fired when the open fix preview action is triggered.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100120 *
121 * @event open-fix-preview
Tao Zhou500437d2020-02-14 16:57:27 +0100122 */
Tao Zhou500437d2020-02-14 16:57:27 +0100123
124 /**
Tao Zhou31f3f102020-04-27 16:15:29 +0200125 * Fired when editing status changed.
126 *
127 * @event comment-editing-changed
128 */
129
130 /**
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100131 * Fired when the comment's timestamp is tapped.
132 *
133 * @event comment-anchor-tap
134 */
Andrew Bonventre28165262016-05-19 17:24:45 -0700135
Ben Rohlfs05750b92021-10-29 08:23:08 +0200136 @query('#editTextarea')
137 textarea?: GrTextarea;
Viktar Donich7ad28922016-05-23 15:24:05 -0700138
Ben Rohlfs05750b92021-10-29 08:23:08 +0200139 @query('#container')
140 container?: HTMLElement;
Dhruv Srivastava0287bf92020-09-11 16:56:38 +0200141
Ben Rohlfs05750b92021-10-29 08:23:08 +0200142 @query('#resolvedCheckbox')
143 resolvedCheckbox?: HTMLInputElement;
Kasper Nilssond43d2a72018-10-19 14:26:41 -0700144
Dhruv Srivastava4063d262022-11-09 18:46:29 +0530145 @query('#confirmDeleteModal')
146 confirmDeleteModal?: HTMLDialogElement;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200147
Kamil Musinc7d3f282022-12-29 13:27:55 +0100148 @query('#confirmDeleteCommentDialog')
149 confirmDeleteDialog?: GrConfirmDeleteCommentDialog;
150
Ben Rohlfs05750b92021-10-29 08:23:08 +0200151 @property({type: Object})
152 comment?: Comment;
153
154 // TODO: Move this out of gr-comment. gr-comment should not have a comments
155 // property. This is only used for hasHumanReply at the moment.
Milutin Kristoficafae0052020-09-17 10:38:08 +0200156 @property({type: Array})
Ben Rohlfs05750b92021-10-29 08:23:08 +0200157 comments?: Comment[];
Milutin Kristoficafae0052020-09-17 10:38:08 +0200158
159 /**
Ben Rohlfs05750b92021-10-29 08:23:08 +0200160 * Initial collapsed state of the comment.
Milutin Kristoficafae0052020-09-17 10:38:08 +0200161 */
Ben Rohlfs05750b92021-10-29 08:23:08 +0200162 @property({type: Boolean, attribute: 'initially-collapsed'})
163 initiallyCollapsed?: boolean;
164
165 /**
Dhruv Srivastavae75f6f72022-08-25 10:14:37 +0200166 * Hide the header for patchset level comments used in GrReplyDialog.
167 */
168 @property({type: Boolean, attribute: 'hide-header'})
169 hideHeader = false;
170
171 /**
Ben Rohlfs05750b92021-10-29 08:23:08 +0200172 * This is the *current* (internal) collapsed state of the comment. Do not set
173 * from the outside. Use `initiallyCollapsed` instead. This is just a
174 * reflected property such that css rules can be based on it.
175 */
176 @property({type: Boolean, reflect: true})
177 collapsed?: boolean;
178
179 @property({type: Boolean, attribute: 'robot-button-disabled'})
180 robotButtonDisabled = false;
181
Dhruv Srivastavaf007abc2022-09-06 11:18:57 +0200182 @property({type: String})
183 messagePlaceholder?: string;
184
Dhruv Srivastava4e5fd112022-08-25 12:01:22 +0200185 // GrReplyDialog requires the patchset level comment to always remain
186 // editable.
187 @property({type: Boolean, attribute: 'permanent-editing-mode'})
188 permanentEditingMode = false;
189
Chris Poucet77226982023-08-10 18:10:04 +0200190 // Whether to disable autosaving
191 @property({type: Boolean})
192 disableAutoSaving = false;
193
Ben Rohlfs2e237552021-11-24 10:34:28 +0100194 @state()
Ben Rohlfs607126f2021-12-07 08:21:52 +0100195 autoSaving?: Promise<DraftInfo>;
Ben Rohlfs2e237552021-11-24 10:34:28 +0100196
Ben Rohlfs05750b92021-10-29 08:23:08 +0200197 @state()
198 changeNum?: NumericChangeId;
199
200 @state()
201 editing = false;
202
203 @state()
Ben Rohlfs05750b92021-10-29 08:23:08 +0200204 repoName?: RepoName;
205
206 /* The 'dirty' state of the comment.message, which will be saved on demand. */
207 @state()
208 messageText = '';
209
210 /* The 'dirty' state of !comment.unresolved, which will be saved on demand. */
211 @state()
212 unresolved = true;
Milutin Kristoficafae0052020-09-17 10:38:08 +0200213
Milutin Kristoficfc5ccca2023-09-11 13:43:45 +0200214 @state()
215 generateSuggestion = true;
216
Milutin Kristofic96ac52e2023-09-18 10:28:58 +0200217 @state()
Milutin Kristofic8f07c382023-10-02 13:13:21 +0200218 generatedSuggestion?: Suggestion;
Milutin Kristofic96ac52e2023-09-18 10:28:58 +0200219
220 @state()
Milutin Kristofic06dfda42023-11-15 09:51:13 +0100221 generatedSuggestionId?: string;
Milutin Kristofic96ac52e2023-09-18 10:28:58 +0200222
Milutin Kristofic74caea4b2023-11-14 20:10:56 +0100223 @state()
Milutin Kristofica31d8942023-11-17 13:32:16 +0100224 suggestionsProvider?: SuggestionsProvider;
225
226 @state()
Milutin Kristofic74caea4b2023-11-14 20:10:56 +0100227 suggestionLoading = false;
228
Ben Rohlfs05750b92021-10-29 08:23:08 +0200229 @property({type: Boolean, attribute: 'show-patchset'})
230 showPatchset = false;
Tao Zhou500437d2020-02-14 16:57:27 +0100231
Ben Rohlfs05750b92021-10-29 08:23:08 +0200232 @property({type: Boolean, attribute: 'show-ported-comment'})
Dhruv Srivastava0287bf92020-09-11 16:56:38 +0200233 showPortedComment = false;
234
Ben Rohlfs05750b92021-10-29 08:23:08 +0200235 @state()
236 account?: AccountDetailInfo;
237
238 @state()
239 isAdmin = false;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100240
Milutin Kristofic15bfb3e2022-08-16 20:01:33 +0200241 @state()
242 isOwner = false;
243
Milutin Kristofic78cdec92023-08-29 14:37:31 +0200244 @state()
245 commentedText?: string;
246
Milutin Kristofic5942c9d2023-11-15 20:34:42 +0100247 @state() private docsBaseUrl = '';
248
Chris Poucetc6e880b2021-11-15 19:57:06 +0100249 private readonly restApiService = getAppContext().restApiService;
Ben Rohlfs43935a42020-12-01 19:14:09 +0100250
Chris Poucetc6e880b2021-11-15 19:57:06 +0100251 private readonly reporting = getAppContext().reportingService;
Milutin Kristoficafae0052020-09-17 10:38:08 +0200252
Chris Poucetbf65b8f2022-01-18 21:18:12 +0000253 private readonly getChangeModel = resolve(this, changeModelToken);
Chris Poucet01422482021-11-30 19:43:28 +0100254
Chris Poucetbb0cf832022-10-24 12:32:10 +0200255 private readonly getCommentsModel = resolve(this, commentsModelToken);
Dhruv Srivastavadb2ab602021-06-24 15:20:29 +0200256
Chris Poucetbb0cf832022-10-24 12:32:10 +0200257 private readonly getUserModel = resolve(this, userModelToken);
Ben Rohlfsf92d3b52021-03-10 23:13:03 +0100258
Milutin Kristofic734df552023-08-07 10:38:50 +0200259 private readonly getPluginLoader = resolve(this, pluginLoaderToken);
260
Milutin Kristofic5942c9d2023-11-15 20:34:42 +0100261 private readonly getConfigModel = resolve(this, configModelToken);
262
Milutin Kristofic4d963732024-01-04 12:10:03 +0100263 private readonly getStorage = resolve(this, storageServiceToken);
264
Milutin Kristofic734df552023-08-07 10:38:50 +0200265 private readonly flagsService = getAppContext().flagsService;
266
Ben Rohlfs05750b92021-10-29 08:23:08 +0200267 private readonly shortcuts = new ShortcutController(this);
Ben Rohlfsf92d3b52021-03-10 23:13:03 +0100268
Milutin Kristofic78cdec92023-08-29 14:37:31 +0200269 private commentModel = new CommentModel(this.restApiService);
Milutin Kristofice9dbbe92023-05-17 21:21:28 +0200270
Ben Rohlfs2e237552021-11-24 10:34:28 +0100271 /**
272 * This is triggered when the user types into the editing textarea. We then
273 * debounce it and call autoSave().
274 */
Frank Borden3801d7d2023-03-27 09:00:58 +0000275 private autoSaveTrigger$ = new Subject();
Ben Rohlfs2e237552021-11-24 10:34:28 +0100276
277 /**
Milutin Kristoficfc5ccca2023-09-11 13:43:45 +0200278 * This is triggered when the user types into the editing textarea. We then
279 * debounce it and call generateSuggestEdit().
280 */
281 private generateSuggestionTrigger$ = new Subject();
282
283 /**
Ben Rohlfs2e237552021-11-24 10:34:28 +0100284 * Set to the content of DraftInfo when entering editing mode.
285 * Only used for "Cancel".
286 */
287 private originalMessage = '';
288
289 /**
290 * Set to the content of DraftInfo when entering editing mode.
291 * Only used for "Cancel".
292 */
293 private originalUnresolved = false;
294
Ben Rohlfs05750b92021-10-29 08:23:08 +0200295 constructor() {
296 super();
Milutin Kristofice9dbbe92023-05-17 21:21:28 +0200297 provide(this, commentModelToken, () => this.commentModel);
Dhruv Srivastavae110a372022-09-08 12:18:33 +0200298 // Allow the shortcuts to bubble up so that GrReplyDialog can respond to
299 // them as well.
300 this.shortcuts.addLocal({key: Key.ESC}, () => this.handleEsc(), {
301 preventDefault: false,
302 });
Dhruv Srivastavaf43eee72022-09-14 11:03:01 +0200303 for (const modifier of [Modifier.CTRL_KEY, Modifier.META_KEY]) {
304 this.shortcuts.addLocal(
305 {key: Key.ENTER, modifiers: [modifier]},
306 () => {
307 this.save();
308 },
309 {preventDefault: false}
310 );
311 }
312 // For Ctrl+s add shorctut with preventDefault so that it does
313 // not bubble up to the browser
314 for (const modifier of [Modifier.CTRL_KEY, Modifier.META_KEY]) {
315 this.shortcuts.addLocal({key: 's', modifiers: [modifier]}, () => {
316 this.save();
317 });
Ben Rohlfsaadbdd12021-10-19 11:49:01 +0200318 }
Milutin Kristofic1ebae372022-11-22 20:35:38 +0100319 this.addEventListener('open-user-suggest-preview', e => {
320 this.handleShowFix(e.detail.code);
321 });
Milutin Kristofic490fa952023-09-12 20:17:36 +0200322 this.addEventListener('add-generated-suggestion', e => {
323 this.handleAddGeneratedSuggestion(e.detail.code);
324 });
Ben Rohlfsb7082e12023-01-23 11:43:48 +0100325 this.messagePlaceholder = 'Mention others with @';
Chris Poucet0b961412022-01-05 16:24:50 +0100326 subscribe(
327 this,
Chris Poucetbb0cf832022-10-24 12:32:10 +0200328 () => this.getUserModel().account$,
Chris Poucet5ec77f02022-05-12 11:25:21 +0200329 x => (this.account = x)
330 );
331 subscribe(
332 this,
Chris Poucetbb0cf832022-10-24 12:32:10 +0200333 () => this.getUserModel().isAdmin$,
Chris Poucet5ec77f02022-05-12 11:25:21 +0200334 x => (this.isAdmin = x)
335 );
336
337 subscribe(
338 this,
339 () => this.getChangeModel().repo$,
340 x => (this.repoName = x)
341 );
342 subscribe(
343 this,
344 () => this.getChangeModel().changeNum$,
Chris Poucetbf65b8f2022-01-18 21:18:12 +0000345 x => (this.changeNum = x)
346 );
347 subscribe(
348 this,
Milutin Kristofic15bfb3e2022-08-16 20:01:33 +0200349 () => this.getChangeModel().isOwner$,
350 x => (this.isOwner = x)
351 );
352 subscribe(
353 this,
Chris Poucet5ec77f02022-05-12 11:25:21 +0200354 () =>
355 this.autoSaveTrigger$.pipe(debounceTime(AUTO_SAVE_DEBOUNCE_DELAY_MS)),
Chris Poucetbf65b8f2022-01-18 21:18:12 +0000356 () => {
357 this.autoSave();
358 }
359 );
Milutin Kristofic5942c9d2023-11-15 20:34:42 +0100360 subscribe(
361 this,
362 () => this.getConfigModel().docsBaseUrl$,
363 docsBaseUrl => (this.docsBaseUrl = docsBaseUrl)
364 );
Milutin Kristoficfc5ccca2023-09-11 13:43:45 +0200365 if (this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT)) {
366 subscribe(
367 this,
368 () =>
369 this.generateSuggestionTrigger$.pipe(
370 debounceTime(GENERATE_SUGGESTION_DEBOUNCE_DELAY_MS)
371 ),
372 () => {
Milutin Kristofic4d963732024-01-04 12:10:03 +0100373 this.generateSuggestEdit();
Milutin Kristoficfc5ccca2023-09-11 13:43:45 +0200374 }
375 );
376 }
Chris Poucet0b961412022-01-05 16:24:50 +0100377 }
378
Milutin Kristofica31d8942023-11-17 13:32:16 +0100379 override connectedCallback() {
380 super.connectedCallback();
381 this.getPluginLoader()
382 .awaitPluginsLoaded()
383 .then(() => {
384 const suggestionsPlugins =
385 this.getPluginLoader().pluginsModel.getState().suggestionsPlugins;
386 // We currently support results from only 1 provider.
387 this.suggestionsProvider = suggestionsPlugins?.[0]?.provider;
388 });
Milutin Kristofic4d963732024-01-04 12:10:03 +0100389
390 const generateSuggestionStoredContent =
391 this.getStorage().getEditableContentItem(
392 ENABLE_GENERATE_SUGGESTION_STORAGE_KEY
393 );
394 if (generateSuggestionStoredContent?.message === 'false') {
395 this.generateSuggestion = false;
396 }
Milutin Kristofica31d8942023-11-17 13:32:16 +0100397 }
398
Gerrit Code Review86b969c2021-08-19 14:33:41 +0000399 override disconnectedCallback() {
Ben Rohlfs05750b92021-10-29 08:23:08 +0200400 // Clean up emoji dropdown.
401 if (this.textarea) this.textarea.closeDropdown();
Ben Rohlfs5f520da2021-03-10 14:58:43 +0100402 super.disconnectedCallback();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100403 }
Andrew Bonventre78792e82016-03-04 17:48:22 -0500404
Ben Rohlfs05750b92021-10-29 08:23:08 +0200405 static override get styles() {
406 return [
Milutin Kristoficaa1c08b2023-09-06 10:34:16 +0200407 formStyles,
Ben Rohlfs05750b92021-10-29 08:23:08 +0200408 sharedStyles,
Dhruv Srivastava4063d262022-11-09 18:46:29 +0530409 modalStyles,
Ben Rohlfs05750b92021-10-29 08:23:08 +0200410 css`
411 :host {
412 display: block;
413 font-family: var(--font-family);
414 padding: var(--spacing-m);
415 }
416 :host([collapsed]) {
417 padding: var(--spacing-s) var(--spacing-m);
418 }
Ben Rohlfs1efb1a72023-04-12 23:25:31 +0200419 :host([error]) {
420 background-color: var(--error-background);
421 border-radius: var(--border-radius);
Ben Rohlfs05750b92021-10-29 08:23:08 +0200422 }
Ben Rohlfs05750b92021-10-29 08:23:08 +0200423 .header {
424 align-items: center;
425 cursor: pointer;
426 display: flex;
Dhruv Srivastavad8f61e72022-09-16 07:34:34 +0000427 padding-bottom: var(--spacing-m);
428 }
429 :host([collapsed]) .header {
430 padding-bottom: 0px;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200431 }
432 .headerLeft > span {
433 font-weight: var(--font-weight-bold);
434 }
435 .headerMiddle {
436 color: var(--deemphasized-text-color);
437 flex: 1;
438 overflow: hidden;
439 }
Ben Rohlfs05750b92021-10-29 08:23:08 +0200440 .draftTooltip {
Ben Rohlfsba361a42022-09-01 12:12:45 +0200441 font-weight: var(--font-weight-bold);
Ben Rohlfs05750b92021-10-29 08:23:08 +0200442 display: inline;
443 }
Ben Rohlfsba361a42022-09-01 12:12:45 +0200444 .draftTooltip gr-icon {
445 color: var(--info-foreground);
446 }
Ben Rohlfs05750b92021-10-29 08:23:08 +0200447 .date {
448 justify-content: flex-end;
449 text-align: right;
450 white-space: nowrap;
451 }
452 span.date {
453 color: var(--deemphasized-text-color);
454 }
455 span.date:hover {
456 text-decoration: underline;
457 }
458 .actions,
459 .robotActions {
460 display: flex;
Milutin Kristoficb7929e72023-09-05 13:13:07 +0200461 justify-content: space-between;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200462 padding-top: 0;
463 }
464 .robotActions {
465 /* Better than the negative margin would be to remove the gr-button
466 * padding, but then we would also need to fix the buttons that are
467 * inserted by plugins. :-/ */
468 margin: 4px 0 -4px;
469 }
470 .action {
471 margin-left: var(--spacing-l);
472 }
Milutin Kristoficb7929e72023-09-05 13:13:07 +0200473 .leftActions,
Ben Rohlfs05750b92021-10-29 08:23:08 +0200474 .rightActions {
475 display: flex;
476 justify-content: flex-end;
477 }
Milutin Kristoficb7929e72023-09-05 13:13:07 +0200478 .leftActions gr-button,
Ben Rohlfs05750b92021-10-29 08:23:08 +0200479 .rightActions gr-button {
480 --gr-button-padding: 0 var(--spacing-s);
481 }
482 .editMessage {
483 display: block;
Dhruv Srivastava694e9372022-09-13 10:29:08 +0200484 margin-bottom: var(--spacing-m);
Ben Rohlfs05750b92021-10-29 08:23:08 +0200485 width: 100%;
486 }
487 .show-hide {
488 margin-left: var(--spacing-s);
489 }
490 .robotId {
491 color: var(--deemphasized-text-color);
492 margin-bottom: var(--spacing-m);
493 }
494 .robotRun {
495 margin-left: var(--spacing-m);
496 }
497 .robotRunLink {
498 margin-left: var(--spacing-m);
499 }
500 /* just for a11y */
501 input.show-hide {
502 display: none;
503 }
504 label.show-hide {
505 cursor: pointer;
506 display: block;
507 }
Chris Poucet1c713862022-07-25 13:12:24 +0200508 label.show-hide gr-icon {
Ben Rohlfs05750b92021-10-29 08:23:08 +0200509 vertical-align: top;
510 }
511 :host([collapsed]) #container .body {
512 padding-top: 0;
513 }
514 #container .collapsedContent {
515 display: block;
516 overflow: hidden;
517 padding-left: var(--spacing-m);
518 text-overflow: ellipsis;
519 white-space: nowrap;
520 }
521 .resolve,
522 .unresolved {
523 align-items: center;
524 display: flex;
525 flex: 1;
526 margin: 0;
527 }
528 .resolve label {
529 color: var(--comment-text-color);
530 }
531 gr-dialog .main {
532 display: flex;
533 flex-direction: column;
534 width: 100%;
535 }
536 #deleteBtn {
537 --gr-button-text-color: var(--deemphasized-text-color);
538 --gr-button-padding: 0;
539 }
540
541 /** Disable select for the caret and actions */
542 .actions,
543 .show-hide {
544 -webkit-user-select: none;
545 -moz-user-select: none;
546 -ms-user-select: none;
547 user-select: none;
548 }
549
Ben Rohlfs05750b92021-10-29 08:23:08 +0200550 .pointer {
551 cursor: pointer;
552 }
553 .patchset-text {
554 color: var(--deemphasized-text-color);
555 margin-left: var(--spacing-s);
556 }
557 .headerLeft gr-account-label {
558 --account-max-length: 130px;
559 width: 150px;
560 }
561 .headerLeft gr-account-label::part(gr-account-label-text) {
562 font-weight: var(--font-weight-bold);
563 }
564 .draft gr-account-label {
565 width: unset;
566 }
Frank Borden0c078842022-09-19 15:47:26 +0200567 .draft gr-formatted-text.message {
Frank Borden3b3a4c92022-09-28 14:14:00 +0200568 display: block;
Frank Borden0c078842022-09-19 15:47:26 +0200569 margin-bottom: var(--spacing-m);
570 }
Ben Rohlfs05750b92021-10-29 08:23:08 +0200571 .portedMessage {
572 margin: 0 var(--spacing-m);
573 }
574 .link-icon {
Chris Poucetc4142042022-06-28 17:51:50 +0200575 margin-left: var(--spacing-m);
Ben Rohlfs05750b92021-10-29 08:23:08 +0200576 cursor: pointer;
577 }
Milutin Kristofic80c3c7e2023-03-16 20:22:28 +0100578 .suggestEdit {
579 /** same height as header */
580 --margin: calc(0px - var(--spacing-s));
581 margin-right: var(--spacing-s);
582 }
583 .suggestEdit gr-icon {
584 color: inherit;
585 margin-right: var(--spacing-s);
586 }
Milutin Kristofic8f07c382023-10-02 13:13:21 +0200587 .info {
588 background-color: var(--info-background);
589 padding: var(--spacing-l) var(--spacing-xl);
590 }
591 .info gr-icon {
592 color: var(--selected-foreground);
593 margin-right: var(--spacing-xl);
594 }
Milutin Kristofic74caea4b2023-11-14 20:10:56 +0100595 /* The basics of .loadingSpin are defined in shared styles. */
596 .loadingSpin {
597 width: calc(var(--line-height-normal) - 2px);
598 height: calc(var(--line-height-normal) - 2px);
599 display: inline-block;
600 vertical-align: top;
601 position: relative;
602 /* Making up for the 2px reduced height above. */
603 top: 1px;
604 }
Ben Rohlfs05750b92021-10-29 08:23:08 +0200605 `,
606 ];
Dhruv Srivastavacf70e792020-07-24 15:35:39 +0200607 }
608
Ben Rohlfs05750b92021-10-29 08:23:08 +0200609 override render() {
Ben Rohlfs1efb1a72023-04-12 23:25:31 +0200610 if (!this.comment) return;
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200611 this.toggleAttribute('saving', isSaving(this.comment));
612 this.toggleAttribute('error', isError(this.comment));
Ben Rohlfs1efb1a72023-04-12 23:25:31 +0200613 const classes = {
614 container: true,
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200615 draft: isDraft(this.comment),
Ben Rohlfs1efb1a72023-04-12 23:25:31 +0200616 };
Ben Rohlfs05750b92021-10-29 08:23:08 +0200617 return html`
Ben Rohlfs7a167842022-09-29 21:55:50 +0200618 <gr-endpoint-decorator name="comment">
619 <gr-endpoint-param name="comment" .value=${this.comment}>
620 </gr-endpoint-param>
621 <gr-endpoint-param name="editing" .value=${this.editing}>
622 </gr-endpoint-param>
Ben Rohlfs57c2c592022-10-25 12:49:11 +0200623 <gr-endpoint-param name="message" .value=${this.messageText}>
624 </gr-endpoint-param>
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200625 <gr-endpoint-param name="isDraft" .value=${isDraft(this.comment)}>
Ben Rohlfs57c2c592022-10-25 12:49:11 +0200626 </gr-endpoint-param>
Ben Rohlfs7a167842022-09-29 21:55:50 +0200627 <div id="container" class=${classMap(classes)}>
628 ${this.renderHeader()}
629 <div class="body">
630 ${this.renderRobotAuthor()} ${this.renderEditingTextarea()}
631 ${this.renderCommentMessage()}
632 <gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
633 ${this.renderHumanActions()} ${this.renderRobotActions()}
Ben Rohlfs7a167842022-09-29 21:55:50 +0200634 </div>
Milutin Kristofic8f07c382023-10-02 13:13:21 +0200635 ${this.renderGeneratedSuggestionPreview()}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200636 </div>
Ben Rohlfs7a167842022-09-29 21:55:50 +0200637 </gr-endpoint-decorator>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200638 ${this.renderConfirmDialog()}
639 `;
640 }
641
Dhruv Srivastavae75f6f72022-08-25 10:14:37 +0200642 private renderHeader() {
643 if (this.hideHeader) return nothing;
644 return html`
645 <div
646 class="header"
647 id="header"
648 @click=${() => (this.collapsed = !this.collapsed)}
649 >
650 <div class="headerLeft">
651 ${this.renderAuthor()} ${this.renderPortedCommentMessage()}
652 ${this.renderDraftLabel()}
653 </div>
654 <div class="headerMiddle">${this.renderCollapsedContent()}</div>
Milutin Kristofic8238de52023-01-12 19:33:45 +0100655 ${this.renderSuggestEditButton()} ${this.renderRunDetails()}
656 ${this.renderDeleteButton()} ${this.renderPatchset()}
Ben Rohlfs0ed33592023-04-12 11:33:27 +0200657 ${this.renderSeparator()} ${this.renderDate()} ${this.renderToggle()}
Dhruv Srivastavae75f6f72022-08-25 10:14:37 +0200658 </div>
659 `;
660 }
661
Ben Rohlfs05750b92021-10-29 08:23:08 +0200662 private renderAuthor() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200663 if (isDraft(this.comment)) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200664 if (isRobot(this.comment)) {
665 const id = this.comment.robot_id;
666 return html`<span class="robotName">${id}</span>`;
667 }
Ben Rohlfs05750b92021-10-29 08:23:08 +0200668 return html`
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200669 <gr-account-label .account=${this.comment?.author ?? this.account}>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200670 </gr-account-label>
671 `;
672 }
673
674 private renderPortedCommentMessage() {
675 if (!this.showPortedComment) return;
676 if (!this.comment?.patch_set) return;
677 return html`
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200678 <a href=${this.getUrlForComment()}>
679 <span class="portedMessage" @click=${this.handlePortedMessageClick}>
Ben Rohlfs95796222021-12-01 16:39:42 +0100680 From patchset ${this.comment?.patch_set}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200681 </span>
682 </a>
683 `;
684 }
685
686 private renderDraftLabel() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200687 if (!isDraft(this.comment)) return;
Ben Rohlfsba361a42022-09-01 12:12:45 +0200688 let label = 'Draft';
Ben Rohlfs05750b92021-10-29 08:23:08 +0200689 let tooltip =
690 'This draft is only visible to you. ' +
691 "To publish drafts, click the 'Reply' or 'Start review' button " +
692 "at the top of the change or press the 'a' key.";
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200693 if (isError(this.comment)) {
Ben Rohlfs05750b92021-10-29 08:23:08 +0200694 label += ' (Failed to save)';
695 tooltip = 'Unable to save draft. Please try to save again.';
696 }
697 return html`
698 <gr-tooltip-content
699 class="draftTooltip"
700 has-tooltip
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200701 title=${tooltip}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200702 max-width="20em"
Ben Rohlfs05750b92021-10-29 08:23:08 +0200703 >
Ben Rohlfsba361a42022-09-01 12:12:45 +0200704 <gr-icon filled icon="rate_review"></gr-icon>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200705 <span class="draftLabel">${label}</span>
706 </gr-tooltip-content>
707 `;
708 }
709
710 private renderCollapsedContent() {
711 if (!this.collapsed) return;
712 return html`
713 <span class="collapsedContent">${this.comment?.message}</span>
714 `;
715 }
716
717 private renderRunDetails() {
718 if (!isRobot(this.comment)) return;
719 if (!this.comment?.url || this.collapsed) return;
720 return html`
721 <div class="runIdMessage message">
722 <div class="runIdInformation">
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200723 <a class="robotRunLink" href=${this.comment.url}>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200724 <span class="robotRun link">Run Details</span>
725 </a>
726 </div>
727 </div>
728 `;
729 }
730
731 /**
732 * Deleting a comment is an admin feature. It means more than just discarding
733 * a draft. It is an action applied to published comments.
734 */
735 private renderDeleteButton() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200736 if (!this.isAdmin || isDraft(this.comment) || isRobot(this.comment)) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200737 if (this.collapsed) return;
738 return html`
739 <gr-button
740 id="deleteBtn"
741 title="Delete Comment"
742 link
743 class="action delete"
Kamil Musin462428b2022-12-29 11:12:08 +0100744 @click=${(e: MouseEvent) => {
745 e.stopPropagation();
746 this.openDeleteCommentModal();
747 }}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200748 >
Chris Poucet1c713862022-07-25 13:12:24 +0200749 <gr-icon id="icon" icon="delete" filled></gr-icon>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200750 </gr-button>
751 `;
752 }
753
754 private renderPatchset() {
755 if (!this.showPatchset) return;
756 assertIsDefined(this.comment?.patch_set, 'comment.patch_set');
757 return html`
758 <span class="patchset-text"> Patchset ${this.comment.patch_set}</span>
759 `;
760 }
761
Ben Rohlfs0ed33592023-04-12 11:33:27 +0200762 private renderSeparator() {
763 // This should match the condition of `renderPatchset()`.
764 if (!this.showPatchset) return;
765 // This should match the condition of `renderDate()`.
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200766 if (this.collapsed) return;
Ben Rohlfs0ed33592023-04-12 11:33:27 +0200767 // Render separator, if both are present: patchset AND date.
768 return html`<span class="separator"></span>`;
769 }
770
Ben Rohlfs05750b92021-10-29 08:23:08 +0200771 private renderDate() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200772 if (this.collapsed) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200773 return html`
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200774 <span class="date" tabindex="0" @click=${this.handleAnchorClick}>
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200775 ${this.renderDateInner()}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200776 </span>
777 `;
778 }
779
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200780 private renderDateInner() {
781 if (isError(this.comment)) return 'Error';
Ben Rohlfsd98a04c2023-05-04 11:31:15 +0200782 if (isSaving(this.comment) && !this.autoSaving) return 'Saving';
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200783 if (isNew(this.comment)) return 'New';
784 return html`
785 <gr-date-formatter
786 withTooltip
787 .dateStr=${this.comment!.updated}
788 ></gr-date-formatter>
789 `;
790 }
791
Ben Rohlfs05750b92021-10-29 08:23:08 +0200792 private renderToggle() {
Chris Poucetb8c06392022-07-08 16:35:43 +0200793 const icon = this.collapsed ? 'expand_more' : 'expand_less';
Ben Rohlfs05750b92021-10-29 08:23:08 +0200794 const ariaLabel = this.collapsed ? 'Expand' : 'Collapse';
795 return html`
796 <div class="show-hide" tabindex="0">
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200797 <label class="show-hide" aria-label=${ariaLabel}>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200798 <input
799 type="checkbox"
800 class="show-hide"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200801 ?checked=${this.collapsed}
802 @change=${() => (this.collapsed = !this.collapsed)}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200803 />
Chris Poucet1c713862022-07-25 13:12:24 +0200804 <gr-icon icon=${icon} id="icon"></gr-icon>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200805 </label>
806 </div>
807 `;
808 }
809
810 private renderRobotAuthor() {
811 if (!isRobot(this.comment) || this.collapsed) return;
812 return html`<div class="robotId">${this.comment.author?.name}</div>`;
813 }
814
815 private renderEditingTextarea() {
816 if (!this.editing || this.collapsed) return;
817 return html`
818 <gr-textarea
819 id="editTextarea"
820 class="editMessage"
821 autocomplete="on"
822 code=""
Ben Rohlfs05750b92021-10-29 08:23:08 +0200823 rows="4"
Dhruv Srivastavaf007abc2022-09-06 11:18:57 +0200824 .placeholder=${this.messagePlaceholder}
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200825 text=${this.messageText}
826 @text-changed=${(e: ValueChangedEvent) => {
Ben Rohlfs05750b92021-10-29 08:23:08 +0200827 // TODO: This is causing a re-render of <gr-comment> on every key
828 // press. Try to avoid always setting `this.messageText` or at least
Ben Rohlfs2e237552021-11-24 10:34:28 +0100829 // debounce it. Most of the code can just inspect the current value
Ben Rohlfs05750b92021-10-29 08:23:08 +0200830 // of the textare instead of needing a dedicated property.
831 this.messageText = e.detail.value;
Ben Rohlfs2e237552021-11-24 10:34:28 +0100832 this.autoSaveTrigger$.next();
Milutin Kristoficfc5ccca2023-09-11 13:43:45 +0200833 this.generateSuggestionTrigger$.next();
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200834 }}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200835 ></gr-textarea>
836 `;
837 }
838
Ben Rohlfs05750b92021-10-29 08:23:08 +0200839 private renderCommentMessage() {
840 if (this.collapsed || this.editing) return;
Frank Bordenf9a29992022-08-24 20:19:23 +0200841
Ben Rohlfs05750b92021-10-29 08:23:08 +0200842 return html`
843 <!--The "message" class is needed to ensure selectability from
844 gr-diff-selection.-->
845 <gr-formatted-text
846 class="message"
Frank Bordenabdd1872022-09-26 12:55:59 +0200847 .markdown=${true}
848 .content=${this.comment?.message ?? ''}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200849 ></gr-formatted-text>
850 `;
851 }
852
853 private renderCopyLinkIcon() {
854 // Only show the icon when the thread contains a published comment.
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200855 if (!this.comment?.in_reply_to && isDraft(this.comment)) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200856 return html`
Chris Poucet1c713862022-07-25 13:12:24 +0200857 <gr-icon
858 icon="link"
859 class="copy link-icon"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200860 @click=${this.handleCopyLink}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200861 title="Copy link to this comment"
Ben Rohlfs05750b92021-10-29 08:23:08 +0200862 role="button"
863 tabindex="0"
Chris Poucet1c713862022-07-25 13:12:24 +0200864 ></gr-icon>
Ben Rohlfs05750b92021-10-29 08:23:08 +0200865 `;
866 }
867
868 private renderHumanActions() {
869 if (!this.account || isRobot(this.comment)) return;
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200870 if (this.collapsed || !isDraft(this.comment)) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200871 return html`
872 <div class="actions">
Milutin Kristoficb7929e72023-09-05 13:13:07 +0200873 <div class="leftActions">
874 <div class="action resolve">
875 <label>
876 <input
877 type="checkbox"
878 id="resolvedCheckbox"
879 ?checked=${!this.unresolved}
880 @change=${this.handleToggleResolved}
881 />
882 Resolved
883 </label>
884 </div>
885 ${this.renderGenerateSuggestEditButton()}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200886 </div>
887 ${this.renderDraftActions()}
888 </div>
889 `;
890 }
891
892 private renderDraftActions() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200893 if (!isDraft(this.comment)) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200894 return html`
895 <div class="rightActions">
Milutin Kristofic1ebae372022-11-22 20:35:38 +0100896 ${this.renderDiscardButton()} ${this.renderEditButton()}
Milutin Kristoficb7929e72023-09-05 13:13:07 +0200897 ${this.renderCancelButton()} ${this.renderSaveButton()}
898 ${this.renderCopyLinkIcon()}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200899 </div>
900 `;
901 }
902
Milutin Kristofic1d219672022-06-21 14:57:25 +0200903 private renderSuggestEditButton() {
Dhruv Srivastava66a15632022-09-06 11:57:34 +0200904 if (
Milutin Kristofic8238de52023-01-12 19:33:45 +0100905 !this.editing ||
Dhruv Srivastava66a15632022-09-06 11:57:34 +0200906 this.permanentEditingMode ||
907 this.comment?.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS
908 ) {
909 return nothing;
910 }
Milutin Kristofic1d219672022-06-21 14:57:25 +0200911 assertIsDefined(this.comment, 'comment');
912 if (hasUserSuggestion(this.comment)) return nothing;
913 // TODO(milutin): remove this check once suggesting on commit message is
914 // fixed. Currently diff line doesn't match commit message line, because
915 // of metadata in diff, which aren't in content api request.
916 if (this.comment.path === SpecialFilePath.COMMIT_MESSAGE) return nothing;
Milutin Kristofic50394b12023-02-01 11:28:52 +0000917 if (this.isOwner) return nothing;
Milutin Kristofic7dec89b2022-09-13 12:11:35 +0200918 return html`<gr-button
919 link
920 class="action suggestEdit"
Milutin Kristofic80c3c7e2023-03-16 20:22:28 +0100921 title="This button copies the text to make a suggestion"
Milutin Kristofic7dec89b2022-09-13 12:11:35 +0200922 @click=${this.createSuggestEdit}
Milutin Kristofic80c3c7e2023-03-16 20:22:28 +0100923 ><gr-icon icon="edit" id="icon" filled></gr-icon> Suggest edit</gr-button
Milutin Kristofic1d219672022-06-21 14:57:25 +0200924 >`;
925 }
926
Ben Rohlfs05750b92021-10-29 08:23:08 +0200927 private renderDiscardButton() {
Dhruv Srivastava705eac92022-09-01 11:33:27 +0200928 if (this.editing || this.permanentEditingMode) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200929 return html`<gr-button
930 link
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200931 ?disabled=${isSaving(this.comment) && !this.autoSaving}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200932 class="action discard"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200933 @click=${this.discard}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200934 >Discard</gr-button
935 >`;
936 }
937
938 private renderEditButton() {
939 if (this.editing) return;
Ben Rohlfs1efb1a72023-04-12 23:25:31 +0200940 return html`<gr-button link class="action edit" @click=${this.edit}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200941 >Edit</gr-button
942 >`;
943 }
944
945 private renderCancelButton() {
Dhruv Srivastava705eac92022-09-01 11:33:27 +0200946 if (!this.editing || this.permanentEditingMode) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200947 return html`
948 <gr-button
949 link
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200950 ?disabled=${isSaving(this.comment) && !this.autoSaving}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200951 class="action cancel"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200952 @click=${this.cancel}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200953 >Cancel</gr-button
954 >
955 `;
956 }
957
958 private renderSaveButton() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +0200959 if (!this.editing) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +0200960 return html`
961 <gr-button
962 link
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200963 ?disabled=${this.isSaveDisabled()}
Ben Rohlfs05750b92021-10-29 08:23:08 +0200964 class="action save"
Dhruv Srivastava705eac92022-09-01 11:33:27 +0200965 @click=${this.handleSaveButtonClicked}
Dhruv Srivastava00831e72022-09-05 08:20:20 +0200966 >${this.permanentEditingMode ? 'Preview' : 'Save'}</gr-button
Ben Rohlfs05750b92021-10-29 08:23:08 +0200967 >
968 `;
969 }
970
Milutin Kristofic490fa952023-09-12 20:17:36 +0200971 private showGeneratedSuggestion() {
972 return (
973 this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT) &&
Milutin Kristofica31d8942023-11-17 13:32:16 +0100974 this.suggestionsProvider &&
Milutin Kristofic490fa952023-09-12 20:17:36 +0200975 this.editing &&
976 !this.permanentEditingMode &&
Milutin Kristofic490fa952023-09-12 20:17:36 +0200977 this.comment &&
Milutin Kristofica31d8942023-11-17 13:32:16 +0100978 this.comment.path &&
Milutin Kristofic8afa8e42023-09-22 20:33:07 +0200979 this.comment.path !== SpecialFilePath.PATCHSET_LEVEL_COMMENTS &&
980 this.comment.path !== SpecialFilePath.COMMIT_MESSAGE &&
Milutin Kristofica31d8942023-11-17 13:32:16 +0100981 (!this.suggestionsProvider.supportedFileExtensions ||
982 this.suggestionsProvider.supportedFileExtensions.includes(
983 getFileExtension(this.comment.path)
984 )) &&
Milutin Kristofic8afa8e42023-09-22 20:33:07 +0200985 this.comment === this.comments?.[0] && // Is first comment
986 (this.comment.range || this.comment.line) && // Disabled for File comments
Milutin Kristofic35c48c32023-12-11 19:52:41 +0100987 !hasUserSuggestion(this.comment) &&
988 this.getChangeModel().getChange()?.is_private !== true
Milutin Kristofic490fa952023-09-12 20:17:36 +0200989 );
990 }
991
Milutin Kristofic8f07c382023-10-02 13:13:21 +0200992 private renderGeneratedSuggestionPreview() {
993 if (
994 !this.showGeneratedSuggestion() ||
995 !this.generateSuggestion ||
996 !this.generatedSuggestion
997 )
998 return nothing;
Milutin Kristofic97683a82023-11-20 20:06:22 +0100999
Milutin Kristofic8f07c382023-10-02 13:13:21 +02001000 if (this.generatedSuggestion.newRange) {
Milutin Kristofic97683a82023-11-20 20:06:22 +01001001 return this.renderGeneratedSuggestionOutOfRange();
1002 } else {
1003 return html`<gr-suggestion-diff-preview
1004 .showAddSuggestionButton=${true}
1005 .suggestion=${this.generatedSuggestion?.replacement}
1006 .uuid=${this.generatedSuggestionId}
1007 ></gr-suggestion-diff-preview>`;
1008 }
1009 }
1010
1011 // TODO(milutin): This is temporary warning, will be removed, once we are
1012 // able to change range of a comment
1013 private renderGeneratedSuggestionOutOfRange() {
1014 if (!this.generatedSuggestion?.newRange) return;
1015 if (
1016 this.flagsService.isEnabled(
1017 KnownExperimentId.ML_SUGGESTED_EDIT_OUT_OF_RANGE
1018 )
1019 ) {
Milutin Kristofic8f07c382023-10-02 13:13:21 +02001020 const range = this.generatedSuggestion.newRange;
1021 return html`<div class="info">
1022 <gr-icon icon="info" filled></gr-icon>
1023 There is a suggestion in range (${range.start_line}, ${range.end_line})
1024 </div>`;
Milutin Kristofic97683a82023-11-20 20:06:22 +01001025 } else {
1026 return nothing;
Milutin Kristofic8f07c382023-10-02 13:13:21 +02001027 }
Milutin Kristofic8f07c382023-10-02 13:13:21 +02001028 }
1029
Milutin Kristofic734df552023-08-07 10:38:50 +02001030 private renderGenerateSuggestEditButton() {
Milutin Kristofic490fa952023-09-12 20:17:36 +02001031 if (!this.showGeneratedSuggestion()) {
Milutin Kristoficb7929e72023-09-05 13:13:07 +02001032 return nothing;
1033 }
Milutin Kristoficdca27ed2023-11-21 21:46:51 +01001034 const tooltip =
1035 'Select to show a generated suggestion based on your comment for commented text. This suggestion can be inserted as a code block in your comment.';
Milutin Kristofic734df552023-08-07 10:38:50 +02001036 return html`
Milutin Kristoficfc5ccca2023-09-11 13:43:45 +02001037 <div class="action">
Milutin Kristoficdca27ed2023-11-21 21:46:51 +01001038 <label title=${tooltip}>
Milutin Kristoficfc5ccca2023-09-11 13:43:45 +02001039 <input
1040 type="checkbox"
1041 id="generateSuggestCheckbox"
1042 ?checked=${this.generateSuggestion}
1043 @change=${() => {
1044 this.generateSuggestion = !this.generateSuggestion;
Milutin Kristofic4d963732024-01-04 12:10:03 +01001045 this.getStorage().setEditableContentItem(
1046 ENABLE_GENERATE_SUGGESTION_STORAGE_KEY,
1047 this.generateSuggestion.toString()
1048 );
1049 if (this.generateSuggestion) {
Milutin Kristoficfc5ccca2023-09-11 13:43:45 +02001050 this.generateSuggestionTrigger$.next();
1051 }
Milutin Kristofic96ac52e2023-09-18 10:28:58 +02001052 this.reporting.reportInteraction(
1053 this.generateSuggestion
1054 ? Interaction.GENERATE_SUGGESTION_ENABLED
1055 : Interaction.GENERATE_SUGGESTION_DISABLED
1056 );
Milutin Kristoficfc5ccca2023-09-11 13:43:45 +02001057 }}
1058 />
Milutin Kristofic74caea4b2023-11-14 20:10:56 +01001059 Generate Suggestion
1060 ${when(
1061 this.suggestionLoading,
1062 () => html`<span class="loadingSpin"></span>`,
Milutin Kristofic97683a82023-11-20 20:06:22 +01001063 () => html`${this.getNumberOfSuggestions()}`
Milutin Kristofic74caea4b2023-11-14 20:10:56 +01001064 )}
Milutin Kristoficfc5ccca2023-09-11 13:43:45 +02001065 </label>
Milutin Kristofic5942c9d2023-11-15 20:34:42 +01001066 <a
1067 href=${getDocUrl(
1068 this.docsBaseUrl,
1069 'user-suggest-edits.html#_generate_suggestion'
1070 )}
1071 target="_blank"
1072 rel="noopener noreferrer"
1073 >
1074 <gr-icon
1075 icon="help"
1076 title="About Generated Suggested Edits"
1077 ></gr-icon>
1078 </a>
Milutin Kristoficfc5ccca2023-09-11 13:43:45 +02001079 </div>
Milutin Kristofic734df552023-08-07 10:38:50 +02001080 `;
1081 }
1082
Milutin Kristofic97683a82023-11-20 20:06:22 +01001083 private getNumberOfSuggestions() {
1084 if (this.generatedSuggestion?.newRange) {
1085 if (
1086 this.flagsService.isEnabled(
1087 KnownExperimentId.ML_SUGGESTED_EDIT_OUT_OF_RANGE
1088 )
1089 ) {
1090 return '(1)';
1091 } else {
1092 return '(0)';
1093 }
1094 }
Milutin Kristofic782ecfa2023-11-24 11:15:15 +01001095 if (this.generatedSuggestion) {
Milutin Kristofic97683a82023-11-20 20:06:22 +01001096 return '(1)';
1097 } else {
1098 return '(0)';
1099 }
1100 }
1101
Milutin Kristofic490fa952023-09-12 20:17:36 +02001102 private handleAddGeneratedSuggestion(code: string) {
Milutin Kristoficfc5ccca2023-09-11 13:43:45 +02001103 const addNewLine = this.messageText.length !== 0;
1104 this.messageText += `${
1105 addNewLine ? '\n' : ''
Milutin Kristofic490fa952023-09-12 20:17:36 +02001106 }${USER_SUGGESTION_START_PATTERN}${code}${'\n```'}`;
Milutin Kristoficfc5ccca2023-09-11 13:43:45 +02001107 }
1108
Milutin Kristofic734df552023-08-07 10:38:50 +02001109 private async generateSuggestEdit() {
Milutin Kristofica31d8942023-11-17 13:32:16 +01001110 const suggestionsProvider = this.suggestionsProvider;
Kamil Musin48ff12a2023-12-05 14:25:27 +01001111 const changeInfo = this.getChangeModel().getChange();
Milutin Kristofic8afa8e42023-09-22 20:33:07 +02001112 if (
Milutin Kristofica31d8942023-11-17 13:32:16 +01001113 !suggestionsProvider ||
Milutin Kristofic8afa8e42023-09-22 20:33:07 +02001114 !this.showGeneratedSuggestion() ||
Kamil Musin48ff12a2023-12-05 14:25:27 +01001115 !changeInfo ||
Milutin Kristofic8afa8e42023-09-22 20:33:07 +02001116 !this.comment ||
1117 !this.comment.patch_set ||
1118 !this.comment.path ||
1119 this.messageText.length === 0
1120 )
Milutin Kristofic734df552023-08-07 10:38:50 +02001121 return;
Milutin Kristofic06dfda42023-11-15 09:51:13 +01001122 this.generatedSuggestionId = uuid();
Milutin Kristofic96ac52e2023-09-18 10:28:58 +02001123 this.reporting.reportInteraction(Interaction.GENERATE_SUGGESTION_REQUEST, {
Milutin Kristofic06dfda42023-11-15 09:51:13 +01001124 uuid: this.generatedSuggestionId,
Milutin Kristofic96ac52e2023-09-18 10:28:58 +02001125 });
Milutin Kristofic74caea4b2023-11-14 20:10:56 +01001126 this.suggestionLoading = true;
Milutin Kristoficd682dc72023-12-13 09:35:47 +01001127 let suggestionResponse;
1128 try {
1129 suggestionResponse = await suggestionsProvider.suggestCode({
1130 prompt: this.messageText,
1131 changeInfo: changeInfo as ChangeInfo,
1132 patchsetNumber: this.comment?.patch_set,
1133 filePath: this.comment.path,
1134 range: this.comment.range,
1135 lineNumber: this.comment.line,
1136 });
1137 } finally {
1138 this.suggestionLoading = false;
1139 }
1140
1141 if (!suggestionResponse) return;
Milutin Kristofic8f07c382023-10-02 13:13:21 +02001142 // TODO(milutin): The suggestionResponse can contain multiple suggestion
1143 // options. We pick the first one for now. In future we shouldn't ignore
1144 // other suggestions.
Milutin Kristofic96ac52e2023-09-18 10:28:58 +02001145 this.reporting.reportInteraction(Interaction.GENERATE_SUGGESTION_RESPONSE, {
Milutin Kristofic06dfda42023-11-15 09:51:13 +01001146 uuid: this.generatedSuggestionId,
Milutin Kristofic8f07c382023-10-02 13:13:21 +02001147 response: suggestionResponse.responseCode,
1148 numSuggestions: suggestionResponse.suggestions.length,
1149 hasNewRange: suggestionResponse.suggestions?.[0]?.newRange !== undefined,
Milutin Kristofic96ac52e2023-09-18 10:28:58 +02001150 });
Milutin Kristofic8f07c382023-10-02 13:13:21 +02001151 const suggestion = suggestionResponse.suggestions?.[0];
1152 if (!suggestion) return;
1153 this.generatedSuggestion = suggestion;
Milutin Kristofic734df552023-08-07 10:38:50 +02001154 }
1155
Ben Rohlfs05750b92021-10-29 08:23:08 +02001156 private renderRobotActions() {
1157 if (!this.account || !isRobot(this.comment)) return;
1158 const endpoint = html`
1159 <gr-endpoint-decorator name="robot-comment-controls">
Ben Rohlfs8003bd32022-04-05 18:24:42 +02001160 <gr-endpoint-param name="comment" .value=${this.comment}>
Ben Rohlfs05750b92021-10-29 08:23:08 +02001161 </gr-endpoint-param>
1162 </gr-endpoint-decorator>
1163 `;
1164 return html`
1165 <div class="robotActions">
1166 ${this.renderCopyLinkIcon()} ${endpoint} ${this.renderShowFixButton()}
1167 ${this.renderPleaseFixButton()}
1168 </div>
1169 `;
1170 }
1171
1172 private renderShowFixButton() {
Kamil Musind4418632023-03-07 10:20:49 +01001173 const fix_suggestions = (this.comment as RobotCommentInfo)?.fix_suggestions;
1174 if (!fix_suggestions || fix_suggestions.length === 0) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +02001175 return html`
1176 <gr-button
1177 link
1178 secondary
1179 class="action show-fix"
Kamil Musind4418632023-03-07 10:20:49 +01001180 @click=${() => this.handleShowFix()}
Ben Rohlfs05750b92021-10-29 08:23:08 +02001181 >
1182 Show Fix
1183 </gr-button>
1184 `;
1185 }
1186
1187 private renderPleaseFixButton() {
1188 if (this.hasHumanReply()) return;
1189 return html`
1190 <gr-button
1191 link
Ben Rohlfs8003bd32022-04-05 18:24:42 +02001192 ?disabled=${this.robotButtonDisabled}
Ben Rohlfs05750b92021-10-29 08:23:08 +02001193 class="action fix"
Ben Rohlfs23843882022-08-04 18:06:27 +02001194 @click=${this.handlePleaseFix}
Ben Rohlfs05750b92021-10-29 08:23:08 +02001195 >
1196 Please Fix
1197 </gr-button>
1198 `;
1199 }
1200
1201 private renderConfirmDialog() {
Ben Rohlfs05750b92021-10-29 08:23:08 +02001202 return html`
Dhruv Srivastava4063d262022-11-09 18:46:29 +05301203 <dialog id="confirmDeleteModal" tabindex="-1">
Ben Rohlfs05750b92021-10-29 08:23:08 +02001204 <gr-confirm-delete-comment-dialog
Kamil Musinc7d3f282022-12-29 13:27:55 +01001205 id="confirmDeleteCommentDialog"
Ben Rohlfs8003bd32022-04-05 18:24:42 +02001206 @confirm=${this.handleConfirmDeleteComment}
Dhruv Srivastava4063d262022-11-09 18:46:29 +05301207 @cancel=${this.closeDeleteCommentModal}
Ben Rohlfs05750b92021-10-29 08:23:08 +02001208 >
1209 </gr-confirm-delete-comment-dialog>
Dhruv Srivastava4063d262022-11-09 18:46:29 +05301210 </dialog>
Ben Rohlfs05750b92021-10-29 08:23:08 +02001211 `;
1212 }
1213
1214 private getUrlForComment() {
Ben Rohlfsba440822023-04-11 18:08:03 +02001215 if (!this.changeNum || !this.repoName || !this.comment?.id) return '';
Ben Rohlfs731738b2022-09-15 15:55:33 +02001216 return createDiffUrl({
1217 changeNum: this.changeNum,
Ben Rohlfsbfc688b2022-10-21 12:38:37 +02001218 repo: this.repoName,
Ben Rohlfsba440822023-04-11 18:08:03 +02001219 commentId: this.comment.id,
Ben Rohlfs731738b2022-09-15 15:55:33 +02001220 });
Dhruv Srivastava0287bf92020-09-11 16:56:38 +02001221 }
1222
Ben Rohlfs05750b92021-10-29 08:23:08 +02001223 private firstWillUpdateDone = false;
1224
1225 firstWillUpdate() {
1226 if (this.firstWillUpdateDone) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +02001227 assertIsDefined(this.comment, 'comment');
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001228 this.firstWillUpdateDone = true;
Ben Rohlfs05750b92021-10-29 08:23:08 +02001229 this.unresolved = this.comment.unresolved ?? true;
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001230 if (this.permanentEditingMode) {
1231 this.edit();
1232 }
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001233 if (isDraft(this.comment)) {
Ben Rohlfs05750b92021-10-29 08:23:08 +02001234 this.collapsed = false;
1235 } else {
1236 this.collapsed = !!this.initiallyCollapsed;
1237 }
1238 }
1239
Dhruv Srivastavaa49dffd2022-10-20 14:04:37 +02001240 override updated(changed: PropertyValues) {
1241 if (changed.has('editing')) {
Dhruv Srivastavae8b86392022-10-20 17:17:21 +02001242 if (this.editing && !this.permanentEditingMode) {
Ben Rohlfsba9329c2023-05-16 10:05:12 +02001243 // Note that this is a bit fragile, because we are relying on the
1244 // comment to become visible soonish. If that does not happen, then we
1245 // will be waiting indefinitely and grab focus at some point in the
1246 // distant future.
Dhruv Srivastavaa49dffd2022-10-20 14:04:37 +02001247 whenVisible(this, () => this.textarea?.putCursorAtEnd());
1248 }
1249 }
Milutin Kristofic490fa952023-09-12 20:17:36 +02001250 if (
1251 changed.has('changeNum') ||
1252 changed.has('comment') ||
Milutin Kristofic06dfda42023-11-15 09:51:13 +01001253 changed.has('generatedSuggestion')
Milutin Kristofic490fa952023-09-12 20:17:36 +02001254 ) {
Milutin Kristofice9dbbe92023-05-17 21:21:28 +02001255 if (
Milutin Kristofice9dbbe92023-05-17 21:21:28 +02001256 !this.changeNum ||
Milutin Kristofic78cdec92023-08-29 14:37:31 +02001257 !this.comment ||
Milutin Kristofic8f07c382023-10-02 13:13:21 +02001258 (!hasUserSuggestion(this.comment) && !this.generatedSuggestion)
Milutin Kristofice9dbbe92023-05-17 21:21:28 +02001259 )
1260 return;
1261 (async () => {
Milutin Kristofic78cdec92023-08-29 14:37:31 +02001262 this.commentedText = await this.commentModel.getCommentedCode(
1263 this.comment,
1264 this.changeNum
1265 );
Milutin Kristofice9dbbe92023-05-17 21:21:28 +02001266 })();
1267 }
Dhruv Srivastavaa49dffd2022-10-20 14:04:37 +02001268 }
1269
Ben Rohlfs05750b92021-10-29 08:23:08 +02001270 override willUpdate(changed: PropertyValues) {
1271 this.firstWillUpdate();
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001272 if (changed.has('comment')) {
1273 if (isDraft(this.comment) && isError(this.comment)) {
1274 this.edit();
1275 }
Milutin Kristofice9dbbe92023-05-17 21:21:28 +02001276 if (this.comment) {
1277 this.commentModel.updateState({
1278 comment: this.comment,
1279 });
1280 }
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001281 }
Ben Rohlfs05750b92021-10-29 08:23:08 +02001282 if (changed.has('editing')) {
Chris Poucetafd0f7c2022-10-04 10:04:43 +00001283 this.onEditingChanged();
Ben Rohlfs05750b92021-10-29 08:23:08 +02001284 }
1285 if (changed.has('unresolved')) {
1286 // The <gr-comment-thread> component wants to change its color based on
1287 // the (dirty) unresolved state, so let's notify it about changes.
Dhruv Srivastava463bb332022-08-31 13:00:49 +02001288 fire(this, 'comment-unresolved-changed', {value: this.unresolved});
1289 }
1290 if (changed.has('messageText')) {
1291 // GrReplyDialog updates it's state when text inside patchset level
1292 // comment changes.
1293 fire(this, 'comment-text-changed', {value: this.messageText});
Ben Rohlfs05750b92021-10-29 08:23:08 +02001294 }
1295 }
1296
1297 private handlePortedMessageClick() {
Ben Rohlfsc1c6afd2021-02-18 13:13:22 +01001298 assertIsDefined(this.comment, 'comment');
Dhruv Srivastavac8df7602021-01-15 10:59:00 +01001299 this.reporting.reportInteraction('navigate-to-original-comment', {
1300 line: this.comment.line,
1301 range: this.comment.range,
1302 });
1303 }
1304
Ben Rohlfs05750b92021-10-29 08:23:08 +02001305 private handleCopyLink() {
Ben Rohlfs44f01042023-02-18 13:27:57 +01001306 fire(this, 'copy-comment-link', {});
Ben Rohlfs05750b92021-10-29 08:23:08 +02001307 }
1308
1309 /** Enter editing mode. */
Ben Rohlfsba9329c2023-05-16 10:05:12 +02001310 edit() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001311 assert(isDraft(this.comment), 'only drafts are editable');
Ben Rohlfs05750b92021-10-29 08:23:08 +02001312 if (this.editing) return;
Ben Rohlfs05750b92021-10-29 08:23:08 +02001313 this.editing = true;
Ben Rohlfs05750b92021-10-29 08:23:08 +02001314 }
1315
1316 // TODO: Move this out of gr-comment. gr-comment should not have a comments
1317 // property.
1318 private hasHumanReply() {
1319 if (!this.comment || !this.comments) return false;
1320 return this.comments.some(
1321 c => c.in_reply_to && c.in_reply_to === this.comment?.id && !isRobot(c)
Milutin Kristoficafae0052020-09-17 10:38:08 +02001322 );
Ben Rohlfs05750b92021-10-29 08:23:08 +02001323 }
1324
1325 // private, but visible for testing
Milutin Kristofic1ebae372022-11-22 20:35:38 +01001326 async createFixPreview(
1327 replacement?: string
1328 ): Promise<OpenFixPreviewEventDetail> {
Ben Rohlfs05750b92021-10-29 08:23:08 +02001329 assertIsDefined(this.comment?.patch_set, 'comment.patch_set');
Ben Rohlfs23843882022-08-04 18:06:27 +02001330 assertIsDefined(this.comment?.path, 'comment.path');
1331
Milutin Kristofic1ebae372022-11-22 20:35:38 +01001332 if (hasUserSuggestion(this.comment) || replacement) {
1333 replacement = replacement ?? getUserSuggestion(this.comment);
Ben Rohlfs23843882022-08-04 18:06:27 +02001334 assert(!!replacement, 'malformed user suggestion');
Milutin Kristofic78cdec92023-08-29 14:37:31 +02001335 let commentedCode = this.commentedText;
1336 if (!commentedCode) {
1337 commentedCode = await this.getCommentedCode();
1338 }
Ben Rohlfs23843882022-08-04 18:06:27 +02001339
1340 return {
1341 fixSuggestions: createUserFixSuggestion(
1342 this.comment,
Milutin Kristofic78cdec92023-08-29 14:37:31 +02001343 commentedCode,
Ben Rohlfs23843882022-08-04 18:06:27 +02001344 replacement
1345 ),
1346 patchNum: this.comment.patch_set,
Milutin Kristoficeb8f6552023-02-09 22:17:35 +01001347 onCloseFixPreviewCallbacks: [
1348 fixApplied => {
1349 if (fixApplied) this.handleAppliedFix();
1350 },
1351 ],
Ben Rohlfs23843882022-08-04 18:06:27 +02001352 };
1353 }
1354 if (isRobot(this.comment) && this.comment.fix_suggestions.length > 0) {
1355 const id = this.comment.robot_id;
1356 return {
1357 fixSuggestions: this.comment.fix_suggestions.map(s => {
1358 return {
1359 ...s,
1360 description: `${id ?? ''} - ${s.description ?? ''}`,
1361 };
1362 }),
1363 patchNum: this.comment.patch_set,
Milutin Kristoficeb8f6552023-02-09 22:17:35 +01001364 onCloseFixPreviewCallbacks: [],
Ben Rohlfs23843882022-08-04 18:06:27 +02001365 };
1366 }
1367 throw new Error('unable to create preview fix event');
Ben Rohlfs05750b92021-10-29 08:23:08 +02001368 }
1369
Chris Poucetafd0f7c2022-10-04 10:04:43 +00001370 private onEditingChanged() {
1371 if (this.editing) {
1372 this.collapsed = false;
1373 this.messageText = this.comment?.message ?? '';
1374 this.unresolved = this.comment?.unresolved ?? true;
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001375 if (!isError(this.comment) && !isSaving(this.comment)) {
1376 this.originalMessage = this.messageText;
1377 this.originalUnresolved = this.unresolved;
1378 }
Chris Poucetafd0f7c2022-10-04 10:04:43 +00001379 }
1380
1381 // Parent components such as the reply dialog might be interested in whether
1382 // come of their child components are in editing mode.
1383 fire(this, 'comment-editing-changed', {
1384 editing: this.editing,
1385 path: this.comment?.path ?? '',
1386 });
Ben Rohlfs05750b92021-10-29 08:23:08 +02001387 }
1388
Ben Rohlfs05750b92021-10-29 08:23:08 +02001389 // private, but visible for testing
1390 isSaveDisabled() {
1391 assertIsDefined(this.comment, 'comment');
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001392 if (isSaving(this.comment) && !this.autoSaving) return true;
Ben Rohlfs2e237552021-11-24 10:34:28 +01001393 return !this.messageText?.trimEnd();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001394 }
1395
Dhruv Srivastavae8b86392022-10-20 17:17:21 +02001396 override focus() {
Ben Rohlfsba9329c2023-05-16 10:05:12 +02001397 // Note that this may not work as intended, because the textarea is not
1398 // rendered yet.
Dhruv Srivastavae8b86392022-10-20 17:17:21 +02001399 this.textarea?.focus();
1400 }
1401
Ben Rohlfs05750b92021-10-29 08:23:08 +02001402 private handleEsc() {
1403 // vim users don't like ESC to cancel/discard, so only do this when the
1404 // comment text is empty.
Ben Rohlfs2e237552021-11-24 10:34:28 +01001405 if (!this.messageText?.trimEnd()) this.cancel();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001406 }
1407
Ben Rohlfs05750b92021-10-29 08:23:08 +02001408 private handleAnchorClick() {
1409 assertIsDefined(this.comment, 'comment');
1410 fire(this, 'comment-anchor-tap', {
1411 number: this.comment.line || FILE,
1412 side: this.comment?.side,
Milutin Kristoficafae0052020-09-17 10:38:08 +02001413 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001414 }
1415
Dhruv Srivastava15950b452022-09-12 10:56:53 +02001416 private async handleSaveButtonClicked() {
1417 await this.save();
1418 if (this.permanentEditingMode) {
1419 this.editing = !this.editing;
1420 }
Dhruv Srivastava705eac92022-09-01 11:33:27 +02001421 }
1422
Ben Rohlfs23843882022-08-04 18:06:27 +02001423 private handlePleaseFix() {
1424 const message = this.comment?.message;
1425 assert(!!message, 'empty message');
1426 const quoted = message.replace(NEWLINE_PATTERN, '\n> ');
1427 const eventDetail: ReplyToCommentEventDetail = {
1428 content: `> ${quoted}\n\nPlease fix.`,
1429 userWantsToEdit: false,
1430 unresolved: true,
1431 };
Ben Rohlfs05750b92021-10-29 08:23:08 +02001432 // Handled by <gr-comment-thread>.
Ben Rohlfs23843882022-08-04 18:06:27 +02001433 fire(this, 'reply-to-comment', eventDetail);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001434 }
1435
Milutin Kristoficeb8f6552023-02-09 22:17:35 +01001436 private handleAppliedFix() {
1437 const message = this.comment?.message;
1438 assert(!!message, 'empty message');
1439 const eventDetail: ReplyToCommentEventDetail = {
1440 content: 'Fix applied.',
1441 userWantsToEdit: false,
1442 unresolved: false,
1443 };
1444 // Handled by <gr-comment-thread>.
1445 fire(this, 'reply-to-comment', eventDetail);
1446 }
1447
Milutin Kristofic1ebae372022-11-22 20:35:38 +01001448 private async handleShowFix(replacement?: string) {
Ben Rohlfs05750b92021-10-29 08:23:08 +02001449 // Handled top-level in the diff and change view components.
Milutin Kristofic1ebae372022-11-22 20:35:38 +01001450 fire(this, 'open-fix-preview', await this.createFixPreview(replacement));
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001451 }
1452
Milutin Kristofic8238de52023-01-12 19:33:45 +01001453 async createSuggestEdit(e: MouseEvent) {
1454 e.stopPropagation();
Ben Rohlfs23843882022-08-04 18:06:27 +02001455 const line = await this.getCommentedCode();
Milutin Kristofic7d557472023-08-21 11:23:36 +02001456 const addNewLine = this.messageText.length !== 0;
1457 this.messageText += `${
1458 addNewLine ? '\n' : ''
1459 }${USER_SUGGESTION_START_PATTERN}${line}${'\n```'}`;
Ben Rohlfs23843882022-08-04 18:06:27 +02001460 }
1461
Milutin Kristofic78cdec92023-08-29 14:37:31 +02001462 // TODO(milutin): Remove once feature flag is rollout and use only model
Ben Rohlfs23843882022-08-04 18:06:27 +02001463 async getCommentedCode() {
Milutin Kristofic1d219672022-06-21 14:57:25 +02001464 assertIsDefined(this.comment, 'comment');
1465 assertIsDefined(this.changeNum, 'changeNum');
Milutin Kristofic1d219672022-06-21 14:57:25 +02001466 const file = await this.restApiService.getFileContent(
1467 this.changeNum,
1468 this.comment.path!,
1469 this.comment.patch_set!
1470 );
Ben Rohlfs23843882022-08-04 18:06:27 +02001471 assert(
1472 !!file && isBase64FileContent(file) && !!file.content,
1473 'file content for comment not found'
1474 );
Milutin Kristofic1d219672022-06-21 14:57:25 +02001475 const line = getContentInCommentRange(file.content, this.comment);
Ben Rohlfs23843882022-08-04 18:06:27 +02001476 assert(!!line, 'file content for comment not found');
1477 return line;
Milutin Kristofic1d219672022-06-21 14:57:25 +02001478 }
1479
Ben Rohlfs05750b92021-10-29 08:23:08 +02001480 // private, but visible for testing
1481 cancel() {
1482 assertIsDefined(this.comment, 'comment');
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001483 assert(isDraft(this.comment), 'only drafts are editable');
Ben Rohlfs2e237552021-11-24 10:34:28 +01001484 this.messageText = this.originalMessage;
1485 this.unresolved = this.originalUnresolved;
1486 this.save();
Ben Rohlfs05750b92021-10-29 08:23:08 +02001487 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001488
Ben Rohlfs2e237552021-11-24 10:34:28 +01001489 async autoSave() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001490 if (isSaving(this.comment) || this.autoSaving) return;
Ben Rohlfs2e237552021-11-24 10:34:28 +01001491 if (!this.editing || !this.comment) return;
Chris Poucet77226982023-08-10 18:10:04 +02001492 if (this.disableAutoSaving) return;
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001493 assert(isDraft(this.comment), 'only drafts are editable');
Ben Rohlfs2e237552021-11-24 10:34:28 +01001494 const messageToSave = this.messageText.trimEnd();
1495 if (messageToSave === '') return;
1496 if (messageToSave === this.comment.message) return;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001497
Ben Rohlfs2e237552021-11-24 10:34:28 +01001498 try {
Ben Rohlfs1efb1a72023-04-12 23:25:31 +02001499 this.autoSaving = this.rawSave({showToast: false});
Ben Rohlfs2e237552021-11-24 10:34:28 +01001500 await this.autoSaving;
1501 } finally {
1502 this.autoSaving = undefined;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001503 }
Ben Rohlfs2e237552021-11-24 10:34:28 +01001504 }
1505
1506 async discard() {
1507 this.messageText = '';
1508 await this.save();
1509 }
1510
Chris Poucet063fa502023-11-21 10:06:35 +01001511 async convertToCommentInputAndOrDiscard(): Promise<CommentInput | undefined> {
Chris Poucet77226982023-08-10 18:10:04 +02001512 if (!this.somethingToSave() || !this.comment) return;
Chris Poucet063fa502023-11-21 10:06:35 +01001513 const messageToSave = this.messageText.trimEnd();
1514 if (messageToSave === '') {
1515 await this.getCommentsModel().discardDraft(id(this.comment));
1516 return undefined;
1517 } else {
1518 return convertToCommentInput({
1519 ...this.comment,
1520 message: this.messageText.trimEnd(),
1521 unresolved: this.unresolved,
1522 });
1523 }
Chris Poucet77226982023-08-10 18:10:04 +02001524 }
1525
Ben Rohlfs2e237552021-11-24 10:34:28 +01001526 async save() {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001527 assert(isDraft(this.comment), 'only drafts are editable');
Ben Rohlfs1efb1a72023-04-12 23:25:31 +02001528 // There is a minimal chance of `isSaving()` being false between iterations
1529 // of the below while loop. But this will be extremely rare and just lead
1530 // to a harmless assertion error. So let's not bother.
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001531 if (isSaving(this.comment) && !this.autoSaving) return;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001532
Ben Rohlfs1efb1a72023-04-12 23:25:31 +02001533 if (!this.permanentEditingMode) {
1534 this.editing = false;
1535 }
1536 if (this.autoSaving) {
1537 this.comment = await this.autoSaving;
1538 }
1539 // Depending on whether `messageToSave` is empty we treat this either as
1540 // a discard or a save action.
1541 const messageToSave = this.messageText.trimEnd();
1542 if (messageToSave === '') {
Ben Rohlfs0d9d0c32023-04-20 18:12:06 +02001543 if (!this.permanentEditingMode || this.somethingToSave()) {
1544 await this.getCommentsModel().discardDraft(id(this.comment));
1545 }
Ben Rohlfs1efb1a72023-04-12 23:25:31 +02001546 } else {
1547 // No need to make a backend call when nothing has changed.
1548 while (this.somethingToSave()) {
1549 this.comment = await this.rawSave({showToast: true});
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001550 if (isError(this.comment)) return;
Ben Rohlfs607126f2021-12-07 08:21:52 +01001551 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001552 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001553 }
1554
Ben Rohlfs1efb1a72023-04-12 23:25:31 +02001555 private somethingToSave() {
1556 if (!this.comment) return false;
1557 return (
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001558 isError(this.comment) ||
Ben Rohlfs1efb1a72023-04-12 23:25:31 +02001559 this.messageText.trimEnd() !== this.comment?.message ||
1560 this.unresolved !== this.comment.unresolved
1561 );
1562 }
1563
Ben Rohlfs2e237552021-11-24 10:34:28 +01001564 /** For sharing between save() and autoSave(). */
Ben Rohlfs1efb1a72023-04-12 23:25:31 +02001565 private rawSave(options: {showToast: boolean}) {
Ben Rohlfs610bb4f2023-04-17 12:34:35 +02001566 assert(isDraft(this.comment), 'only drafts are editable');
1567 assert(!isSaving(this.comment), 'saving already in progress');
Chris Poucet6c6b54f2021-12-09 02:53:13 +01001568 return this.getCommentsModel().saveDraft(
Ben Rohlfs2e237552021-11-24 10:34:28 +01001569 {
1570 ...this.comment,
Ben Rohlfs1efb1a72023-04-12 23:25:31 +02001571 message: this.messageText.trimEnd(),
Ben Rohlfs2e237552021-11-24 10:34:28 +01001572 unresolved: this.unresolved,
1573 },
1574 options.showToast
1575 );
1576 }
1577
Ben Rohlfs05750b92021-10-29 08:23:08 +02001578 private handleToggleResolved() {
1579 this.unresolved = !this.unresolved;
Dhruv Srivastava73f9edc2021-12-02 11:23:27 +01001580 if (!this.editing) {
1581 // messageText is only assigned a value if the comment reaches editing
1582 // state, however it is possible that the user toggles the resolved state
1583 // without editing the comment in which case we assign the correct value
1584 // to messageText here
1585 this.messageText = this.comment?.message ?? '';
1586 this.save();
1587 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001588 }
1589
Kamil Musin9c8833a2022-12-29 12:05:08 +01001590 private openDeleteCommentModal() {
1591 this.confirmDeleteModal?.showModal();
Kamil Musinc7d3f282022-12-29 13:27:55 +01001592 whenVisible(this.confirmDeleteDialog!, () => {
1593 this.confirmDeleteDialog!.resetFocus();
1594 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001595 }
1596
Dhruv Srivastava4063d262022-11-09 18:46:29 +05301597 private closeDeleteCommentModal() {
Dhruv Srivastava4063d262022-11-09 18:46:29 +05301598 this.confirmDeleteModal?.close();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001599 }
1600
Ben Rohlfs05750b92021-10-29 08:23:08 +02001601 /**
1602 * Deleting a *published* comment is an admin feature. It means more than just
1603 * discarding a draft.
Ben Rohlfs05750b92021-10-29 08:23:08 +02001604 */
1605 // private, but visible for testing
Kamil Musind88622f2023-01-02 11:52:57 +01001606 async handleConfirmDeleteComment() {
Kamil Musinc7d3f282022-12-29 13:27:55 +01001607 if (!this.confirmDeleteDialog || !this.confirmDeleteDialog.message) {
Milutin Kristoficafae0052020-09-17 10:38:08 +02001608 throw new Error('missing confirm delete dialog');
1609 }
Ben Rohlfs05750b92021-10-29 08:23:08 +02001610 assertIsDefined(this.changeNum, 'changeNum');
1611 assertIsDefined(this.comment, 'comment');
Kamil Musind88622f2023-01-02 11:52:57 +01001612
1613 await this.getCommentsModel().deleteComment(
1614 this.changeNum,
1615 this.comment,
Kamil Musinc7d3f282022-12-29 13:27:55 +01001616 this.confirmDeleteDialog.message
Kamil Musind88622f2023-01-02 11:52:57 +01001617 );
1618 this.closeDeleteCommentModal();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +01001619 }
1620}
1621
Milutin Kristoficafae0052020-09-17 10:38:08 +02001622declare global {
1623 interface HTMLElementTagNameMap {
1624 'gr-comment': GrComment;
1625 }
Ben Rohlfs5b3c6552023-02-18 13:02:46 +01001626 interface HTMLElementEventMap {
1627 'copy-comment-link': CustomEvent<{}>;
1628 }
Milutin Kristoficafae0052020-09-17 10:38:08 +02001629}