Migrate the UserService/Model to not be a singleton - Rename userService to userModel on appContext - Merge user-model.ts and user-service.ts - Rename UserService to UserModel - Move all observables onto UserModel - Inject UserModel in the models/services that were directly accessing the observables Google-Bug-Id: b/206459178, b/207628953 Change-Id: Icf4f81c877efb58e289678a7331bde7254142a58
diff --git a/polygerrit-ui/FE_Style_Guide.md b/polygerrit-ui/FE_Style_Guide.md index f5bbf00..6673cdf 100644 --- a/polygerrit-ui/FE_Style_Guide.md +++ b/polygerrit-ui/FE_Style_Guide.md
@@ -187,11 +187,11 @@ export class MyCustomElement extends ...{ constructor() { super(); //This is mandatory to call parent constructor - this._userService = appContext.userService; + this._userModel = appContext.userModel; } //... _getUserName() { - return this._userService.activeUserName(); + return this._userModel.activeUserName(); } } ``` @@ -203,12 +203,12 @@ export class MyCustomElement extends ...{ created() { // Incorrect: assign all dependencies in the constructor - this._userService = appContext.userService; + this._userModel = appContext.userModel; } //... _getUserName() { // Incorrect: use appContext outside of a constructor - return appContext.userService.activeUserName(); + return appContext.userModel.activeUserName(); } } ``` @@ -237,7 +237,7 @@ constructor() { super(); // Assign services here - this._userService = appContext.userService; + this._userModel = appContext.userModel; // Code from the created method - put it before existing actions in constructor createdAction1(); createdAction2();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts index f309171..1076990 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts +++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
@@ -50,7 +50,6 @@ import {deepClone} from '../../../utils/deep-util'; import {LitElement, PropertyValues, css, html} from 'lit'; import {customElement, property, state} from 'lit/decorators'; -import {preferences$} from '../../../services/user/user-model'; import {subscribe} from '../../lit/subscription-controller'; const STATES = { @@ -122,11 +121,13 @@ @state() private pluginConfigChanged = false; + private readonly userModel = getAppContext().userModel; + private readonly restApiService = getAppContext().restApiService; constructor() { super(); - subscribe(this, preferences$, prefs => { + subscribe(this, this.userModel.preferences$, prefs => { if (prefs?.download_scheme) { // Note (issue 5180): normalize the download scheme with lower-case. this.selectedScheme = prefs.download_scheme.toLowerCase();
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 0df3a3d..f59fdec 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
@@ -65,7 +65,6 @@ import {modifierPressed} from '../../../utils/dom-util'; import {DropdownLink} from '../../shared/gr-dropdown/gr-dropdown'; import {fontStyles} from '../../../styles/gr-font-styles'; -import {account$} from '../../../services/user/user-model'; import { changeComments$, threads$, @@ -412,6 +411,8 @@ private showAllChips = new Map<RunStatus | Category, boolean>(); + private userModel = getAppContext().userModel; + private checksService = getAppContext().checksService; constructor() { @@ -428,7 +429,7 @@ subscribe(this, topLevelActionsLatest$, x => (this.actions = x)); subscribe(this, changeComments$, x => (this.changeComments = x)); subscribe(this, threads$, x => (this.commentThreads = x)); - subscribe(this, account$, x => (this.selfAccount = x)); + subscribe(this, this.userModel.account$, x => (this.selfAccount = x)); } static override get styles() {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts index 7d8bb86..8b530f6 100644 --- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -202,7 +202,6 @@ hasAttention, } from '../../../utils/attention-set-util'; import {listen} from '../../../services/shortcuts/shortcuts-service'; -import {preferenceDiffViewMode$} from '../../../services/user/user-model'; import {change$, changeLoading$} from '../../../services/change/change-model'; const MIN_LINES_FOR_COMMIT_COLLAPSE = 18; @@ -562,7 +561,8 @@ readonly restApiService = getAppContext().restApiService; - private readonly userService = getAppContext().userService; + // Private but used in tests. + readonly userModel = getAppContext().userModel; private readonly commentsService = getAppContext().commentsService; @@ -648,7 +648,7 @@ }) ); this.subscriptions.push( - preferenceDiffViewMode$.subscribe(diffViewMode => { + this.userModel.preferenceDiffViewMode$.subscribe(diffViewMode => { this.diffViewMode = diffViewMode; }) ); @@ -790,9 +790,9 @@ _handleToggleDiffMode() { if (this.diffViewMode === DiffViewMode.SIDE_BY_SIDE) { - this.userService.updatePreferences({diff_view: DiffViewMode.UNIFIED}); + this.userModel.updatePreferences({diff_view: DiffViewMode.UNIFIED}); } else { - this.userService.updatePreferences({ + this.userModel.updatePreferences({ diff_view: DiffViewMode.SIDE_BY_SIDE, }); }
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts index 6104356..67599ad 100644 --- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -27,7 +27,6 @@ MessageTag, PrimaryTab, createDefaultPreferences, - createDefaultDiffPrefs, } from '../../../constants/constants'; import {GrEditConstants} from '../../edit/gr-edit-constants'; import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints'; @@ -107,7 +106,6 @@ import {ParsedChangeInfo} from '../../../types/types'; import {GrRelatedChangesList} from '../gr-related-changes-list/gr-related-changes-list'; import {ChangeStates} from '../../shared/gr-change-status/gr-change-status'; -import {_testOnly_setState as setUserState} from '../../../services/user/user-model'; import {_testOnly_setState as setChangeState} from '../../../services/change/change-model'; import {FocusTarget, GrReplyDialog} from '../gr-reply-dialog/gr-reply-dialog'; import {GrOverlay} from '../../shared/gr-overlay/gr-overlay'; @@ -818,10 +816,7 @@ ...createDefaultPreferences(), diff_view: DiffViewMode.SIDE_BY_SIDE, }; - setUserState({ - preferences: prefs, - diffPreferences: createDefaultDiffPrefs(), - }); + element.userModel.setPreferences(prefs); element._handleToggleDiffMode(); assert.isTrue( updatePreferencesStub.calledWith({diff_view: DiffViewMode.UNIFIED}) @@ -831,10 +826,7 @@ ...createDefaultPreferences(), diff_view: DiffViewMode.UNIFIED, }; - setUserState({ - preferences: newPrefs, - diffPreferences: createDefaultDiffPrefs(), - }); + element.userModel.setPreferences(newPrefs); await flush(); element._handleToggleDiffMode(); assert.isTrue( @@ -1745,6 +1737,7 @@ '#replyDialog' ); const openSpy = sinon.spy(dialog, 'open'); + await flush(); await waitUntil(() => openSpy.called && !!openSpy.lastCall.args[1]); assert.equal(openSpy.lastCall.args[1], '> quote text\n\n'); });
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts index f400814..f286098 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts +++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -83,12 +83,9 @@ import {ParsedChangeInfo, PatchSetFile} from '../../../types/types'; import {Timing} from '../../../constants/reporting'; import {RevisionInfo} from '../../shared/revision-info/revision-info'; -import { - diffPreferences$, - sizeBarInChangeTable$, -} from '../../../services/user/user-model'; import {changeComments$} from '../../../services/comments/comments-model'; import {listen} from '../../../services/shortcuts/shortcuts-service'; +import {select} from '../../../utils/observable-util'; export const DEFAULT_NUM_FILES_SHOWN = 200; @@ -317,7 +314,7 @@ private readonly restApiService = getAppContext().restApiService; - private readonly userService = getAppContext().userService; + private readonly userModel = getAppContext().userModel; private readonly browserModel = getAppContext().browserModel; @@ -377,26 +374,23 @@ override connectedCallback() { super.connectedCallback(); - this.subscriptions.push( + this.subscriptions = [ changeComments$.subscribe(changeComments => { this.changeComments = changeComments; - }) - ); - this.subscriptions.push( + }), this.browserModel.diffViewMode$.subscribe( diffView => (this.diffViewMode = diffView) - ) - ); - this.subscriptions.push( - diffPreferences$.subscribe(diffPreferences => { + ), + this.userModel.diffPreferences$.subscribe(diffPreferences => { this.diffPrefs = diffPreferences; - }) - ); - this.subscriptions.push( - sizeBarInChangeTable$.subscribe(sizeBarInChangeTable => { + }), + select( + this.userModel.preferences$, + prefs => !!prefs?.size_bar_in_change_table + ).subscribe(sizeBarInChangeTable => { this._showSizeBars = sizeBarInChangeTable; - }) - ); + }), + ]; getPluginLoader() .awaitPluginsLoaded() @@ -1648,7 +1642,7 @@ } _handleReloadingDiffPreference() { - this.userService.getDiffPreferences(); + this.userModel.getDiffPreferences(); } /**
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts index bd51aab..cedddec 100644 --- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts +++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -57,7 +57,6 @@ changeNum$, repo$, } from '../../../services/change/change-model'; -import {loggedIn$} from '../../../services/user/user-model'; /** * The content of the enum is also used in the UI for the button text. @@ -263,6 +262,8 @@ @property({type: Object, computed: '_computeLabelExtremes(labels.*)'}) _labelExtremes: {[labelName: string]: VotingRangeInfo} = {}; + private readonly userModel = getAppContext().userModel; + private readonly reporting = getAppContext().reportingService; private readonly shortcuts = getAppContext().shortcutsService; @@ -282,7 +283,7 @@ }) ); this.subscriptions.push( - loggedIn$.subscribe(x => { + this.userModel.loggedIn$.subscribe(x => { this.showReplyButtons = x; }) );
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 29c8eca..1391257 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
@@ -15,6 +15,7 @@ * limitations under the License. */ import {Subscription} from 'rxjs'; +import {map, distinctUntilChanged} from 'rxjs/operators'; import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator'; import '../../shared/gr-dropdown/gr-dropdown'; import '../../shared/gr-icons/gr-icons'; @@ -37,7 +38,6 @@ import {DropdownLink} from '../../shared/gr-dropdown/gr-dropdown'; import {getAppContext} from '../../../services/app-context'; import {serverConfig$} from '../../../services/config/config-model'; -import {myTopMenuItems$} from '../../../services/user/user-model'; import {assertIsDefined} from '../../../utils/common-util'; type MainHeaderLink = RequireProperties<DropdownLink, 'url' | 'name'>; @@ -158,7 +158,7 @@ private readonly jsAPI = getAppContext().jsApiService; - private readonly userService = getAppContext().userService; + private readonly userModel = getAppContext().userModel; private subscriptions: Subscription[] = []; @@ -168,18 +168,23 @@ } override connectedCallback() { - // TODO(brohlfs): This just ensures that the userService is instantiated at + // 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.userService); + assertIsDefined(this.userModel); super.connectedCallback(); this._loadAccount(); this.subscriptions.push( - myTopMenuItems$.subscribe(items => { - this._userLinks = items.map(this._createHeaderLink); - }) + this.userModel.preferences$ + .pipe( + map(preferences => preferences?.my ?? []), + distinctUntilChanged() + ) + .subscribe(items => { + this._userLinks = items.map(this._createHeaderLink); + }) ); this.subscriptions.push( serverConfig$.subscribe(config => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts index 0d63360..fd30c6a 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts +++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
@@ -50,7 +50,7 @@ // Private but accessed by tests. readonly browserModel = getAppContext().browserModel; - private readonly userService = getAppContext().userService; + private readonly userModel = getAppContext().userModel; private subscriptions: Subscription[] = []; @@ -83,7 +83,7 @@ */ setMode(newMode: DiffViewMode) { if (this.saveOnChange && this.mode && this.mode !== newMode) { - this.userService.updatePreferences({diff_view: newMode}); + this.userModel.updatePreferences({diff_view: newMode}); } this.mode = newMode; let announcement;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts index 7f7f265..f469799 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts +++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts
@@ -19,7 +19,6 @@ import './gr-diff-preferences-dialog'; import {GrDiffPreferencesDialog} from './gr-diff-preferences-dialog'; import {createDefaultDiffPrefs} from '../../../constants/constants'; -import {updateDiffPreferences} from '../../../services/user/user-model'; import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions'; const basicFixture = fixtureFromElement('gr-diff-preferences-dialog'); @@ -37,7 +36,6 @@ line_wrapping: true, }; element.diffPrefs = originalDiffPrefs; - updateDiffPreferences(originalDiffPrefs); await flush(); element.open(); await flush();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts index c4e2488..ca89b1d 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts +++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -118,10 +118,6 @@ import {Subscription, combineLatest} from 'rxjs'; import {listen} from '../../../services/shortcuts/shortcuts-service'; import { - preferences$, - diffPreferences$, -} from '../../../services/user/user-model'; -import { diffPath$, currentPatchNum$, change$, @@ -359,11 +355,12 @@ private readonly restApiService = getAppContext().restApiService; - private readonly userService = getAppContext().userService; + // Private but used in tests. + readonly userModel = getAppContext().userModel; private readonly changeService = getAppContext().changeService; - // Private but used in tests + // Private but used in tests. readonly browserModel = getAppContext().browserModel; // We just want to make sure that CommentsService is instantiated. @@ -401,12 +398,12 @@ ); this.subscriptions.push( - preferences$.subscribe(preferences => { + this.userModel.preferences$.subscribe(preferences => { this._userPrefs = preferences; }) ); this.subscriptions.push( - diffPreferences$.subscribe(diffPreferences => { + this.userModel.diffPreferences$.subscribe(diffPreferences => { this._prefs = diffPreferences; }) ); @@ -423,7 +420,12 @@ // properties since the method will be called anytime a property updates // but we only want to call this on the initial load. this.subscriptions.push( - combineLatest(currentPatchNum$, routerView$, diffPath$, diffPreferences$) + combineLatest( + currentPatchNum$, + routerView$, + diffPath$, + this.userModel.diffPreferences$ + ) .pipe( filter( ([currentPatchNum, routerView, path, diffPrefs]) => @@ -784,9 +786,9 @@ _handleToggleDiffMode() { if (!this._userPrefs) return; if (this._userPrefs.diff_view === DiffViewMode.SIDE_BY_SIDE) { - this.userService.updatePreferences({diff_view: DiffViewMode.UNIFIED}); + this.userModel.updatePreferences({diff_view: DiffViewMode.UNIFIED}); } else { - this.userService.updatePreferences({ + this.userModel.updatePreferences({ diff_view: DiffViewMode.SIDE_BY_SIDE, }); } @@ -1764,7 +1766,7 @@ } _handleReloadingDiffPreference() { - this.userService.getDiffPreferences(); + this.userModel.getDiffPreferences(); } _computeCanEdit(
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js index 2367342..9076420 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -31,7 +31,6 @@ import {EditPatchSetNum} from '../../../types/common.js'; import {CursorMoveResult} from '../../../api/core.js'; import {Side} from '../../../api/diff.js'; -import {_testOnly_setState as setUserModelState, _testOnly_getState as getUserModelState} from '../../../services/user/user-model.js'; import {_testOnly_setState as setChangeModelState} from '../../../services/change/change-model.js'; import {_testOnly_setState as setCommentState} from '../../../services/comments/comments-model.js'; @@ -1199,7 +1198,7 @@ ...createDefaultDiffPrefs(), manual_review: true, }; - setUserModelState({...getUserModelState(), diffPreferences}); + element.userModel.setDiffPreferences(diffPreferences); setChangeModelState({change: createChange(), diffPath: '/COMMIT_MSG'}); setRouterModelState({ @@ -1216,8 +1215,7 @@ assert.isTrue(getReviewedStub.called); // if prefs are updated then the reviewed status should not be set again - setUserModelState({...getUserModelState(), - diffPreferences: createDefaultDiffPrefs()}); + element.userModel.setDiffPreferences(createDefaultDiffPrefs()); await flush(); assert.isFalse(saveReviewedStub.called); @@ -1237,7 +1235,7 @@ ...createDefaultDiffPrefs(), manual_review: false, }; - setUserModelState({...getUserModelState(), diffPreferences}); + element.userModel.setDiffPreferences(diffPreferences); setChangeModelState({change: createChange(), diffPath: '/COMMIT_MSG'}); @@ -1262,8 +1260,7 @@ .callsFake(() => Promise.resolve()); sinon.stub(element.$.diffHost, 'reload'); - setUserModelState({...getUserModelState(), - diffPreferences: createDefaultDiffPrefs()}); + element.userModel.setDiffPreferences(createDefaultDiffPrefs()); setChangeModelState({change: createChange(), diffPath: '/COMMIT_MSG'}); setRouterModelState({
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts index 4657020..23d9693 100644 --- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts +++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
@@ -25,7 +25,6 @@ import {DiffPreferencesInfo, IgnoreWhitespaceType} from '../../../types/diff'; import {GrSelect} from '../gr-select/gr-select'; import {getAppContext} from '../../../services/app-context'; -import {diffPreferences$} from '../../../services/user/user-model'; export interface GrDiffPreferences { $: { @@ -56,14 +55,14 @@ @property({type: Object}) diffPrefs?: DiffPreferencesInfo; - private readonly userService = getAppContext().userService; + private readonly userModel = getAppContext().userModel; private subscriptions: Subscription[] = []; override connectedCallback() { super.connectedCallback(); this.subscriptions.push( - diffPreferences$.subscribe(diffPreferences => { + this.userModel.diffPreferences$.subscribe(diffPreferences => { this.diffPrefs = diffPreferences; }) ); @@ -142,7 +141,7 @@ async save() { if (!this.diffPrefs) return; - await this.userService.updateDiffPreference(this.diffPrefs); + await this.userModel.updateDiffPreference(this.diffPrefs); this.hasUnsavedChanges = false; }
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts index 68d8d7d..6eb19da 100644 --- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts +++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
@@ -27,7 +27,6 @@ import {getAppContext} from '../../../services/app-context'; import {queryAndAssert} from '../../../utils/common-util'; import {GrShellCommand} from '../gr-shell-command/gr-shell-command'; -import {preferences$} from '../../../services/user/user-model'; declare global { interface HTMLElementEventMap { @@ -73,7 +72,8 @@ private readonly restApiService = getAppContext().restApiService; - private readonly userService = getAppContext().userService; + // Private but used in tests. + readonly userModel = getAppContext().userModel; private subscriptions: Subscription[] = []; @@ -83,7 +83,7 @@ this._loggedIn = loggedIn; }); this.subscriptions.push( - preferences$.subscribe(prefs => { + this.userModel.preferences$.subscribe(prefs => { if (prefs?.download_scheme) { // Note (issue 5180): normalize the download scheme with lower-case. this.selectedScheme = prefs.download_scheme.toLowerCase(); @@ -113,7 +113,7 @@ if (scheme && scheme !== this.selectedScheme) { this.set('selectedScheme', scheme); if (this._loggedIn) { - this.userService.updatePreferences({ + this.userModel.updatePreferences({ download_scheme: this.selectedScheme, }); }
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts index 6cbef79..bd0ca70 100644 --- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts +++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
@@ -19,7 +19,6 @@ import './gr-download-commands'; import {GrDownloadCommands} from './gr-download-commands'; import {isHidden, queryAndAssert, stubRestApi} from '../../../test/test-utils'; -import {updatePreferences} from '../../../services/user/user-model'; import {createPreferences} from '../../../test/test-data-generators'; import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions'; import {GrShellCommand} from '../gr-shell-command/gr-shell-command'; @@ -116,7 +115,7 @@ test('loads scheme from preferences', async () => { const element = basicFixture.instantiate(); await flush(); - updatePreferences({ + element.userModel.setPreferences({ ...createPreferences(), download_scheme: 'repo', }); @@ -126,7 +125,7 @@ test('normalize scheme from preferences', async () => { const element = basicFixture.instantiate(); await flush(); - updatePreferences({ + element.userModel.setPreferences({ ...createPreferences(), download_scheme: 'REPO', });
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts index 47d5f03..001b71d 100644 --- a/polygerrit-ui/app/services/app-context-init.ts +++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -26,7 +26,7 @@ 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 {UserService} from './user/user-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'; @@ -74,15 +74,19 @@ assertIsDefined(ctx.restApiService, 'restApiService'); return new ConfigService(ctx.restApiService!); }, - userService: (ctx: Partial<AppContext>) => { + userModel: (ctx: Partial<AppContext>) => { assertIsDefined(ctx.restApiService, 'restApiService'); - return new UserService(ctx.restApiService!); + return new UserModel(ctx.restApiService!); }, shortcutsService: (ctx: Partial<AppContext>) => { + assertIsDefined(ctx.userModel, 'userModel'); assertIsDefined(ctx.reportingService, 'reportingService'); - return new ShortcutsService(ctx.reportingService!); + return new ShortcutsService(ctx.userModel, ctx.reportingService!); }, - browserModel: (_ctx: Partial<AppContext>) => new BrowserModel(), + browserModel: (ctx: Partial<AppContext>) => { + assertIsDefined(ctx.userModel, 'userModel'); + return new BrowserModel(ctx.userModel!); + }, }; return create<AppContext>(appRegistry); }
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts index 87a18ef..d5e595d 100644 --- a/polygerrit-ui/app/services/app-context.ts +++ b/polygerrit-ui/app/services/app-context.ts
@@ -25,7 +25,7 @@ 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 {UserService} from './user/user-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'; @@ -42,7 +42,7 @@ jsApiService: JsApiService; storageService: StorageService; configService: ConfigService; - userService: UserService; + userModel: UserModel; browserModel: BrowserModel; shortcutsService: ShortcutsService; }
diff --git a/polygerrit-ui/app/services/browser/browser-model.ts b/polygerrit-ui/app/services/browser/browser-model.ts index b15091a..a675cdd 100644 --- a/polygerrit-ui/app/services/browser/browser-model.ts +++ b/polygerrit-ui/app/services/browser/browser-model.ts
@@ -17,8 +17,8 @@ import {BehaviorSubject, Observable, combineLatest} from 'rxjs'; import {distinctUntilChanged, map} from 'rxjs/operators'; import {Finalizable} from '../registry'; -import {preferenceDiffViewMode$} from '../user/user-model'; import {DiffViewMode} from '../../api/diff'; +import {UserModel} from '../user/user-model'; // This value is somewhat arbitrary and not based on research or calculations. const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 850; @@ -42,7 +42,7 @@ return this.privateState$; } - constructor() { + constructor(readonly userModel: UserModel) { const screenWidth$ = this.privateState$.pipe( map( state => @@ -55,7 +55,7 @@ // the user model. this.diffViewMode$ = combineLatest([ screenWidth$, - preferenceDiffViewMode$, + userModel.preferenceDiffViewMode$, ]).pipe( map(([isScreenTooSmall, preferenceDiffViewMode]) => { if (isScreenTooSmall) return DiffViewMode.UNIFIED;
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts index 1f9e083..8f14077 100644 --- a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts +++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
@@ -15,13 +15,13 @@ * limitations under the License. */ import {Subscription} from 'rxjs'; +import {map, distinctUntilChanged} from 'rxjs/operators'; import { config, Shortcut, ShortcutHelpItem, ShortcutSection, } from './shortcuts-config'; -import {disableShortcuts$} from '../user/user-model'; import { ComboKey, eventMatchesShortcut, @@ -33,6 +33,7 @@ } from '../../utils/dom-util'; import {ReportingService} from '../gr-reporting/gr-reporting'; import {Finalizable} from '../registry'; +import {UserModel} from '../user/user-model'; export type SectionView = Array<{binding: string[][]; text: string}>; @@ -98,7 +99,10 @@ private readonly subscriptions: Subscription[] = []; - constructor(readonly reporting?: ReportingService) { + constructor( + readonly userModel: UserModel, + readonly reporting?: ReportingService + ) { for (const section of config.keys()) { const items = config.get(section) ?? []; for (const item of items) { @@ -106,7 +110,12 @@ } } this.subscriptions.push( - disableShortcuts$.subscribe(x => (this.shortcutsDisabled = x)) + this.userModel.preferences$ + .pipe( + map(preferences => preferences?.disable_keyboard_shortcuts ?? false), + distinctUntilChanged() + ) + .subscribe(x => (this.shortcutsDisabled = x)) ); this.keydownListener = (e: KeyboardEvent) => { if (!isComboKey(e.key)) return;
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts index a024159..274cb87 100644 --- a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts +++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
@@ -24,6 +24,7 @@ import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions'; import {SinonFakeTimers} from 'sinon'; import {Key, Modifier} from '../../utils/dom-util'; +import {getAppContext} from '../app-context'; async function keyEventOn( el: HTMLElement, @@ -45,7 +46,10 @@ let service: ShortcutsService; setup(() => { - service = new ShortcutsService(); + service = new ShortcutsService( + getAppContext().userModel, + getAppContext().reportingService + ); }); suite('shouldSuppress', () => {
diff --git a/polygerrit-ui/app/services/user/user-model.ts b/polygerrit-ui/app/services/user/user-model.ts index f772ebe..0b6ef6b 100644 --- a/polygerrit-ui/app/services/user/user-model.ts +++ b/polygerrit-ui/app/services/user/user-model.ts
@@ -14,16 +14,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import {from, of, BehaviorSubject, Observable, Subscription} from 'rxjs'; +import {switchMap} from 'rxjs/operators'; +import { + DiffPreferencesInfo as DiffPreferencesInfoAPI, + DiffViewMode, +} from '../../api/diff'; import {AccountDetailInfo, PreferencesInfo} from '../../types/common'; -import {BehaviorSubject, Observable} from 'rxjs'; -import {map, distinctUntilChanged} from 'rxjs/operators'; import { createDefaultPreferences, createDefaultDiffPrefs, } from '../../constants/constants'; -import {DiffPreferencesInfo, DiffViewMode} from '../../api/diff'; +import {RestApiService} from '../gr-rest-api/gr-rest-api'; +import {DiffPreferencesInfo} from '../../types/diff'; +import {Finalizable} from '../registry'; +import {select} from '../../utils/observable-util'; -interface UserState { +export interface UserState { /** * Keeps being defined even when credentials have expired. */ @@ -32,83 +40,122 @@ diffPreferences: DiffPreferencesInfo; } -const initialState: UserState = { - preferences: createDefaultPreferences(), - diffPreferences: createDefaultDiffPrefs(), -}; +export class UserModel implements Finalizable { + private readonly privateState$: BehaviorSubject<UserState> = + new BehaviorSubject({ + preferences: createDefaultPreferences(), + diffPreferences: createDefaultDiffPrefs(), + }); -const privateState$ = new BehaviorSubject(initialState); + readonly account$: Observable<AccountDetailInfo | undefined> = select( + this.privateState$, + userState => userState.account + ); -export function _testOnly_resetState() { - // We cannot assign a new subject to privateState$, because all the selectors - // have already subscribed to the original subject. So we have to emit the - // initial state on the existing subject. - privateState$.next({...initialState}); + /** Note that this may still be true, even if credentials have expired. */ + readonly loggedIn$: Observable<boolean> = select( + this.account$, + account => !!account + ); + + readonly preferences$: Observable<PreferencesInfo> = select( + this.privateState$, + userState => userState.preferences + ); + + readonly diffPreferences$: Observable<DiffPreferencesInfo> = select( + this.privateState$, + userState => userState.diffPreferences + ); + + readonly preferenceDiffViewMode$: Observable<DiffViewMode> = select( + this.preferences$, + preference => preference.diff_view ?? DiffViewMode.SIDE_BY_SIDE + ); + + private readonly subscriptions: Subscription[] = []; + + get userState$(): Observable<UserState> { + return this.privateState$; + } + + constructor(readonly restApiService: RestApiService) { + this.subscriptions = [ + from(this.restApiService.getAccount()).subscribe( + (account?: AccountDetailInfo) => { + this.setAccount(account); + } + ), + this.account$ + .pipe( + switchMap(account => { + if (!account) return of(createDefaultPreferences()); + return from(this.restApiService.getPreferences()); + }) + ) + .subscribe((preferences?: PreferencesInfo) => { + this.setPreferences(preferences ?? createDefaultPreferences()); + }), + this.account$ + .pipe( + switchMap(account => { + if (!account) return of(createDefaultDiffPrefs()); + return from(this.restApiService.getDiffPreferences()); + }) + ) + .subscribe((diffPrefs?: DiffPreferencesInfoAPI) => { + this.setDiffPreferences(diffPrefs ?? createDefaultDiffPrefs()); + }), + ]; + } + + finalize() { + for (const s of this.subscriptions) { + s.unsubscribe(); + } + this.subscriptions.splice(0, this.subscriptions.length); + } + + updatePreferences(prefs: Partial<PreferencesInfo>) { + this.restApiService + .savePreferences(prefs) + .then((newPrefs: PreferencesInfo | undefined) => { + if (!newPrefs) return; + this.setPreferences(newPrefs); + }); + } + + updateDiffPreference(diffPrefs: DiffPreferencesInfo) { + return this.restApiService + .saveDiffPreferences(diffPrefs) + .then((response: Response) => { + this.restApiService.getResponseObject(response).then(obj => { + const newPrefs = obj as unknown as DiffPreferencesInfo; + if (!newPrefs) return; + this.setDiffPreferences(newPrefs); + }); + }); + } + + getDiffPreferences() { + return this.restApiService.getDiffPreferences().then(prefs => { + if (!prefs) return; + this.setDiffPreferences(prefs); + }); + } + + setPreferences(preferences: PreferencesInfo) { + const current = this.privateState$.getValue(); + this.privateState$.next({...current, preferences}); + } + + setDiffPreferences(diffPreferences: DiffPreferencesInfo) { + const current = this.privateState$.getValue(); + this.privateState$.next({...current, diffPreferences}); + } + + private setAccount(account?: AccountDetailInfo) { + const current = this.privateState$.getValue(); + this.privateState$.next({...current, account}); + } } - -export function _testOnly_setState(state: UserState) { - privateState$.next(state); -} - -export function _testOnly_getState() { - return privateState$.getValue(); -} - -// Re-exporting as Observable so that you can only subscribe, but not emit. -export const userState$: Observable<UserState> = privateState$; - -export function updateAccount(account?: AccountDetailInfo) { - const current = privateState$.getValue(); - privateState$.next({...current, account}); -} - -export function updatePreferences(preferences: PreferencesInfo) { - const current = privateState$.getValue(); - privateState$.next({...current, preferences}); -} - -export function updateDiffPreferences(diffPreferences: DiffPreferencesInfo) { - const current = privateState$.getValue(); - privateState$.next({...current, diffPreferences}); -} - -export const account$ = userState$.pipe( - map(userState => userState.account), - distinctUntilChanged() -); - -/** Note that this may still be true, even if credentials have expired. */ -export const loggedIn$ = account$.pipe( - map(account => !!account), - distinctUntilChanged() -); - -export const preferences$ = userState$.pipe( - map(userState => userState.preferences), - distinctUntilChanged() -); - -export const diffPreferences$ = userState$.pipe( - map(userState => userState.diffPreferences), - distinctUntilChanged() -); - -export const preferenceDiffViewMode$ = preferences$.pipe( - map(preference => preference.diff_view ?? DiffViewMode.SIDE_BY_SIDE), - distinctUntilChanged() -); - -export const myTopMenuItems$ = preferences$.pipe( - map(preferences => preferences?.my ?? []), - distinctUntilChanged() -); - -export const sizeBarInChangeTable$ = preferences$.pipe( - map(prefs => !!prefs?.size_bar_in_change_table), - distinctUntilChanged() -); - -export const disableShortcuts$ = preferences$.pipe( - map(preferences => preferences?.disable_keyboard_shortcuts ?? false), - distinctUntilChanged() -);
diff --git a/polygerrit-ui/app/services/user/user-service.ts b/polygerrit-ui/app/services/user/user-service.ts deleted file mode 100644 index d2bca85..0000000 --- a/polygerrit-ui/app/services/user/user-service.ts +++ /dev/null
@@ -1,103 +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 {from, of, Subscription} from 'rxjs'; -import {switchMap} from 'rxjs/operators'; -import {AccountDetailInfo, PreferencesInfo} from '../../types/common'; -import { - account$, - updateAccount, - updatePreferences, - updateDiffPreferences, -} from './user-model'; -import { - createDefaultPreferences, - createDefaultDiffPrefs, -} from '../../constants/constants'; -import {RestApiService} from '../gr-rest-api/gr-rest-api'; -import {DiffPreferencesInfo} from '../../types/diff'; -import {Finalizable} from '../registry'; - -export class UserService implements Finalizable { - private readonly subscriptions: Subscription[] = []; - - constructor(readonly restApiService: RestApiService) { - from(this.restApiService.getAccount()).subscribe( - (account?: AccountDetailInfo) => { - updateAccount(account); - } - ); - this.subscriptions.push( - account$ - .pipe( - switchMap(account => { - if (!account) return of(createDefaultPreferences()); - return from(this.restApiService.getPreferences()); - }) - ) - .subscribe((preferences?: PreferencesInfo) => { - updatePreferences(preferences ?? createDefaultPreferences()); - }) - ); - this.subscriptions.push( - account$ - .pipe( - switchMap(account => { - if (!account) return of(createDefaultDiffPrefs()); - return from(this.restApiService.getDiffPreferences()); - }) - ) - .subscribe((diffPrefs?: DiffPreferencesInfo) => { - updateDiffPreferences(diffPrefs ?? createDefaultDiffPrefs()); - }) - ); - } - - finalize() { - for (const s of this.subscriptions) { - s.unsubscribe(); - } - this.subscriptions.splice(0, this.subscriptions.length); - } - - updatePreferences(prefs: Partial<PreferencesInfo>) { - this.restApiService - .savePreferences(prefs) - .then((newPrefs: PreferencesInfo | undefined) => { - if (!newPrefs) return; - updatePreferences(newPrefs); - }); - } - - updateDiffPreference(diffPrefs: DiffPreferencesInfo) { - return this.restApiService - .saveDiffPreferences(diffPrefs) - .then((response: Response) => { - this.restApiService.getResponseObject(response).then(obj => { - const newPrefs = obj as unknown as DiffPreferencesInfo; - if (!newPrefs) return; - updateDiffPreferences(newPrefs); - }); - }); - } - - getDiffPreferences() { - return this.restApiService.getDiffPreferences().then(prefs => { - if (!prefs) return; - updateDiffPreferences(prefs); - }); - } -}
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts index f8fd534..f041354 100644 --- a/polygerrit-ui/app/test/common-test-setup.ts +++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -46,14 +46,12 @@ } from '../scripts/polymer-resin-install'; import {_testOnly_allTasks} from '../utils/async-util'; import {cleanUpStorage} from '../services/storage/gr-storage_mock'; -import {updatePreferences} from '../services/user/user-model'; -import {createDefaultPreferences} from '../constants/constants'; + import {getAppContext} from '../services/app-context'; import {_testOnly_resetState as resetChangeState} from '../services/change/change-model'; import {_testOnly_resetState as resetChecksState} from '../services/checks/checks-model'; import {_testOnly_resetState as resetCommentsState} from '../services/comments/comments-model'; import {_testOnly_resetState as resetRouterState} from '../services/router/router-model'; -import {_testOnly_resetState as resetUserState} from '../services/user/user-model'; declare global { interface Window { @@ -122,7 +120,6 @@ resetChecksState(); resetCommentsState(); resetRouterState(); - resetUserState(); const shortcuts = getAppContext().shortcutsService; assert.isTrue(shortcuts._testOnly_isEmpty()); @@ -220,7 +217,6 @@ cancelAllTasks(); cleanUpStorage(); // Reset state - updatePreferences(createDefaultPreferences()); _testOnlyFinalizeAppContext(); const testTeardownTimestampMs = new Date().getTime(); const elapsedMs = testTeardownTimestampMs - testSetupTimestampMs;
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts index 03c5967..a710b00 100644 --- a/polygerrit-ui/app/test/test-app-context-init.ts +++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -29,7 +29,7 @@ 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 {UserService} from '../services/user/user-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'; @@ -68,15 +68,19 @@ assertIsDefined(ctx.restApiService, 'restApiService'); return new ConfigService(ctx.restApiService!); }, - userService: (ctx: Partial<AppContext>) => { + userModel: (ctx: Partial<AppContext>) => { assertIsDefined(ctx.restApiService, 'restApiService'); - return new UserService(ctx.restApiService!); + return new UserModel(ctx.restApiService!); }, shortcutsService: (ctx: Partial<AppContext>) => { + assertIsDefined(ctx.userModel, 'userModel'); assertIsDefined(ctx.reportingService, 'reportingService'); - return new ShortcutsService(ctx.reportingService!); + return new ShortcutsService(ctx.userModel!, ctx.reportingService!); }, - browserModel: (_ctx: Partial<AppContext>) => new BrowserModel(), + browserModel: (ctx: Partial<AppContext>) => { + assertIsDefined(ctx.userModel, 'userModel'); + return new BrowserModel(ctx.userModel!); + }, }; appContext = create<AppContext>(appRegistry); injectAppContext(appContext);
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts index 2b57e98..f2da972 100644 --- a/polygerrit-ui/app/test/test-utils.ts +++ b/polygerrit-ui/app/test/test-utils.ts
@@ -24,7 +24,7 @@ import {AuthService} from '../services/gr-auth/gr-auth'; import {ReportingService} from '../services/gr-reporting/gr-reporting'; import {CommentsService} from '../services/comments/comments-service'; -import {UserService} from '../services/user/user-service'; +import {UserModel} from '../services/user/user-model'; import {ShortcutsService} from '../services/shortcuts/shortcuts-service'; import {queryAndAssert, query} from '../utils/common-util'; import {FlagsService} from '../services/flags/flags'; @@ -116,8 +116,8 @@ return sinon.stub(getAppContext().commentsService, method); } -export function stubUsers<K extends keyof UserService>(method: K) { - return sinon.stub(getAppContext().userService, method); +export function stubUsers<K extends keyof UserModel>(method: K) { + return sinon.stub(getAppContext().userModel, method); } export function stubShortcuts<K extends keyof ShortcutsService>(method: K) {
diff --git a/polygerrit-ui/app/utils/observable-util.ts b/polygerrit-ui/app/utils/observable-util.ts new file mode 100644 index 0000000..e39aa48 --- /dev/null +++ b/polygerrit-ui/app/utils/observable-util.ts
@@ -0,0 +1,27 @@ +/** + * @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 {Observable} from 'rxjs'; +import {distinctUntilChanged, map, shareReplay} from 'rxjs/operators'; +import {deepEqual} from './deep-util'; + +export function select<A, B>(obs$: Observable<A>, mapper: (_: A) => B) { + return obs$.pipe( + map(mapper), + distinctUntilChanged(deepEqual), + shareReplay(1) + ); +}