Merge "Submit Requirements - Do not show SR hovercard in dashboard"
diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
index aa32169..460ad60 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -79,6 +79,7 @@
           "/dashboard/*",
           "/groups/self",
           "/settings/*",
+          "/topic/*",
           "/Documentation/q/*");
 
   /**
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
index 43f6730..8ffc0aa 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
@@ -47,9 +47,12 @@
   QuickLabelInfo,
   Timestamp,
 } from '../../../types/common';
-import {hasOwnProperty} from '../../../utils/common-util';
+import {assertNever, hasOwnProperty} from '../../../utils/common-util';
 import {pluralize} from '../../../utils/string-util';
 import {ChangeStates} from '../../shared/gr-change-status/gr-change-status';
+import {KnownExperimentId} from '../../../services/flags/flags';
+import {getRequirements, iconForStatus} from '../../../utils/label-util';
+import {SubmitRequirementStatus} from '../../../api/rest-api';
 
 enum ChangeSize {
   XS = 10,
@@ -121,8 +124,20 @@
   @property({type: Array})
   _dynamicCellEndpoints?: string[];
 
+  @property({type: Boolean})
+  _isSubmitRequirementsUiEnabled = false;
+
   reporting: ReportingService = appContext.reportingService;
 
+  private readonly flagsService = appContext.flagsService;
+
+  override ready() {
+    super.ready();
+    this._isSubmitRequirementsUiEnabled = this.flagsService.isEnabled(
+      KnownExperimentId.SUBMIT_REQUIREMENTS_UI
+    );
+  }
+
   override connectedCallback() {
     super.connectedCallback();
     getPluginLoader()
@@ -167,8 +182,33 @@
   }
 
   _computeLabelClass(change: ChangeInfo | undefined, labelName: string) {
-    const category = this._computeLabelCategory(change, labelName);
     const classes = ['cell', 'label'];
+    if (this._isSubmitRequirementsUiEnabled) {
+      const requirements = getRequirements(change).filter(
+        sr => sr.name === labelName
+      );
+      if (requirements.length === 1) {
+        const status = requirements[0].status;
+        switch (status) {
+          case SubmitRequirementStatus.SATISFIED:
+            classes.push('u-green');
+            break;
+          case SubmitRequirementStatus.UNSATISFIED:
+            classes.push('u-red');
+            break;
+          case SubmitRequirementStatus.OVERRIDDEN:
+            classes.push('u-green');
+            break;
+          case SubmitRequirementStatus.NOT_APPLICABLE:
+            classes.push('u-gray-background');
+            break;
+          default:
+            assertNever(status, `Unsupported status: ${status}`);
+        }
+        return classes.sort().join(' ');
+      }
+    }
+    const category = this._computeLabelCategory(change, labelName);
     switch (category) {
       case LabelCategory.NOT_APPLICABLE:
         classes.push('u-gray-background');
@@ -196,6 +236,14 @@
   }
 
   _computeLabelIcon(change: ChangeInfo | undefined, labelName: string): string {
+    if (this._isSubmitRequirementsUiEnabled) {
+      const requirements = getRequirements(change).filter(
+        sr => sr.name === labelName
+      );
+      if (requirements.length === 1) {
+        return `gr-icons:${iconForStatus(requirements[0].status)}`;
+      }
+    }
     const category = this._computeLabelCategory(change, labelName);
     switch (category) {
       case LabelCategory.APPROVED:
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts
index 1ae0992..2a3936f 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts
@@ -25,6 +25,9 @@
 
 @customElement('gr-key-binding-display')
 export class GrKeyBindingDisplay extends LitElement {
+  @property({type: Array})
+  binding: string[][] = [];
+
   static override get styles() {
     return [
       css`
@@ -53,9 +56,6 @@
     return html`${items}`;
   }
 
-  @property({type: Array})
-  binding: string[][] = [];
-
   _computeModifiers(binding: string[]) {
     return binding.slice(0, binding.length - 1);
   }
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
index 8610999..2e51e64 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
@@ -16,15 +16,14 @@
  */
 import '../../shared/gr-button/gr-button';
 import '../gr-key-binding-display/gr-key-binding-display';
-import '../../../styles/shared-styles';
-import '../../../styles/gr-font-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-keyboard-shortcuts-dialog_html';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 import {
   ShortcutSection,
   SectionView,
 } from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
-import {property, customElement} from '@polymer/decorators';
 import {appContext} from '../../../services/app-context';
 import {ShortcutViewListener} from '../../../services/shortcuts/shortcuts-service';
 
@@ -40,11 +39,7 @@
 }
 
 @customElement('gr-keyboard-shortcuts-dialog')
-export class GrKeyboardShortcutsDialog extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
-
+export class GrKeyboardShortcutsDialog extends LitElement {
   /**
    * Fired when the user presses the close button.
    *
@@ -67,9 +62,107 @@
       this._onDirectoryUpdated(d);
   }
 
-  override ready() {
-    super.ready();
-    this._ensureAttribute('role', 'dialog');
+  static override get styles() {
+    return [
+      sharedStyles,
+      fontStyles,
+      css`
+        :host {
+          display: block;
+          max-height: 100vh;
+          overflow-y: auto;
+        }
+        header {
+          padding: var(--spacing-l);
+        }
+        main {
+          display: flex;
+          padding: 0 var(--spacing-xxl) var(--spacing-xxl);
+        }
+        .column {
+          flex: 50%;
+        }
+        header {
+          align-items: center;
+          border-bottom: 1px solid var(--border-color);
+          display: flex;
+          justify-content: space-between;
+        }
+        table caption {
+          font-weight: var(--font-weight-bold);
+          padding-top: var(--spacing-l);
+          text-align: left;
+        }
+        tr {
+          height: 32px;
+        }
+        td {
+          padding: var(--spacing-xs) 0;
+        }
+        td:first-child,
+        th:first-child {
+          padding-right: var(--spacing-m);
+          text-align: right;
+          width: 160px;
+          color: var(--deemphasized-text-color);
+        }
+        td:second-child {
+          min-width: 200px;
+        }
+        th {
+          color: var(--deemphasized-text-color);
+          text-align: left;
+        }
+        .header {
+          font-weight: var(--font-weight-bold);
+          padding-top: var(--spacing-l);
+        }
+        .modifier {
+          font-weight: var(--font-weight-normal);
+        }
+      `,
+    ];
+  }
+
+  override render() {
+    return html`<header>
+        <h3 class="heading-3">Keyboard shortcuts</h3>
+        <gr-button link="" @click=${this.handleCloseTap}>Close</gr-button>
+      </header>
+      <main>
+        <div class="column">
+          ${this._left?.map(section => this.renderSection(section))}
+        </div>
+        <div class="column">
+          ${this._right?.map(section => this.renderSection(section))}
+        </div>
+      </main>
+      <footer></footer>`;
+  }
+
+  private renderSection(section: SectionShortcut) {
+    return html`<table>
+      <caption>
+        ${section.section}
+      </caption>
+      <thead>
+        <tr>
+          <th>Key</th>
+          <th>Action</th>
+        </tr>
+      </thead>
+      <tbody>
+        ${section.shortcuts?.map(
+          shortcut => html`<tr>
+            <td>
+              <gr-key-binding-display .binding=${shortcut.binding}>
+              </gr-key-binding-display>
+            </td>
+            <td>${shortcut.text}</td>
+          </tr>`
+        )}
+      </tbody>
+    </table>`;
   }
 
   override connectedCallback() {
@@ -82,7 +175,7 @@
     super.disconnectedCallback();
   }
 
-  _handleCloseTap(e: MouseEvent) {
+  private handleCloseTap(e: MouseEvent) {
     e.preventDefault();
     e.stopPropagation();
     this.dispatchEvent(
@@ -142,7 +235,7 @@
       });
     }
 
-    this.set('_left', left);
-    this.set('_right', right);
+    this._right = right;
+    this._left = left;
   }
 }
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.ts b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.ts
deleted file mode 100644
index 4992daa..0000000
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
-  <style include="gr-font-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
-  </style>
-  <style include="shared-styles">
-    :host {
-      display: block;
-      max-height: 100vh;
-      overflow-y: auto;
-    }
-    header {
-      padding: var(--spacing-l);
-    }
-    main {
-      display: flex;
-      padding: 0 var(--spacing-xxl) var(--spacing-xxl);
-    }
-    .column {
-      flex: 50%;
-    }
-    header {
-      align-items: center;
-      border-bottom: 1px solid var(--border-color);
-      display: flex;
-      justify-content: space-between;
-    }
-    table caption {
-      font-weight: var(--font-weight-bold);
-      padding-top: var(--spacing-l);
-      text-align: left;
-    }
-    tr {
-      height: 32px;
-    }
-    td {
-      padding: var(--spacing-xs) 0;
-    }
-    td:first-child,
-    th:first-child {
-      padding-right: var(--spacing-m);
-      text-align: right;
-      width: 160px;
-      color: var(--deemphasized-text-color);
-    }
-    td:second-child {
-      min-width: 200px;
-    }
-    th {
-      color: var(--deemphasized-text-color);
-      text-align: left;
-    }
-    .header {
-      font-weight: var(--font-weight-bold);
-      padding-top: var(--spacing-l);
-    }
-    .modifier {
-      font-weight: var(--font-weight-normal);
-    }
-  </style>
-  <header>
-    <h3 class="heading-3">Keyboard shortcuts</h3>
-    <gr-button link="" on-click="_handleCloseTap">Close</gr-button>
-  </header>
-  <main>
-    <div class="column">
-      <template is="dom-repeat" items="[[_left]]">
-        <table>
-          <caption>
-            [[item.section]]
-          </caption>
-          <thead>
-            <tr>
-              <th>Key</th>
-              <th>Action</th>
-            </tr>
-          </thead>
-          <tbody>
-            <template is="dom-repeat" items="[[item.shortcuts]]" as="shortcut">
-              <tr>
-                <td>
-                  <gr-key-binding-display binding="[[shortcut.binding]]">
-                  </gr-key-binding-display>
-                </td>
-                <td>[[shortcut.text]]</td>
-              </tr>
-            </template>
-          </tbody>
-        </table>
-      </template>
-    </div>
-    <div class="column">
-      <template is="dom-repeat" items="[[_right]]">
-        <table>
-          <caption>
-            [[item.section]]
-          </caption>
-          <thead>
-            <tr>
-              <th>Key</th>
-              <th>Action</th>
-            </tr>
-          </thead>
-          <tbody>
-            <template is="dom-repeat" items="[[item.shortcuts]]" as="shortcut">
-              <tr>
-                <td>
-                  <gr-key-binding-display binding="[[shortcut.binding]]">
-                  </gr-key-binding-display>
-                </td>
-                <td>[[shortcut.text]]</td>
-              </tr>
-            </template>
-          </tbody>
-        </table>
-      </template>
-    </div>
-  </main>
-  <footer></footer>
-`;
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.ts b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.ts
index 2c76704..7fc52f5 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.ts
@@ -16,6 +16,7 @@
  */
 
 import '../../../test/common-test-setup-karma';
+import './gr-keyboard-shortcuts-dialog';
 import {GrKeyboardShortcutsDialog} from './gr-keyboard-shortcuts-dialog';
 import {
   SectionView,
@@ -27,8 +28,9 @@
 suite('gr-keyboard-shortcuts-dialog tests', () => {
   let element: GrKeyboardShortcutsDialog;
 
-  setup(() => {
+  setup(async () => {
     element = basicFixture.instantiate();
+    await flush();
   });
 
   function update(directory: Map<ShortcutSection, SectionView>) {
diff --git a/polygerrit-ui/app/services/comments/comments-service.ts b/polygerrit-ui/app/services/comments/comments-service.ts
index 5896b52..b5745a8 100644
--- a/polygerrit-ui/app/services/comments/comments-service.ts
+++ b/polygerrit-ui/app/services/comments/comments-service.ts
@@ -39,24 +39,45 @@
 export class CommentsService {
   private discardedDrafts?: UIDraft[] = [];
 
+  private changeNum?: NumericChangeId;
+
+  private patchNum?: PatchSetNum;
+
   constructor(readonly restApiService: RestApiService) {
     discardedDrafts$.subscribe(
       discardedDrafts => (this.discardedDrafts = discardedDrafts)
     );
     changeNum$.subscribe(changeNum => {
+      this.changeNum = changeNum;
       updateStateReset();
-      if (!changeNum) return;
-      this.reloadComments(changeNum);
-      this.reloadRobotComments(changeNum);
-      this.reloadDrafts(changeNum);
+      this.reloadAllComments();
     });
     combineLatest([changeNum$, currentPatchNum$]).subscribe(
-      ([changeNum, currentPatchNum]) => {
-        if (!changeNum || !currentPatchNum) return;
-        this.reloadPortedComments(changeNum, currentPatchNum);
-        this.reloadPortedDrafts(changeNum, currentPatchNum);
+      ([changeNum, patchNum]) => {
+        this.changeNum = changeNum;
+        this.patchNum = patchNum;
+        this.reloadAllPortedComments();
       }
     );
+    document.addEventListener('reload', () => {
+      this.reloadAllComments();
+      this.reloadAllPortedComments();
+    });
+  }
+
+  // Note that this does *not* reload ported comments.
+  reloadAllComments() {
+    if (!this.changeNum) return;
+    this.reloadComments(this.changeNum);
+    this.reloadRobotComments(this.changeNum);
+    this.reloadDrafts(this.changeNum);
+  }
+
+  reloadAllPortedComments() {
+    if (!this.changeNum) return;
+    if (!this.patchNum) return;
+    this.reloadPortedComments(this.changeNum, this.patchNum);
+    this.reloadPortedDrafts(this.changeNum, this.patchNum);
   }
 
   reloadComments(changeNum: NumericChangeId): Promise<void> {
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
index 3c9e058..18b321a 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
@@ -180,13 +180,13 @@
   Shortcut.CURSOR_NEXT_CHANGE,
   ShortcutSection.ACTIONS,
   'Select next change',
-  {key: 'j'}
+  {key: 'j', allowRepeat: true}
 );
 describe(
   Shortcut.CURSOR_PREV_CHANGE,
   ShortcutSection.ACTIONS,
   'Select previous change',
-  {key: 'k'}
+  {key: 'k', allowRepeat: true}
 );
 describe(
   Shortcut.OPEN_CHANGE,
@@ -316,15 +316,15 @@
   Shortcut.NEXT_LINE,
   ShortcutSection.DIFFS,
   'Go to next line',
-  {key: 'j'},
-  {key: Key.DOWN}
+  {key: 'j', allowRepeat: true},
+  {key: Key.DOWN, allowRepeat: true}
 );
 describe(
   Shortcut.PREV_LINE,
   ShortcutSection.DIFFS,
   'Go to previous line',
-  {key: 'k'},
-  {key: Key.UP}
+  {key: 'k', allowRepeat: true},
+  {key: Key.UP, allowRepeat: true}
 );
 describe(
   Shortcut.VISIBLE_LINE,
@@ -480,15 +480,15 @@
   Shortcut.CURSOR_NEXT_FILE,
   ShortcutSection.FILE_LIST,
   'Select next file',
-  {key: 'j'},
-  {key: Key.DOWN}
+  {key: 'j', allowRepeat: true},
+  {key: Key.DOWN, allowRepeat: true}
 );
 describe(
   Shortcut.CURSOR_PREV_FILE,
   ShortcutSection.FILE_LIST,
   'Select previous file',
-  {key: 'k'},
-  {key: Key.UP}
+  {key: 'k', allowRepeat: true},
+  {key: Key.UP, allowRepeat: true}
 );
 describe(
   Shortcut.OPEN_FILE,
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
index a26fa08..f2e9e98 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
@@ -137,7 +137,7 @@
     listener: (e: KeyboardEvent) => void
   ) {
     const wrappedListener = (e: KeyboardEvent) => {
-      if (e.repeat) return;
+      if (e.repeat && !shortcut.allowRepeat) return;
       if (!eventMatchesShortcut(e, shortcut)) return;
       if (shortcut.combo) {
         if (!this.isInSpecificComboKeyMode(shortcut.combo)) return;
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
index 05c4f53..a024159 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
@@ -213,7 +213,10 @@
           {
             shortcut: Shortcut.NEXT_LINE,
             text: 'Go to next line',
-            bindings: [{key: 'j'}, {key: 'ArrowDown'}],
+            bindings: [
+              {allowRepeat: true, key: 'j'},
+              {allowRepeat: true, key: 'ArrowDown'},
+            ],
           },
         ],
         [ShortcutSection.NAVIGATION]: [
@@ -234,7 +237,10 @@
           {
             shortcut: Shortcut.NEXT_LINE,
             text: 'Go to next line',
-            bindings: [{key: 'j'}, {key: 'ArrowDown'}],
+            bindings: [
+              {allowRepeat: true, key: 'j'},
+              {allowRepeat: true, key: 'ArrowDown'},
+            ],
           },
         ],
         [ShortcutSection.EVERYWHERE]: [
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index e2fa8fe..bd0f742 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -338,6 +338,8 @@
   combo?: ComboKey;
   /** Defaults to no modifiers. */
   modifiers?: Modifier[];
+  /** Defaults to false. If true, then `event.repeat === true` is allowed. */
+  allowRepeat?: boolean;
 }
 
 const ALPHA_NUM = new RegExp(/^[A-Za-z0-9]$/);
@@ -406,7 +408,7 @@
   }
 ) {
   const wrappedListener = (e: KeyboardEvent) => {
-    if (e.repeat) return;
+    if (e.repeat && !shortcut.allowRepeat) return;
     if (options.shouldSuppress && shouldSuppress(e)) return;
     if (eventMatchesShortcut(e, shortcut)) {
       listener(e);