Merge "Fix Revert Submission related changes"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index e866829..f99973a 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -7060,8 +7060,7 @@
 
 [[revert-submission-info]]
 === RevertSubmissionInfo
-The `RevertSubmissionInfo` describes the revert changes. The changes are sorted
-by update time.
+The `RevertSubmissionInfo` entity describes the revert changes.
 
 [options="header",cols="1,6"]
 |==============================
diff --git a/WORKSPACE b/WORKSPACE
index e7317af..4a7add7 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -638,36 +638,36 @@
     sha1 = "5e3bda828a80c7a21dfbe2308d1755759c2fd7b4",
 )
 
-OW2_VERS = "7.0"
+OW2_VERS = "7.2"
 
 maven_jar(
     name = "ow2-asm",
     artifact = "org.ow2.asm:asm:" + OW2_VERS,
-    sha1 = "d74d4ba0dee443f68fb2dcb7fcdb945a2cd89912",
+    sha1 = "fa637eb67eb7628c915d73762b681ae7ff0b9731",
 )
 
 maven_jar(
     name = "ow2-asm-analysis",
     artifact = "org.ow2.asm:asm-analysis:" + OW2_VERS,
-    sha1 = "4b310d20d6f1c6b7197a75f1b5d69f169bc8ac1f",
+    sha1 = "b6e6abe057f23630113f4167c34bda7086691258",
 )
 
 maven_jar(
     name = "ow2-asm-commons",
     artifact = "org.ow2.asm:asm-commons:" + OW2_VERS,
-    sha1 = "478006d07b7c561ae3a92ddc1829bca81ae0cdd1",
+    sha1 = "ca2954e8d92a05bacc28ff465b25c70e0f512497",
 )
 
 maven_jar(
     name = "ow2-asm-tree",
     artifact = "org.ow2.asm:asm-tree:" + OW2_VERS,
-    sha1 = "29bc62dcb85573af6e62e5b2d735ef65966c4180",
+    sha1 = "3a23cc36edaf8fc5a89cb100182758ccb5991487",
 )
 
 maven_jar(
     name = "ow2-asm-util",
     artifact = "org.ow2.asm:asm-util:" + OW2_VERS,
-    sha1 = "18d4d07010c24405129a6dbb0e92057f8779fb9d",
+    sha1 = "a3ae34e57fa8a4040e28247291d0cc3d6b8c7bcf",
 )
 
 AUTO_VALUE_VERSION = "1.7"
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
index defaeea..025a1f9 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
@@ -103,6 +103,12 @@
                     .indexVersion(i.getSchema().getVersion())
                     .build())) {
           i.replace(accountState.get());
+        } catch (RuntimeException e) {
+          throw new StorageException(
+              String.format(
+                  "Failed to replace account %d in index version %d",
+                  id.get(), i.getSchema().getVersion()),
+              e);
         }
       } else {
         try (TraceTimer traceTimer =
@@ -113,6 +119,12 @@
                     .indexVersion(i.getSchema().getVersion())
                     .build())) {
           i.delete(id);
+        } catch (RuntimeException e) {
+          throw new StorageException(
+              String.format(
+                  "Failed to delete account %d from index version %d",
+                  id.get(), i.getSchema().getVersion()),
+              e);
         }
       }
     }
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index 5211a07..e0f6bec 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -208,6 +209,12 @@
                   .indexVersion(i.getSchema().getVersion())
                   .build())) {
         i.replace(cd);
+      } catch (RuntimeException e) {
+        throw new StorageException(
+            String.format(
+                "Failed to replace change %d in index version %d (current patch set = %d)",
+                cd.getId().get(), i.getSchema().getVersion(), cd.currentPatchSet().number()),
+            e);
       }
     }
     fireChangeIndexedEvent(cd.project().get(), cd.getId().get());
@@ -417,6 +424,12 @@
                     .indexVersion(i.getSchema().getVersion())
                     .build())) {
           i.delete(id);
+        } catch (RuntimeException e) {
+          throw new StorageException(
+              String.format(
+                  "Failed to delete change %d from index version %d",
+                  id.get(), i.getSchema().getVersion()),
+              e);
         }
       }
       fireChangeDeletedFromIndexEvent(id.get());
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
index 8859daec..e9897e8 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
@@ -102,6 +102,12 @@
                     .indexVersion(i.getSchema().getVersion())
                     .build())) {
           i.replace(internalGroup.get());
+        } catch (RuntimeException e) {
+          throw new StorageException(
+              String.format(
+                  "Failed to replace group %s in index version %d",
+                  uuid.get(), i.getSchema().getVersion()),
+              e);
         }
       } else {
         try (TraceTimer traceTimer =
@@ -112,6 +118,12 @@
                     .indexVersion(i.getSchema().getVersion())
                     .build())) {
           i.delete(uuid);
+        } catch (RuntimeException e) {
+          throw new StorageException(
+              String.format(
+                  "Failed to delete group %s from index version %d",
+                  uuid.get(), i.getSchema().getVersion()),
+              e);
         }
       }
     }
diff --git a/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java b/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
index 24f7073..22517ad 100644
--- a/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
@@ -18,6 +18,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.events.ProjectIndexedListener;
 import com.google.gerrit.index.project.ProjectData;
 import com.google.gerrit.index.project.ProjectIndex;
@@ -89,6 +90,12 @@
                     .indexVersion(i.getSchema().getVersion())
                     .build())) {
           i.replace(projectData);
+        } catch (RuntimeException e) {
+          throw new StorageException(
+              String.format(
+                  "Failed to replace project %s in index version %d",
+                  nameKey.get(), i.getSchema().getVersion()),
+              e);
         }
       }
       fireProjectIndexedEvent(nameKey.get());
@@ -103,6 +110,12 @@
                     .indexVersion(i.getSchema().getVersion())
                     .build())) {
           i.delete(nameKey);
+        } catch (RuntimeException e) {
+          throw new StorageException(
+              String.format(
+                  "Failed to delete project %s from index version %d",
+                  nameKey.get(), i.getSchema().getVersion()),
+              e);
         }
       }
     }
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index e5e01fe..be33405 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -309,7 +309,7 @@
       }
       cherryPickInput.base = null;
     }
-    results.sort(Comparator.comparing(c -> c.updated));
+    results.sort(Comparator.comparing(c -> c.revertOf));
     RevertSubmissionInfo revertSubmissionInfo = new RevertSubmissionInfo();
     revertSubmissionInfo.revertChanges = results;
     return revertSubmissionInfo;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
index 3087988..61dd31a 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
-import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
@@ -684,10 +683,6 @@
 
   @Test
   @GerritConfig(name = "change.submitWholeTopic", value = "true")
-
-  // In this test and some of the following, we use @UseClockStep because the test depends on the
-  // update time.
-  @UseClockStep
   public void revertSubmissionDifferentRepositoriesWithDependantChange() throws Exception {
     projectOperations.newProject().name("secondProject").create();
     TestRepository<InMemoryRepository> secondRepo =
@@ -709,19 +704,16 @@
         getChangeApis(gApi.changes().id(resultCommits.get(1).getChangeId()).revertSubmission());
 
     assertThat(revertChanges).hasSize(3);
-    // Ensures that revertChanges[i] is the revert of resultCommits[i] because the reverts are by
-    // update time.
-    Collections.reverse(resultCommits);
 
-    String sha1FirstChange = resultCommits.get(0).getCommit().getName();
+    String sha1RevertOfTheSecondChange = revertChanges.get(1).current().commit(false).commit;
     String sha1SecondChange = resultCommits.get(1).getCommit().getName();
-    String sha1SecondRevert = revertChanges.get(1).current().commit(false).commit;
+    String sha1ThirdChange = resultCommits.get(2).getCommit().getName();
     assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit)
-        .isEqualTo(sha1FirstChange);
+        .isEqualTo(sha1RevertOfTheSecondChange);
     assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit)
         .isEqualTo(sha1SecondChange);
     assertThat(revertChanges.get(2).current().commit(false).parents.get(0).commit)
-        .isEqualTo(sha1SecondRevert);
+        .isEqualTo(sha1ThirdChange);
 
     assertThat(revertChanges.get(0).current().files().get("a.txt").linesDeleted).isEqualTo(1);
     assertThat(revertChanges.get(1).current().files().get("b.txt").linesDeleted).isEqualTo(1);
@@ -779,7 +771,7 @@
     gApi.changes().id(secondResult.getChangeId()).current().submit();
     List<ChangeApi> revertChanges =
         getChangeApis(gApi.changes().id(firstResult.getChangeId()).revertSubmission());
-
+    Collections.reverse(revertChanges);
     String sha1SecondChange = secondResult.getCommit().getName();
     String sha1FirstRevert = revertChanges.get(0).current().commit(false).commit;
     assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit)
@@ -809,6 +801,7 @@
     gApi.changes().id(unrelated).current().submit();
     List<ChangeApi> revertChanges =
         getChangeApis(gApi.changes().id(firstResult.getChangeId()).revertSubmission());
+    Collections.reverse(revertChanges);
     String sha1SecondChange = secondResult.getCommit().getName();
     String sha1FirstRevert = revertChanges.get(0).current().commit(false).commit;
     assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit)
@@ -828,7 +821,6 @@
 
   @Test
   @GerritConfig(name = "change.submitWholeTopic", value = "true")
-  @UseClockStep
   public void revertSubmissionDifferentRepositories() throws Exception {
     projectOperations.newProject().name("secondProject").create();
     TestRepository<InMemoryRepository> secondRepo =
@@ -865,7 +857,6 @@
 
   @Test
   @GerritConfig(name = "change.submitWholeTopic", value = "true")
-  @UseClockStep
   public void revertSubmissionMultipleBranches() throws Exception {
     List<PushOneCommit.Result> resultCommits = new ArrayList<>();
     String topic = "topic";
@@ -883,16 +874,16 @@
     List<ChangeApi> revertChanges =
         getChangeApis(gApi.changes().id(resultCommits.get(1).getChangeId()).revertSubmission());
     assertThat(revertChanges.get(0).current().files().get("c.txt").linesDeleted).isEqualTo(1);
-    assertThat(revertChanges.get(1).current().files().get("b.txt").linesDeleted).isEqualTo(1);
-    assertThat(revertChanges.get(2).current().files().get("a.txt").linesDeleted).isEqualTo(1);
+    assertThat(revertChanges.get(1).current().files().get("a.txt").linesDeleted).isEqualTo(1);
+    assertThat(revertChanges.get(2).current().files().get("b.txt").linesDeleted).isEqualTo(1);
     String sha1FirstChange = resultCommits.get(0).getCommit().getName();
     String sha1ThirdChange = resultCommits.get(2).getCommit().getName();
-    String sha1SecondRevert = revertChanges.get(1).current().commit(false).commit;
+    String sha1SecondRevert = revertChanges.get(2).current().commit(false).commit;
     assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit)
         .isEqualTo(sha1FirstChange);
-    assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit)
-        .isEqualTo(sha1ThirdChange);
     assertThat(revertChanges.get(2).current().commit(false).parents.get(0).commit)
+        .isEqualTo(sha1ThirdChange);
+    assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit)
         .isEqualTo(sha1SecondRevert);
 
     assertThat(revertChanges).hasSize(3);
@@ -901,7 +892,6 @@
 
   @Test
   @GerritConfig(name = "change.submitWholeTopic", value = "true")
-  @UseClockStep
   public void revertSubmissionDependantAndUnrelatedWithMerge() throws Exception {
     String topic = "topic";
     PushOneCommit.Result firstResult =
@@ -930,6 +920,7 @@
 
     List<ChangeApi> revertChanges =
         getChangeApis(gApi.changes().id(secondResult.getChangeId()).revertSubmission());
+    Collections.reverse(revertChanges);
     assertThat(revertChanges.get(0).current().files().get("c.txt").linesDeleted).isEqualTo(1);
     assertThat(revertChanges.get(1).current().files().get("b.txt").linesDeleted).isEqualTo(1);
     assertThat(revertChanges.get(2).current().files().get("a.txt").linesDeleted).isEqualTo(1);
@@ -950,7 +941,6 @@
 
   @Test
   @GerritConfig(name = "change.submitWholeTopic", value = "true")
-  @UseClockStep
   public void revertSubmissionUnrelatedWithTwoMergeCommits() throws Exception {
     String topic = "topic";
     PushOneCommit.Result firstResult =
@@ -980,6 +970,7 @@
 
     List<ChangeApi> revertChanges =
         getChangeApis(gApi.changes().id(secondResult.getChangeId()).revertSubmission());
+    Collections.reverse(revertChanges);
     assertThat(revertChanges.get(0).current().files().get("c.txt").linesDeleted).isEqualTo(1);
     assertThat(revertChanges.get(1).current().files().get("b.txt").linesDeleted).isEqualTo(1);
     assertThat(revertChanges.get(2).current().files().get("a.txt").linesDeleted).isEqualTo(1);
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
index 023f43e..dad09f9 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.RefState;
@@ -119,15 +120,17 @@
 
   private void updateProjectConfigWithoutIndexUpdate(
       Project.NameKey project, Consumer<ProjectConfig> update) throws Exception {
-    assertThrows(
-        UnsupportedOperationException.class,
-        () -> {
-          try (AutoCloseable ignored = disableProjectIndex()) {
-            try (ProjectConfigUpdate u = updateProject(project)) {
-              update.accept(u.getConfig());
-              u.save();
-            }
-          }
-        });
+    StorageException storageException =
+        assertThrows(
+            StorageException.class,
+            () -> {
+              try (AutoCloseable ignored = disableProjectIndex()) {
+                try (ProjectConfigUpdate u = updateProject(project)) {
+                  update.accept(u.getConfig());
+                  u.save();
+                }
+              }
+            });
+    assertThat(storageException.getCause()).isInstanceOf(UnsupportedOperationException.class);
   }
 }
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 461e2b9..41f2ec7 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -46,6 +46,9 @@
 
 ```sh
 ./polygerrit-ui/run-server.sh
+
+// or
+npm run start
 ```
 
 Then visit <http://localhost:8081>.
@@ -185,7 +188,7 @@
 
 ## Template Type Safety
 
-> **Warning**: This feature is temporary disabled, because it doesn't work with Polymer 2 and Polymer 3. 
+> **Warning**: This feature is temporary disabled, because it doesn't work with Polymer 2 and Polymer 3.
 Some of the checks are made by polymer linter.
 
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
index 69d3281..f11e5ed 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
@@ -75,7 +75,10 @@
       return editable === 'false';
     }
 
-    _computeChecked(value) {
+    /**
+     * @param {?boolean} value - fallback to false if undefined
+     */
+    _computeChecked(value = false) {
       return JSON.parse(value);
     }
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index b86213a..c9f881f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -51,6 +51,7 @@
 <link rel="import" href="../gr-included-in-dialog/gr-included-in-dialog.html">
 <link rel="import" href="../gr-messages-list/gr-messages-list.html">
 <link rel="import" href="../gr-related-changes-list/gr-related-changes-list.html">
+<link rel="import" href="../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html">
 <link rel="import" href="../gr-reply-dialog/gr-reply-dialog.html">
 <link rel="import" href="../gr-thread-list/gr-thread-list.html">
 <link rel="import" href="../gr-upload-help-dialog/gr-upload-help-dialog.html">
@@ -635,6 +636,11 @@
             on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list>
       </template>
     </div>
+    <gr-apply-fix-dialog
+      id="applyFixDialog"
+      prefs="[[_diffPrefs]]"
+      change="[[_change]]"
+      change-num="[[_changeNum]]"></gr-apply-fix-dialog>
     <gr-overlay id="downloadOverlay" with-backdrop>
       <gr-download-dialog
           id="downloadDialog"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 5962c2b..ed62918 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -381,6 +381,10 @@
           this._handleCommitMessageSave.bind(this));
       this.addEventListener('editable-content-cancel',
           this._handleCommitMessageCancel.bind(this));
+      this.addEventListener('open-fix-preview',
+          this._onOpenFixPreview.bind(this));
+      this.addEventListener('close-fix-preview',
+          this._onCloseFixPreview.bind(this));
       this.listen(window, 'scroll', '_handleScroll');
       this.listen(document, 'visibilitychange', '_handleVisibilityChange');
     }
@@ -420,6 +424,14 @@
       });
     }
 
+    _onOpenFixPreview(e) {
+      this.$.applyFixDialog.open(e);
+    }
+
+    _onCloseFixPreview(e) {
+      this._reload();
+    }
+
     _handleToggleDiffMode(e) {
       if (this.shouldSuppressKeyboardShortcut(e) ||
           this.modifierPressed(e)) { return; }
@@ -1461,7 +1473,7 @@
       let coreDataPromise;
 
       // If the patch number is specified
-      if (this._patchRange.patchNum) {
+      if (this._patchRange && this._patchRange.patchNum) {
         // Because a specific patchset is specified, reload the resources that
         // are keyed by patch number or patch range.
         const patchResourcesLoaded = this._reloadPatchNumDependentResources();
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index c0c42b5..5c98281 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -241,7 +241,7 @@
         detail.inBackgroundTab = contextInfo.isInBackgroundTab;
       }
       if (contextInfo && contextInfo.startTimeMs) {
-        detail.startTime = contextInfo.startTimeMs;
+        detail.eventStart = contextInfo.startTimeMs;
       }
       document.dispatchEvent(new CustomEvent(type, {detail}));
       if (opt_noLog) { return; }
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
index 7728450..c7aa896 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -261,7 +261,7 @@
           'timing-report', 'UI Latency', 'timeAction', 0,
           {startTimeMs: 42}
       ));
-      assert.equal(dispatchStub.getCall(2).args[0].detail.startTime, 42);
+      assert.equal(dispatchStub.getCall(2).args[0].detail.eventStart, 42);
     });
 
     suite('plugins', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html
new file mode 100644
index 0000000..1569094
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html
@@ -0,0 +1,91 @@
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
+<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../gr-diff/gr-diff.html">
+
+<dom-module id="gr-apply-fix-dialog">
+  <template>
+    <style include="shared-styles">
+      gr-diff {
+        --content-width: 90vw;
+      }
+      .diffContainer {
+        padding: var(--spacing-l) 0;
+        border-bottom: 1px solid var(--border-color);
+      }
+      .file-name {
+        display: block;
+        padding: var(--spacing-s) var(--spacing-l);
+        background-color: var(--background-color-secondary);
+        border-bottom: 1px solid var(--border-color);
+      }
+      .fixActions {
+        display: flex;
+        justify-content: flex-end;
+      }
+      gr-button {
+        margin-left: var(--spacing-m);
+      }
+      .fix-picker {
+        display: flex;
+        align-items: center;
+        margin-right: var(--spacing-l);
+      }
+    </style>
+    <gr-overlay id="applyFixOverlay" with-backdrop>
+      <gr-dialog
+        id="applyFixDialog"
+        on-confirm="_handleApplyFix"
+        confirm-label="[[_getApplyFixButtonLabel(_isApplyFixLoading)]]"
+        disabled="[[_isApplyFixLoading]]"
+        on-cancel="onCancel">
+        <div slot="header">[[_robotId]] - [[getFixDescription(_currentFix)]]</div>
+        <div slot="main">
+          <template is="dom-repeat" items="[[_currentPreviews]]">
+            <div class="file-name">
+              <span>[[item.filepath]]</span>
+            </div>
+            <div class="diffContainer">
+              <gr-diff
+              prefs="[[overridePartialPrefs(prefs)]]"
+              change-num="[[changeNum]]"
+              path="[[item.filepath]]"
+              diff="[[item.preview]]"></gr-diff>
+            </div>
+          </template>
+        </div>
+        <div slot="footer" class="fix-picker" hidden$="[[hasSingleFix(_fixSuggestions)]]">
+          <span>Suggested fix [[addOneTo(_selectedFixIdx)]] of [[_fixSuggestions.length]]</span>
+          <gr-button on-click="_onPrevFixClick" disabled$="[[_noPrevFix(_selectedFixIdx)]]">
+            <iron-icon icon="gr-icons:chevron-left"></iron-icon>
+          </gr-button>
+          <gr-button on-click="_onNextFixClick" disabled$="[[_noNextFix(_selectedFixIdx)]]">
+            <iron-icon icon="gr-icons:chevron-right"></iron-icon>
+          </gr-button>
+        </div>
+      </gr-dialog>
+    </gr-overlay>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-apply-fix-dialog.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
new file mode 100644
index 0000000..5130620
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
@@ -0,0 +1,192 @@
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function() {
+  'use strict';
+  Polymer({
+    is: 'gr-apply-fix-dialog',
+
+    properties: {
+      // Diff rendering preference API response.
+      prefs: Array,
+      // ChangeInfo API response object.
+      change: Object,
+      changeNum: String,
+      _patchNum: Number,
+      // robot ID associated with a robot comment.
+      _robotId: String,
+      // Selected FixSuggestionInfo entity from robot comment API response.
+      _currentFix: Object,
+      // Flattened /preview API response DiffInfo map object.
+      _currentPreviews: {type: Array, value: () => []},
+      // FixSuggestionInfo entities from robot comment API response.
+      _fixSuggestions: Array,
+      _isApplyFixLoading: {
+        type: Boolean,
+        value: false,
+      },
+      // Index of currently showing suggested fix.
+      _selectedFixIdx: Number,
+    },
+
+    behaviors: [
+      Gerrit.FireBehavior,
+    ],
+
+    /**
+     * Given robot comment CustomEvent objevt, fetch diffs associated
+     * with first robot comment suggested fix and open dialog.
+     *
+     * @param {*} e CustomEvent to be passed from gr-comment with
+     * robot comment detail.
+     * @return {Promise<undefined>} Promise that resolves either when all
+     * preview diffs are fetched or no fix suggestions in custom event detail.
+     */
+    open(e) {
+      this._patchNum = e.detail.patchNum;
+      this._fixSuggestions = e.detail.comment.fix_suggestions;
+      this._robotId = e.detail.comment.robot_id;
+      if (this._fixSuggestions == null || this._fixSuggestions.length == 0) {
+        return Promise.resolve();
+      }
+      this._selectedFixIdx = 0;
+      const promises = [];
+      promises.push(
+          this._showSelectedFixSuggestion(this._fixSuggestions[0]),
+          this.$.applyFixOverlay.open()
+      );
+      return Promise.all(promises)
+          .then(() => {
+            // ensures gr-overlay repositions overlay in center
+            this.$.applyFixOverlay.fire('iron-resize');
+          });
+    },
+
+    _showSelectedFixSuggestion(fixSuggestion) {
+      this._currentFix = fixSuggestion;
+      return this._fetchFixPreview(fixSuggestion.fix_id);
+    },
+
+    _fetchFixPreview(fixId) {
+      return this.$.restAPI
+          .getRobotCommentFixPreview(this.changeNum, this._patchNum, fixId)
+          .then(res => {
+            if (res != null) {
+              const previews = Object.keys(res).map(key =>
+                ({filepath: key, preview: res[key]}));
+              this._currentPreviews = previews;
+            }
+          }).catch(err => {
+            this._close();
+            this.dispatchEvent(new CustomEvent('show-error', {
+              bubbles: true,
+              composed: true,
+              detail: {message: `Error generating fix preview: ${err}`},
+            }));
+          });
+    },
+
+    hasSingleFix(_fixSuggestions) {
+      return (_fixSuggestions || {}).length === 1;
+    },
+
+    overridePartialPrefs(prefs) {
+      // generate a smaller gr-diff than fullscreen for dialog
+      return Object.assign({}, prefs, {line_length: 50});
+    },
+
+    onCancel(e) {
+      if (e) {
+        e.stopPropagation();
+      }
+      this._close();
+    },
+
+    addOneTo(_selectedFixIdx) {
+      return _selectedFixIdx + 1;
+    },
+
+    _onPrevFixClick(e) {
+      if (e) e.stopPropagation();
+      if (this._selectedFixIdx >= 1 && this._fixSuggestions != null) {
+        this._selectedFixIdx -= 1;
+        return this._showSelectedFixSuggestion(
+            this._fixSuggestions[this._selectedFixIdx]);
+      }
+    },
+
+    _onNextFixClick(e) {
+      if (e) e.stopPropagation();
+      if (this._selectedFixIdx < this._fixSuggestions.length &&
+        this._fixSuggestions != null) {
+        this._selectedFixIdx += 1;
+        return this._showSelectedFixSuggestion(
+            this._fixSuggestions[this._selectedFixIdx]);
+      }
+    },
+
+    _noPrevFix(_selectedFixIdx) {
+      return _selectedFixIdx === 0;
+    },
+
+    _noNextFix(_selectedFixIdx) {
+      if (this._fixSuggestions == null) return true;
+      return _selectedFixIdx === this._fixSuggestions.length - 1;
+    },
+
+    _close() {
+      this._currentFix = {};
+      this._currentPreviews = [];
+      this._isApplyFixLoading = false;
+
+      this.dispatchEvent(new CustomEvent('close-fix-preview', {
+        bubbles: true,
+        composed: true,
+      }));
+      this.$.applyFixOverlay.close();
+    },
+
+    _getApplyFixButtonLabel(isLoading) {
+      return isLoading ? 'Saving...' : 'Apply Fix';
+    },
+
+    _handleApplyFix(e) {
+      if (e) {
+        e.stopPropagation();
+      }
+      if (this._currentFix == null || this._currentFix.fix_id == null) {
+        return;
+      }
+      this._isApplyFixLoading = true;
+      return this.$.restAPI.applyFixSuggestion(this.changeNum, this._patchNum,
+          this._currentFix.fix_id).then(res => {
+        Gerrit.Nav.navigateToChange(this.change, 'edit', this._patchNum);
+        this._close();
+      }).catch(err => {
+        this.dispatchEvent(new CustomEvent('show-error', {
+          bubbles: true,
+          composed: true,
+          detail: {message: `Error applying fix suggestion: ${err}`},
+        }));
+      });
+    },
+
+    getFixDescription(currentFix) {
+      return currentFix != null && currentFix.description ?
+        currentFix.description : '';
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
new file mode 100644
index 0000000..5a2193d
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
@@ -0,0 +1,191 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the 'License');
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an 'AS IS' BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<meta name='viewport' content='width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes'>
+<title>gr-apply-fix-dialog</title>
+<script src='/test/common-test-setup.js'></script>
+<script src='/bower_components/webcomponentsjs/custom-elements-es5-adapter.js'></script>
+
+<script src='/bower_components/webcomponentsjs/webcomponents-lite.js'></script>
+<script src='/bower_components/web-component-tester/browser.js'></script>
+<link rel='import' href='../../../test/common-test-setup.html' />
+
+<link rel='import' href='./gr-apply-fix-dialog.html'>
+
+<script>void (0);</script>
+
+<test-fixture id='basic'>
+  <template>
+    <gr-apply-fix-dialog></gr-apply-fix-dialog>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-apply-fix-dialog tests', () => {
+    let element;
+    let sandbox;
+    const ROBOT_COMMENT = {
+      robot_id: 'robot_1',
+      fix_suggestions: [{fix_id: 'fix_1'}, {fix_id: 'fix_2'}],
+    };
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+      element.changeNum = '1';
+      element._patchNum = 2;
+      element.change = {
+        _number: '1',
+        project: 'project',
+      };
+      element.prefs = {
+        font_size: 12,
+        line_length: 100,
+        tab_size: 4,
+      };
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('dialog opens fetch and sets previews', done => {
+      sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
+          .returns(Promise.resolve({
+            f1: {
+              meta_a: {},
+              meta_b: {},
+              content: [
+                {
+                  ab: ['loqlwkqll'],
+                },
+                {
+                  b: ['qwqqsqw'],
+                },
+                {
+                  ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'],
+                },
+              ],
+            },
+            f2: {
+              meta_a: {},
+              meta_b: {},
+              content: [
+                {
+                  ab: ['eqweqweqwex'],
+                },
+                {
+                  b: ['zassdasd'],
+                },
+                {
+                  ab: ['zassdasd', 'dasdasda', 'asdasdad'],
+                },
+              ],
+            },
+          }));
+      sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
+
+      element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT}})
+          .then(() => {
+            assert.equal(element._currentFix.fix_id, 'fix_1');
+            assert.equal(element._currentPreviews.length, 2);
+            assert.equal(element._robotId, 'robot_1');
+            done();
+          });
+    });
+
+    test('preview endpoint throws error should reset dialog', done => {
+      element.addEventListener('show-error', () => {
+        assert.deepEqual(element._currentFix, {});
+        assert.equal(element._currentPreviews.length, 0);
+        done();
+      });
+      sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview',
+          () => Promise.reject(new Error('backend error')));
+
+      element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT}});
+    });
+
+    test('apply fix button should call apply' +
+    'and navigate to change view', done => {
+      sandbox.stub(element.$.restAPI, 'applyFixSuggestion')
+          .returns(Promise.resolve());
+      sandbox.stub(Gerrit.Nav, 'navigateToChange');
+      element._currentFix = {fix_id: '123'};
+
+      element._handleApplyFix().then(() => {
+        assert.isTrue(element.$.restAPI.applyFixSuggestion
+            .calledWithExactly('1', 2, '123'));
+        assert.isTrue(Gerrit.Nav.navigateToChange.calledWithExactly({
+          _number: '1',
+          project: 'project',
+        }, 'edit', 2));
+
+        // reset gr-apply-fix-dialog and close
+        assert.deepEqual(element._currentFix, {});
+        assert.equal(element._currentPreviews.length, 0);
+        done();
+      });
+    });
+
+    test('select fix forward and back of multiple suggested fixes', done => {
+      sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
+          .returns(Promise.resolve({
+            f1: {
+              meta_a: {},
+              meta_b: {},
+              content: [
+                {
+                  ab: ['loqlwkqll'],
+                },
+                {
+                  b: ['qwqqsqw'],
+                },
+                {
+                  ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'],
+                },
+              ],
+            },
+            f2: {
+              meta_a: {},
+              meta_b: {},
+              content: [
+                {
+                  ab: ['eqweqweqwex'],
+                },
+                {
+                  b: ['zassdasd'],
+                },
+                {
+                  ab: ['zassdasd', 'dasdasda', 'asdasdad'],
+                },
+              ],
+            },
+          }));
+      sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
+
+      element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT}})
+          .then(() => {
+            element._onNextFixClick();
+            assert.equal(element._currentFix.fix_id, 'fix_2');
+            element._onPrevFixClick();
+            assert.equal(element._currentFix.fix_id, 'fix_1');
+            done();
+          });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index 9292121..62b35e4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -357,7 +357,9 @@
     }
 
     for (const layer of this.layers) {
-      layer.annotate(contentText, lineNumberEl, line);
+      if (typeof layer.annotate == 'function') {
+        layer.annotate(contentText, lineNumberEl, line);
+      }
     }
 
     td.appendChild(contentText);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index cd53510..7275ae5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -37,6 +37,7 @@
 <link rel="import" href="../../shared/revision-info/revision-info.html">
 <link rel="import" href="../gr-comment-api/gr-comment-api.html">
 <link rel="import" href="../gr-diff-cursor/gr-diff-cursor.html">
+<link rel="import" href="../gr-apply-fix-dialog/gr-apply-fix-dialog.html">
 <link rel="import" href="../gr-diff-host/gr-diff-host.html">
 <link rel="import" href="../gr-diff-mode-selector/gr-diff-mode-selector.html">
 <link rel="import" href="../gr-diff-preferences-dialog/gr-diff-preferences-dialog.html">
@@ -343,6 +344,12 @@
         on-comment-anchor-tap="_onLineSelected"
         on-line-selected="_onLineSelected">
     </gr-diff-host>
+    <gr-apply-fix-dialog
+      id="applyFixDialog"
+      prefs="[[_prefs]]"
+      change="[[_change]]"
+      change-num="[[_changeNum]]">
+    </gr-apply-fix-dialog>
     <gr-diff-preferences-dialog
         id="diffPreferencesDialog"
         diff-prefs="{{_prefs}}"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 883fa01..23c6741 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -250,6 +250,8 @@
         this._loggedIn = loggedIn;
       });
 
+      this.addEventListener('open-fix-preview',
+          this._onOpenFixPreview.bind(this));
       this.$.cursor.push('diffs', this.$.diffHost);
     }
 
@@ -365,6 +367,10 @@
       this.$.cursor.moveUp();
     }
 
+    _onOpenFixPreview(e) {
+      this.$.applyFixDialog.open(e);
+    }
+
     _handleNextLineOrFileWithComments(e) {
       if (this.shouldSuppressKeyboardShortcut(e)) { return; }
       if (e.detail.keyboardEvent.shiftKey &&
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
index d1d60f1..ea68e5f 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
@@ -358,6 +358,15 @@
                 Please Fix
               </gr-button>
             </template>
+            <gr-button
+                link
+                secondary
+                class="action show-fix"
+                hidden$="[[_hasNoFix(comment)]]"
+                on-click="_handleShowFix"
+                disabled="[[robotButtonDisabled]]">
+              Show Fix
+            </gr-button>
             <gr-endpoint-decorator name="robot-comment-controls">
               <gr-endpoint-param name="comment" value="[[comment]]">
               </gr-endpoint-param>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
index 62d15e5..0c45c85 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -49,6 +49,12 @@
      */
 
     /**
+     * Fired when the show fix preview action is triggered.
+     *
+     * @event open-fix-preview
+     */
+
+    /**
      * Fired when this comment is discarded.
      *
      * @event comment-discard
@@ -505,6 +511,18 @@
       }));
     }
 
+    _handleShowFix() {
+      this.dispatchEvent(new CustomEvent('open-fix-preview', {
+        bubbles: true,
+        composed: true,
+        detail: this._getEventPayload(),
+      }));
+    }
+
+    _hasNoFix(comment) {
+      return !comment || !comment.fix_suggestions;
+    }
+
     _handleDiscard(e) {
       e.preventDefault();
       this.$.reporting.recordDraftInteraction();
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
index 81dfaeb..ae8f3f6 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
@@ -1007,5 +1007,17 @@
       flushAsynchronousOperations();
       assert.isNotNull(element.$$('.robotActions gr-button'));
     });
+
+    test('_handleShowFix fires open-fix-preview event', done => {
+      element.addEventListener('open-fix-preview', e => {
+        assert.deepEqual(e.detail, element._getEventPayload());
+        done();
+      });
+      element.comment = {fix_suggestions: [{}]};
+      element.isRobotComment = true;
+      flushAsynchronousOperations();
+
+      MockInteractions.tap(element.$$('.show-fix'));
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html
index e754a55..77418df 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html
@@ -60,6 +60,7 @@
       <header class="font-h3"><slot name="header"></slot></header>
       <main><slot name="main"></slot></main>
       <footer>
+        <slot name="footer"></slot>
         <gr-button id="cancel" class$="[[_computeCancelClass(cancelLabel)]]" link on-click="_handleCancelTap">
           [[cancelLabel]]
         </gr-button>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index a8c5bb3..fdb108f8 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -1860,6 +1860,25 @@
       });
     }
 
+    getRobotCommentFixPreview(changeNum, patchNum, fixId) {
+      return this._getChangeURLAndFetch({
+        changeNum,
+        patchNum,
+        endpoint: `/fixes/${encodeURIComponent(fixId)}/preview`,
+        reportEndpointAsId: true,
+      });
+    }
+
+    applyFixSuggestion(changeNum, patchNum, fixId) {
+      return this._getChangeURLAndSend({
+        method: 'POST',
+        changeNum,
+        patchNum,
+        endpoint: `/fixes/${encodeURIComponent(fixId)}/apply`,
+        reportEndpointAsId: true,
+      });
+    }
+
     // Deprecated, prefer to use putChangeCommitMessage instead.
     saveChangeCommitMessageEdit(changeNum, message) {
       return this._getChangeURLAndSend({
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index cca7d04..26078cd 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -117,6 +117,7 @@
     'diff/gr-diff-view/gr-diff-view_test.html',
     'diff/gr-diff/gr-diff-group_test.html',
     'diff/gr-diff/gr-diff_test.html',
+    'diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html',
     'diff/gr-patch-range-select/gr-patch-range-select_test.html',
     'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html',
     'diff/gr-selection-action-box/gr-selection-action-box_test.html',
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index 3b699c2..ba52ce8 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -39,12 +39,10 @@
 
 var (
 	plugins               = flag.String("plugins", "", "comma seperated plugin paths to serve")
-	port                  = flag.String("port", ":8081", "Port to serve HTTP requests on")
+	port                  = flag.String("port", "localhost:8081", "address to serve HTTP requests on")
 	host                  = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to")
 	scheme                = flag.String("scheme", "https", "URL scheme")
 	cdnPattern            = regexp.MustCompile("https://cdn.googlesource.com/polygerrit_ui/[0-9.]*")
-	webComponentPattern   = regexp.MustCompile("webcomponentsjs-p2")
-	grAppPattern = regexp.MustCompile("gr-app-p2")
 	bundledPluginsPattern = regexp.MustCompile("https://cdn.googlesource.com/polygerrit_assets/[0-9.]*")
 )
 
@@ -189,13 +187,9 @@
 	buf.ReadFrom(reader)
 	original := buf.String()
 
-	// Replace the webcomponentsjs-p2 with webcomponentsjs
-	replaced := webComponentPattern.ReplaceAllString(original, "webcomponentsjs")
-	replaced = grAppPattern.ReplaceAllString(replaced, "gr-app")
-
 	// Simply remove all CDN references, so files are loaded from the local file system  or the proxy
 	// server instead.
-	replaced = cdnPattern.ReplaceAllString(replaced, "")
+	replaced := cdnPattern.ReplaceAllString(original, "")
 
 	// Modify window.INITIAL_DATA so that it has the same effect as injectLocalPlugins. To achieve
 	// this let's add JavaScript lines at the end of the <script>...</script> snippet that also