Apply Edits without opening the show edit dialog

Since the suggested edits are display inline now,
there is no need to open "show edit" dialog just
to click "apply edit".

We leave both buttons for testing purposes and for
checking usage numbers. It can be still usesful
to see suggestions in different diff mode.

Google-Bug-Id: b/306779632
Release-Notes: skip
Change-Id: I689f2e616c9edb5e3bc494e02ffb39bce032599a
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index 7e6e23b..1c82c6e 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -421,7 +421,7 @@
         this.currentFix.fix_id
       );
     }
-    if (res && res.ok) {
+    if (res?.ok) {
       this.getNavigation().setUrl(
         createChangeUrl({
           change,
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 10a134e..3e341b8 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -94,6 +94,7 @@
     'comment-unresolved-changed': ValueChangedEvent<boolean>;
     'comment-text-changed': ValueChangedEvent<string>;
     'comment-anchor-tap': CustomEvent<CommentAnchorTapEventDetail>;
+    'apply-user-suggestion': CustomEvent;
   }
 }
 
@@ -316,6 +317,9 @@
         this.save();
       });
     }
+    this.addEventListener('apply-user-suggestion', () => {
+      this.handleAppliedFix();
+    });
     this.addEventListener('open-user-suggest-preview', e => {
       this.handleShowFix(e.detail.code);
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
index 69cdedd..f591388 100644
--- a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
+++ b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
@@ -7,7 +7,7 @@
 import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {getAppContext} from '../../../services/app-context';
-import {Comment} from '../../../types/common';
+import {Comment, EDIT, BasePatchSetNum, RepoName} from '../../../types/common';
 import {anyLineTooLong} from '../../../utils/diff-util';
 import {
   DiffLayer,
@@ -26,8 +26,10 @@
 import {userModelToken} from '../../../models/user/user-model';
 import {createUserFixSuggestion} from '../../../utils/comment-util';
 import {commentModelToken} from '../gr-comment-model/gr-comment-model';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {fire} from '../../../utils/event-util';
 import {Interaction, Timing} from '../../../constants/reporting';
+import {createChangeUrl} from '../../../models/views/change';
 
 declare global {
   interface HTMLElementEventMap {
@@ -64,6 +66,8 @@
   @state()
   previewLoadedFor?: string;
 
+  @state() repo?: RepoName;
+
   @state()
   changeNum?: NumericChangeId;
 
@@ -90,6 +94,8 @@
 
   private readonly getCommentModel = resolve(this, commentModelToken);
 
+  private readonly getNavigation = resolve(this, navigationToken);
+
   private readonly syntaxLayer = new GrSyntaxLayerWorker(
     resolve(this, highlightServiceToken),
     () => getAppContext().reportingService
@@ -121,6 +127,11 @@
       () => this.getCommentModel().commentedText$,
       commentedText => (this.commentedText = commentedText)
     );
+    subscribe(
+      this,
+      () => this.getChangeModel().repo$,
+      x => (this.repo = x)
+    );
   }
 
   static override get styles() {
@@ -233,6 +244,48 @@
     return res;
   }
 
+  /**
+   * Applies a fix previewed in `suggestion-diff-preview`,
+   * navigating to the new change URL with the EDIT patchset.
+   *
+   * Similar code flow is in gr-apply-fix-dialog.handleApplyFix
+   * Used in gr-user-suggestion-fix
+   */
+  public async applyFix() {
+    if (
+      !this.changeNum ||
+      !this.comment?.patch_set ||
+      !this.suggestion ||
+      !this.commentedText
+    )
+      return;
+    const changeNum = this.changeNum;
+    const basePatchNum = this.comment?.patch_set as BasePatchSetNum;
+    const fixSuggestions = createUserFixSuggestion(
+      this.comment,
+      this.commentedText,
+      this.suggestion
+    );
+    this.reporting.time(Timing.APPLY_FIX_LOAD);
+    const res = await this.restApiService.applyFixSuggestion(
+      this.changeNum,
+      this.comment?.patch_set,
+      fixSuggestions[0].replacements
+    );
+    this.reporting.timeEnd(Timing.APPLY_FIX_LOAD);
+    if (res?.ok) {
+      this.getNavigation().setUrl(
+        createChangeUrl({
+          changeNum,
+          repo: this.repo!,
+          patchNum: EDIT,
+          basePatchNum,
+        })
+      );
+      fire(this, 'apply-user-suggestion', {});
+    }
+  }
+
   private overridePartialDiffPrefs() {
     if (!this.diffPrefs) return undefined;
     return {
diff --git a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
index 6322123..2befe26 100644
--- a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
+++ b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
@@ -8,12 +8,16 @@
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
 import '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
 import {css, html, LitElement, nothing} from 'lit';
-import {customElement, state} from 'lit/decorators.js';
+import {customElement, state, query} from 'lit/decorators.js';
 import {fire} from '../../../utils/event-util';
 import {getDocUrl} from '../../../utils/url-util';
 import {subscribe} from '../../lit/subscription-controller';
 import {resolve} from '../../../models/dependency';
 import {configModelToken} from '../../../models/config/config-model';
+import {GrSuggestionDiffPreview} from '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
+import {changeModelToken} from '../../../models/change/change-model';
+import {Comment, PatchSetNumber} from '../../../types/common';
+import {commentModelToken} from '../gr-comment-model/gr-comment-model';
 
 declare global {
   interface HTMLElementEventMap {
@@ -29,10 +33,23 @@
 
 @customElement('gr-user-suggestion-fix')
 export class GrUserSuggestionsFix extends LitElement {
+  @query('gr-suggestion-diff-preview')
+  suggestionDiffPreview?: GrSuggestionDiffPreview;
+
   @state() private docsBaseUrl = '';
 
+  @state() private applyingFix = false;
+
+  @state() latestPatchNum?: PatchSetNumber;
+
+  @state() comment?: Comment;
+
   private readonly getConfigModel = resolve(this, configModelToken);
 
+  private readonly getChangeModel = resolve(this, changeModelToken);
+
+  private readonly getCommentModel = resolve(this, commentModelToken);
+
   constructor() {
     super();
     subscribe(
@@ -40,6 +57,16 @@
       () => this.getConfigModel().docsBaseUrl$,
       docsBaseUrl => (this.docsBaseUrl = docsBaseUrl)
     );
+    subscribe(
+      this,
+      () => this.getChangeModel().latestPatchNum$,
+      x => (this.latestPatchNum = x)
+    );
+    subscribe(
+      this,
+      () => this.getCommentModel().comment$,
+      comment => (this.comment = comment)
+    );
   }
 
   static override get styles() {
@@ -94,6 +121,17 @@
           >
             Show edit
           </gr-button>
+          <gr-button
+            secondary
+            flatten
+            .loading=${this.applyingFix}
+            .disabled=${this.isApplyEditDisabled()}
+            class="action show-fix"
+            @click=${this.handleApplyFix}
+            .title=${this.computeApplyEditTooltip()}
+          >
+            Apply edit
+          </gr-button>
         </div>
       </div>
       <gr-suggestion-diff-preview
@@ -105,6 +143,25 @@
     if (!this.textContent) return;
     fire(this, 'open-user-suggest-preview', {code: this.textContent});
   }
+
+  async handleApplyFix() {
+    if (!this.textContent) return;
+    this.applyingFix = true;
+    await this.suggestionDiffPreview?.applyFix();
+    this.applyingFix = false;
+  }
+
+  private isApplyEditDisabled() {
+    if (this.comment?.patch_set === undefined) return true;
+    return this.comment.patch_set !== this.latestPatchNum;
+  }
+
+  private computeApplyEditTooltip() {
+    if (this.comment?.patch_set === undefined) return '';
+    return this.comment.patch_set !== this.latestPatchNum
+      ? 'You cannot apply this fix because it is from a previous patchset'
+      : '';
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
index b7d73b3..52fd687 100644
--- a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
@@ -67,6 +67,16 @@
               tabindex="0"
               flatten=""
               >Show edit</gr-button
+            ><gr-button
+              aria-disabled="true"
+              disabled=""
+              class="action show-fix"
+              secondary=""
+              role="button"
+              tabindex="-1"
+              flatten=""
+              title="You cannot apply this fix because it is from a previous patchset"
+              >Apply edit</gr-button
             >
           </div>
         </div>