Merge "Increase hover delay for token highlight"
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
index 037e11f..88ade26 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
@@ -29,7 +29,6 @@
 } from '../../../types/common';
 import {InheritedBooleanInfoConfiguredValue} from '../../../constants/constants';
 import {getAppContext} from '../../../services/app-context';
-import {serverConfig$} from '../../../services/config/config-model';
 import {formStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {LitElement, PropertyValues, css, html} from 'lit';
@@ -77,6 +76,8 @@
 
   private readonly restApiService = getAppContext().restApiService;
 
+  private readonly configModel = getAppContext().configModel;
+
   constructor() {
     super();
     this.query = (input: string) => this.getRepoBranchesSuggestions(input);
@@ -86,7 +87,7 @@
     super.connectedCallback();
     if (!this.repoName) return;
 
-    subscribe(this, serverConfig$, config => {
+    subscribe(this, this.configModel.serverConfig$, config => {
       this.privateChangesEnabled =
         config?.change?.disable_private_changes ?? false;
     });
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 64c0106..7ef4487 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
@@ -314,10 +314,14 @@
   }
 
   renderOverrideLabels(requirement: SubmitRequirementResultInfo) {
+    if (requirement.status !== SubmitRequirementStatus.OVERRIDDEN) return;
     const requirementLabels = extractAssociatedLabels(
       requirement,
       'onlyOverride'
-    );
+    ).filter(label => {
+      const allLabels = this.change?.labels ?? {};
+      return hasVotes(allLabels[label]);
+    });
     return requirementLabels.map(
       label => html`<span class="overrideLabel">${label}</span>`
     );
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 342f54b..ca53d28 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -64,7 +64,6 @@
 } from '../../types/common';
 import {labels$, latestPatchNum$} from '../../services/change/change-model';
 import {getAppContext} from '../../services/app-context';
-import {repoConfig$} from '../../services/config/config-model';
 import {spinnerStyles} from '../../styles/gr-spinner-styles';
 import {
   getLabelStatus,
@@ -540,6 +539,8 @@
 
   private changeService = getAppContext().changeService;
 
+  private configModel = getAppContext().configModel;
+
   static override get styles() {
     return [
       sharedStyles,
@@ -563,7 +564,7 @@
 
   constructor() {
     super();
-    subscribe(this, repoConfig$, x => (this.repoConfig = x));
+    subscribe(this, this.configModel.repoConfig$, x => (this.repoConfig = x));
   }
 
   override render() {
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
index 1391257..be36640 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -37,8 +37,6 @@
 import {AuthType} from '../../../constants/constants';
 import {DropdownLink} from '../../shared/gr-dropdown/gr-dropdown';
 import {getAppContext} from '../../../services/app-context';
-import {serverConfig$} from '../../../services/config/config-model';
-import {assertIsDefined} from '../../../utils/common-util';
 
 type MainHeaderLink = RequireProperties<DropdownLink, 'url' | 'name'>;
 
@@ -160,6 +158,8 @@
 
   private readonly userModel = getAppContext().userModel;
 
+  private readonly configModel = getAppContext().configModel;
+
   private subscriptions: Subscription[] = [];
 
   override ready() {
@@ -168,11 +168,6 @@
   }
 
   override connectedCallback() {
-    // TODO(brohlfs): This just ensures that the userModel is instantiated at
-    // all. We need the service to manage the model, but we are not making any
-    // direct calls. Will need to find a better solution to this problem ...
-    assertIsDefined(this.userModel);
-
     super.connectedCallback();
     this._loadAccount();
 
@@ -187,7 +182,7 @@
         })
     );
     this.subscriptions.push(
-      serverConfig$.subscribe(config => {
+      this.configModel.serverConfig$.subscribe(config => {
         if (!config) return;
         this._retrieveFeedbackURL(config);
         this._retrieveRegisterURL(config);
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index c89fe20..ebfe407 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -235,10 +235,6 @@
 
   constructor() {
     super();
-    // We just want to instantiate this service somewhere. It is reacting to
-    // model changes and updates the config model, but at the moment the service
-    // is not called from anywhere.
-    getAppContext().configService;
     document.addEventListener(EventType.PAGE_ERROR, e => {
       this._handlePageError(e);
     });
diff --git a/polygerrit-ui/app/elements/topic/gr-topic-tree-project.ts b/polygerrit-ui/app/elements/topic/gr-topic-tree-repo.ts
similarity index 76%
rename from polygerrit-ui/app/elements/topic/gr-topic-tree-project.ts
rename to polygerrit-ui/app/elements/topic/gr-topic-tree-repo.ts
index c3fed91..d5bf096 100644
--- a/polygerrit-ui/app/elements/topic/gr-topic-tree-project.ts
+++ b/polygerrit-ui/app/elements/topic/gr-topic-tree-repo.ts
@@ -19,26 +19,26 @@
 import {customElement, property} from 'lit/decorators';
 import {LitElement, html} from 'lit-element/lit-element';
 import '../shared/gr-button/gr-button';
-import {ChangeInfo} from '../../api/rest-api';
+import {ChangeInfo, RepoName} from '../../api/rest-api';
 
 /**
- * A view of changes that all belong to the same project.
+ * A view of changes that all belong to the same repository.
  */
-@customElement('gr-topic-tree-project')
-export class GrTopicTreeProject extends LitElement {
+@customElement('gr-topic-tree-repo')
+export class GrTopicTreeRepo extends LitElement {
   @property({type: String})
-  projectName?: string;
+  repoName?: RepoName;
 
   @property({type: Array})
   changes?: ChangeInfo[];
 
   override render() {
-    if (this.projectName === undefined || this.changes === undefined) {
+    if (this.repoName === undefined || this.changes === undefined) {
       return;
     }
-    // TODO: Groups of related changes should be separated within the project.
+    // TODO: Groups of related changes should be separated within the repository.
     return html`
-      <h2>Project ${this.projectName}</h2>
+      <h2>Repo ${this.repoName}</h2>
       ${this.changes.map(change => this.renderTreeRow(change))}
     `;
   }
@@ -50,6 +50,6 @@
 
 declare global {
   interface HTMLElementTagNameMap {
-    'gr-topic-tree-project': GrTopicTreeProject;
+    'gr-topic-tree-repo': GrTopicTreeRepo;
   }
 }
diff --git a/polygerrit-ui/app/elements/topic/gr-topic-tree-project_test.ts b/polygerrit-ui/app/elements/topic/gr-topic-tree-repo_test.ts
similarity index 69%
rename from polygerrit-ui/app/elements/topic/gr-topic-tree-project_test.ts
rename to polygerrit-ui/app/elements/topic/gr-topic-tree-repo_test.ts
index 39398bd..2e903b5 100644
--- a/polygerrit-ui/app/elements/topic/gr-topic-tree-project_test.ts
+++ b/polygerrit-ui/app/elements/topic/gr-topic-tree-repo_test.ts
@@ -15,27 +15,28 @@
  * limitations under the License.
  */
 
+import {RepoName} from '../../api/rest-api';
 import '../../test/common-test-setup-karma';
 import {createChange} from '../../test/test-data-generators';
 import {queryAndAssert} from '../../test/test-utils';
-import './gr-topic-tree-project';
-import {GrTopicTreeProject} from './gr-topic-tree-project';
+import './gr-topic-tree-repo';
+import {GrTopicTreeRepo} from './gr-topic-tree-repo';
 
-const basicFixture = fixtureFromElement('gr-topic-tree-project');
-const projectName = 'myProject';
+const basicFixture = fixtureFromElement('gr-topic-tree-repo');
+const repoName = 'myRepo' as RepoName;
 
-suite('gr-topic-tree-project tests', () => {
-  let element: GrTopicTreeProject;
+suite('gr-topic-tree-repo tests', () => {
+  let element: GrTopicTreeRepo;
 
   setup(async () => {
     element = basicFixture.instantiate();
-    element.projectName = projectName;
+    element.repoName = repoName;
     element.changes = [createChange()];
     await element.updateComplete;
   });
 
-  test('shows project name', () => {
+  test('shows repository name', () => {
     const heading = queryAndAssert<HTMLHeadingElement>(element, 'h2');
-    assert.equal(heading.textContent, `Project ${projectName}`);
+    assert.equal(heading.textContent, `Repo ${repoName}`);
   });
 });
diff --git a/polygerrit-ui/app/elements/topic/gr-topic-tree.ts b/polygerrit-ui/app/elements/topic/gr-topic-tree.ts
index da67e26..f5140f6 100644
--- a/polygerrit-ui/app/elements/topic/gr-topic-tree.ts
+++ b/polygerrit-ui/app/elements/topic/gr-topic-tree.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import './gr-topic-tree-project';
+import './gr-topic-tree-repo';
 import {customElement, property, state} from 'lit/decorators';
 import {LitElement, html, PropertyValues} from 'lit-element/lit-element';
 import {getAppContext} from '../../services/app-context';
@@ -24,7 +24,7 @@
 
 /**
  * A tree-like dashboard showing changes related to a topic, organized by
- * project.
+ * repository.
  */
 @customElement('gr-topic-tree')
 export class GrTopicTree extends LitElement {
@@ -32,7 +32,7 @@
   topicName?: string;
 
   @state()
-  private changesByProject = new Map<RepoName, ChangeInfo[]>();
+  private changesByRepo = new Map<RepoName, ChangeInfo[]>();
 
   private restApiService = getAppContext().restApiService;
 
@@ -45,17 +45,17 @@
 
   override render() {
     // TODO: organize into <table> for column alignment.
-    return Array.from(this.changesByProject).map(([projectName, changes]) =>
-      this.renderProjectSection(projectName, changes)
+    return Array.from(this.changesByRepo).map(([repoName, changes]) =>
+      this.renderRepoSection(repoName, changes)
     );
   }
 
-  private renderProjectSection(projectName: RepoName, changes: ChangeInfo[]) {
+  private renderRepoSection(repoName: RepoName, changes: ChangeInfo[]) {
     return html`
-      <gr-topic-tree-project
-        .projectName=${projectName}
+      <gr-topic-tree-repo
+        .repoName=${repoName}
         .changes=${changes}
-      ></gr-topic-tree-project>
+      ></gr-topic-tree-repo>
     `;
   }
 
@@ -67,12 +67,12 @@
     if (!changes) {
       return;
     }
-    this.changesByProject.clear();
+    this.changesByRepo.clear();
     for (const change of changes) {
-      if (this.changesByProject.has(change.project)) {
-        this.changesByProject.get(change.project)!.push(change);
+      if (this.changesByRepo.has(change.project)) {
+        this.changesByRepo.get(change.project)!.push(change);
       } else {
-        this.changesByProject.set(change.project, [change]);
+        this.changesByRepo.set(change.project, [change]);
       }
     }
     this.requestUpdate();
diff --git a/polygerrit-ui/app/elements/topic/gr-topic-tree_test.ts b/polygerrit-ui/app/elements/topic/gr-topic-tree_test.ts
index f214520..a2873d9 100644
--- a/polygerrit-ui/app/elements/topic/gr-topic-tree_test.ts
+++ b/polygerrit-ui/app/elements/topic/gr-topic-tree_test.ts
@@ -21,49 +21,53 @@
 import {queryAll, stubRestApi} from '../../test/test-utils';
 import './gr-topic-tree';
 import {GrTopicTree} from './gr-topic-tree';
-import {GrTopicTreeProject} from './gr-topic-tree-project';
+import {GrTopicTreeRepo} from './gr-topic-tree-repo';
 
 const basicFixture = fixtureFromElement('gr-topic-tree');
 
-function createChangeForProject(projectName: string): ChangeInfo {
-  return {...createChange(), project: projectName as RepoName};
+const repo1Name = 'repo1' as RepoName;
+const repo2Name = 'repo2' as RepoName;
+const repo3Name = 'repo3' as RepoName;
+
+function createChangeForRepo(repoName: string): ChangeInfo {
+  return {...createChange(), project: repoName as RepoName};
 }
 
 suite('gr-topic-tree tests', () => {
   let element: GrTopicTree;
-  const project1Changes = [
-    createChangeForProject('project1'),
-    createChangeForProject('project1'),
+  const repo1Changes = [
+    createChangeForRepo(repo1Name),
+    createChangeForRepo(repo1Name),
   ];
-  const project2Changes = [
-    createChangeForProject('project2'),
-    createChangeForProject('project2'),
+  const repo2Changes = [
+    createChangeForRepo(repo2Name),
+    createChangeForRepo(repo2Name),
   ];
-  const project3Changes = [
-    createChangeForProject('project3'),
-    createChangeForProject('project3'),
+  const repo3Changes = [
+    createChangeForRepo(repo3Name),
+    createChangeForRepo(repo3Name),
   ];
 
   setup(async () => {
     stubRestApi('getChanges')
       .withArgs(undefined, 'topic:myTopic')
-      .resolves([...project1Changes, ...project2Changes, ...project3Changes]);
+      .resolves([...repo1Changes, ...repo2Changes, ...repo3Changes]);
     element = basicFixture.instantiate();
     element.topicName = 'myTopic';
     await element.updateComplete;
   });
 
-  test('groups changes by project', () => {
-    const projectSections = queryAll<GrTopicTreeProject>(
+  test('groups changes by repo', () => {
+    const repoSections = queryAll<GrTopicTreeRepo>(
       element,
-      'gr-topic-tree-project'
+      'gr-topic-tree-repo'
     );
-    assert.lengthOf(projectSections, 3);
-    assert.equal(projectSections[0].projectName, 'project1');
-    assert.sameMembers(projectSections[0].changes!, project1Changes);
-    assert.equal(projectSections[1].projectName, 'project2');
-    assert.sameMembers(projectSections[1].changes!, project2Changes);
-    assert.equal(projectSections[2].projectName, 'project3');
-    assert.sameMembers(projectSections[2].changes!, project3Changes);
+    assert.lengthOf(repoSections, 3);
+    assert.equal(repoSections[0].repoName, repo1Name);
+    assert.sameMembers(repoSections[0].changes!, repo1Changes);
+    assert.equal(repoSections[1].repoName, repo2Name);
+    assert.sameMembers(repoSections[1].changes!, repo2Changes);
+    assert.equal(repoSections[2].repoName, repo3Name);
+    assert.sameMembers(repoSections[2].changes!, repo3Changes);
   });
 });
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
index 0297795..17e0994 100644
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
@@ -92,8 +92,8 @@
     storageService: (_ctx: Partial<AppContext>) => {
       throw new Error('storageService is not implemented');
     },
-    configService: (_ctx: Partial<AppContext>) => {
-      throw new Error('configService is not implemented');
+    configModel: (_ctx: Partial<AppContext>) => {
+      throw new Error('configModel is not implemented');
     },
     userModel: (_ctx: Partial<AppContext>) => {
       throw new Error('userModel is not implemented');
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 001b71d..bfc56b4 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -25,12 +25,12 @@
 import {ChecksService} from './checks/checks-service';
 import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element';
 import {GrStorageService} from './storage/gr-storage_impl';
-import {ConfigService} from './config/config-service';
 import {UserModel} from './user/user-model';
 import {CommentsService} from './comments/comments-service';
 import {ShortcutsService} from './shortcuts/shortcuts-service';
 import {BrowserModel} from './browser/browser-model';
 import {assertIsDefined} from '../utils/common-util';
+import {ConfigModel} from './config/config-model';
 
 /**
  * The AppContext lazy initializator for all services
@@ -70,9 +70,9 @@
       return new GrJsApiInterface(ctx.reportingService!);
     },
     storageService: (_ctx: Partial<AppContext>) => new GrStorageService(),
-    configService: (ctx: Partial<AppContext>) => {
+    configModel: (ctx: Partial<AppContext>) => {
       assertIsDefined(ctx.restApiService, 'restApiService');
-      return new ConfigService(ctx.restApiService!);
+      return new ConfigModel(ctx.restApiService!);
     },
     userModel: (ctx: Partial<AppContext>) => {
       assertIsDefined(ctx.restApiService, 'restApiService');
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts
index d5e595d..53064fa 100644
--- a/polygerrit-ui/app/services/app-context.ts
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -24,11 +24,11 @@
 import {ChecksService} from './checks/checks-service';
 import {JsApiService} from '../elements/shared/gr-js-api-interface/gr-js-api-types';
 import {StorageService} from './storage/gr-storage';
-import {ConfigService} from './config/config-service';
 import {UserModel} from './user/user-model';
 import {CommentsService} from './comments/comments-service';
 import {ShortcutsService} from './shortcuts/shortcuts-service';
 import {BrowserModel} from './browser/browser-model';
+import {ConfigModel} from './config/config-model';
 
 export interface AppContext {
   flagsService: FlagsService;
@@ -41,7 +41,7 @@
   checksService: ChecksService;
   jsApiService: JsApiService;
   storageService: StorageService;
-  configService: ConfigService;
+  configModel: ConfigModel;
   userModel: UserModel;
   browserModel: BrowserModel;
   shortcutsService: ShortcutsService;
diff --git a/polygerrit-ui/app/services/config/config-model.ts b/polygerrit-ui/app/services/config/config-model.ts
index f5e10c5..c0e6028 100644
--- a/polygerrit-ui/app/services/config/config-model.ts
+++ b/polygerrit-ui/app/services/config/config-model.ts
@@ -14,40 +14,74 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {ConfigInfo, ServerInfo} from '../../types/common';
-import {BehaviorSubject, Observable} from 'rxjs';
-import {map, distinctUntilChanged} from 'rxjs/operators';
+import {ConfigInfo, RepoName, ServerInfo} from '../../types/common';
+import {BehaviorSubject, from, Observable, of, Subscription} from 'rxjs';
+import {switchMap} from 'rxjs/operators';
+import {Finalizable} from '../registry';
+import {RestApiService} from '../gr-rest-api/gr-rest-api';
+import {repo$} from '../change/change-model';
+import {select} from '../../utils/observable-util';
 
-interface ConfigState {
+export interface ConfigState {
   repoConfig?: ConfigInfo;
   serverConfig?: ServerInfo;
 }
 
-// TODO: Figure out how to best enforce immutability of all states. Use Immer?
-// Use DeepReadOnly?
-const initialState: ConfigState = {};
+export class ConfigModel implements Finalizable {
+  // TODO: Figure out how to best enforce immutability of all states. Use Immer?
+  // Use DeepReadOnly?
+  private initialState: ConfigState = {};
 
-const privateState$ = new BehaviorSubject(initialState);
+  private privateState$ = new BehaviorSubject(this.initialState);
 
-// Re-exporting as Observable so that you can only subscribe, but not emit.
-export const configState$: Observable<ConfigState> = privateState$;
+  // Re-exporting as Observable so that you can only subscribe, but not emit.
+  public configState$: Observable<ConfigState> =
+    this.privateState$.asObservable();
 
-export function updateRepoConfig(repoConfig?: ConfigInfo) {
-  const current = privateState$.getValue();
-  privateState$.next({...current, repoConfig});
+  public repoConfig$ = select(
+    this.privateState$,
+    configState => configState.repoConfig
+  );
+
+  public serverConfig$ = select(
+    this.privateState$,
+    configState => configState.serverConfig
+  );
+
+  private subscriptions: Subscription[];
+
+  constructor(readonly restApiService: RestApiService) {
+    this.subscriptions = [
+      from(this.restApiService.getConfig()).subscribe((config?: ServerInfo) => {
+        this.updateServerConfig(config);
+      }),
+      repo$
+        .pipe(
+          switchMap((repo?: RepoName) => {
+            if (repo === undefined) return of(undefined);
+            return from(this.restApiService.getProjectConfig(repo));
+          })
+        )
+        .subscribe((repoConfig?: ConfigInfo) => {
+          this.updateRepoConfig(repoConfig);
+        }),
+    ];
+  }
+
+  updateRepoConfig(repoConfig?: ConfigInfo) {
+    const current = this.privateState$.getValue();
+    this.privateState$.next({...current, repoConfig});
+  }
+
+  updateServerConfig(serverConfig?: ServerInfo) {
+    const current = this.privateState$.getValue();
+    this.privateState$.next({...current, serverConfig});
+  }
+
+  finalize() {
+    for (const s of this.subscriptions) {
+      s.unsubscribe();
+    }
+    this.subscriptions = [];
+  }
 }
-
-export function updateServerConfig(serverConfig?: ServerInfo) {
-  const current = privateState$.getValue();
-  privateState$.next({...current, serverConfig});
-}
-
-export const repoConfig$ = configState$.pipe(
-  map(configState => configState.repoConfig),
-  distinctUntilChanged()
-);
-
-export const serverConfig$ = configState$.pipe(
-  map(configState => configState.serverConfig),
-  distinctUntilChanged()
-);
diff --git a/polygerrit-ui/app/services/config/config-service.ts b/polygerrit-ui/app/services/config/config-service.ts
deleted file mode 100644
index 667f347..0000000
--- a/polygerrit-ui/app/services/config/config-service.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * @license
- * Copyright (C) 2021 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 {updateRepoConfig, updateServerConfig} from './config-model';
-import {repo$} from '../change/change-model';
-import {switchMap} from 'rxjs/operators';
-import {ConfigInfo, RepoName, ServerInfo} from '../../types/common';
-import {from, of, Subscription} from 'rxjs';
-import {RestApiService} from '../gr-rest-api/gr-rest-api';
-import {Finalizable} from '../registry';
-
-export class ConfigService implements Finalizable {
-  private readonly subscriptions: Subscription[] = [];
-
-  constructor(readonly restApiService: RestApiService) {
-    this.subscriptions.push(
-      from(this.restApiService.getConfig()).subscribe((config?: ServerInfo) => {
-        updateServerConfig(config);
-      })
-    );
-    this.subscriptions.push(
-      repo$
-        .pipe(
-          switchMap((repo?: RepoName) => {
-            if (repo === undefined) return of(undefined);
-            return from(this.restApiService.getProjectConfig(repo));
-          })
-        )
-        .subscribe((repoConfig?: ConfigInfo) => {
-          updateRepoConfig(repoConfig);
-        })
-    );
-  }
-
-  finalize() {
-    for (const s of this.subscriptions) {
-      s.unsubscribe();
-    }
-    this.subscriptions.splice(0, this.subscriptions.length);
-  }
-}
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index a710b00..8bcd395b 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -28,11 +28,11 @@
 import {ChangeService} from '../services/change/change-service';
 import {ChecksService} from '../services/checks/checks-service';
 import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element';
-import {ConfigService} from '../services/config/config-service';
 import {UserModel} from '../services/user/user-model';
 import {CommentsService} from '../services/comments/comments-service';
 import {ShortcutsService} from '../services/shortcuts/shortcuts-service';
 import {BrowserModel} from '../services/browser/browser-model';
+import {ConfigModel} from '../services/config/config-model';
 
 let appContext: (AppContext & Finalizable) | undefined;
 
@@ -64,9 +64,9 @@
       return new GrJsApiInterface(ctx.reportingService!);
     },
     storageService: (_ctx: Partial<AppContext>) => grStorageMock,
-    configService: (ctx: Partial<AppContext>) => {
+    configModel: (ctx: Partial<AppContext>) => {
       assertIsDefined(ctx.restApiService, 'restApiService');
-      return new ConfigService(ctx.restApiService!);
+      return new ConfigModel(ctx.restApiService!);
     },
     userModel: (ctx: Partial<AppContext>) => {
       assertIsDefined(ctx.restApiService, 'restApiService');
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index bd0f742..2c38276 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -454,7 +454,10 @@
     // Suppress shortcuts if the key is 'enter'
     // and target is an anchor or button or paper-tab.
     (e.keyCode === 13 &&
-      (tagName === 'A' || tagName === 'BUTTON' || tagName === 'PAPER-TAB'))
+      (tagName === 'A' ||
+        tagName === 'BUTTON' ||
+        tagName === 'GR-BUTTON' ||
+        tagName === 'PAPER-TAB'))
   ) {
     return true;
   }
diff --git a/polygerrit-ui/app/utils/dom-util_test.ts b/polygerrit-ui/app/utils/dom-util_test.ts
index e139805..28157d9 100644
--- a/polygerrit-ui/app/utils/dom-util_test.ts
+++ b/polygerrit-ui/app/utils/dom-util_test.ts
@@ -326,6 +326,15 @@
       });
     });
 
+    test('suppress "enter" shortcut event from <gr-button>', async () => {
+      await keyEventOn(
+        document.createElement('gr-button'),
+        e => assert.isTrue(shouldSuppress(e)),
+        13,
+        'enter'
+      );
+    });
+
     test('suppress "enter" shortcut event from <a>', async () => {
       await keyEventOn(document.createElement('a'), e => {
         assert.isFalse(shouldSuppress(e));