Merge branch 'stable-3.4'

* stable-3.4:
  gr-confirm-move-dialog: Fix _getProjectBranchesSuggestions
  Clean up upload change help dialog
  Fix binding of DELETE REST calls from plugins

Change-Id: I7abf2c7112f854d4bdad2191d9b227dd31ccbc62
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index a7240e2..0849c56 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -153,7 +153,7 @@
 Tag the plugins:
 
 ----
-  git submodule foreach '[ "$path" == "modules/jgit" ] || git tag -s -m "v$version" "v$version"'
+  git submodule foreach '[ "$sm_path" == "modules/jgit" ] || git tag -s -m "v$version" "v$version"'
 ----
 
 [[build-gerrit]]
@@ -324,7 +324,7 @@
 Push the new Release Tag on the plugins:
 
 ----
-  git submodule foreach git push gerrit-review tag v$version
+  git submodule foreach '[ "$sm_path" == "modules/jgit" ] || git push gerrit-review tag "v$version"'
 ----
 
 [[upload-documentation]]
diff --git a/Documentation/note-db.txt b/Documentation/note-db.txt
index a13cbfb..b376d6e 100644
--- a/Documentation/note-db.txt
+++ b/Documentation/note-db.txt
@@ -36,6 +36,67 @@
 not available in 3.0, so any upgrade from Gerrit 2.x to 3.x must go through
 2.16 to effect the NoteDb upgrade.
 
+== Format
+
+Each review ("change") in Gerrit is numbered. The different revisions
+("patchsets") of a change 12345 are stored under
+----
+  refs/changes/45/12345/${PATCHSET_NUMBER}
+----
+
+The revisions are stored as commits to the main project, ie. if you
+fetch this ref, you can check out the proposed change.
+
+A change 12345 has its review metadata under
+----
+  refs/changes/45/12345/meta
+----
+The metadata is a notes branch. The commit messages on the branch hold
+modifications to global data of the change (votes, global comments). The inline
+comments are in a
+link:https://git.eclipse.org/r/plugins/gitiles/jgit/jgit/\+/master/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java[NoteMap],
+where the key is the commit SHA-1 of the patchset
+that the comment refers to, and the value is JSON data. The format of the
+JSON is in the
+link:https://gerrit.googlesource.com/gerrit/\+/master/java/com/google/gerrit/server/notedb/RevisionNoteData.java[RevisionNoteData]
+which contains 
+link:https://gerrit.googlesource.com/gerrit/\+/master/java/com/google/gerrit/entities/Comment.java[Comment] entities.
+
+For example:
+----
+   {
+      "key": {
+        "uuid": "c7be1334_47885e36",
+        "filename":
+"java/com/google/gerrit/server/restapi/project/CommitsCollection.java",
+        "patchSetId": 7
+      },
+      "lineNbr": 158,
+      "author": {
+        "id": 1026112
+      },
+      "writtenOn": "2019-11-06T09:00:50Z",
+      "side": 1,
+      "message": "nit: factor this out in a variable, use
+toImmutableList as collector",
+      "range": {
+        "startLine": 156,
+        "startChar": 32,
+        "endLine": 158,
+        "endChar": 66
+      },
+      "revId": "071c601d6ee1a2a9f520415fd9efef8e00f9cf60",
+      "serverId": "173816e5-2b9a-37c3-8a2e-48639d4f1153",
+      "unresolved": true
+    },
+----
+
+Automated systems may post "robot comments" instead of normal
+comments, which are an extension of the previous comment, defined in
+the
+link:https://gerrit.googlesource.com/gerrit/\+/master/java/com/google/gerrit/entities/RobotComment.java[RobotComment]
+class.
+
 [[migration]]
 == Migration
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 516b2fe..721900f 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -3576,6 +3576,9 @@
 Deletes a single vote from a change. Note, that even when the last vote of
 a reviewer is removed the reviewer itself is still listed on the change.
 
+If another user removed a user's vote, the user with the deleted vote will be
+added to the attention set.
+
 .Request
 ----
   DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/John%20Doe/votes/Code-Review HTTP/1.0
@@ -5940,6 +5943,9 @@
 Note, that even when the last vote of a reviewer is removed the reviewer itself
 is still listed on the change.
 
+If another user removed a user's vote, the user with the deleted vote will be
+added to the attention set.
+
 .Request
 ----
   DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/reviewers/John%20Doe/votes/Code-Review HTTP/1.0
@@ -6118,6 +6124,7 @@
 * The change is marked ready for review.
 * As an owner/uploader, when someone replies on your change.
 * As a reviewer, when the owner/uploader replies.
+* When the user's vote is deleted by another user.
 
 Users are removed from the attention set if one the following apply:
 
diff --git a/Documentation/user-attention-set.txt b/Documentation/user-attention-set.txt
index 4697afc..ea927c7 100644
--- a/Documentation/user-attention-set.txt
+++ b/Documentation/user-attention-set.txt
@@ -47,6 +47,8 @@
   attention set.
 * For merged and abandoned changes the owner is added only when a human creates
   an unresolved comment.
+* If another user removed a user's vote, the user with the deleted vote will be
+  added to the attention set.
 * Only owner, uploader, reviewers and ccs can be in the attention set.
 * The rules for service accounts are different, see link:#bots[Bots].
 
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index 4b813df..cc73e9a 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -36,9 +36,11 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.change.AddToAttentionSetOp;
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.change.VoteResource;
@@ -58,6 +60,7 @@
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.HashMap;
@@ -79,6 +82,8 @@
   private final RemoveReviewerControl removeReviewerControl;
   private final ProjectCache projectCache;
   private final MessageIdGenerator messageIdGenerator;
+  private final AddToAttentionSetOp.Factory attentionSetOpfactory;
+  private final Provider<CurrentUser> currentUserProvider;
 
   @Inject
   DeleteVote(
@@ -92,7 +97,9 @@
       NotifyResolver notifyResolver,
       RemoveReviewerControl removeReviewerControl,
       ProjectCache projectCache,
-      MessageIdGenerator messageIdGenerator) {
+      MessageIdGenerator messageIdGenerator,
+      AddToAttentionSetOp.Factory attentionSetOpFactory,
+      Provider<CurrentUser> currentUserProvider) {
     this.updateFactory = updateFactory;
     this.approvalsUtil = approvalsUtil;
     this.psUtil = psUtil;
@@ -104,6 +111,8 @@
     this.removeReviewerControl = removeReviewerControl;
     this.projectCache = projectCache;
     this.messageIdGenerator = messageIdGenerator;
+    this.attentionSetOpfactory = attentionSetOpFactory;
+    this.currentUserProvider = currentUserProvider;
   }
 
   @Override
@@ -140,6 +149,14 @@
               r.getReviewerUser().state(),
               rsrc.getLabel(),
               input));
+      if (!r.getReviewerUser().getAccountId().equals(currentUserProvider.get().getAccountId())) {
+        bu.addOp(
+            change.getId(),
+            attentionSetOpfactory.create(
+                r.getReviewerUser().getAccountId(),
+                /* reason= */ "Their vote was deleted",
+                /* notify= */ false));
+      }
       bu.execute();
     }
 
diff --git a/java/com/google/gerrit/testing/TestLoggingActivator.java b/java/com/google/gerrit/testing/TestLoggingActivator.java
index 6b5d8fd..b3ad862 100644
--- a/java/com/google/gerrit/testing/TestLoggingActivator.java
+++ b/java/com/google/gerrit/testing/TestLoggingActivator.java
@@ -48,11 +48,6 @@
           .put("org.openid4java.server.RealmVerifier", Level.ERROR)
           .put("org.openid4java.message.AuthSuccess", Level.ERROR)
 
-          // Silence non-critical messages from c3p0 (if used).
-          .put("com.mchange.v2.c3p0", Level.WARN)
-          .put("com.mchange.v2.resourcepool", Level.WARN)
-          .put("com.mchange.v2.sql", Level.WARN)
-
           // Silence non-critical messages from apache.http.
           .put("org.apache.http", Level.WARN)
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index d2a48be1..6555b50 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.entities.AttentionSetUpdate;
 import com.google.gerrit.entities.AttentionSetUpdate.Operation;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelId;
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AttentionSetInput;
@@ -1758,6 +1759,40 @@
         .isEqualTo(Operation.REMOVE);
   }
 
+  @Test
+  public void deleteSelfVotesDoesNotAddToAttentionSet() throws Exception {
+    PushOneCommit.Result r = createChange();
+    approve(r.getChangeId());
+    gApi.changes()
+        .id(r.getChangeId())
+        .current()
+        .reviewer(admin.id().toString())
+        .deleteVote(LabelId.CODE_REVIEW);
+
+    assertThat(getAttentionSetUpdates(r.getChange().getId())).isEmpty();
+  }
+
+  @Test
+  public void deleteVotesOfOthersAddThemToAttentionSet() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    requestScopeOperations.setApiUser(user.id());
+    recommend(r.getChangeId());
+
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.changes()
+        .id(r.getChangeId())
+        .current()
+        .reviewer(user.id().toString())
+        .deleteVote(LabelId.CODE_REVIEW);
+
+    AttentionSetUpdate attentionSet =
+        Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user));
+    assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id());
+    assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD);
+    assertThat(attentionSet).hasReasonThat().isEqualTo("Their vote was deleted");
+  }
+
   private List<AttentionSetUpdate> getAttentionSetUpdatesForUser(
       PushOneCommit.Result r, TestAccount account) {
     return getAttentionSetUpdates(r.getChange().getId()).stream()
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index b97d9f2..cbeb59d 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -705,6 +706,23 @@
   }
 
   @Test
+  public void byProjectWithHidden() throws Exception {
+    TestRepository<Repo> hiddenProject = createProject("hiddenProject");
+    insert(hiddenProject, newChange(hiddenProject));
+    projectOperations
+        .project(Project.nameKey("hiddenProject"))
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+
+    TestRepository<Repo> visibleProject = createProject("visibleProject");
+    Change visibleChange = insert(visibleProject, newChange(visibleProject));
+    assertQuery("project:visibleProject", visibleChange);
+    assertQuery("project:hiddenProject");
+    assertQuery("project:visibleProject OR project:hiddenProject", visibleChange);
+  }
+
+  @Test
   public void byParentOf() throws Exception {
     TestRepository<Repo> repo1 = createProject("repo1");
     RevCommit commit1 = repo1.parseBody(repo1.commit().message("message").create());
diff --git a/package.json b/package.json
index 7f4ac44..3c40690 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
     "prettier": "2.0.5",
     "rollup": "^2.3.4",
     "terser": "^4.8.0",
-    "typescript": "4.0.5"
+    "typescript": "4.1.4"
   },
   "scripts": {
     "clean": "git clean -fdx && bazel clean --expunge",
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 3cd520b..30c774f 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 3cd520b1521ff7c558d0cd95274628a3a20de30a
+Subproject commit 30c774f30c1709f71efc250a195dd6fb50c7503b
diff --git a/plugins/delete-project b/plugins/delete-project
index bfe159d..549de03 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit bfe159d3007db0f07e967473b53f679ba8f432df
+Subproject commit 549de033d60b13aaeef45ce5c4bf42be39506268
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
index 7b43dfc..80c58ca 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
@@ -297,9 +297,18 @@
     }
     // Restore inheritFrom.
     if (this._inheritsFrom) {
-      this._inheritsFrom = {...this.originalInheritsFrom};
-      this._inheritFromFilter =
-        'name' in this._inheritsFrom ? this._inheritsFrom.name : undefined;
+      // Can't assign this._inheritsFrom = {...this.originalInheritsFrom}
+      // directly, because this._inheritsFrom is declared as
+      // '...|null|undefined` and typescript reports error when trying
+      // to access .name property (because 'name' in null and 'name' in undefined
+      // lead to runtime error)
+      // After migrating to Typescript v4.2 the code below can be rewritten as
+      // const copy = {...this.originalInheritsFrom};
+      const copy: ProjectInfo | {} = this.originalInheritsFrom
+        ? {...this.originalInheritsFrom}
+        : {};
+      this._inheritsFrom = copy;
+      this._inheritFromFilter = 'name' in copy ? copy.name : undefined;
     }
     if (!this._local) {
       return;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 42876d9..4ae3c68 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -171,7 +171,7 @@
   method: HttpMethod.POST,
 };
 
-function isQuckApproveAction(
+function isQuickApproveAction(
   action: UIActionInfo
 ): action is QuickApproveUIActionInfo {
   return (action as QuickApproveUIActionInfo).key === QUICK_APPROVE_ACTION.key;
@@ -943,9 +943,10 @@
       const status = this._getLabelStatus(labelInfo);
       if (status === LabelStatus.NEED) {
         if (result) {
-          // More than one label is missing, so it's unclear which to quick
-          // approve, return null;
-          return null;
+          // More than one label is missing, so check if Code Review can be
+          // given
+          result = null;
+          break;
         }
         result = label;
       } else if (
@@ -999,7 +1000,7 @@
       throw new Error('_topLevelSecondaryActions must be set');
     }
     this._topLevelSecondaryActions = this._topLevelSecondaryActions.filter(
-      sa => !isQuckApproveAction(sa)
+      sa => !isQuickApproveAction(sa)
     );
     this._hideQuickApproveAction = true;
   }
@@ -1271,7 +1272,7 @@
         this._showActionDialog(this.$.confirmAbandonDialog);
         break;
       case QUICK_APPROVE_ACTION.key: {
-        const action = this._allActionValues.find(isQuckApproveAction);
+        const action = this._allActionValues.find(isQuickApproveAction);
         if (!action) {
           return;
         }
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
index 8324fdd..c193e60 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
@@ -1629,23 +1629,52 @@
         assert.deepEqual(payload.labels, {foo: 1});
       });
 
-      test('not added when multiple labels are required', () => {
+      test('not added when multiple labels are required without code review',
+          () => {
+            element.change = {
+              current_revision: 'abc1234',
+              labels: {
+                foo: {values: {}},
+                bar: {values: {}},
+              },
+              permitted_labels: {
+                foo: [' 0', '+1'],
+                bar: [' 0', '+1', '+2'],
+              },
+            };
+            flush();
+            const approveButton =
+            element.shadowRoot
+                .querySelector('gr-button[data-action-key=\'review\']');
+            assert.isNull(approveButton);
+          });
+
+      test('code review shown with multiple missing approval', () => {
         element.change = {
           current_revision: 'abc1234',
           labels: {
-            foo: {values: {}},
-            bar: {values: {}},
+            'foo': {values: {}},
+            'bar': {values: {}},
+            'Code-Review': {
+              approved: {},
+              values: {
+                ' 0': '',
+                '+1': '',
+                '+2': '',
+              },
+            },
           },
           permitted_labels: {
-            foo: [' 0', '+1'],
-            bar: [' 0', '+1', '+2'],
+            'foo': [' 0', '+1'],
+            'bar': [' 0', '+1', '+2'],
+            'Code-Review': [' 0', '+1', '+2'],
           },
         };
         flush();
         const approveButton =
             element.shadowRoot
                 .querySelector('gr-button[data-action-key=\'review\']');
-        assert.isNull(approveButton);
+        assert.isOk(approveButton);
       });
 
       test('button label for missing approval', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index aae8bb6..94324f3d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -470,7 +470,6 @@
                     html`<gr-avatar
                       .account="${account}"
                       image-size="32"
-                      aria-label="Account avatar"
                     ></gr-avatar>`
                 )}
                 ${countUnresolvedComments} unresolved</gr-summary-chip
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 155c77b..eadcc30 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -155,6 +155,7 @@
   ThreadListModifiedEvent,
   TabState,
   EventType,
+  CloseFixPreviewEvent,
 } from '../../../types/events';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {GrMessagesList} from '../gr-messages-list/gr-messages-list';
@@ -676,7 +677,7 @@
       this._handleCommitMessageCancel()
     );
     this.addEventListener('open-fix-preview', e => this._onOpenFixPreview(e));
-    this.addEventListener('close-fix-preview', () => this._onCloseFixPreview());
+    this.addEventListener('close-fix-preview', e => this._onCloseFixPreview(e));
     window.addEventListener('scroll', this.handleScroll);
     document.addEventListener('visibilitychange', this.handleVisibilityChange);
 
@@ -742,8 +743,8 @@
     this.$.applyFixDialog.open(e);
   }
 
-  _onCloseFixPreview() {
-    this._reload();
+  _onCloseFixPreview(e: CloseFixPreviewEvent) {
+    if (e.detail.fixApplied) this._reload();
   }
 
   _handleToggleDiffMode(e: CustomKeyboardEvent) {
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
index ec1235e..c75b8c4 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
@@ -718,16 +718,15 @@
 
     let button: TemplateResult | typeof nothing = nothing;
     if (collapsible) {
-      if (this.showAll) {
-        button = html`<gr-button link="" @click="${this.toggle}"
-          >Show less<iron-icon icon="gr-icons:expand-less"></iron-icon
-        ></gr-button>`;
-      } else {
-        button = html`<gr-button link="" @click="${this.toggle}"
-          >Show all (${this.length})
-          <iron-icon icon="gr-icons:expand-more"></iron-icon
-        ></gr-button>`;
+      let buttonText = 'Show less';
+      let buttonIcon = 'expand-less';
+      if (!this.showAll) {
+        buttonText = `Show all (${this.length})`;
+        buttonIcon = 'expand-more';
       }
+      button = html`<gr-button link="" @click="${this.toggle}"
+        >${buttonText}<iron-icon icon="gr-icons:${buttonIcon}"></iron-icon
+      ></gr-button>`;
     }
 
     return html`<div class="container">${title}${button}</div>
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
index 382e17c..f6bd04e 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
@@ -128,7 +128,7 @@
             on-click="_handleAllComments"
             checked="[[_showAllComments(_draftsOnly, unresolvedOnly)]]"
           />
-          <label for="all">
+          <label for="allRadio">
             All ([[_countAllThreads(threads)]])
           </label>
       </template>
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 716d142..0bf9f1c 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
@@ -37,12 +37,14 @@
 import {isRobot} from '../../../utils/comment-util';
 import {OpenFixPreviewEvent} from '../../../types/events';
 import {appContext} from '../../../services/app-context';
-import {fireEvent} from '../../../utils/event-util';
+import {fireCloseFixPreview, fireEvent} from '../../../utils/event-util';
 import {ParsedChangeInfo} from '../../../types/types';
+import {GrButton} from '../../shared/gr-button/gr-button';
 
 export interface GrApplyFixDialog {
   $: {
     applyFixOverlay: GrOverlay;
+    nextFix: GrButton;
   };
 }
 
@@ -168,7 +170,7 @@
         }
       })
       .catch(err => {
-        this._close();
+        this._close(false);
         throw err;
       });
   }
@@ -186,7 +188,7 @@
     if (e) {
       e.stopPropagation();
     }
-    this._close();
+    this._close(false);
   }
 
   addOneTo(_selectedFixIdx: number) {
@@ -225,12 +227,12 @@
     return _selectedFixIdx === fixSuggestions.length - 1;
   }
 
-  _close() {
+  _close(fixApplied: boolean) {
     this._currentFix = undefined;
     this._currentPreviews = [];
     this._isApplyFixLoading = false;
 
-    fireEvent(this, 'close-fix-preview');
+    fireCloseFixPreview(this, fixApplied);
     this.$.applyFixOverlay.close();
   }
 
@@ -282,7 +284,7 @@
             EditPatchSetNum,
             patchNum as BasePatchSetNum
           );
-          this._close();
+          this._close(true);
         }
         this._isApplyFixLoading = false;
       });
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.js b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.js
deleted file mode 100644
index d78961c..0000000
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.js
+++ /dev/null
@@ -1,272 +0,0 @@
-/**
- * @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.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-apply-fix-dialog.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-apply-fix-dialog');
-
-suite('gr-apply-fix-dialog tests', () => {
-  let element;
-
-  const ROBOT_COMMENT_WITH_TWO_FIXES = {
-    robot_id: 'robot_1',
-    fix_suggestions: [{fix_id: 'fix_1'}, {fix_id: 'fix_2'}],
-  };
-
-  const ROBOT_COMMENT_WITH_ONE_FIX = {
-    robot_id: 'robot_1',
-    fix_suggestions: [{fix_id: 'fix_1'}],
-  };
-
-  setup(() => {
-    element = basicFixture.instantiate();
-    element.changeNum = '1';
-    element._patchNum = 2;
-    element.change = {
-      _number: '1',
-      project: 'project',
-      revisions: {
-        abcd: {_number: 1},
-        efgh: {_number: 2},
-      },
-      current_revision: 'efgh',
-    };
-    element.prefs = {
-      font_size: 12,
-      line_length: 100,
-      tab_size: 4,
-    };
-  });
-
-  suite('dialog open', () => {
-    setup(() => {
-      stubRestApi('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'],
-                },
-              ],
-            },
-          }));
-      sinon.stub(element.$.applyFixOverlay, 'open')
-          .returns(Promise.resolve());
-    });
-
-    test('dialog opens fetch and sets previews', done => {
-      element.open({detail: {patchNum: 2,
-        comment: ROBOT_COMMENT_WITH_TWO_FIXES}})
-          .then(() => {
-            assert.equal(element._currentFix.fix_id, 'fix_1');
-            assert.equal(element._currentPreviews.length, 2);
-            assert.equal(element._robotId, 'robot_1');
-            const button = element.shadowRoot.querySelector(
-                '#applyFixDialog').shadowRoot.querySelector('#confirm');
-            assert.isFalse(button.hasAttribute('disabled'));
-            assert.equal(button.getAttribute('title'), '');
-            done();
-          });
-    });
-
-    test('tooltip is hidden if apply fix is loading', done => {
-      element.open({detail: {patchNum: 2,
-        comment: ROBOT_COMMENT_WITH_TWO_FIXES}})
-          .then(() => {
-            element._isApplyFixLoading = true;
-            const button = element.shadowRoot.querySelector(
-                '#applyFixDialog').shadowRoot.querySelector('#confirm');
-            assert.isTrue(button.hasAttribute('disabled'));
-            assert.equal(button.getAttribute('title'), '');
-            done();
-          });
-    });
-
-    test('apply fix button is disabled on older patchset', done => {
-      element.change = {
-        _number: '1',
-        project: 'project',
-        revisions: {
-          abcd: {_number: 1},
-          efgh: {_number: 2},
-        },
-        current_revision: 'abcd',
-      };
-      element.open({detail: {patchNum: 2,
-        comment: ROBOT_COMMENT_WITH_ONE_FIX}})
-          .then(() => {
-            flush(() => {
-              const button = element.shadowRoot.querySelector(
-                  '#applyFixDialog').shadowRoot.querySelector('#confirm');
-              assert.isTrue(button.hasAttribute('disabled'));
-              assert.equal(button.getAttribute('title'),
-                  'Fix can only be applied to the latest patchset');
-              done();
-            });
-          });
-    });
-  });
-
-  test('next button state updated when suggestions changed', done => {
-    stubRestApi('getRobotCommentFixPreview').returns(Promise.resolve({}));
-    sinon.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
-
-    element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_ONE_FIX}})
-        .then(() => assert.isTrue(element.$.nextFix.disabled))
-        .then(() =>
-          element.open({detail: {patchNum: 2,
-            comment: ROBOT_COMMENT_WITH_TWO_FIXES}}))
-        .then(() => {
-          assert.isFalse(element.$.nextFix.disabled);
-          done();
-        });
-  });
-
-  test('preview endpoint throws error should reset dialog', async () => {
-    stubRestApi('getRobotCommentFixPreview').returns(
-        Promise.reject(new Error('backend error')));
-    element.open({detail: {patchNum: 2,
-      comment: ROBOT_COMMENT_WITH_TWO_FIXES}});
-    await flush();
-    assert.equal(element._currentFix, undefined);
-  });
-
-  test('apply fix button should call apply ' +
-  'and navigate to change view', () => {
-    const stub = stubRestApi('applyFixSuggestion').returns(
-        Promise.resolve({ok: true}));
-    sinon.stub(GerritNav, 'navigateToChange');
-    element._currentFix = {fix_id: '123'};
-
-    return element._handleApplyFix().then(() => {
-      assert.isTrue(stub.calledWithExactly('1', 2, '123'));
-      assert.isTrue(GerritNav.navigateToChange.calledWithExactly({
-        _number: '1',
-        project: 'project',
-        revisions: {
-          abcd: {_number: 1},
-          efgh: {_number: 2},
-        },
-        current_revision: 'efgh',
-      }, 'edit', 2));
-
-      // reset gr-apply-fix-dialog and close
-      assert.equal(element._currentFix, undefined);
-      assert.equal(element._currentPreviews.length, 0);
-    });
-  });
-
-  test('should not navigate to change view if incorect reponse', done => {
-    const stub = stubRestApi('applyFixSuggestion').returns(Promise.resolve({}));
-    sinon.stub(GerritNav, 'navigateToChange');
-    element._currentFix = {fix_id: '123'};
-
-    element._handleApplyFix().then(() => {
-      assert.isTrue(stub.calledWithExactly('1', 2, '123'));
-      assert.isTrue(GerritNav.navigateToChange.notCalled);
-
-      assert.equal(element._isApplyFixLoading, false);
-      done();
-    });
-  });
-
-  test('select fix forward and back of multiple suggested fixes', done => {
-    stubRestApi('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'],
-              },
-            ],
-          },
-        }));
-    sinon.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
-
-    element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_TWO_FIXES}})
-        .then(() => {
-          element._onNextFixClick();
-          assert.equal(element._currentFix.fix_id, 'fix_2');
-          element._onPrevFixClick();
-          assert.equal(element._currentFix.fix_id, 'fix_1');
-          done();
-        });
-  });
-
-  test('server-error should throw for failed apply call', async () => {
-    stubRestApi('applyFixSuggestion').returns(
-        Promise.reject(new Error('backend error')));
-    sinon.stub(GerritNav, 'navigateToChange');
-    element._currentFix = {fix_id: '123'};
-    let expectedError;
-    await element._handleApplyFix().catch(e => {
-      expectedError = e;
-    });
-    assert.isOk(expectedError);
-    assert.isFalse(GerritNav.navigateToChange.called);
-  });
-});
-
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
new file mode 100644
index 0000000..d3d7615
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
@@ -0,0 +1,376 @@
+/**
+ * @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.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-apply-fix-dialog';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
+import {GrApplyFixDialog} from './gr-apply-fix-dialog';
+import {
+  BasePatchSetNum,
+  EditPatchSetNum,
+  PatchSetNum,
+  RobotId,
+  RobotRunId,
+  Timestamp,
+  UrlEncodedCommentId,
+} from '../../../types/common';
+import {
+  createFixSuggestionInfo,
+  createParsedChange,
+  createRevisions,
+  getCurrentRevision,
+} from '../../../test/test-data-generators';
+import {createDefaultDiffPrefs} from '../../../constants/constants';
+import {DiffInfo} from '../../../types/diff';
+import {UIRobot} from '../../../utils/comment-util';
+import {
+  CloseFixPreviewEventDetail,
+  EventType,
+  OpenFixPreviewEventDetail,
+} from '../../../types/events';
+import {GrButton} from '../../shared/gr-button/gr-button';
+
+const basicFixture = fixtureFromElement('gr-apply-fix-dialog');
+
+suite('gr-apply-fix-dialog tests', () => {
+  let element: GrApplyFixDialog;
+
+  const ROBOT_COMMENT_WITH_TWO_FIXES: UIRobot = {
+    id: '1' as UrlEncodedCommentId,
+    updated: '2018-02-08 18:49:18.000000000' as Timestamp,
+    robot_id: 'robot_1' as RobotId,
+    robot_run_id: 'run_1' as RobotRunId,
+    properties: {},
+    fix_suggestions: [
+      createFixSuggestionInfo('fix_1'),
+      createFixSuggestionInfo('fix_2'),
+    ],
+  };
+
+  const ROBOT_COMMENT_WITH_ONE_FIX: UIRobot = {
+    id: '2' as UrlEncodedCommentId,
+    updated: '2018-02-08 18:49:18.000000000' as Timestamp,
+    robot_id: 'robot_1' as RobotId,
+    robot_run_id: 'run_1' as RobotRunId,
+    properties: {},
+    fix_suggestions: [createFixSuggestionInfo('fix_1')],
+  };
+
+  function getConfirmButton(): GrButton {
+    return queryAndAssert(
+      queryAndAssert(element, '#applyFixDialog'),
+      '#confirm'
+    );
+  }
+
+  setup(() => {
+    element = basicFixture.instantiate();
+    const change = {
+      ...createParsedChange(),
+      revisions: createRevisions(2),
+      current_revision: getCurrentRevision(1),
+    };
+    element.changeNum = change._number;
+    element._patchNum = change.revisions[change.current_revision]._number;
+    element.change = change;
+    element.prefs = {
+      ...createDefaultDiffPrefs(),
+      font_size: 12,
+      line_length: 100,
+      tab_size: 4,
+    };
+  });
+
+  suite('dialog open', () => {
+    setup(() => {
+      const diffInfo1: DiffInfo = {
+        meta_a: {
+          name: 'f1',
+          content_type: 'text',
+          lines: 10,
+        },
+        meta_b: {
+          name: 'f1',
+          content_type: 'text',
+          lines: 12,
+        },
+        content: [
+          {
+            ab: ['loqlwkqll'],
+          },
+          {
+            b: ['qwqqsqw'],
+          },
+          {
+            ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'],
+          },
+        ],
+        change_type: 'MODIFIED',
+        intraline_status: 'OK',
+      };
+
+      const diffInfo2: DiffInfo = {
+        meta_a: {
+          name: 'f2',
+          content_type: 'text',
+          lines: 10,
+        },
+        meta_b: {
+          name: 'f2',
+          content_type: 'text',
+          lines: 12,
+        },
+        content: [
+          {
+            ab: ['eqweqweqwex'],
+          },
+          {
+            b: ['zassdasd'],
+          },
+          {
+            ab: ['zassdasd', 'dasdasda', 'asdasdad'],
+          },
+        ],
+        change_type: 'MODIFIED',
+        intraline_status: 'OK',
+      };
+
+      stubRestApi('getRobotCommentFixPreview').returns(
+        Promise.resolve({
+          f1: diffInfo1,
+          f2: diffInfo2,
+        })
+      );
+      sinon.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
+    });
+
+    test('dialog opens fetch and sets previews', async () => {
+      await element.open(
+        new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
+          detail: {
+            patchNum: 2 as PatchSetNum,
+            comment: ROBOT_COMMENT_WITH_TWO_FIXES,
+          },
+        })
+      );
+      assert.equal(element._currentFix!.fix_id, 'fix_1');
+      assert.equal(element._currentPreviews.length, 2);
+      assert.equal(element._robotId, 'robot_1' as RobotId);
+      const button = getConfirmButton();
+      assert.isFalse(button.hasAttribute('disabled'));
+      assert.equal(button.getAttribute('title'), '');
+    });
+
+    test('tooltip is hidden if apply fix is loading', async () => {
+      await element.open(
+        new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
+          detail: {
+            patchNum: 2 as PatchSetNum,
+            comment: ROBOT_COMMENT_WITH_TWO_FIXES,
+          },
+        })
+      );
+      element._isApplyFixLoading = true;
+      const button = getConfirmButton();
+      assert.isTrue(button.hasAttribute('disabled'));
+      assert.equal(button.getAttribute('title'), '');
+    });
+
+    test('apply fix button is disabled on older patchset', async () => {
+      element.change = element.change = {
+        ...createParsedChange(),
+        revisions: createRevisions(2),
+        current_revision: getCurrentRevision(0),
+      };
+      await element.open(
+        new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
+          detail: {
+            patchNum: 2 as PatchSetNum,
+            comment: ROBOT_COMMENT_WITH_ONE_FIX,
+          },
+        })
+      );
+      await flush();
+      const button = getConfirmButton();
+      assert.isTrue(button.hasAttribute('disabled'));
+      assert.equal(
+        button.getAttribute('title'),
+        'Fix can only be applied to the latest patchset'
+      );
+    });
+  });
+
+  test('next button state updated when suggestions changed', async () => {
+    stubRestApi('getRobotCommentFixPreview').returns(Promise.resolve({}));
+    sinon.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
+
+    await element.open(
+      new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
+        detail: {
+          patchNum: 2 as PatchSetNum,
+          comment: ROBOT_COMMENT_WITH_ONE_FIX,
+        },
+      })
+    );
+    assert.isTrue(element.$.nextFix.disabled);
+    await element.open(
+      new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
+        detail: {
+          patchNum: 2 as PatchSetNum,
+          comment: ROBOT_COMMENT_WITH_TWO_FIXES,
+        },
+      })
+    );
+    assert.isFalse(element.$.nextFix.disabled);
+  });
+
+  test('preview endpoint throws error should reset dialog', async () => {
+    stubRestApi('getRobotCommentFixPreview').returns(
+      Promise.reject(new Error('backend error'))
+    );
+    element.open(
+      new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
+        detail: {
+          patchNum: 2 as PatchSetNum,
+          comment: ROBOT_COMMENT_WITH_TWO_FIXES,
+        },
+      })
+    );
+    await flush();
+    assert.equal(element._currentFix, undefined);
+  });
+
+  test('apply fix button should call apply, navigate to change view and fire close', async () => {
+    const applyFixSuggestionStub = stubRestApi('applyFixSuggestion').returns(
+      Promise.resolve(new Response(null, {status: 200}))
+    );
+    const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
+    element._currentFix = createFixSuggestionInfo('123');
+
+    const closeFixPreviewEventSpy = sinon.spy();
+    // Element is recreated after each test, removeEventListener isn't required
+    element.addEventListener(
+      EventType.CLOSE_FIX_PREVIEW,
+      closeFixPreviewEventSpy
+    );
+    await element._handleApplyFix(new CustomEvent('confirm'));
+
+    sinon.assert.calledOnceWithExactly(
+      applyFixSuggestionStub,
+      element.change!._number,
+      2 as PatchSetNum,
+      '123'
+    );
+    sinon.assert.calledWithExactly(
+      navigateToChangeStub,
+      element.change!,
+      EditPatchSetNum,
+      element.change!.revisions[2]._number as BasePatchSetNum
+    );
+
+    sinon.assert.calledOnceWithExactly(
+      closeFixPreviewEventSpy,
+      new CustomEvent<CloseFixPreviewEventDetail>(EventType.CLOSE_FIX_PREVIEW, {
+        detail: {
+          fixApplied: true,
+        },
+      })
+    );
+
+    // reset gr-apply-fix-dialog and close
+    assert.equal(element._currentFix, undefined);
+    assert.equal(element._currentPreviews.length, 0);
+  });
+
+  test('should not navigate to change view if incorect reponse', async () => {
+    const applyFixSuggestionStub = stubRestApi('applyFixSuggestion').returns(
+      Promise.resolve(new Response(null, {status: 500}))
+    );
+    const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
+    element._currentFix = createFixSuggestionInfo('fix_123');
+
+    await element._handleApplyFix(new CustomEvent('confirm'));
+    sinon.assert.calledWithExactly(
+      applyFixSuggestionStub,
+      element.change!._number,
+      2 as PatchSetNum,
+      'fix_123'
+    );
+    assert.isTrue(navigateToChangeStub.notCalled);
+
+    assert.equal(element._isApplyFixLoading, false);
+  });
+
+  test('select fix forward and back of multiple suggested fixes', async () => {
+    sinon.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
+
+    await element.open(
+      new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
+        detail: {
+          patchNum: 2 as PatchSetNum,
+          comment: ROBOT_COMMENT_WITH_TWO_FIXES,
+        },
+      })
+    );
+    element._onNextFixClick(new CustomEvent('click'));
+    assert.equal(element._currentFix!.fix_id, 'fix_2');
+    element._onPrevFixClick(new CustomEvent('click'));
+    assert.equal(element._currentFix!.fix_id, 'fix_1');
+  });
+
+  test('server-error should throw for failed apply call', async () => {
+    stubRestApi('applyFixSuggestion').returns(
+      Promise.reject(new Error('backend error'))
+    );
+    const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
+    element._currentFix = createFixSuggestionInfo('fix_123');
+
+    const closeFixPreviewEventSpy = sinon.spy();
+    // Element is recreated after each test, removeEventListener isn't required
+    element.addEventListener(
+      EventType.CLOSE_FIX_PREVIEW,
+      closeFixPreviewEventSpy
+    );
+
+    let expectedError;
+    await element._handleApplyFix(new CustomEvent('click')).catch(e => {
+      expectedError = e;
+    });
+    assert.isOk(expectedError);
+    assert.isFalse(navigateToChangeStub.called);
+    sinon.assert.notCalled(closeFixPreviewEventSpy);
+  });
+
+  test('onCancel fires close with correct parameters', () => {
+    const closeFixPreviewEventSpy = sinon.spy();
+    // Element is recreated after each test, removeEventListener isn't required
+    element.addEventListener(
+      EventType.CLOSE_FIX_PREVIEW,
+      closeFixPreviewEventSpy
+    );
+    element.onCancel(new CustomEvent('cancel'));
+    sinon.assert.calledOnceWithExactly(
+      closeFixPreviewEventSpy,
+      new CustomEvent<CloseFixPreviewEventDetail>(EventType.CLOSE_FIX_PREVIEW, {
+        detail: {
+          fixApplied: false,
+        },
+      })
+    );
+  });
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
index 24a7d78..874f572 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
@@ -72,6 +72,7 @@
 declare global {
   interface HTMLElementEventMap {
     'diff-context-expanded': CustomEvent<DiffContextExpandedEventDetail>;
+    'content-load-needed': CustomEvent<ContentLoadNeededEventDetail>;
   }
 }
 
@@ -511,14 +512,14 @@
             end_line: lastRange.right.end_line,
           },
         };
-        fire<ContentLoadNeededEventDetail>(button, 'content-load-needed', {
+        fire(button, 'content-load-needed', {
           lineRange,
         });
       });
     } else {
       button.addEventListener('click', e => {
         e.stopPropagation();
-        fire<DiffContextExpandedEventDetail>(button, 'diff-context-expanded', {
+        fire(button, 'diff-context-expanded', {
           groups,
           section,
           numLines,
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
index 85931b4..746d101 100644
--- a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
@@ -837,8 +837,9 @@
           // mark-reviewed and then press ] to go to the next file'.
           (tagName === 'INPUT' && type !== 'checkbox') ||
           tagName === 'TEXTAREA' ||
-          // Suppress shortcuts if the key is 'enter' and target is an anchor.
-          (e.keyCode === 13 && tagName === 'A')
+          // Suppress shortcuts if the key is 'enter'
+          // and target is an anchor or button.
+          (e.keyCode === 13 && (tagName === 'A' || tagName === 'BUTTON'))
         ) {
           return true;
         }
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index f877771..01cee4c 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -174,7 +174,7 @@
       --header-text-color: var(--primary-text-color);
 
       /* diff colors */
-      --dark-add-highlight-color: #133820;
+      --dark-add-highlight-color: var(--green-tonal); 
       --dark-rebased-add-highlight-color: rgba(11, 255, 155, 0.15);
       --dark-rebased-remove-highlight-color: rgba(255, 139, 6, 0.15);
       --dark-remove-highlight-color: #62110f;
@@ -187,7 +187,7 @@
       --diff-selection-background-color: #3a71d8;
       --diff-tab-indicator-color: var(--deemphasized-text-color);
       --diff-trailing-whitespace-indicator: #ff9ad2;
-      --light-add-highlight-color: #0f401f;
+      --light-add-highlight-color: #182b1f;
       --light-rebased-add-highlight-color: #487165;
       --diff-moved-in-background: #1d4042;
       --diff-moved-out-background: #230e34;
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 3bfe743..778e58d 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -65,6 +65,8 @@
   RelatedChangeAndCommitInfo,
   SubmittedTogetherInfo,
   RelatedChangesInfo,
+  FixSuggestionInfo,
+  FixId,
 } from '../types/common';
 import {
   AccountsVisibility,
@@ -614,3 +616,11 @@
     non_visible_changes: 0,
   };
 }
+
+export function createFixSuggestionInfo(fixId = 'fix_1'): FixSuggestionInfo {
+  return {
+    fix_id: fixId as FixId,
+    description: `Fix ${fixId}`,
+    replacements: [],
+  };
+}
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index 11155c1..7bde788 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -32,6 +32,7 @@
   MOVED_LINK_CLICKED = 'moved-link-clicked',
   NETWORK_ERROR = 'network-error',
   OPEN_FIX_PREVIEW = 'open-fix-preview',
+  CLOSE_FIX_PREVIEW = 'close-fix-preview',
   PAGE_ERROR = 'page-error',
   RELOAD = 'reload',
   REPLY = 'reply',
@@ -54,6 +55,7 @@
     'iron-announce': IronAnnounceEvent;
     'moved-link-clicked': MovedLinkClickedEvent;
     'open-fix-preview': OpenFixPreviewEvent;
+    'close-fix-preview': CloseFixPreviewEvent;
     /* prettier-ignore */
     'reload': ReloadEvent;
     /* prettier-ignore */
@@ -133,6 +135,11 @@
 }
 export type OpenFixPreviewEvent = CustomEvent<OpenFixPreviewEventDetail>;
 
+export interface CloseFixPreviewEventDetail {
+  fixApplied: boolean;
+}
+export type CloseFixPreviewEvent = CustomEvent<CloseFixPreviewEventDetail>;
+
 export interface PageErrorEventDetail {
   response?: Response;
 }
diff --git a/polygerrit-ui/app/utils/change-metadata-util.ts b/polygerrit-ui/app/utils/change-metadata-util.ts
index c3eb846..d39b10b 100644
--- a/polygerrit-ui/app/utils/change-metadata-util.ts
+++ b/polygerrit-ui/app/utils/change-metadata-util.ts
@@ -41,11 +41,11 @@
     Metadata.REVIEWERS,
     Metadata.REPO_BRANCH,
     Metadata.SUBMITTED,
+    Metadata.TOPIC,
   ],
   SHOW_IF_SET: [
     Metadata.CC,
     Metadata.HASHTAGS,
-    Metadata.TOPIC,
     Metadata.UPLOADER,
     Metadata.AUTHOR,
     Metadata.COMMITTER,
diff --git a/polygerrit-ui/app/utils/date-util.ts b/polygerrit-ui/app/utils/date-util.ts
index 3af8c59..a780af5 100644
--- a/polygerrit-ui/app/utils/date-util.ts
+++ b/polygerrit-ui/app/utils/date-util.ts
@@ -90,15 +90,6 @@
   const diff = now.valueOf() - date.valueOf();
   return diff < 180 * Duration.DAY;
 }
-interface Options {
-  month?: string;
-  year?: string;
-  day?: string;
-  hour?: string;
-  hour12?: boolean;
-  minute?: string;
-  second?: string;
-}
 
 // TODO(dmfilippov): TS-Fix review this type. All fields here must be optional,
 // but this require some changes in the code. During JS->TS migration
@@ -118,7 +109,7 @@
 }
 
 export function formatDate(date: Date, format: string) {
-  const options: Options = {};
+  const options: Intl.DateTimeFormatOptions = {};
   if (format.includes('MM')) {
     if (format.includes('MMM')) {
       options.month = 'short';
diff --git a/polygerrit-ui/app/utils/event-util.ts b/polygerrit-ui/app/utils/event-util.ts
index 080955f..da075f1 100644
--- a/polygerrit-ui/app/utils/event-util.ts
+++ b/polygerrit-ui/app/utils/event-util.ts
@@ -20,15 +20,8 @@
 import {
   DialogChangeEventDetail,
   EventType,
-  IronAnnounceEventDetail,
-  NetworkErrorEventDetail,
-  PageErrorEventDetail,
-  ServerErrorEventDetail,
-  ShowAlertEventDetail,
   SwitchTabEventDetail,
   TabState,
-  ThreadListModifiedDetail,
-  TitleChangeEventDetail,
 } from '../types/events';
 
 export function fireEvent(target: EventTarget, type: string) {
@@ -40,6 +33,34 @@
   );
 }
 
+type HTMLElementEventDetailType<
+  K extends keyof HTMLElementEventMap
+> = HTMLElementEventMap[K] extends CustomEvent<infer DT>
+  ? unknown extends DT
+    ? never
+    : DT
+  : never;
+
+type DocumentEventDetailType<
+  K extends keyof DocumentEventMap
+> = DocumentEventMap[K] extends CustomEvent<infer DT>
+  ? unknown extends DT
+    ? never
+    : DT
+  : never;
+
+export function fire<K extends keyof DocumentEventMap>(
+  target: Document,
+  type: K,
+  detail: DocumentEventDetailType<K>
+): void;
+
+export function fire<K extends keyof HTMLElementEventMap>(
+  target: EventTarget,
+  type: K,
+  detail: HTMLElementEventDetailType<K>
+): void;
+
 export function fire<T>(target: EventTarget, type: string, detail: T) {
   target.dispatchEvent(
     new CustomEvent<T>(type, {
@@ -51,27 +72,27 @@
 }
 
 export function fireAlert(target: EventTarget, message: string) {
-  fire<ShowAlertEventDetail>(target, EventType.SHOW_ALERT, {message});
+  fire(target, EventType.SHOW_ALERT, {message});
 }
 
 export function firePageError(response?: Response | null) {
   if (response === null) response = undefined;
-  fire<PageErrorEventDetail>(document, EventType.PAGE_ERROR, {response});
+  fire(document, EventType.PAGE_ERROR, {response});
 }
 
 export function fireServerError(response: Response, request?: FetchRequest) {
-  fire<ServerErrorEventDetail>(document, EventType.SERVER_ERROR, {
+  fire(document, EventType.SERVER_ERROR, {
     response,
     request,
   });
 }
 
 export function fireNetworkError(error: Error) {
-  fire<NetworkErrorEventDetail>(document, EventType.NETWORK_ERROR, {error});
+  fire(document, EventType.NETWORK_ERROR, {error});
 }
 
 export function fireTitleChange(target: EventTarget, title: string) {
-  fire<TitleChangeEventDetail>(target, EventType.TITLE_CHANGE, {title});
+  fire(target, EventType.TITLE_CHANGE, {title});
 }
 
 // TODO(milutin) - remove once new gr-dialog will do it out of the box
@@ -80,11 +101,11 @@
   target: EventTarget,
   detail: DialogChangeEventDetail
 ) {
-  fire<DialogChangeEventDetail>(target, EventType.DIALOG_CHANGE, detail);
+  fire(target, EventType.DIALOG_CHANGE, detail);
 }
 
 export function fireIronAnnounce(target: EventTarget, text: string) {
-  fire<IronAnnounceEventDetail>(target, EventType.IRON_ANNOUNCE, {text});
+  fire(target, EventType.IRON_ANNOUNCE, {text});
 }
 
 export function fireThreadListModifiedEvent(
@@ -92,7 +113,7 @@
   rootId: UrlEncodedCommentId,
   path: string
 ) {
-  fire<ThreadListModifiedDetail>(target, EventType.THREAD_LIST_MODIFIED, {
+  fire(target, EventType.THREAD_LIST_MODIFIED, {
     rootId,
     path,
   });
@@ -105,7 +126,11 @@
   tabState?: TabState
 ) {
   const detail: SwitchTabEventDetail = {tab, scrollIntoView, tabState};
-  fire<SwitchTabEventDetail>(target, EventType.SHOW_PRIMARY_TAB, detail);
+  fire(target, EventType.SHOW_PRIMARY_TAB, detail);
+}
+
+export function fireCloseFixPreview(target: EventTarget, fixApplied: boolean) {
+  fire(target, EventType.CLOSE_FIX_PREVIEW, {fixApplied});
 }
 
 export function waitForEventOnce<K extends keyof HTMLElementEventMap>(
diff --git a/resources/log4j.properties b/resources/log4j.properties
index 39246b3..2898cfc 100644
--- a/resources/log4j.properties
+++ b/resources/log4j.properties
@@ -41,11 +41,5 @@
 log4j.logger.org.openid4java.server.RealmVerifier=ERROR
 log4j.logger.org.openid4java.message.AuthSuccess=ERROR
 
-# Silence non-critical messages from c3p0 (if used).
-#
-log4j.logger.com.mchange.v2.c3p0=WARN
-log4j.logger.com.mchange.v2.resourcepool=WARN
-log4j.logger.com.mchange.v2.sql=WARN
-
 # Silence non-critical messages from apache.http
 log4j.logger.org.apache.http=WARN
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 718f8d0..3705407 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-acceptance-framework</artifactId>
-  <version>3.4.0-SNAPSHOT</version>
+  <version>3.5.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Acceptance Test Framework</name>
   <description>Framework for Gerrit's acceptance tests</description>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index a415f24..ba35ca2 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-extension-api</artifactId>
-  <version>3.4.0-SNAPSHOT</version>
+  <version>3.5.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index 5e58fdd..b7954c7 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-api</artifactId>
-  <version>3.4.0-SNAPSHOT</version>
+  <version>3.5.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index 3b3a055..118cf39 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-war</artifactId>
-  <version>3.4.0-SNAPSHOT</version>
+  <version>3.5.0-SNAPSHOT</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
diff --git a/tools/node_tools/package.json b/tools/node_tools/package.json
index bfc5191..2f33078 100644
--- a/tools/node_tools/package.json
+++ b/tools/node_tools/package.json
@@ -16,7 +16,7 @@
     "rollup": "^2.3.4",
     "rollup-plugin-node-resolve": "^5.2.0",
     "rollup-plugin-terser": "^5.1.3",
-    "typescript": "4.0.5"
+    "typescript": "4.1.4"
   },
   "devDependencies": {},
   "license": "Apache-2.0",
diff --git a/tools/node_tools/yarn.lock b/tools/node_tools/yarn.lock
index 767f285..abb54b0 100644
--- a/tools/node_tools/yarn.lock
+++ b/tools/node_tools/yarn.lock
@@ -7893,10 +7893,10 @@
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
-typescript@4.0.5:
-  version "4.0.5"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389"
-  integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==
+typescript@4.1.4:
+  version "4.1.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.4.tgz#f058636e2f4f83f94ddaae07b20fd5e14598432f"
+  integrity sha512-+Uru0t8qIRgjuCpiSPpfGuhHecMllk5Zsazj5LZvVsEStEjmIRRBZe+jHjGQvsgS7M1wONy2PQXd67EMyV6acg==
 
 typical@^2.6.0, typical@^2.6.1:
   version "2.6.1"
diff --git a/version.bzl b/version.bzl
index 066d07e..f2e0d0c 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
 # Used by :api_install and :api_deploy targets
 # when talking to the destination repository.
 #
-GERRIT_VERSION = "3.4.0-SNAPSHOT"
+GERRIT_VERSION = "3.5.0-SNAPSHOT"
diff --git a/yarn.lock b/yarn.lock
index adb731c..d18d5fd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9680,10 +9680,10 @@
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
-typescript@4.0.5:
-  version "4.0.5"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389"
-  integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==
+typescript@4.1.4:
+  version "4.1.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.4.tgz#f058636e2f4f83f94ddaae07b20fd5e14598432f"
+  integrity sha512-+Uru0t8qIRgjuCpiSPpfGuhHecMllk5Zsazj5LZvVsEStEjmIRRBZe+jHjGQvsgS7M1wONy2PQXd67EMyV6acg==
 
 typical@^2.6.1:
   version "2.6.1"