Merge "Fix occurrences of class$=... without [[...]]"
diff --git a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
index 8395d12..fa9a820 100644
--- a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
@@ -89,7 +89,10 @@
           .map(query -> query.replaceAll("\\$\\{user}", "self"))
           .collect(toImmutableList());
   public static final ImmutableSet<ListChangesOption> DASHBOARD_OPTIONS =
-      ImmutableSet.of(ListChangesOption.LABELS, ListChangesOption.DETAILED_ACCOUNTS);
+      ImmutableSet.of(
+          ListChangesOption.LABELS,
+          ListChangesOption.DETAILED_ACCOUNTS,
+          ListChangesOption.SUBMIT_REQUIREMENTS);
 
   public static final ImmutableSet<ListChangesOption> CHANGE_DETAIL_OPTIONS =
       ImmutableSet.of(
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 03886a9..4a17b5c 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -901,7 +901,7 @@
       Config rc, String section, String subsection, String varName, boolean useRange) {
     Permission.Builder perm = Permission.builder(varName);
     loadPermissionRules(rc, section, subsection, varName, perm, useRange);
-    return ImmutableList.copyOf(perm.build().getRules());
+    return perm.build().getRules();
   }
 
   private void loadPermissionRules(
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
index 402bb51..df836e0 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.entities.SubmitRequirementExpression;
 import com.google.gerrit.entities.SubmitRequirementExpressionResult;
 import com.google.gerrit.entities.SubmitRequirementResult;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
-import java.util.Map;
 
 public interface SubmitRequirementsEvaluator {
   /**
@@ -31,7 +31,7 @@
    * @param includeLegacy if set to true, evaluate legacy {@link
    *     com.google.gerrit.entities.SubmitRecord}s and convert them to submit requirements.
    */
-  Map<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(
+  ImmutableMap<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(
       ChangeData cd, boolean includeLegacy);
 
   /** Evaluate a single {@link SubmitRequirement} using change data. */
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
index 64c9a4c..16645a5 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
@@ -82,7 +82,7 @@
   }
 
   @Override
-  public Map<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(
+  public ImmutableMap<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(
       ChangeData cd, boolean includeLegacy) {
     Map<SubmitRequirement, SubmitRequirementResult> projectConfigRequirements = getRequirements(cd);
     Map<SubmitRequirement, SubmitRequirementResult> result = projectConfigRequirements;
diff --git a/plugins/gitiles b/plugins/gitiles
index 97ce60f..a0709a4 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit 97ce60f8bb4dbf40dde79cf56db6425c384dabcf
+Subproject commit a0709a402ee1d4fe3921fd81e575ec48a053cc9f
diff --git a/polygerrit-ui/app/api/checks.ts b/polygerrit-ui/app/api/checks.ts
index d1c320f..17b0ba0 100644
--- a/polygerrit-ui/app/api/checks.ts
+++ b/polygerrit-ui/app/api/checks.ts
@@ -97,6 +97,11 @@
   actions?: Action[];
 
   /**
+   * Shown prominently in the change summary below the run chips.
+   */
+  summaryMessage?: string;
+
+  /**
    * Top-level links that are not associated with a specific run or result.
    * Will be shown as icons in the header of the Checks tab.
    */
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
index 84cf6d9..b4fa7cc 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
@@ -75,10 +75,10 @@
     ></gr-user-header>
     <h1 class="assistive-tech-only">Dashboard</h1>
     <gr-change-list
-      show-star=""
+      showstar=""
       account="[[account]]"
       preferences="[[preferences]]"
-      selected-index="[[_selectedChangeIndex]]"
+      selectedindex="[[_selectedChangeIndex]]"
       sections="[[_results]]"
       on-selected-index-changed="_handleSelectedIndexChanged"
       on-toggle-star="_handleToggleStar"
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 c90359e..3298e53 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
@@ -405,6 +405,9 @@
   @state()
   actions: Action[] = [];
 
+  @state()
+  messages: string[] = [];
+
   private showAllChips = new Map<RunStatus | Category, boolean>();
 
   private getCommentsModel = resolve(this, commentsModelToken);
@@ -447,6 +450,11 @@
     );
     subscribe(
       this,
+      this.checksModel.topLevelMessagesLatest$,
+      x => (this.messages = x)
+    );
+    subscribe(
+      this,
       this.getCommentsModel().changeComments$,
       x => (this.changeComments = x)
     );
@@ -556,10 +564,18 @@
         .actions #moreMessage {
           display: none;
         }
+        .summaryMessage {
+          line-height: var(--line-height-normal);
+          color: var(--primary-text-color);
+        }
       `,
     ];
   }
 
+  private renderSummaryMessage() {
+    return this.messages.map(m => html`<div class="summaryMessage">${m}</div>`);
+  }
+
   private renderActions() {
     const actions = this.actions ?? [];
     const summaryActions = actions.filter(a => a.summary).slice(0, 2);
@@ -794,7 +810,8 @@
                   class="loadingSpin"
                   ?hidden="${!this.someProvidersAreLoading}"
                 ></span>
-                ${this.renderErrorMessages()}${this.renderChecksLogin()}${this.renderActions()}
+                ${this.renderErrorMessages()} ${this.renderChecksLogin()}
+                ${this.renderSummaryMessage()} ${this.renderActions()}
               </div>
             </td>
           </tr>
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 4294e3f..38430b0 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
@@ -171,17 +171,17 @@
           </tr>
         </thead>
         <tbody>
-          ${submit_requirements.map(requirement =>
-            this.renderRequirement(requirement)
+          ${submit_requirements.map((requirement, index) =>
+            this.renderRequirement(requirement, index)
           )}
         </tbody>
       </table>
       ${this.disableHovercards
         ? ''
         : submit_requirements.map(
-            requirement => html`
+            (requirement, index) => html`
               <gr-submit-requirement-hovercard
-                for="requirement-${charsOnly(requirement.name)}"
+                for="requirement-${index}-${charsOnly(requirement.name)}"
                 .requirement="${requirement}"
                 .change="${this.change}"
                 .account="${this.account}"
@@ -192,9 +192,9 @@
       ${this.renderTriggerVotes()}`;
   }
 
-  renderRequirement(requirement: SubmitRequirementResultInfo) {
+  renderRequirement(requirement: SubmitRequirementResultInfo, index: number) {
     return html`
-      <tr id="requirement-${charsOnly(requirement.name)}">
+      <tr id="requirement-${index}-${charsOnly(requirement.name)}">
         <td>${this.renderStatus(requirement.status)}</td>
         <td class="name">
           <gr-limited-text
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
index 5a094ef..323c70f 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
@@ -85,7 +85,7 @@
         </tr>
       </thead>
       <tbody>
-        <tr id="requirement-Verified">
+        <tr id="requirement-0-Verified">
           <td>
             <iron-icon
               aria-label="satisfied"
@@ -111,7 +111,7 @@
         </tr>
       </tbody>
     </table>
-    <gr-submit-requirement-hovercard for="requirement-Verified">
+    <gr-submit-requirement-hovercard for="requirement-0-Verified">
     </gr-submit-requirement-hovercard>
   `);
   });
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 4929b7c..f4bbc5d 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -676,6 +676,7 @@
       [],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -683,6 +684,7 @@
       [],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -690,6 +692,7 @@
       [],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -697,6 +700,7 @@
       [],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -704,6 +708,7 @@
       [],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -711,6 +716,7 @@
       [],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
   }
@@ -721,6 +727,7 @@
       [fakeRun0],
       fakeActions,
       fakeLinks,
+      'ETA: 1 min',
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -728,6 +735,7 @@
       [fakeRun1],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -735,6 +743,7 @@
       [fakeRun2],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -742,6 +751,7 @@
       [fakeRun3],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -749,6 +759,7 @@
       fakeRun4Att,
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -756,6 +767,7 @@
       [fakeRun5],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
   }
@@ -764,7 +776,8 @@
     plugin: string,
     runs: CheckRun[],
     actions: Action[] = [],
-    links: Link[] = []
+    links: Link[] = [],
+    summaryMessage: string | undefined = undefined
   ) {
     const newRuns = this.runs.includes(runs[0]) ? [] : runs;
     this.checksModel.updateStateSetResults(
@@ -772,6 +785,7 @@
       newRuns,
       actions,
       links,
+      summaryMessage,
       ChecksPatchset.LATEST
     );
   }
@@ -843,7 +857,13 @@
         <gr-button
           link
           @click="${() =>
-            this.toggle('f0', [fakeRun0], fakeActions, fakeLinks)}"
+            this.toggle(
+              'f0',
+              [fakeRun0],
+              fakeActions,
+              fakeLinks,
+              'ETA: 1 min'
+            )}"
           >0</gr-button
         >
         <gr-button link @click="${() => this.toggle('f1', [fakeRun1])}"
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index ee40ad5..151556c 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -72,6 +72,7 @@
 import {notDeepEqual} from '../../../utils/deep-util';
 import {resolve} from '../../../models/dependency';
 import {commentsModelToken} from '../../../models/comments/comments-model';
+import {whenRendered} from '../../../utils/dom-util';
 
 const NEWLINE_PATTERN = /\n/g;
 
@@ -626,8 +627,11 @@
 
   override firstUpdated() {
     if (this.shouldScrollIntoView) {
-      this.commentBox?.focus();
-      this.scrollIntoView();
+      whenRendered(this, () => {
+        this.expandCollapseComments(false);
+        this.commentBox?.focus();
+        this.scrollIntoView({block: 'center'});
+      });
     }
   }
 
diff --git a/polygerrit-ui/app/services/checks/checks-model.ts b/polygerrit-ui/app/services/checks/checks-model.ts
index 2bdee51..e58e594 100644
--- a/polygerrit-ui/app/services/checks/checks-model.ts
+++ b/polygerrit-ui/app/services/checks/checks-model.ts
@@ -122,6 +122,7 @@
   errorMessage?: string;
   /** Presence of loginCallback implicitly means that the provider is in NOT_LOGGED_IN state. */
   loginCallback?: () => void;
+  summaryMessage?: string;
   runs: CheckRun[];
   actions: Action[];
   links: Link[];
@@ -239,6 +240,13 @@
     )
   );
 
+  public topLevelMessagesLatest$ = select(this.checksLatest$, state => {
+    const messages = Object.values(state).map(
+      providerState => providerState.summaryMessage
+    );
+    return messages.filter(m => m !== undefined) as string[];
+  });
+
   public topLevelActionsSelected$ = select(this.checksSelected$, state =>
     Object.values(state).reduce(
       (allActions: Action[], providerState: ChecksProviderState) => [
@@ -444,6 +452,7 @@
     runs: CheckRunApi[],
     actions: Action[] = [],
     links: Link[] = [],
+    summaryMessage: string | undefined,
     patchset: ChecksPatchset
   ) {
     const attemptMap = createAttemptMap(runs);
@@ -483,6 +492,7 @@
       }),
       actions: [...actions],
       links: [...links],
+      summaryMessage,
     };
     this.subject$.next(nextState);
   }
@@ -701,6 +711,7 @@
                 response.runs ?? [],
                 response.actions ?? [],
                 response.links ?? [],
+                response.summaryMessage,
                 patchset
               );
               break;
diff --git a/polygerrit-ui/app/services/checks/checks-model_test.ts b/polygerrit-ui/app/services/checks/checks-model_test.ts
index 1bb0f8a..c2588fe 100644
--- a/polygerrit-ui/app/services/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/services/checks/checks-model_test.ts
@@ -120,6 +120,7 @@
       RUNS,
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     assert.isFalse(current.loading);
@@ -132,6 +133,7 @@
       RUNS,
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     assert.isFalse(current.loading);
@@ -144,6 +146,7 @@
       RUNS,
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     assert.lengthOf(current.runs, 1);
@@ -156,6 +159,7 @@
       RUNS,
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     assert.equal(
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index b96ebe6..34f0bc1 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -490,3 +490,18 @@
   }
   return false;
 }
+
+/** Executes the given callback when the element's height is > 0. */
+export function whenRendered(el: HTMLElement, callback: () => void) {
+  if (el.clientHeight > 0) {
+    callback();
+    return;
+  }
+  const obs = new ResizeObserver(() => {
+    if (el.clientHeight > 0) {
+      callback();
+      obs.unobserve(el);
+    }
+  });
+  obs.observe(el);
+}