Merge "Migrate config-model to new pattern"
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/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/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');