Merge "Move gr-keyboard-shorcuts-dialog to lit"
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/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);