Refactor gr-checks-fix-preview to use gr-suggestion-diff-preview

To avoid duplication and having consistency gr-suggestion-diff-preview
is modified to not use comment but more general path, patchset
and optional commentId

Google-Bug-Id: b/360288262
Release-Notes: skip
Change-Id: Ia37fd467936880f52b210c262c8fe2d8ab53867f
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-fix-preview.ts b/polygerrit-ui/app/elements/checks/gr-checks-fix-preview.ts
index 1780839..1c8be76 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-fix-preview.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-fix-preview.ts
@@ -3,23 +3,12 @@
  * Copyright 2024 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '../../embed/diff/gr-diff/gr-diff';
-import {css, html, LitElement, nothing, PropertyValues} from 'lit';
-import {customElement, property, state} from 'lit/decorators.js';
-import {getAppContext} from '../../services/app-context';
-import {EDIT, BasePatchSetNum, RepoName} from '../../types/common';
-import {anyLineTooLong} from '../../utils/diff-util';
-import {Timing} from '../../constants/reporting';
-import {
-  DiffInfo,
-  DiffLayer,
-  DiffPreferencesInfo,
-  DiffViewMode,
-  RenderPreferences,
-} from '../../api/diff';
-import {GrSyntaxLayerWorker} from '../../embed/diff/gr-syntax-layer/gr-syntax-layer-worker';
+import '../shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview';
+import {GrSuggestionDiffPreview} from '../shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview';
+import {css, html, LitElement, nothing} from 'lit';
+import {customElement, query, property, state} from 'lit/decorators.js';
+import {BasePatchSetNum, RepoName} from '../../types/common';
 import {resolve} from '../../models/dependency';
-import {highlightServiceToken} from '../../services/highlight/highlight-service';
 import {
   FixSuggestionInfo,
   NumericChangeId,
@@ -27,28 +16,13 @@
 } from '../../api/rest-api';
 import {changeModelToken} from '../../models/change/change-model';
 import {subscribe} from '../lit/subscription-controller';
-import {DiffPreview} from '../diff/gr-apply-fix-dialog/gr-apply-fix-dialog';
-import {userModelToken} from '../../models/user/user-model';
-import {navigationToken} from '../core/gr-navigation/gr-navigation';
 import {fire} from '../../utils/event-util';
-import {createChangeUrl} from '../../models/views/change';
 import {OpenFixPreviewEventDetail} from '../../types/events';
 
 /**
- * This component renders a <gr-diff> and an "apply fix" button and can be used
- * when showing check results that have a fix for an easy preview and a quick
- * apply-fix experience.
- *
- * There is a certain overlap with similar components for comment fixes:
- * GrSuggestionDiffPreview also renders a <gr-diff> and fetches a diff preview,
- * it relies on a `comment` (and the comment model) to be available. It supports
- * both a `string` fix and `FixSuggestionInfo`. It also differs in logging and
- * event handling. And it misses the header component that we need for the
- * buttons.
- *
- * There is also `GrUserSuggestionsFix` which wraps `GrSuggestionDiffPreview`
- * and has the header that we also need. But it is very targeted to be used for
- * user suggestions and inside comments.
+ * There is a certain overlap with `GrUserSuggestionsFix` which wraps
+ * `GrSuggestionDiffPreview` and has the header that we also need.
+ * But it is very targeted to be used for user suggestions and inside comments.
  *
  * So there is certainly an opportunity for cleanup and unification, but at the
  * time of component creation it did not feel wortwhile investing into this
@@ -56,6 +30,9 @@
  */
 @customElement('gr-checks-fix-preview')
 export class GrChecksFixPreview extends LitElement {
+  @query('gr-suggestion-diff-preview')
+  suggestionDiffPreview?: GrSuggestionDiffPreview;
+
   @property({type: Object})
   fixSuggestionInfo?: FixSuggestionInfo;
 
@@ -63,9 +40,6 @@
   patchSet?: PatchSetNumber;
 
   @state()
-  layers: DiffLayer[] = [];
-
-  @state()
   repo?: RepoName;
 
   @state()
@@ -74,37 +48,13 @@
   @state()
   latestPatchNum?: PatchSetNumber;
 
-  @state()
-  diff?: DiffPreview;
+  @state() previewLoaded = false;
 
   @state()
   applyingFix = false;
 
-  @state()
-  diffPrefs?: DiffPreferencesInfo;
-
-  @state()
-  renderPrefs: RenderPreferences = {
-    disable_context_control_buttons: true,
-    show_file_comment_button: false,
-    hide_line_length_indicator: true,
-  };
-
-  private readonly reporting = getAppContext().reportingService;
-
-  private readonly restApiService = getAppContext().restApiService;
-
   private readonly getChangeModel = resolve(this, changeModelToken);
 
-  private readonly getUserModel = resolve(this, userModelToken);
-
-  private readonly getNavigation = resolve(this, navigationToken);
-
-  private readonly syntaxLayer = new GrSyntaxLayerWorker(
-    resolve(this, highlightServiceToken),
-    () => getAppContext().reportingService
-  );
-
   constructor() {
     super();
     subscribe(
@@ -119,15 +69,6 @@
     );
     subscribe(
       this,
-      () => this.getUserModel().diffPreferences$,
-      diffPreferences => {
-        if (!diffPreferences) return;
-        this.diffPrefs = diffPreferences;
-        this.syntaxLayer.setEnabled(!!this.diffPrefs.syntax_highlighting);
-      }
-    );
-    subscribe(
-      this,
       () => this.getChangeModel().repo$,
       x => (this.repo = x)
     );
@@ -150,11 +91,6 @@
         .header .title {
           flex: 1;
         }
-        .diff-container {
-          border: 1px solid var(--border-color);
-          border-top: none;
-          border-bottom: none;
-        }
         .loading {
           border: 1px solid var(--border-color);
           padding: var(--spacing-xl);
@@ -163,12 +99,6 @@
     ];
   }
 
-  override willUpdate(changed: PropertyValues) {
-    if (changed.has('fixSuggestionInfo')) {
-      this.fetchDiffPreview().then(diff => (this.diff = diff));
-    }
-  }
-
   override render() {
     if (!this.fixSuggestionInfo) return nothing;
     return html`${this.renderHeader()}${this.renderDiff()}`;
@@ -185,7 +115,7 @@
             class="showFix"
             secondary
             flatten
-            .disabled=${!this.diff}
+            .disabled=${!this.previewLoaded}
             @click=${this.showFix}
           >
             Show fix side-by-side
@@ -207,48 +137,16 @@
   }
 
   private renderDiff() {
-    if (!this.diff) {
-      return html`<div class="loading">Loading fix preview ...</div>`;
-    }
-    const diff = this.diff.preview;
-    if (!anyLineTooLong(diff)) {
-      this.syntaxLayer.process(diff);
-    }
     return html`
-      <div class="diff-container">
-        <gr-diff
-          .prefs=${this.getDiffPrefs()}
-          .path=${this.diff.filepath}
-          .diff=${diff}
-          .layers=${this.layers}
-          .renderPrefs=${this.renderPrefs}
-          .viewMode=${DiffViewMode.UNIFIED}
-        ></gr-diff>
-      </div>
+      <gr-suggestion-diff-preview
+        .fixSuggestionInfo=${this.fixSuggestionInfo}
+        .patchSet=${this.patchSet}
+        .codeText=${'Loading fix preview ...'}
+        @preview-loaded=${() => (this.previewLoaded = true)}
+      ></gr-suggestion-diff-preview>
     `;
   }
 
-  /**
-   * Calls the REST API to convert the fix into a DiffInfo.
-   */
-  private async fetchDiffPreview(): Promise<DiffPreview | undefined> {
-    if (!this.changeNum || !this.patchSet || !this.fixSuggestionInfo) return;
-    const pathsToDiffs: {[path: string]: DiffInfo} | undefined =
-      await this.restApiService.getFixPreview(
-        this.changeNum,
-        this.patchSet,
-        this.fixSuggestionInfo.replacements
-      );
-
-    if (!pathsToDiffs) return;
-    const diffs = Object.keys(pathsToDiffs).map(filepath => {
-      const diff = pathsToDiffs[filepath];
-      return {filepath, preview: diff};
-    });
-    // Showing diff for one file only.
-    return diffs?.[0];
-  }
-
   private showFix() {
     if (!this.patchSet || !this.fixSuggestionInfo) return;
     const eventDetail: OpenFixPreviewEventDetail = {
@@ -268,61 +166,21 @@
     if (!changeNum || !basePatchNum || !this.fixSuggestionInfo) return;
 
     this.applyingFix = true;
-    this.reporting.time(Timing.APPLY_FIX_LOAD);
-    const res = await this.restApiService.applyFixSuggestion(
-      changeNum,
-      basePatchNum,
-      this.fixSuggestionInfo.replacements,
-      this.latestPatchNum
-    );
-    this.applyingFix = false;
-    this.reporting.timeEnd(Timing.APPLY_FIX_LOAD, {
-      method: '1-click',
-      description: this.fixSuggestionInfo.description,
-    });
-    if (res?.ok) this.navigateToEditPatchset();
-  }
-
-  private navigateToEditPatchset() {
-    const changeNum = this.changeNum;
-    const repo = this.repo;
-    const basePatchNum = this.patchSet;
-    if (!changeNum || !repo || !basePatchNum) return;
-
-    const url = createChangeUrl({
-      changeNum,
-      repo,
-      patchNum: EDIT,
-      basePatchNum,
-      // We have to force reload, because the EDIT patchset is otherwise not yet known.
-      forceReload: true,
-    });
-    this.getNavigation().setUrl(url);
-  }
-
-  /**
-   * We have to override some diff prefs of the user, because for example in the context of showing
-   * an inline diff for fixes we do not want to show context lines around the changes lines of code
-   * as we would normally do for a diff.
-   */
-  private getDiffPrefs() {
-    if (!this.diffPrefs) return undefined;
-    return {
-      ...this.diffPrefs,
-      context: 0,
-      line_length: Math.min(this.diffPrefs.line_length, 100),
-      line_wrapping: true,
-    };
+    try {
+      await this.suggestionDiffPreview?.applyFix();
+    } finally {
+      this.applyingFix = false;
+    }
   }
 
   private isApplyEditDisabled() {
     if (this.patchSet === undefined) return true;
-    return !this.diff;
+    return !this.previewLoaded;
   }
 
   private computeApplyFixTooltip() {
     if (this.patchSet === undefined) return '';
-    if (!this.diff) return 'Fix is still loading ...';
+    if (!this.previewLoaded) return 'Fix is still loading ...';
     return '';
   }
 }
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-fix-preview_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-fix-preview_test.ts
index 8a9997a..7dbac27 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-fix-preview_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-fix-preview_test.ts
@@ -6,9 +6,10 @@
 import * as sinon from 'sinon';
 import '../../test/common-test-setup';
 import './gr-checks-results';
+import './gr-checks-fix-preview';
 import {html} from 'lit';
 import {fixture, assert} from '@open-wc/testing';
-import {createCheckFix, createDiff} from '../../test/test-data-generators';
+import {createCheckFix} from '../../test/test-data-generators';
 import {GrChecksFixPreview} from './gr-checks-fix-preview';
 import {rectifyFix} from '../../models/checks/checks-util';
 import {
@@ -16,12 +17,10 @@
   mockPromise,
   queryAndAssert,
   stubRestApi,
-  waitUntil,
 } from '../../test/test-utils';
 import {NumericChangeId, PatchSetNumber, RepoName} from '../../api/rest-api';
 import {FilePathToDiffInfoMap} from '../../types/common';
-import {testResolver} from '../../test/common-test-setup';
-import {navigationToken} from '../core/gr-navigation/gr-navigation';
+import {GrSuggestionDiffPreview} from '../shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview';
 
 suite('gr-checks-fix-preview test', () => {
   let element: GrChecksFixPreview;
@@ -45,11 +44,6 @@
     await element.updateComplete;
   });
 
-  const loadDiff = async () => {
-    promise.resolve({'foo.c': createDiff()});
-    await waitUntil(() => !!element.diff);
-  };
-
   test('renders loading', async () => {
     assert.shadowDom.equal(
       element,
@@ -84,58 +78,14 @@
             </gr-button>
           </div>
         </div>
-        <div class="loading">Loading fix preview ...</div>
-      `
-    );
-  });
-
-  test('renders diff', async () => {
-    await loadDiff();
-    assert.shadowDom.equal(
-      element,
-      /* HTML */ `
-        <div class="header">
-          <div class="title">
-            <span> Attached Fix </span>
-          </div>
-          <div>
-            <gr-button
-              class="showFix"
-              aria-disabled="false"
-              flatten=""
-              role="button"
-              secondary=""
-              tabindex="0"
-            >
-              Show fix side-by-side
-            </gr-button>
-            <gr-button
-              class="applyFix"
-              aria-disabled="false"
-              flatten=""
-              primary=""
-              role="button"
-              tabindex="0"
-              title=""
-            >
-              Apply fix
-            </gr-button>
-          </div>
-        </div>
-        <div class="diff-container">
-          <gr-diff
-            class="disable-context-control-buttons hide-line-length-indicator"
-            style="--line-limit-marker: 100ch; --content-width: none; --diff-max-width: none; --font-size: 12px;"
-          >
-          </gr-diff>
-        </div>
+        <gr-suggestion-diff-preview></gr-suggestion-diff-preview>
       `
     );
   });
 
   test('show-fix', async () => {
-    await loadDiff();
-
+    element.previewLoaded = true;
+    await element.updateComplete;
     const stub = sinon.stub();
     element.addEventListener('open-fix-preview', stub);
 
@@ -152,9 +102,13 @@
   });
 
   test('apply-fix', async () => {
-    await loadDiff();
-
-    const setUrlSpy = sinon.stub(testResolver(navigationToken), 'setUrl');
+    element.previewLoaded = true;
+    await element.updateComplete;
+    const diffPreview = queryAndAssert<GrSuggestionDiffPreview>(
+      element,
+      'gr-suggestion-diff-preview'
+    );
+    const applyFixSpy = sinon.spy(diffPreview, 'applyFix');
     stubRestApi('applyFixSuggestion').returns(
       Promise.resolve({ok: true} as Response)
     );
@@ -163,10 +117,6 @@
     assert.isFalse(button.hasAttribute('disabled'));
     button.click();
 
-    await waitUntil(() => setUrlSpy.called);
-    assert.equal(
-      setUrlSpy.lastCall.args[0],
-      '/c/test-repo/+/123/5..edit?forceReload=true'
-    );
+    assert.isTrue(applyFixSpy.called);
   });
 });
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 ea1c20d..cf5ea9d 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -1179,6 +1179,8 @@
         id="suggestionDiffPreview"
         .uuid=${this.generatedSuggestionId}
         .fixSuggestionInfo=${this.generatedFixSuggestion}
+        .patchSet=${this.comment?.patch_set}
+        .commentId=${this.comment?.id}
       ></gr-suggestion-diff-preview>`;
     } else {
       return nothing;
diff --git a/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts b/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
index a70ca07..137c853 100644
--- a/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
+++ b/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
@@ -214,6 +214,8 @@
       </div>
       <gr-suggestion-diff-preview
         .fixSuggestionInfo=${this.comment?.fix_suggestions?.[0]}
+        .patchSet=${this.comment?.patch_set}
+        .commentId=${this.comment?.id}
         @preview-loaded=${() => (this.previewLoaded = true)}
       ></gr-suggestion-diff-preview>`;
   }
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 9d8b327..136979d 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
@@ -8,7 +8,6 @@
 import {customElement, property, state} from 'lit/decorators.js';
 import {getAppContext} from '../../../services/app-context';
 import {
-  Comment,
   EDIT,
   BasePatchSetNum,
   PatchSetNumber,
@@ -30,7 +29,6 @@
 import {subscribe} from '../../lit/subscription-controller';
 import {DiffPreview} from '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog';
 import {userModelToken} from '../../../models/user/user-model';
-import {commentModelToken} from '../gr-comment-model/gr-comment-model';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {fire} from '../../../utils/event-util';
 import {Timing} from '../../../constants/reporting';
@@ -58,6 +56,8 @@
   @property({type: Object})
   fixSuggestionInfo?: FixSuggestionInfo;
 
+  // Used to determine if the preview has been loaded
+  // this is identical to previewLoadedFor !== undefined and can be removed
   @property({type: Boolean, attribute: 'previewed', reflect: true})
   previewed = false;
 
@@ -65,8 +65,12 @@
   @property({type: String})
   uuid?: string;
 
-  @state()
-  comment?: Comment;
+  @property({type: Number})
+  patchSet?: BasePatchSetNum;
+
+  // Optional. Used in logging.
+  @property({type: String})
+  commentId?: string;
 
   @state()
   layers: DiffLayer[] = [];
@@ -78,7 +82,7 @@
    * fix suggestion info currently in gr-comment.
    */
   @state()
-  public previewLoadedFor?: string | FixSuggestionInfo;
+  public previewLoadedFor?: FixSuggestionInfo;
 
   @state() repo?: RepoName;
 
@@ -110,8 +114,6 @@
 
   private readonly getUserModel = resolve(this, userModelToken);
 
-  private readonly getCommentModel = resolve(this, commentModelToken);
-
   private readonly getNavigation = resolve(this, navigationToken);
 
   private readonly syntaxLayer = new GrSyntaxLayerWorker(
@@ -128,14 +130,6 @@
     );
     subscribe(
       this,
-      () => this.getChangeModel().revisions$,
-      revisions =>
-        (this.hasEdit = Object.values(revisions).some(
-          info => info._number === EDIT
-        ))
-    );
-    subscribe(
-      this,
       () => this.getChangeModel().latestPatchNum$,
       x => (this.latestPatchNum = x)
     );
@@ -150,11 +144,6 @@
     );
     subscribe(
       this,
-      () => this.getCommentModel().comment$,
-      comment => (this.comment = comment)
-    );
-    subscribe(
-      this,
       () => this.getChangeModel().repo$,
       x => (this.repo = x)
     );
@@ -198,11 +187,9 @@
     if (
       changed.has('fixSuggestionInfo') ||
       changed.has('changeNum') ||
-      changed.has('comment')
+      changed.has('patchSet')
     ) {
-      if (this.previewLoadedFor !== this.fixSuggestionInfo) {
-        this.fetchFixPreview();
-      }
+      this.fetchFixPreview();
     }
   }
 
@@ -236,13 +223,12 @@
   }
 
   private async fetchFixPreview() {
-    if (!this.changeNum || !this.comment?.patch_set || !this.fixSuggestionInfo)
-      return;
+    if (!this.changeNum || !this.patchSet || !this.fixSuggestionInfo) return;
 
     this.reporting.time(Timing.PREVIEW_FIX_LOAD);
     const res = await this.restApiService.getFixPreview(
       this.changeNum,
-      this.comment?.patch_set,
+      this.patchSet,
       this.fixSuggestionInfo.replacements
     );
     if (!res) return;
@@ -251,7 +237,7 @@
     });
     this.reporting.timeEnd(Timing.PREVIEW_FIX_LOAD, {
       uuid: this.uuid,
-      commentId: this.comment?.id ?? '',
+      commentId: this.commentId ?? '',
     });
     if (currentPreviews.length > 0) {
       this.preview = currentPreviews[0];
@@ -276,7 +262,7 @@
 
   public async applyFix() {
     const changeNum = this.changeNum;
-    const basePatchNum = this.comment?.patch_set as BasePatchSetNum;
+    const basePatchNum = this.patchSet;
     const fixSuggestion = this.fixSuggestionInfo;
     if (!changeNum || !basePatchNum || !fixSuggestion) return;
 
@@ -293,7 +279,7 @@
       fileExtension: getFileExtension(
         fixSuggestion?.replacements?.[0].path ?? ''
       ),
-      commentId: this.comment?.id ?? '',
+      commentId: this.commentId ?? '',
     });
     if (res?.ok) {
       this.getNavigation().setUrl(
@@ -305,7 +291,7 @@
           forceReload: !this.hasEdit,
         })
       );
-      fire(this, 'reload-diff', {path: this.comment?.path});
+      fire(this, 'reload-diff', {path: fixSuggestion.replacements[0].path});
       fire(this, 'apply-user-suggestion', {});
     }
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview_test.ts b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview_test.ts
index 39d937d..593d0a6 100644
--- a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview_test.ts
@@ -52,10 +52,9 @@
 
   test('render diff', async () => {
     stubFlags('isEnabled').returns(true);
+    element.previewLoadedFor = createFixSuggestionInfo();
     element.codeText =
       '  private handleClick(e: MouseEvent) {\ne.stopPropagation();\ne.preventDefault();';
-    element.previewLoadedFor =
-      '  private handleClick(e: MouseEvent) {\ne.stopPropagation();\ne.preventDefault();';
     element.preview = {
       filepath:
         'polygerrit-ui/app/elements/change/gr-change-summary/gr-summary-chip_test.ts',
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 3c3d52e..42a2434 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
@@ -152,6 +152,8 @@
         </div>
       </div>
       <gr-suggestion-diff-preview
+        .patchSet=${this.comment?.patch_set}
+        .commentId=${this.comment?.id}
         .fixSuggestionInfo=${fixSuggestions[0]}
         .codeText=${code}
         @preview-loaded=${() => (this.previewLoaded = true)}