Merge "Only force the comment re-creation workaround for Safari"
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 57191c5..8006c61 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -43,6 +43,7 @@
 import com.google.gerrit.exceptions.NotSignedInException;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.SchemaUtil;
@@ -521,22 +522,14 @@
 
   @Operator
   public Predicate<ChangeData> mergedBefore(String value) throws QueryParseException {
-    if (!args.index.getSchema().hasField(ChangeField.MERGED_ON)) {
-      throw new QueryParseException(
-          String.format(
-              "'%s' operator is not supported by change index version", OPERATOR_MERGED_BEFORE));
-    }
+    checkFieldAvailable(ChangeField.MERGED_ON, OPERATOR_MERGED_BEFORE);
     return new BeforePredicate(
         ChangeField.MERGED_ON, ChangeQueryBuilder.OPERATOR_MERGED_BEFORE, value);
   }
 
   @Operator
   public Predicate<ChangeData> mergedAfter(String value) throws QueryParseException {
-    if (!args.index.getSchema().hasField(ChangeField.MERGED_ON)) {
-      throw new QueryParseException(
-          String.format(
-              "'%s' operator is not supported by change index version", OPERATOR_MERGED_AFTER));
-    }
+    checkFieldAvailable(ChangeField.MERGED_ON, OPERATOR_MERGED_AFTER);
     return new AfterPredicate(
         ChangeField.MERGED_ON, ChangeQueryBuilder.OPERATOR_MERGED_AFTER, value);
   }
@@ -626,10 +619,7 @@
     }
 
     if ("attention".equalsIgnoreCase(value)) {
-      if (!args.index.getSchema().hasField(ChangeField.ATTENTION_SET_USERS)) {
-        throw new QueryParseException(
-            "'has:attention' operator is not supported by change index version");
-      }
+      checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "has:attention");
       return new IsAttentionPredicate();
     }
 
@@ -672,20 +662,13 @@
     }
 
     if ("uploader".equalsIgnoreCase(value)) {
-      if (!args.getSchema().hasField(ChangeField.UPLOADER)) {
-        throw new QueryParseException(
-            "'is:uploader' operator is not supported by change index version");
-      }
+      checkFieldAvailable(ChangeField.UPLOADER, "is:uploader");
       return ChangePredicates.uploader(self());
     }
 
     if ("reviewer".equalsIgnoreCase(value)) {
-      if (args.getSchema().hasField(ChangeField.WIP)) {
-        return Predicate.and(
-            Predicate.not(new BooleanPredicate(ChangeField.WIP)),
-            ReviewerPredicate.reviewer(self()));
-      }
-      return ReviewerPredicate.reviewer(self());
+      return Predicate.and(
+          Predicate.not(new BooleanPredicate(ChangeField.WIP)), ReviewerPredicate.reviewer(self()));
     }
 
     if ("cc".equalsIgnoreCase(value)) {
@@ -700,25 +683,16 @@
     }
 
     if ("merge".equalsIgnoreCase(value)) {
-      if (args.getSchema().hasField(ChangeField.MERGE)) {
-        return new BooleanPredicate(ChangeField.MERGE);
-      }
-      throw new QueryParseException("'is:merge' operator is not supported by change index version");
+      checkFieldAvailable(ChangeField.MERGE, "is:merge");
+      return new BooleanPredicate(ChangeField.MERGE);
     }
 
     if ("private".equalsIgnoreCase(value)) {
-      if (args.getSchema().hasField(ChangeField.PRIVATE)) {
-        return new BooleanPredicate(ChangeField.PRIVATE);
-      }
-      throw new QueryParseException(
-          "'is:private' operator is not supported by change index version");
+      return new BooleanPredicate(ChangeField.PRIVATE);
     }
 
     if ("attention".equalsIgnoreCase(value)) {
-      if (!args.index.getSchema().hasField(ChangeField.ATTENTION_SET_USERS)) {
-        throw new QueryParseException(
-            "'is:attention' operator is not supported by change index version");
-      }
+      checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "is:attention");
       return new IsAttentionPredicate();
     }
 
@@ -747,26 +721,17 @@
     }
 
     if ("started".equalsIgnoreCase(value)) {
-      if (args.getSchema().hasField(ChangeField.STARTED)) {
-        return new BooleanPredicate(ChangeField.STARTED);
-      }
-      throw new QueryParseException(
-          "'is:started' operator is not supported by change index version");
+      checkFieldAvailable(ChangeField.STARTED, "is:started");
+      return new BooleanPredicate(ChangeField.STARTED);
     }
 
     if ("wip".equalsIgnoreCase(value)) {
-      if (args.getSchema().hasField(ChangeField.WIP)) {
-        return new BooleanPredicate(ChangeField.WIP);
-      }
-      throw new QueryParseException("'is:wip' operator is not supported by change index version");
+      return new BooleanPredicate(ChangeField.WIP);
     }
 
     if ("cherrypick".equalsIgnoreCase(value)) {
-      if (args.getSchema().hasField(ChangeField.CHERRY_PICK)) {
-        return new BooleanPredicate(ChangeField.CHERRY_PICK);
-      }
-      throw new QueryParseException(
-          "'is:cherrypick' operator is not supported by change index version");
+      checkFieldAvailable(ChangeField.CHERRY_PICK, "is:cherrypick");
+      return new BooleanPredicate(ChangeField.CHERRY_PICK);
     }
 
     // for plugins the value will be operandName_pluginName
@@ -883,10 +848,7 @@
       return ChangePredicates.hashtag(hashtag);
     }
 
-    if (!args.index.getSchema().hasField(ChangeField.FUZZY_HASHTAG)) {
-      throw new QueryParseException(
-          "'inhashtag' operator is not supported by change index version");
-    }
+    checkFieldAvailable(ChangeField.FUZZY_HASHTAG, "inhashtag");
     return ChangePredicates.fuzzyHashtag(hashtag);
   }
 
@@ -942,10 +904,7 @@
 
   @Operator
   public Predicate<ChangeData> extension(String ext) throws QueryParseException {
-    if (args.getSchema().hasField(ChangeField.EXTENSION)) {
-      return new FileExtensionPredicate(ext);
-    }
-    throw new QueryParseException("'extension' operator is not supported by change index version");
+    return new FileExtensionPredicate(ext);
   }
 
   @Operator
@@ -955,19 +914,12 @@
 
   @Operator
   public Predicate<ChangeData> onlyextensions(String extList) throws QueryParseException {
-    if (args.getSchema().hasField(ChangeField.ONLY_EXTENSIONS)) {
-      return new FileExtensionListPredicate(extList);
-    }
-    throw new QueryParseException(
-        "'onlyextensions' operator is not supported by change index version");
+    return new FileExtensionListPredicate(extList);
   }
 
   @Operator
   public Predicate<ChangeData> footer(String footer) throws QueryParseException {
-    if (args.getSchema().hasField(ChangeField.FOOTER)) {
-      return ChangePredicates.footer(footer);
-    }
-    throw new QueryParseException("'footer' operator is not supported by change index version");
+    return ChangePredicates.footer(footer);
   }
 
   @Operator
@@ -977,13 +929,10 @@
 
   @Operator
   public Predicate<ChangeData> directory(String directory) throws QueryParseException {
-    if (args.getSchema().hasField(ChangeField.DIRECTORY)) {
-      if (directory.startsWith("^")) {
-        return new RegexDirectoryPredicate(directory);
-      }
-      return ChangePredicates.directory(directory);
+    if (directory.startsWith("^")) {
+      return new RegexDirectoryPredicate(directory);
     }
-    throw new QueryParseException("'directory' operator is not supported by change index version");
+    return ChangePredicates.directory(directory);
   }
 
   @Operator
@@ -1055,7 +1004,7 @@
     // If the vote piece looks like Code-Review=NEED with a valid non-numeric
     // submit record status, interpret as a submit record query.
     int eq = name.indexOf('=');
-    if (args.getSchema().hasField(ChangeField.SUBMIT_RECORD) && eq > 0) {
+    if (eq > 0) {
       String statusName = name.substring(eq + 1).toUpperCase();
       if (!isInt(statusName) && !MagicLabelValue.tryParse(statusName).isPresent()) {
         SubmitRecord.Label.Status status =
@@ -1197,9 +1146,7 @@
   @Operator
   public Predicate<ChangeData> uploader(String who)
       throws QueryParseException, IOException, ConfigInvalidException {
-    if (!args.getSchema().hasField(ChangeField.UPLOADER)) {
-      throw new QueryParseException("'uploader' operator is not supported by change index version");
-    }
+    checkFieldAvailable(ChangeField.UPLOADER, "uploader");
     return uploader(parseAccount(who, (AccountState s) -> true));
   }
 
@@ -1214,10 +1161,7 @@
   @Operator
   public Predicate<ChangeData> attention(String who)
       throws QueryParseException, IOException, ConfigInvalidException {
-    if (!args.index.getSchema().hasField(ChangeField.ATTENTION_SET_USERS)) {
-      throw new QueryParseException(
-          "'attention' operator is not supported by change index version");
-    }
+    checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "attention");
     return attention(parseAccount(who, (AccountState s) -> true));
   }
 
@@ -1262,9 +1206,7 @@
 
   @Operator
   public Predicate<ChangeData> uploaderin(String group) throws QueryParseException, IOException {
-    if (!args.getSchema().hasField(ChangeField.UPLOADER)) {
-      throw new QueryParseException("'uploader' operator is not supported by change index version");
-    }
+    checkFieldAvailable(ChangeField.UPLOADER, "uploaderin");
 
     GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
     if (g == null) {
@@ -1309,10 +1251,7 @@
     if (Objects.equals(byState, Predicate.<ChangeData>any())) {
       return Predicate.any();
     }
-    if (args.getSchema().hasField(ChangeField.WIP)) {
-      return Predicate.and(Predicate.not(new BooleanPredicate(ChangeField.WIP)), byState);
-    }
-    return byState;
+    return Predicate.and(Predicate.not(new BooleanPredicate(ChangeField.WIP)), byState);
   }
 
   @Operator
@@ -1488,20 +1427,14 @@
 
   @Operator
   public Predicate<ChangeData> author(String who) throws QueryParseException {
-    if (args.getSchema().hasField(ChangeField.EXACT_AUTHOR)) {
-      return getAuthorOrCommitterPredicate(
-          who.trim(), ChangePredicates::exactAuthor, ChangePredicates::author);
-    }
-    return getAuthorOrCommitterFullTextPredicate(who.trim(), ChangePredicates::author);
+    return getAuthorOrCommitterPredicate(
+        who.trim(), ChangePredicates::exactAuthor, ChangePredicates::author);
   }
 
   @Operator
   public Predicate<ChangeData> committer(String who) throws QueryParseException {
-    if (args.getSchema().hasField(ChangeField.EXACT_COMMITTER)) {
-      return getAuthorOrCommitterPredicate(
-          who.trim(), ChangePredicates::exactCommitter, ChangePredicates::committer);
-    }
-    return getAuthorOrCommitterFullTextPredicate(who.trim(), ChangePredicates::committer);
+    return getAuthorOrCommitterPredicate(
+        who.trim(), ChangePredicates::exactCommitter, ChangePredicates::committer);
   }
 
   @Operator
@@ -1524,41 +1457,31 @@
     if (value == null || Ints.tryParse(value) == null) {
       throw new QueryParseException("'revertof' must be an integer");
     }
-    if (args.getSchema().hasField(ChangeField.REVERT_OF)) {
-      return ChangePredicates.revertOf(Change.id(Ints.tryParse(value)));
-    }
-    throw new QueryParseException("'revertof' operator is not supported by change index version");
+    return ChangePredicates.revertOf(Change.id(Ints.tryParse(value)));
   }
 
   @Operator
   public Predicate<ChangeData> submissionId(String value) throws QueryParseException {
-    if (args.getSchema().hasField(ChangeField.SUBMISSIONID)) {
-      return ChangePredicates.submissionId(value);
-    }
-    throw new QueryParseException(
-        "'submissionid' operator is not supported by change index version");
+    return ChangePredicates.submissionId(value);
   }
 
   @Operator
   public Predicate<ChangeData> cherryPickOf(String value) throws QueryParseException {
-    if (args.getSchema().hasField(ChangeField.CHERRY_PICK_OF_CHANGE)
-        && args.getSchema().hasField(ChangeField.CHERRY_PICK_OF_PATCHSET)) {
-      if (Ints.tryParse(value) != null) {
-        return ChangePredicates.cherryPickOf(Change.id(Ints.tryParse(value)));
-      }
-      try {
-        PatchSet.Id patchSetId = PatchSet.Id.parse(value);
-        return ChangePredicates.cherryPickOf(patchSetId);
-      } catch (IllegalArgumentException e) {
-        throw new QueryParseException(
-            "'"
-                + value
-                + "' is not a valid input. It must be in the 'ChangeNumber[,PatchsetNumber]' format.",
-            e);
-      }
+    checkFieldAvailable(ChangeField.CHERRY_PICK_OF_CHANGE, "cherryPickOf");
+    checkFieldAvailable(ChangeField.CHERRY_PICK_OF_PATCHSET, "cherryPickOf");
+    if (Ints.tryParse(value) != null) {
+      return ChangePredicates.cherryPickOf(Change.id(Ints.tryParse(value)));
     }
-    throw new QueryParseException(
-        "'cherrypickof' operator is not supported by change index version");
+    try {
+      PatchSet.Id patchSetId = PatchSet.Id.parse(value);
+      return ChangePredicates.cherryPickOf(patchSetId);
+    } catch (IllegalArgumentException e) {
+      throw new QueryParseException(
+          "'"
+              + value
+              + "' is not a valid input. It must be in the 'ChangeNumber[,PatchsetNumber]' format.",
+          e);
+    }
   }
 
   @Override
@@ -1617,6 +1540,14 @@
     return Predicate.or(predicates);
   }
 
+  protected void checkFieldAvailable(FieldDef<ChangeData, ?> field, String operator)
+      throws QueryParseException {
+    if (!args.index.getSchema().hasField(field)) {
+      throw new QueryParseException(
+          String.format("'%s' operator is not supported by change index version", operator));
+    }
+  }
+
   private Predicate<ChangeData> getAuthorOrCommitterPredicate(
       String who,
       Function<String, Predicate<ChangeData>> exactPredicateFunc,
@@ -1731,11 +1662,9 @@
       String who, ReviewerStateInternal state, boolean forDefaultField)
       throws QueryParseException, IOException, ConfigInvalidException {
     Predicate<ChangeData> reviewerByEmailPredicate = null;
-    if (args.index.getSchema().hasField(ChangeField.REVIEWER_BY_EMAIL)) {
-      Address address = Address.tryParse(who);
-      if (address != null) {
-        reviewerByEmailPredicate = ReviewerByEmailPredicate.forState(address, state);
-      }
+    Address address = Address.tryParse(who);
+    if (address != null) {
+      reviewerByEmailPredicate = ReviewerByEmailPredicate.forState(address, state);
     }
 
     Predicate<ChangeData> reviewerPredicate = null;
diff --git a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
index e63714f..cd0fee3 100644
--- a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.query.QueryBuilder;
 import com.google.inject.Inject;
 
@@ -32,4 +33,10 @@
   SubmitRequirementChangeQueryBuilder(Arguments args) {
     super(def, args);
   }
+
+  @Override
+  protected void checkFieldAvailable(FieldDef<ChangeData, ?> field, String operator) {
+    // Submit requirements don't rely on the index, so they can be used regardless of index schema
+    // version.
+  }
 }
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts
index 2b9abfd..4432cc82 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts
@@ -20,6 +20,7 @@
 import {customElement, property} from 'lit/decorators';
 import {ChangeInfo, SubmitRequirementStatus} from '../../../api/rest-api';
 import {changeIsMerged} from '../../../utils/change-util';
+import {getRequirements} from '../../../utils/label-util';
 
 @customElement('gr-change-list-column-requirements')
 export class GrChangeListColumRequirements extends LitElement {
@@ -52,9 +53,7 @@
       return this.renderState('check', 'Merged');
     }
 
-    const submitRequirements = (this.change?.submit_requirements ?? []).filter(
-      req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE
-    );
+    const submitRequirements = getRequirements(this.change);
     if (!submitRequirements.length) return html`n/a`;
     const numOfRequirements = submitRequirements.length;
     const numOfSatisfiedRequirements = submitRequirements.filter(
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 82deeb5..3e5d11c 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
@@ -131,7 +131,11 @@
   ChangeComments,
   GrCommentApi,
 } from '../../diff/gr-comment-api/gr-comment-api';
-import {assertIsDefined, hasOwnProperty} from '../../../utils/common-util';
+import {
+  assertIsDefined,
+  hasOwnProperty,
+  query,
+} from '../../../utils/common-util';
 import {GrEditControls} from '../../edit/gr-edit-controls/gr-edit-controls';
 import {
   CommentThread,
@@ -234,7 +238,6 @@
     downloadOverlay: GrOverlay;
     downloadDialog: GrDownloadDialog;
     replyOverlay: GrOverlay;
-    replyDialog: GrReplyDialog;
     mainContent: HTMLDivElement;
     changeStar: GrChangeStar;
     actions: GrChangeActions;
@@ -519,7 +522,7 @@
   _activeTabs: string[] = [PrimaryTab.FILES, SecondaryTab.CHANGE_LOG];
 
   @property({type: Boolean})
-  unresolvedOnly = false;
+  unresolvedOnly = true;
 
   @property({type: Boolean})
   _showAllRobotComments = false;
@@ -544,6 +547,10 @@
   @property({type: String})
   scrollCommentId?: UrlEncodedCommentId;
 
+  /** Just reflects the `opened` prop of the overlay. */
+  @property({type: Boolean})
+  replyOverlayOpened = false;
+
   @property({
     type: Array,
     computed: '_computeResolveWeblinks(_change, _commitInfo, _serverConfig)',
@@ -558,8 +565,6 @@
 
   private readonly shortcuts = appContext.shortcutsService;
 
-  private replyDialogResizeObserver?: ResizeObserver;
-
   override keyboardShortcuts(): ShortcutListener[] {
     return [
       listen(Shortcut.SEND_REPLY, _ => {}), // docOnly
@@ -678,11 +683,6 @@
       }
     });
 
-    this.replyDialogResizeObserver = new ResizeObserver(() =>
-      this.$.replyOverlay.center()
-    );
-    this.replyDialogResizeObserver.observe(this.$.replyDialog);
-
     getPluginLoader()
       .awaitPluginsLoaded()
       .then(() => {
@@ -865,7 +865,7 @@
       const hasUnresolvedThreads =
         (this._commentThreads ?? []).filter(thread => isUnresolved(thread))
           .length > 0;
-      if (hasUnresolvedThreads) this.unresolvedOnly = true;
+      if (!hasUnresolvedThreads) this.unresolvedOnly = false;
     }
 
     this.reporting.reportInteraction(Interaction.SHOW_TAB, {
@@ -1055,7 +1055,7 @@
 
   _handleReplyTap(e: MouseEvent) {
     e.preventDefault();
-    this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+    this._openReplyDialog(FocusTarget.ANY);
   }
 
   onReplyOverlayCanceled() {
@@ -1099,8 +1099,7 @@
         .split('\n')
         .map(line => '> ' + line)
         .join('\n') + '\n\n';
-    this.$.replyDialog.quote = quoteStr;
-    this._openReplyDialog(this.$.replyDialog.FocusTarget.BODY);
+    this._openReplyDialog(FocusTarget.BODY, quoteStr);
   }
 
   _handleHideBackgroundContent() {
@@ -1137,9 +1136,9 @@
   }
 
   _handleShowReplyDialog(e: CustomEvent<{value: {ccsOnly: boolean}}>) {
-    let target = this.$.replyDialog.FocusTarget.REVIEWERS;
+    let target = FocusTarget.REVIEWERS;
     if (e.detail.value && e.detail.value.ccsOnly) {
-      target = this.$.replyDialog.FocusTarget.CCS;
+      target = FocusTarget.CCS;
     }
     this._openReplyDialog(target);
   }
@@ -1217,27 +1216,43 @@
       basePatchNum: value.basePatchNum,
     };
 
-    this.$.fileList.collapseAllDiffs();
     this._patchRange = patchRange;
     this.scrollCommentId = value.commentId;
 
     const patchKnown =
       !patchRange.patchNum ||
       (this._allPatchSets ?? []).some(ps => ps.num === patchRange.patchNum);
+    // _allPatchsets does not know value.patchNum so force a reload.
+    const forceReload = value.forceReload || !patchKnown;
 
-    // If the change has already been loaded and the parameter change is only
-    // in the patch range, then don't do a full reload.
-    if (this._changeNum !== undefined && patchChanged && patchKnown) {
-      if (!patchRange.patchNum) {
-        patchRange.patchNum = computeLatestPatchNum(this._allPatchSets);
-        rightPatchNumChanged = true;
+    // If changeNum is defined that means the change has already been
+    // rendered once before so a full reload is not required.
+    if (this._changeNum !== undefined && !forceReload) {
+      if (patchChanged) {
+        // We need to collapse all diffs when params change so that a non
+        // existing diff is not requested. See Issue 125270 for more details.
+        this.$.fileList.collapseAllDiffs();
+        if (!patchRange.patchNum) {
+          patchRange.patchNum = computeLatestPatchNum(this._allPatchSets);
+          rightPatchNumChanged = true;
+        }
+        this._reloadPatchNumDependentResources(rightPatchNumChanged).then(
+          () => {
+            this._sendShowChangeEvent();
+          }
+        );
       }
-      this._reloadPatchNumDependentResources(rightPatchNumChanged).then(() => {
-        this._sendShowChangeEvent();
-      });
+
+      // If there is no change in patchset or changeNum, such as when user goes
+      // to the diff view and then comes back to change page then there is no
+      // need to reload anything and we render the change view component as is.
       return;
     }
 
+    // We need to collapse all diffs when params change so that a non existing
+    // diff is not requested. See Issue 125270 for more details.
+    this.$.fileList.collapseAllDiffs();
+
     this._initialLoadComplete = false;
     this._changeNum = value.changeNum;
     this.loadData(true).then(() => {
@@ -1253,8 +1268,8 @@
 
   _initActiveTabs(params?: AppElementChangeViewParams) {
     let primaryTab = PrimaryTab.FILES;
-    if (params && params.queryMap && params.queryMap.has('tab')) {
-      primaryTab = params.queryMap.get('tab') as PrimaryTab;
+    if (params?.tab) {
+      primaryTab = params?.tab as PrimaryTab;
     } else if (params && 'commentId' in params) {
       primaryTab = PrimaryTab.COMMENT_THREADS;
     }
@@ -1390,7 +1405,7 @@
       }
 
       if (this.viewState.showReplyDialog) {
-        this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+        this._openReplyDialog(FocusTarget.ANY);
         this.set('viewState.showReplyDialog', false);
       }
     });
@@ -1496,7 +1511,7 @@
         fireEvent(this, 'show-auth-required');
         return;
       }
-      this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+      this._openReplyDialog(FocusTarget.ANY);
     });
   }
 
@@ -1681,12 +1696,17 @@
     });
   }
 
-  _openReplyDialog(section?: FocusTarget) {
+  _openReplyDialog(focusTarget?: FocusTarget, quote?: string) {
     if (!this._change) return;
-    this.$.replyOverlay.open().finally(() => {
+    const overlay = this.$.replyOverlay;
+    overlay.open().finally(async () => {
       // the following code should be executed no matter open succeed or not
+      const dialog = query<GrReplyDialog>(this, '#replyDialog');
+      assertIsDefined(dialog, 'reply dialog');
       this._resetReplyOverlayFocusStops();
-      this.$.replyDialog.open(section);
+      dialog.open(focusTarget, quote);
+      const observer = new ResizeObserver(() => overlay.center());
+      observer.observe(dialog);
     });
     fireDialogChange(this, {opened: true});
     this._changeViewAriaHidden = true;
@@ -2017,8 +2037,8 @@
    *
    * @param isLocationChange Reloads the related changes
    * when true and ends reporting events that started on location change.
-   * @param clearPatchset Reloads the related changes
-   * ignoring any patchset choice made.
+   * @param clearPatchset Reloads the change ignoring any patchset
+   * choice made.
    * @return A promise that resolves when the core data has loaded.
    * Some non-core data loading may still be in-flight when the core data
    * promise resolves.
@@ -2026,7 +2046,14 @@
   loadData(isLocationChange?: boolean, clearPatchset?: boolean): Promise<void> {
     if (this.isChangeObsolete()) return Promise.resolve();
     if (clearPatchset && this._change) {
-      GerritNav.navigateToChange(this._change);
+      GerritNav.navigateToChange(
+        this._change,
+        undefined,
+        undefined,
+        undefined,
+        undefined,
+        true
+      );
       return Promise.resolve();
     }
     this._loading = true;
@@ -2470,18 +2497,34 @@
     ) {
       patchNum = this._patchRange.patchNum;
     }
-    GerritNav.navigateToChange(this._change, patchNum, undefined, true);
+    GerritNav.navigateToChange(
+      this._change,
+      patchNum,
+      undefined,
+      true,
+      undefined,
+      true
+    );
   }
 
   _handleStopEditTap() {
     assertIsDefined(this._change, '_change');
     if (!this._patchRange)
       throw new Error('missing required _patchRange property');
-    GerritNav.navigateToChange(this._change, this._patchRange.patchNum);
+    GerritNav.navigateToChange(
+      this._change,
+      this._patchRange.patchNum,
+      undefined,
+      undefined,
+      undefined,
+      true
+    );
   }
 
   _resetReplyOverlayFocusStops() {
-    this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops());
+    const dialog = query<GrReplyDialog>(this, '#replyDialog');
+    if (!dialog) return;
+    this.$.replyOverlay.setFocusStops(dialog.getFocusStops());
   }
 
   _handleToggleStar(e: CustomEvent<{change: ChangeInfo; starred: boolean}>) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index d42fc17..f125670 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -695,24 +695,27 @@
     no-cancel-on-esc-key=""
     scroll-action="lock"
     with-backdrop=""
+    opened="{{replyOverlayOpened}}"
     on-iron-overlay-canceled="onReplyOverlayCanceled"
   >
-    <gr-reply-dialog
-      id="replyDialog"
-      change="{{_change}}"
-      patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
-      permitted-labels="[[_change.permitted_labels]]"
-      draft-comment-threads="[[_draftCommentThreads]]"
-      project-config="[[_projectConfig]]"
-      server-config="[[_serverConfig]]"
-      can-be-started="[[_canStartReview]]"
-      on-send="_handleReplySent"
-      on-cancel="_handleReplyCancel"
-      on-autogrow="_handleReplyAutogrow"
-      on-send-disabled-changed="_resetReplyOverlayFocusStops"
-      hidden$="[[!_loggedIn]]"
-    >
-    </gr-reply-dialog>
+    <template is="dom-if" if="[[replyOverlayOpened]]">
+      <gr-reply-dialog
+        id="replyDialog"
+        change="{{_change}}"
+        patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
+        permitted-labels="[[_change.permitted_labels]]"
+        draft-comment-threads="[[_draftCommentThreads]]"
+        project-config="[[_projectConfig]]"
+        server-config="[[_serverConfig]]"
+        can-be-started="[[_canStartReview]]"
+        on-send="_handleReplySent"
+        on-cancel="_handleReplyCancel"
+        on-autogrow="_handleReplyAutogrow"
+        on-send-disabled-changed="_resetReplyOverlayFocusStops"
+        hidden$="[[!_loggedIn]]"
+      >
+      </gr-reply-dialog>
+    </template>
   </gr-overlay>
   <gr-comment-api id="commentAPI"></gr-comment-api>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index acdaefe..591aa41 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -37,7 +37,14 @@
 import {EventType, PluginApi} from '../../../api/plugin';
 
 import 'lodash/lodash';
-import {mockPromise, stubRestApi, stubUsers} from '../../../test/test-utils';
+import {
+  mockPromise,
+  queryAndAssert,
+  stubRestApi,
+  stubUsers,
+  waitQueryAndAssert,
+  waitUntil,
+} from '../../../test/test-utils';
 import {
   createAppElementChangeViewParams,
   createApproval,
@@ -97,6 +104,8 @@
 import {appContext} from '../../../services/app-context';
 import {ChangeStates} from '../../shared/gr-change-status/gr-change-status';
 import {_testOnly_setState} from '../../../services/user/user-model';
+import {FocusTarget, GrReplyDialog} from '../gr-reply-dialog/gr-reply-dialog';
+import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
 
 const pluginApi = _testOnly_initGerritPluginApi();
 const fixture = fixtureFromElement('gr-change-view');
@@ -548,13 +557,12 @@
 
     test('param change should switch primary tab correctly', async () => {
       assert.equal(element._activeTabs[0], PrimaryTab.FILES);
-      const queryMap = new Map<string, string>();
-      queryMap.set('tab', PrimaryTab.FINDINGS);
       // view is required
+      element._changeNum = undefined;
       element.params = {
         ...createAppElementChangeViewParams(),
         ...element.params,
-        queryMap,
+        tab: PrimaryTab.FINDINGS,
       };
       await flush();
       assert.equal(element._activeTabs[0], PrimaryTab.FINDINGS);
@@ -562,13 +570,11 @@
 
     test('invalid param change should not switch primary tab', async () => {
       assert.equal(element._activeTabs[0], PrimaryTab.FILES);
-      const queryMap = new Map<string, string>();
-      queryMap.set('tab', 'random');
       // view is required
       element.params = {
         ...createAppElementChangeViewParams(),
         ...element.params,
-        queryMap,
+        tab: 'random',
       };
       await flush();
       assert.equal(element._activeTabs[0], PrimaryTab.FILES);
@@ -681,9 +687,7 @@
       element.$.replyOverlay.close();
       assert.isFalse(element.$.replyOverlay.opened);
       assert(
-        openSpy.lastCall.calledWithExactly(
-          element.$.replyDialog.FocusTarget.ANY
-        ),
+        openSpy.lastCall.calledWithExactly(FocusTarget.ANY),
         '_openReplyDialog should have been passed ANY'
       );
       assert.equal(openSpy.callCount, 1);
@@ -705,7 +709,8 @@
         },
       };
       const handlerSpy = sinon.spy(element, '_handleHideBackgroundContent');
-      element.$.replyDialog.dispatchEvent(
+      const overlay = queryAndAssert<GrOverlay>(element, '#replyOverlay');
+      overlay.dispatchEvent(
         new CustomEvent('fullscreen-overlay-opened', {
           composed: true,
           bubbles: true,
@@ -732,7 +737,8 @@
         },
       };
       const handlerSpy = sinon.spy(element, '_handleShowBackgroundContent');
-      element.$.replyDialog.dispatchEvent(
+      const overlay = queryAndAssert<GrOverlay>(element, '#replyOverlay');
+      overlay.dispatchEvent(
         new CustomEvent('fullscreen-overlay-closed', {
           composed: true,
           bubbles: true,
@@ -1306,12 +1312,12 @@
       .callsFake(() => Promise.resolve([undefined, undefined, undefined]));
     flush();
     const collapseStub = sinon.stub(element.$.fileList, 'collapseAllDiffs');
-
     const value: AppElementChangeViewParams = {
       ...createAppElementChangeViewParams(),
       view: GerritView.CHANGE,
       patchNum: 1 as RevisionPatchSetNum,
     };
+    element._changeNum = undefined;
     element.params = value;
     await flush();
     assert.isTrue(reloadStub.calledOnce);
@@ -1369,7 +1375,7 @@
     assert.isTrue(reloadPortedCommentsStub.calledOnce);
   });
 
-  test('reload entire page when patchRange doesnt change', async () => {
+  test('do not reload entire page when patchRange doesnt change', async () => {
     const reloadStub = sinon
       .stub(element, 'loadData')
       .callsFake(() => Promise.resolve());
@@ -1377,13 +1383,15 @@
     const value: AppElementChangeViewParams =
       createAppElementChangeViewParams();
     element.params = value;
+    // change already loaded
+    assert.isOk(element._changeNum);
     await flush();
-    assert.isTrue(reloadStub.calledOnce);
+    assert.isFalse(reloadStub.calledOnce);
     element._initialLoadComplete = true;
     element.params = {...value};
     await flush();
-    assert.isTrue(reloadStub.calledTwice);
-    assert.isTrue(collapseStub.calledTwice);
+    assert.isFalse(reloadStub.calledTwice);
+    assert.isFalse(collapseStub.calledTwice);
   });
 
   test('do not handle new change numbers', async () => {
@@ -1608,9 +1616,7 @@
     const openStub = sinon.stub(element, '_openReplyDialog');
     tap(element.$.replyBtn);
     assert(
-      openStub.lastCall.calledWithExactly(
-        element.$.replyDialog.FocusTarget.ANY
-      ),
+      openStub.lastCall.calledWithExactly(FocusTarget.ANY),
       '_openReplyDialog should have been passed ANY'
     );
     assert.equal(openStub.callCount, 1);
@@ -1629,18 +1635,12 @@
           bubbles: true,
         })
       );
-      assert(
-        openStub.lastCall.calledWithExactly(
-          element.$.replyDialog.FocusTarget.BODY
-        ),
-        '_openReplyDialog should have been passed BODY'
-      );
-      assert.equal(openStub.callCount, 1);
+      assert.isTrue(openStub.calledOnce);
+      assert.equal(openStub.lastCall.args[0], FocusTarget.BODY);
     }
   );
 
   test('reply dialog focus can be controlled', () => {
-    const FocusTarget = element.$.replyDialog.FocusTarget;
     const openStub = sinon.stub(element, '_openReplyDialog');
 
     const e = new CustomEvent('show-reply-dialog', {
@@ -1716,7 +1716,6 @@
 
   suite('reply dialog tests', () => {
     setup(() => {
-      sinon.stub(element.$.replyDialog, '_draftChanged');
       element._change = {
         ...createChangeViewChange(),
         revisions: createRevisions(1),
@@ -1747,52 +1746,18 @@
       assert.isTrue(openReplyDialogStub.calledOnce);
     });
 
-    test('reply from comment adds quote text', () => {
+    test('reply from comment adds quote text', async () => {
       const e = new CustomEvent('', {
         detail: {message: {message: 'quote text'}},
       });
       element._handleMessageReply(e);
-      assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
-    });
-
-    test('reply from comment replaces quote text', () => {
-      element.$.replyDialog.draft = '> old quote text\n\n some draft text';
-      element.$.replyDialog.quote = '> old quote text\n\n';
-      const e = new CustomEvent('', {
-        detail: {message: {message: 'quote text'}},
-      });
-      element._handleMessageReply(e);
-      assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
-    });
-
-    test('reply from same comment preserves quote text', () => {
-      element.$.replyDialog.draft = '> quote text\n\n some draft text';
-      element.$.replyDialog.quote = '> quote text\n\n';
-      const e = new CustomEvent('', {
-        detail: {message: {message: 'quote text'}},
-      });
-      element._handleMessageReply(e);
-      assert.equal(
-        element.$.replyDialog.draft,
-        '> quote text\n\n some draft text'
+      const dialog = await waitQueryAndAssert<GrReplyDialog>(
+        element,
+        '#replyDialog'
       );
-      assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
-    });
-
-    test('reply from top of page contains previous draft', () => {
-      const div = document.createElement('div');
-      element.$.replyDialog.draft = '> quote text\n\n some draft text';
-      element.$.replyDialog.quote = '> quote text\n\n';
-      const e = {
-        target: div,
-        preventDefault: sinon.spy(),
-      } as unknown as MouseEvent;
-      element._handleReplyTap(e);
-      assert.equal(
-        element.$.replyDialog.draft,
-        '> quote text\n\n some draft text'
-      );
-      assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
+      const openSpy = sinon.spy(dialog, 'open');
+      await waitUntil(() => openSpy.called && !!openSpy.lastCall.args[1]);
+      assert.equal(openSpy.lastCall.args[1], '> quote text\n\n');
     });
   });
 
@@ -2097,7 +2062,7 @@
     test('no edit exists in revisions, non-latest patchset', async () => {
       const promise = mockPromise();
       sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
-        assert.equal(args.length, 4);
+        assert.equal(args.length, 6);
         assert.equal(args[1], 1 as PatchSetNum); // patchNum
         assert.equal(args[3], true); // opt_isEdit
         promise.resolve();
@@ -2114,7 +2079,7 @@
     test('no edit exists in revisions, latest patchset', async () => {
       const promise = mockPromise();
       sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
-        assert.equal(args.length, 4);
+        assert.equal(args.length, 6);
         // No patch should be specified when patchNum == latest.
         assert.isNotOk(args[1]); // patchNum
         assert.equal(args[3], true); // opt_isEdit
@@ -2138,7 +2103,7 @@
     navigateToChangeStub.restore();
     const promise = mockPromise();
     sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
-      assert.equal(args.length, 2);
+      assert.equal(args.length, 6);
       assert.equal(args[1], 1 as PatchSetNum); // patchNum
       promise.resolve();
     });
@@ -2256,6 +2221,8 @@
         appContext.reportingService,
         'changeFullyLoaded'
       );
+      // reset so reload is triggered
+      element._changeNum = undefined;
       element.params = {
         ...createAppElementChangeViewParams(),
         changeNum: TEST_NUMERIC_CHANGE_ID,
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index b8932e5..5109b72 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -240,9 +240,6 @@
   @property({type: String, observer: '_draftChanged'})
   draft = '';
 
-  @property({type: String})
-  quote = '';
-
   @property({type: Object})
   filterReviewerSuggestion: (input: Suggestion) => boolean;
 
@@ -427,7 +424,13 @@
     super.disconnectedCallback();
   }
 
-  open(focusTarget?: FocusTarget) {
+  /**
+   * Note that this method is not actually *opening* the dialog. Opening and
+   * showing the dialog is dealt with by the overlay. This method is used by the
+   * change view for initializing the dialog after opening the overlay. Maybe it
+   * should be called `onOpened()` or `initialize()`?
+   */
+  open(focusTarget?: FocusTarget, quote?: string) {
     assertIsDefined(this.change, 'change');
     this.knownLatestState = LatestPatchState.CHECKING;
     this.changeService.fetchChangeUpdates(this.change).then(result => {
@@ -437,10 +440,9 @@
     });
 
     this._focusOn(focusTarget);
-    if (this.quote && this.quote.length) {
-      // If a reply quote has been provided, use it and clear the property.
-      this.draft = this.quote;
-      this.quote = '';
+    if (quote?.length) {
+      // If a reply quote has been provided, use it.
+      this.draft = quote;
     } else {
       // Otherwise, check for an unsaved draft in localstorage.
       this.draft = this._loadStoredDraft();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index 5a11f47..cf31a4f 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -18,9 +18,11 @@
 import '../../../test/common-test-setup-karma';
 import './gr-reply-dialog';
 import {
+  addListenerForTest,
   mockPromise,
   queryAll,
   queryAndAssert,
+  stubRestApi,
   stubStorage,
 } from '../../../test/test-utils';
 import {
@@ -29,8 +31,6 @@
   SpecialFilePath,
 } from '../../../constants/constants';
 import {appContext} from '../../../services/app-context';
-import {addListenerForTest} from '../../../test/test-utils';
-import {stubRestApi} from '../../../test/test-utils';
 import {JSON_PREFIX} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
 import {StandardLabels} from '../../../utils/label-util';
 import {
@@ -44,7 +44,7 @@
   pressAndReleaseKeyOn,
   tap,
 } from '@polymer/iron-test-helpers/mock-interactions';
-import {GrReplyDialog} from './gr-reply-dialog';
+import {FocusTarget, GrReplyDialog} from './gr-reply-dialog';
 import {
   AccountId,
   AccountInfo,
@@ -1317,11 +1317,9 @@
     const storedDraft = 'hello world';
     const quote = '> foo bar';
     getDraftCommentStub.returns({message: storedDraft});
-    element.quote = quote;
-    element.open();
+    element.open(FocusTarget.ANY, quote);
     assert.isFalse(getDraftCommentStub.called);
     assert.equal(element.draft, quote);
-    assert.isNotOk(element.quote);
   });
 
   test('updates stored draft on edits', async () => {
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index 157336d..2f3e1e2 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -33,6 +33,7 @@
 import {
   extractAssociatedLabels,
   getAllUniqueApprovals,
+  getRequirements,
   hasNeutralStatus,
   hasVotes,
   iconForStatus,
@@ -139,18 +140,9 @@
   }
 
   override render() {
-    let submit_requirements = orderSubmitRequirements(
-      this.change?.submit_requirements ?? []
-    ).filter(req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE);
-
-    const hasNonLegacyRequirements = submit_requirements.some(
-      req => req.is_legacy === false
+    const submit_requirements = orderSubmitRequirements(
+      getRequirements(this.change)
     );
-    if (hasNonLegacyRequirements) {
-      submit_requirements = submit_requirements.filter(
-        req => req.is_legacy === false
-      );
-    }
 
     return html` <h3
         class="metadata-title heading-3"
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
index b8f2630..6b4006c 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
@@ -256,11 +256,9 @@
   edit?: boolean;
   host?: string;
   messageHash?: string;
-  queryMap?: Map<string, string> | URLSearchParams;
   commentId?: UrlEncodedCommentId;
-
-  // TODO(TS): querystring isn't set anywhere, try to remove
-  querystring?: string;
+  forceReload?: boolean;
+  tab?: string;
 }
 
 export interface GenerateUrlRepoViewParameters {
@@ -612,7 +610,8 @@
     patchNum?: PatchSetNum,
     basePatchNum?: BasePatchSetNum,
     isEdit?: boolean,
-    messageHash?: string
+    messageHash?: string,
+    forceReload?: boolean
   ) {
     if (basePatchNum === ParentPatchSetNum) {
       basePatchNum = undefined;
@@ -628,6 +627,7 @@
       edit: isEdit,
       host: change.internalHost || undefined,
       messageHash,
+      forceReload,
     });
   },
 
@@ -649,17 +649,28 @@
    * @param redirect redirect to a change - if true, the current
    *     location (i.e. page which makes redirect) is not added to a history.
    *     I.e. back/forward buttons skip current location
-   *
+   * @param forceReload Some views are smart about how to handle the reload
+   *     of the view. In certain cases we want to force the view to reload
+   *     and re-render everything.
    */
+  // TODO(dhruvsri): move the arguments into one options object
   navigateToChange(
     change: Pick<ChangeInfo, '_number' | 'project' | 'internalHost'>,
     patchNum?: PatchSetNum,
     basePatchNum?: BasePatchSetNum,
     isEdit?: boolean,
-    redirect?: boolean
+    redirect?: boolean,
+    forceReload?: boolean
   ) {
     this._navigate(
-      this.getUrlForChange(change, patchNum, basePatchNum, isEdit),
+      this.getUrlForChange(
+        change,
+        patchNum,
+        basePatchNum,
+        isEdit,
+        undefined,
+        forceReload
+      ),
       redirect
     );
   },
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index f0cff85..65ac9df 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -530,17 +530,22 @@
       range = '/' + range;
     }
     let suffix = `${range}`;
-    if (params.querystring) {
-      suffix += '?' + params.querystring;
-    } else if (params.edit) {
-      suffix += ',edit';
+    let queryString = '';
+    if (params.forceReload) {
+      queryString = 'forceReload=true';
     }
-    if (params.messageHash) {
-      suffix += params.messageHash;
+    if (params.edit) {
+      suffix += ',edit';
     }
     if (params.commentId) {
       suffix = suffix + `/comments/${params.commentId}`;
     }
+    if (queryString) {
+      suffix += '?' + queryString;
+    }
+    if (params.messageHash) {
+      suffix += params.messageHash;
+    }
     if (params.project) {
       const encodedProject = encodeURL(params.project, true);
       return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
@@ -1563,9 +1568,20 @@
       basePatchNum: convertToPatchSetNum(ctx.params[4]) as BasePatchSetNum,
       patchNum: convertToPatchSetNum(ctx.params[6]),
       view: GerritView.CHANGE,
-      queryMap: ctx.queryMap,
     };
 
+    if (ctx.queryMap.has('forceReload')) {
+      params.forceReload = true;
+      history.replaceState(
+        null,
+        '',
+        location.href.replace(/[?&]forceReload=true/, '')
+      );
+    }
+
+    const tab = ctx.queryMap.get('tab');
+    if (tab) params.tab = tab;
+
     this.reporting.setRepoName(params.project);
     this.reporting.setChangeId(changeNum);
     this._redirectOrNavigate(params);
@@ -1661,13 +1677,24 @@
     // Parameter order is based on the regex group number matched.
     const project = ctx.params[0] as RepoName;
     const changeNum = Number(ctx.params[1]) as NumericChangeId;
-    this._redirectOrNavigate({
+    const params: GenerateUrlChangeViewParameters = {
       project,
       changeNum,
       patchNum: convertToPatchSetNum(ctx.params[3]),
       view: GerritView.CHANGE,
       edit: true,
-    });
+      tab: ctx.queryMap.get('tab') ?? '',
+    };
+    if (ctx.queryMap.has('forceReload')) {
+      params.forceReload = true;
+      history.replaceState(
+        null,
+        '',
+        location.href.replace(/[?&]forceReload=true/, '')
+      );
+    }
+    this._redirectOrNavigate(params);
+
     this.reporting.setRepoName(project);
     this.reporting.setChangeId(changeNum);
   }
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
index b91bf0c..7f1a40b 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
@@ -312,28 +312,14 @@
         changeNum: '1234',
         project: 'test',
       };
-      const paramsWithQuery = {
-        view: GerritView.CHANGE,
-        changeNum: '1234',
-        project: 'test',
-        querystring: 'revert&foo=bar',
-      };
 
       assert.equal(element._generateUrl(params), '/c/test/+/1234');
-      assert.equal(element._generateUrl(paramsWithQuery),
-          '/c/test/+/1234?revert&foo=bar');
 
       params.patchNum = 10;
       assert.equal(element._generateUrl(params), '/c/test/+/1234/10');
-      paramsWithQuery.patchNum = 10;
-      assert.equal(element._generateUrl(paramsWithQuery),
-          '/c/test/+/1234/10?revert&foo=bar');
 
       params.basePatchNum = 5;
       assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10');
-      paramsWithQuery.basePatchNum = 5;
-      assert.equal(element._generateUrl(paramsWithQuery),
-          '/c/test/+/1234/5..10?revert&foo=bar');
 
       params.messageHash = '#123';
       assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10#123');
@@ -1382,7 +1368,6 @@
             changeNum: 1234,
             basePatchNum: 4,
             patchNum: 7,
-            queryMap: new Map(),
           });
           assert.isFalse(redirectStub.called);
           assert.isTrue(normalizeRangeStub.called);
@@ -1549,6 +1534,7 @@
             null,
             3, // 3 Patch num
           ],
+          queryMap: new Map(),
         };
         const appParams = {
           project: 'foo/bar',
@@ -1556,6 +1542,7 @@
           view: GerritView.CHANGE,
           patchNum: 3,
           edit: true,
+          tab: '',
         };
 
         element._handleChangeEditRoute(ctx);
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index 24ebd67..a295588 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -259,7 +259,14 @@
 
   _viewEditInChangeView() {
     if (this._change)
-      GerritNav.navigateToChange(this._change, undefined, undefined, true);
+      GerritNav.navigateToChange(
+        this._change,
+        undefined,
+        undefined,
+        true,
+        undefined,
+        true
+      );
   }
 
   _getFileData(
diff --git a/polygerrit-ui/app/elements/gr-app-types.ts b/polygerrit-ui/app/elements/gr-app-types.ts
index 6c8bdb9..39070bd 100644
--- a/polygerrit-ui/app/elements/gr-app-types.ts
+++ b/polygerrit-ui/app/elements/gr-app-types.ts
@@ -124,8 +124,9 @@
   edit?: boolean;
   patchNum?: RevisionPatchSetNum;
   basePatchNum?: BasePatchSetNum;
-  queryMap?: Map<string, string> | URLSearchParams;
   commentId?: UrlEncodedCommentId;
+  forceReload?: boolean;
+  tab?: string;
 }
 
 export interface AppElementJustRegisteredParams {
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 391d56f..ed8d792 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -26,6 +26,7 @@
 import {CommentsService} from '../services/comments/comments-service';
 import {UserService} from '../services/user/user-service';
 import {ShortcutsService} from '../services/shortcuts/shortcuts-service';
+import {queryAndAssert, query} from '../utils/common-util';
 export {query, queryAll, queryAndAssert} from '../utils/common-util';
 
 export interface MockPromise extends Promise<unknown> {
@@ -167,22 +168,33 @@
   document.head.querySelector('#dark-theme')?.remove();
 }
 
+export async function waitQueryAndAssert<E extends Element = Element>(
+  el: Element | null | undefined,
+  selector: string
+): Promise<E> {
+  await waitUntil(
+    () => !!query<E>(el, selector),
+    `The element '${selector}' did not appear in the DOM within 1000 ms.`
+  );
+  return queryAndAssert<E>(el, selector);
+}
+
 export function waitUntil(
   predicate: () => boolean,
-  maxMillis = 100
+  message = 'The waitUntil() predicate is still false after 1000 ms.'
 ): Promise<void> {
   const start = Date.now();
-  let sleep = 1;
+  let sleep = 0;
   return new Promise((resolve, reject) => {
     const waiter = () => {
       if (predicate()) {
         return resolve();
       }
-      if (Date.now() - start >= maxMillis) {
-        return reject(new Error('Took to long to waitUntil'));
+      if (Date.now() - start >= 1000) {
+        return reject(new Error(message));
       }
       setTimeout(waiter, sleep);
-      sleep *= 2;
+      sleep = sleep === 0 ? 1 : sleep * 4;
     };
     waiter();
   });
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index 9e9d0a3..0fd4ca4 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 import {
+  ChangeInfo,
   isQuickLabelInfo,
   SubmitRequirementResultInfo,
   SubmitRequirementStatus,
@@ -28,6 +29,7 @@
   LabelNameToInfoMap,
   VotingRangeInfo,
 } from '../types/common';
+import {ParsedChangeInfo} from '../types/types';
 import {assertNever, unique} from './common-util';
 
 // Name of the standard Code-Review label.
@@ -243,6 +245,28 @@
   }
 }
 
+/**
+ * Show only applicable.
+ * If there are only legacy requirements, show all legacy requirements.
+ * If there is at least one non-legacy requirement, filter legacy requirements.
+ */
+export function getRequirements(change?: ParsedChangeInfo | ChangeInfo) {
+  let submit_requirements = (change?.submit_requirements ?? []).filter(
+    req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE
+  );
+
+  const hasNonLegacyRequirements = submit_requirements.some(
+    req => req.is_legacy === false
+  );
+  if (hasNonLegacyRequirements) {
+    submit_requirements = submit_requirements.filter(
+      req => req.is_legacy === false
+    );
+  }
+
+  return submit_requirements;
+}
+
 // TODO(milutin): This may be temporary for demo purposes
 export const PRIORITY_REQUIREMENTS_ORDER: string[] = [
   StandardLabels.CODE_REVIEW,
diff --git a/polygerrit-ui/app/utils/label-util_test.ts b/polygerrit-ui/app/utils/label-util_test.ts
index 2d59294..6af883b 100644
--- a/polygerrit-ui/app/utils/label-util_test.ts
+++ b/polygerrit-ui/app/utils/label-util_test.ts
@@ -24,6 +24,7 @@
   getRepresentativeValue,
   getVotingRange,
   getVotingRangeOrDefault,
+  getRequirements,
   hasNeutralStatus,
   labelCompare,
   LabelStatus,
@@ -38,9 +39,14 @@
 } from '../types/common';
 import {
   createAccountWithEmail,
+  createChange,
   createSubmitRequirementExpressionInfo,
   createSubmitRequirementResultInfo,
 } from '../test/test-data-generators';
+import {
+  SubmitRequirementResultInfo,
+  SubmitRequirementStatus,
+} from '../api/rest-api';
 
 const VALUES_0 = {
   '0': 'neutral',
@@ -270,4 +276,47 @@
       assert.deepEqual(labels, ['Verified', 'Build-cop-override']);
     });
   });
+
+  suite('getRequirements()', () => {
+    function createChangeInfoWith(
+      submit_requirements: SubmitRequirementResultInfo[]
+    ) {
+      return {
+        ...createChange(),
+        submit_requirements,
+      };
+    }
+    test('only legacy', () => {
+      const requirement = {
+        ...createSubmitRequirementResultInfo(),
+        is_legacy: true,
+      };
+      const change = createChangeInfoWith([requirement]);
+      assert.deepEqual(getRequirements(change), [requirement]);
+    });
+    test('legacy and non-legacy - filter legacy', () => {
+      const requirement = {
+        ...createSubmitRequirementResultInfo(),
+        is_legacy: true,
+      };
+      const requirement2 = {
+        ...createSubmitRequirementResultInfo(),
+        is_legacy: false,
+      };
+      const change = createChangeInfoWith([requirement, requirement2]);
+      assert.deepEqual(getRequirements(change), [requirement2]);
+    });
+    test('filter not applicable', () => {
+      const requirement = {
+        ...createSubmitRequirementResultInfo(),
+        is_legacy: true,
+      };
+      const requirement2 = {
+        ...createSubmitRequirementResultInfo(),
+        status: SubmitRequirementStatus.NOT_APPLICABLE,
+      };
+      const change = createChangeInfoWith([requirement, requirement2]);
+      assert.deepEqual(getRequirements(change), [requirement]);
+    });
+  });
 });