diff --git a/npm/ng-packs/package.json b/npm/ng-packs/package.json index 42284c0e07..aa37107242 100644 --- a/npm/ng-packs/package.json +++ b/npm/ng-packs/package.json @@ -61,7 +61,6 @@ "@ngxs/devtools-plugin": "^3.7.0", "@ngxs/logger-plugin": "^3.7.0", "@ngxs/router-plugin": "^3.7.0", - "@ngxs/storage-plugin": "^3.7.0", "@ngxs/store": "^3.7.0", "@schematics/angular": "~10.0.5", "@swimlane/ngx-datatable": "^17.1.0", diff --git a/npm/ng-packs/packages/account/src/lib/components/login/login.component.ts b/npm/ng-packs/packages/account/src/lib/components/login/login.component.ts index 3a6a8663cc..4a9318bf21 100644 --- a/npm/ng-packs/packages/account/src/lib/components/login/login.component.ts +++ b/npm/ng-packs/packages/account/src/lib/components/login/login.component.ts @@ -1,4 +1,4 @@ -import { AuthService, SetRemember, ConfigState } from '@abp/ng.core'; +import { AuthService, ConfigState } from '@abp/ng.core'; import { ToasterService } from '@abp/ng.theme.shared'; import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; @@ -65,8 +65,6 @@ export class LoginComponent implements OnInit { }), finalize(() => (this.inProgress = false)), ) - .subscribe(() => { - this.store.dispatch(new SetRemember(this.form.get('remember').value)); - }); + .subscribe(() => {}); } } diff --git a/npm/ng-packs/packages/account/src/lib/components/tenant-box/tenant-box.component.ts b/npm/ng-packs/packages/account/src/lib/components/tenant-box/tenant-box.component.ts index 92d35fcf2e..4fcb77f96c 100644 --- a/npm/ng-packs/packages/account/src/lib/components/tenant-box/tenant-box.component.ts +++ b/npm/ng-packs/packages/account/src/lib/components/tenant-box/tenant-box.component.ts @@ -1,4 +1,4 @@ -import { ABP, GetAppConfiguration, SessionState, SetTenant } from '@abp/ng.core'; +import { ABP, GetAppConfiguration, SessionStateService } from '@abp/ng.core'; import { ToasterService } from '@abp/ng.theme.shared'; import { Component } from '@angular/core'; import { Select, Store } from '@ngxs/store'; @@ -13,8 +13,7 @@ import { AccountService } from '../../services/account.service'; }) export class TenantBoxComponent implements Account.TenantBoxComponentInputs, Account.TenantBoxComponentOutputs { - @Select(SessionState.getTenant) - currentTenant$: Observable; + currentTenant$ = this.sessionState.getTenant$(); name: string; @@ -26,11 +25,12 @@ export class TenantBoxComponent private store: Store, private toasterService: ToasterService, private accountService: AccountService, + private sessionState: SessionStateService, ) {} onSwitch() { - const tenant = this.store.selectSnapshot(SessionState.getTenant); - this.name = (tenant || ({} as ABP.BasicItem)).name; + const tenant = this.sessionState.getTenant; + this.name = tenant?.name; this.isModalVisible = true; } @@ -57,7 +57,8 @@ export class TenantBoxComponent } private setTenant(tenant: ABP.BasicItem) { - return this.store.dispatch([new SetTenant(tenant), new GetAppConfiguration()]); + this.sessionState.setTenant(tenant); + return this.store.dispatch(new GetAppConfiguration()); } private showError() { diff --git a/npm/ng-packs/packages/core/ng-package.json b/npm/ng-packs/packages/core/ng-package.json index 045f0e8844..7236b0ca66 100644 --- a/npm/ng-packs/packages/core/ng-package.json +++ b/npm/ng-packs/packages/core/ng-package.json @@ -9,7 +9,6 @@ "@abp/utils", "@angular/localize", "@ngxs/router-plugin", - "@ngxs/storage-plugin", "@ngxs/store", "angular-oauth2-oidc", "just-compare", diff --git a/npm/ng-packs/packages/core/package.json b/npm/ng-packs/packages/core/package.json index 3c2a3381ac..d05476aba3 100644 --- a/npm/ng-packs/packages/core/package.json +++ b/npm/ng-packs/packages/core/package.json @@ -10,7 +10,6 @@ "@abp/utils": "^3.3.1", "@angular/localize": "~10.0.10", "@ngxs/router-plugin": "^3.7.0", - "@ngxs/storage-plugin": "^3.7.0", "@ngxs/store": "^3.7.0", "angular-oauth2-oidc": "^10.0.0", "just-clone": "^3.1.0", diff --git a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts index 3d6870f577..387437cc04 100644 --- a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts +++ b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts @@ -74,7 +74,7 @@ export class DynamicLayoutComponent { } private listenToLanguageChange() { - this.subscription.addOne(this.localizationService.languageChange, () => { + this.subscription.addOne(this.localizationService.languageChange$, () => { this.isLayoutVisible = false; setTimeout(() => (this.isLayoutVisible = true), 0); }); diff --git a/npm/ng-packs/packages/core/src/lib/core.module.ts b/npm/ng-packs/packages/core/src/lib/core.module.ts index 3756bff1bb..6e1e9147b3 100644 --- a/npm/ng-packs/packages/core/src/lib/core.module.ts +++ b/npm/ng-packs/packages/core/src/lib/core.module.ts @@ -4,11 +4,6 @@ import { APP_INITIALIZER, Injector, ModuleWithProviders, NgModule } from '@angul import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { NgxsRouterPluginModule } from '@ngxs/router-plugin'; -import { - NgxsStoragePluginModule, - NGXS_STORAGE_PLUGIN_OPTIONS, - StorageOption, -} from '@ngxs/storage-plugin'; import { NgxsModule, NGXS_PLUGINS } from '@ngxs/store'; import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc'; import { AbstractNgModelComponent } from './abstracts/ng-model.component'; @@ -38,7 +33,6 @@ import { LocalizationService } from './services/localization.service'; import { ConfigState } from './states/config.state'; import { ProfileState } from './states/profile.state'; import { ReplaceableComponentsState } from './states/replaceable-components.state'; -import { SessionState } from './states/session.state'; import { coreOptionsFactory, CORE_OPTIONS } from './tokens/options.token'; import { noop } from './utils/common-utils'; import './utils/date-extensions'; @@ -121,9 +115,8 @@ export class BaseCoreModule {} imports: [ BaseCoreModule, LocalizationModule, - NgxsModule.forFeature([ReplaceableComponentsState, ProfileState, SessionState, ConfigState]), + NgxsModule.forFeature([ReplaceableComponentsState, ProfileState, ConfigState]), NgxsRouterPluginModule.forRoot(), - NgxsStoragePluginModule.forRoot(), OAuthModule.forRoot(), HttpClientXsrfModule.withOptions({ cookieName: 'XSRF-TOKEN', @@ -225,18 +218,6 @@ export class CoreModule { useFactory: noop, }, { provide: OAuthStorage, useFactory: storageFactory }, - { - provide: NGXS_STORAGE_PLUGIN_OPTIONS, - useValue: { - storage: StorageOption.LocalStorage, - serialize: JSON.stringify, - deserialize: JSON.parse, - beforeSerialize: ngxsStoragePluginSerialize, - afterDeserialize: ngxsStoragePluginSerialize, - ...options.ngxsStoragePluginOptions, - key: [...(options.ngxsStoragePluginOptions?.key || []), 'SessionState'], - }, - }, ], }; } diff --git a/npm/ng-packs/packages/core/src/lib/interceptors/api.interceptor.ts b/npm/ng-packs/packages/core/src/lib/interceptors/api.interceptor.ts index b27c7fa80a..79ec96809c 100644 --- a/npm/ng-packs/packages/core/src/lib/interceptors/api.interceptor.ts +++ b/npm/ng-packs/packages/core/src/lib/interceptors/api.interceptor.ts @@ -1,16 +1,20 @@ +import { HttpHandler, HttpHeaders, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { HttpInterceptor, HttpHandler, HttpRequest, HttpHeaders } from '@angular/common/http'; -import { OAuthService } from 'angular-oauth2-oidc'; import { Store } from '@ngxs/store'; -import { SessionState } from '../states'; -import { StartLoader, StopLoader } from '../actions/loader.actions'; +import { OAuthService } from 'angular-oauth2-oidc'; import { finalize } from 'rxjs/operators'; +import { StartLoader, StopLoader } from '../actions/loader.actions'; +import { SessionStateService } from '../services/session-state.service'; @Injectable({ providedIn: 'root', }) export class ApiInterceptor implements HttpInterceptor { - constructor(private oAuthService: OAuthService, private store: Store) {} + constructor( + private oAuthService: OAuthService, + private store: Store, + private sessionState: SessionStateService, + ) {} intercept(request: HttpRequest, next: HttpHandler) { this.store.dispatch(new StartLoader(request)); @@ -32,12 +36,12 @@ export class ApiInterceptor implements HttpInterceptor { headers['Authorization'] = `Bearer ${token}`; } - const lang = this.store.selectSnapshot(SessionState.getLanguage); + const lang = this.sessionState.getLanguage(); if (!existingHeaders?.has('Accept-Language') && lang) { headers['Accept-Language'] = lang; } - const tenant = this.store.selectSnapshot(SessionState.getTenant); + const tenant = this.sessionState.getTenant(); if (!existingHeaders?.has('__tenant') && tenant) { headers['__tenant'] = tenant.id; } diff --git a/npm/ng-packs/packages/core/src/lib/models/common.ts b/npm/ng-packs/packages/core/src/lib/models/common.ts index 8a4e283bd9..6e4eead683 100644 --- a/npm/ng-packs/packages/core/src/lib/models/common.ts +++ b/npm/ng-packs/packages/core/src/lib/models/common.ts @@ -1,6 +1,5 @@ import { EventEmitter, Type } from '@angular/core'; import { Router } from '@angular/router'; -import { NgxsStoragePluginOptions } from '@ngxs/storage-plugin'; import { Subject } from 'rxjs'; import { eLayoutType } from '../enums/common'; import { Config } from './config'; @@ -11,7 +10,6 @@ export namespace ABP { registerLocaleFn: (locale: string) => Promise; skipGetAppConfiguration?: boolean; sendNullsAsQueryParam?: boolean; - ngxsStoragePluginOptions?: NgxsStoragePluginOptions & { key?: string[] }; } export interface Test { diff --git a/npm/ng-packs/packages/core/src/lib/models/session.ts b/npm/ng-packs/packages/core/src/lib/models/session.ts index 110fad53f5..2680ea51d9 100644 --- a/npm/ng-packs/packages/core/src/lib/models/session.ts +++ b/npm/ng-packs/packages/core/src/lib/models/session.ts @@ -1,9 +1,9 @@ -import { ABP } from '../models'; +import { ApplicationConfiguration } from './application-configuration'; export namespace Session { export interface State { language: string; - tenant: ABP.BasicItem; + tenant: ApplicationConfiguration.CurrentTenant; sessionDetail: SessionDetail; } diff --git a/npm/ng-packs/packages/core/src/lib/services/auth.service.ts b/npm/ng-packs/packages/core/src/lib/services/auth.service.ts index 94a48c00a4..d694691701 100644 --- a/npm/ng-packs/packages/core/src/lib/services/auth.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/auth.service.ts @@ -8,9 +8,9 @@ import { switchMap, take, tap } from 'rxjs/operators'; import snq from 'snq'; import { GetAppConfiguration, SetEnvironment } from '../actions/config.actions'; import { ConfigState } from '../states/config.state'; -import { SessionState } from '../states/session.state'; import { AuthFlowStrategy, AUTH_FLOW_STRATEGY } from '../strategies/auth-flow.strategy'; import { RestService } from './rest.service'; +import { SessionStateService } from './session-state.service'; @Injectable({ providedIn: 'root', @@ -29,6 +29,7 @@ export class AuthService { private rest: RestService, private oAuthService: OAuthService, private store: Store, + private sessionState: SessionStateService, @Optional() @Inject('ACCOUNT_OPTIONS') private options: any, ) { this.setStrategy(); @@ -55,7 +56,7 @@ export class AuthService { } login(username: string, password: string): Observable { - const tenant = this.store.selectSnapshot(SessionState.getTenant); + const tenant = this.sessionState.getTenant(); return from( this.oAuthService.fetchTokenUsingPasswordFlow( diff --git a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts index 6fdce35d07..cf07f58d56 100644 --- a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts @@ -1,36 +1,37 @@ import { registerLocaleData } from '@angular/common'; import { Injectable, Injector, NgZone, Optional, SkipSelf } from '@angular/core'; import { ActivatedRouteSnapshot, Router } from '@angular/router'; -import { Actions, ofActionSuccessful, Store } from '@ngxs/store'; -import { noop, Observable } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; -import { SetLanguage } from '../actions/session.actions'; +import { Store } from '@ngxs/store'; +import { noop, Observable, of, Subject } from 'rxjs'; +import { map, mapTo, switchMap } from 'rxjs/operators'; +import { GetAppConfiguration } from '../actions/config.actions'; import { ABP } from '../models/common'; import { Config } from '../models/config'; import { ConfigState } from '../states/config.state'; import { CORE_OPTIONS } from '../tokens/options.token'; import { createLocalizer, createLocalizerWithFallback } from '../utils/localization-utils'; +import { SessionStateService } from './session-state.service'; type ShouldReuseRoute = (future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot) => boolean; @Injectable({ providedIn: 'root' }) export class LocalizationService { + private latestLang = this.sessionState.getLanguage(); + private _languageChange$ = new Subject(); + /** * Returns currently selected language */ get currentLang(): string { - return this.store.selectSnapshot(state => state.SessionState.language); + return this.latestLang; } - get languageChange(): Observable { - return this.actions.pipe( - ofActionSuccessful(SetLanguage), - filter((action: SetLanguage) => action.dispatchAppConfiguration !== false), - ); + get languageChange$(): Observable { + return this._languageChange$.asObservable(); } constructor( - private actions: Actions, + private sessionState: SessionStateService, private store: Store, private injector: Injector, private ngZone: NgZone, @@ -44,7 +45,17 @@ export class LocalizationService { } private listenToSetLanguage() { - this.languageChange.subscribe(({ payload }) => this.registerLocale(payload)); + this.sessionState + .onLanguageChange$() + .pipe( + switchMap(lang => { + return this.store.dispatch(new GetAppConfiguration()).pipe(mapTo(lang)); + }), + ) + .subscribe(lang => { + this.registerLocale(lang); + this._languageChange$.next(lang); + }); } registerLocale(locale: string) { @@ -57,6 +68,7 @@ export class LocalizationService { return registerLocaleFn(locale).then(module => { if (module?.default) registerLocaleData(module.default); + this.latestLang = locale; this.ngZone.run(async () => { await router.navigateByUrl(router.url).catch(noop); diff --git a/npm/ng-packs/packages/core/src/lib/services/multi-tenancy.service.ts b/npm/ng-packs/packages/core/src/lib/services/multi-tenancy.service.ts index 3a66a99311..39505b45d5 100644 --- a/npm/ng-packs/packages/core/src/lib/services/multi-tenancy.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/multi-tenancy.service.ts @@ -1,10 +1,9 @@ import { Injectable } from '@angular/core'; -import { Store } from '@ngxs/store'; import { Observable } from 'rxjs'; -import { SetTenant } from '../actions/session.actions'; import { ABP } from '../models/common'; import { FindTenantResultDto } from '../models/find-tenant-result-dto'; import { RestService } from './rest.service'; +import { SessionStateService } from './session-state.service'; @Injectable({ providedIn: 'root' }) export class MultiTenancyService { @@ -12,7 +11,7 @@ export class MultiTenancyService { set domainTenant(value: ABP.BasicItem) { this._domainTenant = value; - this.store.dispatch(new SetTenant(value)); + this.sessionState.setTenant(value); } get domainTenant() { @@ -23,7 +22,7 @@ export class MultiTenancyService { apiName = 'abp'; - constructor(private restService: RestService, private store: Store) {} + constructor(private restService: RestService, private sessionState: SessionStateService) {} findTenantByName(name: string, headers: ABP.Dictionary): Observable { return this.restService.request( diff --git a/npm/ng-packs/packages/core/src/lib/services/session-state.service.ts b/npm/ng-packs/packages/core/src/lib/services/session-state.service.ts index 2875e02824..9d2cc24ed2 100644 --- a/npm/ng-packs/packages/core/src/lib/services/session-state.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/session-state.service.ts @@ -1,44 +1,71 @@ import { Injectable } from '@angular/core'; -import { Store } from '@ngxs/store'; -import { - SetLanguage, - SetRemember, - SetTenant, - ModifyOpenedTabCount, -} from '../actions/session.actions'; -import { SessionState } from '../states'; +import { ApplicationConfiguration } from '../models/application-configuration'; +import { Session } from '../models/session'; +import { InternalStore } from '../utils/internal-store-utils'; +import compare from 'just-compare'; + +export interface SessionDetail { + openedTabCount: number; + lastExitTime: number; + remember: boolean; +} @Injectable({ providedIn: 'root', }) export class SessionStateService { - constructor(private store: Store) {} + private readonly store = new InternalStore({} as Session.State); - getLanguage() { - return this.store.selectSnapshot(SessionState.getLanguage); + private updateLocalStorage = () => { + localStorage.setItem('abpSession', JSON.stringify(this.store.state)); + }; + + constructor() { + this.init(); } - getTenant() { - return this.store.selectSnapshot(SessionState.getTenant); + private init() { + const session = localStorage.getItem('abpSession'); + if (session) { + this.store.patch(JSON.parse(session)); + } + + this.store.sliceUpdate(state => state).subscribe(this.updateLocalStorage); + } + + onLanguageChange$() { + return this.store.sliceUpdate(state => state.language); } - getSessionDetail() { - return this.store.selectSnapshot(SessionState.getSessionDetail); + onTenantChange$() { + return this.store.sliceUpdate(state => state.tenant); } - dispatchSetLanguage(...args: ConstructorParameters) { - return this.store.dispatch(new SetLanguage(...args)); + getLanguage() { + return this.store.state.language; + } + + getLanguage$() { + return this.store.sliceState(state => state.language); + } + + getTenant() { + return this.store.state.tenant; } - dispatchSetTenant(...args: ConstructorParameters) { - return this.store.dispatch(new SetTenant(...args)); + getTenant$() { + return this.store.sliceState(state => state.tenant); } - dispatchSetRemember(...args: ConstructorParameters) { - return this.store.dispatch(new SetRemember(...args)); + setTenant(tenant: ApplicationConfiguration.CurrentTenant) { + if (compare(tenant, this.store.state.tenant)) return; + + this.store.patch({ tenant }); } - dispatchModifyOpenedTabCount(...args: ConstructorParameters) { - return this.store.dispatch(new ModifyOpenedTabCount(...args)); + setLanguage(language: string) { + if (language === this.store.state.language) return; + + this.store.patch({ language }); } } diff --git a/npm/ng-packs/packages/core/src/lib/states/config.state.ts b/npm/ng-packs/packages/core/src/lib/states/config.state.ts index 736acb56a7..d11c7b44c9 100644 --- a/npm/ng-packs/packages/core/src/lib/states/config.state.ts +++ b/npm/ng-packs/packages/core/src/lib/states/config.state.ts @@ -6,11 +6,10 @@ import { catchError, switchMap, tap } from 'rxjs/operators'; import snq from 'snq'; import { GetAppConfiguration, SetEnvironment } from '../actions/config.actions'; import { RestOccurError } from '../actions/rest.actions'; -import { SetLanguage } from '../actions/session.actions'; import { ApplicationConfiguration } from '../models/application-configuration'; import { Config } from '../models/config'; +import { SessionStateService } from '../services/session-state.service'; import { interpolate } from '../utils/string-utils'; -import { SessionState } from './session.state'; @State({ name: 'ConfigState', @@ -211,7 +210,11 @@ export class ConfigState { return selector; } - constructor(private http: HttpClient, private store: Store) {} + constructor( + private http: HttpClient, + private store: Store, + private sessionState: SessionStateService, + ) {} @Action(GetAppConfiguration) addData({ patchState, dispatch }: StateContext) { @@ -226,7 +229,7 @@ export class ConfigState { }), ), switchMap(configuration => { - if (this.store.selectSnapshot(SessionState.getLanguage)) return of(null); + if (this.sessionState.getLanguage()) return of(null); let lang = configuration.localization.currentCulture.cultureName; if (lang.includes(';')) { @@ -234,7 +237,7 @@ export class ConfigState { } document.documentElement.setAttribute('lang', lang); - return dispatch(new SetLanguage(lang, false)); + return of(null).pipe(tap(() => this.sessionState.setLanguage(lang))); }), catchError((err: HttpErrorResponse) => { dispatch(new RestOccurError(err)); diff --git a/npm/ng-packs/packages/core/src/lib/states/index.ts b/npm/ng-packs/packages/core/src/lib/states/index.ts index 7b48292caa..76a5510ac9 100644 --- a/npm/ng-packs/packages/core/src/lib/states/index.ts +++ b/npm/ng-packs/packages/core/src/lib/states/index.ts @@ -1,4 +1,3 @@ export * from './replaceable-components.state'; export * from './config.state'; export * from './profile.state'; -export * from './session.state'; diff --git a/npm/ng-packs/packages/core/src/lib/states/session.state.ts b/npm/ng-packs/packages/core/src/lib/states/session.state.ts deleted file mode 100644 index a9bb89d3f0..0000000000 --- a/npm/ng-packs/packages/core/src/lib/states/session.state.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Injectable } from '@angular/core'; -import { - Action, - Actions, - ofActionSuccessful, - Selector, - State, - StateContext, - Store, -} from '@ngxs/store'; -import { OAuthService } from 'angular-oauth2-oidc'; -import { fromEvent } from 'rxjs'; -import { take } from 'rxjs/operators'; -import { GetAppConfiguration } from '../actions/config.actions'; -import { - ModifyOpenedTabCount, - SetLanguage, - SetRemember, - SetTenant, -} from '../actions/session.actions'; -import { ABP, Session } from '../models'; - -@State({ - name: 'SessionState', - defaults: { sessionDetail: { openedTabCount: 0 } } as Session.State, -}) -@Injectable() -export class SessionState { - @Selector() - static getLanguage({ language }: Session.State): string { - return language; - } - - @Selector() - static getTenant({ tenant }: Session.State): ABP.BasicItem { - return tenant; - } - - @Selector() - static getSessionDetail({ sessionDetail }: Session.State): Session.SessionDetail { - return sessionDetail; - } - - constructor(private oAuthService: OAuthService, private store: Store, private actions: Actions) { - actions - .pipe(ofActionSuccessful(GetAppConfiguration)) - .pipe(take(1)) - .subscribe(() => { - const sessionDetail = this.store.selectSnapshot(SessionState)?.sessionDetail || {}; - - const fiveMinutesBefore = new Date().valueOf() - 5 * 60 * 1000; - - if ( - sessionDetail.lastExitTime && - sessionDetail.openedTabCount === 0 && - this.oAuthService.hasValidAccessToken() && - sessionDetail.remember === false && - sessionDetail.lastExitTime < fiveMinutesBefore - ) { - this.oAuthService.logOut(); - } - - this.store.dispatch(new ModifyOpenedTabCount('increase')); - - fromEvent(window, 'unload').subscribe(event => { - this.store.dispatch(new ModifyOpenedTabCount('decrease')); - }); - }); - } - - @Action(SetLanguage) - setLanguage( - { patchState, dispatch }: StateContext, - { payload, dispatchAppConfiguration = true }: SetLanguage, - ) { - patchState({ - language: payload, - }); - - if (dispatchAppConfiguration) return dispatch(new GetAppConfiguration()); - } - - @Action(SetTenant) - setTenant({ patchState }: StateContext, { payload }: SetTenant) { - patchState({ - tenant: payload, - }); - } - - @Action(SetRemember) - setRemember( - { getState, patchState }: StateContext, - { payload: remember }: SetRemember, - ) { - const { sessionDetail } = getState(); - - patchState({ - sessionDetail: { - ...sessionDetail, - remember, - }, - }); - } - - @Action(ModifyOpenedTabCount) - modifyOpenedTabCount( - { getState, patchState }: StateContext, - { operation }: ModifyOpenedTabCount, - ) { - // tslint:disable-next-line: prefer-const - let { openedTabCount, lastExitTime, ...detail } = - getState().sessionDetail || ({ openedTabCount: 0 } as Session.SessionDetail); - - if (operation === 'increase') { - openedTabCount++; - } else if (operation === 'decrease') { - openedTabCount--; - lastExitTime = new Date().valueOf(); - } - - if (!openedTabCount || openedTabCount < 0) { - openedTabCount = 0; - } - - patchState({ - sessionDetail: { - openedTabCount, - lastExitTime, - ...detail, - }, - }); - } -} diff --git a/npm/ng-packs/packages/core/src/lib/tests/api.interceptor.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/api.interceptor.spec.ts index 8285a6a9f4..ed42194755 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/api.interceptor.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/api.interceptor.spec.ts @@ -4,30 +4,34 @@ import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; import { Store } from '@ngxs/store'; import { OAuthService } from 'angular-oauth2-oidc'; import { Subject, timer } from 'rxjs'; -import { ApiInterceptor } from '../interceptors'; import { StartLoader, StopLoader } from '../actions'; +import { ApiInterceptor } from '../interceptors'; +import { SessionStateService } from '../services'; describe('ApiInterceptor', () => { let spectator: SpectatorService; let interceptor: ApiInterceptor; let store: SpyObject; let oauthService: SpyObject; + let sessionState: SpyObject; const createService = createServiceFactory({ service: ApiInterceptor, - mocks: [OAuthService, Store], + mocks: [OAuthService, Store, SessionStateService], }); beforeEach(() => { spectator = createService(); interceptor = spectator.service; store = spectator.inject(Store); + sessionState = spectator.inject(SessionStateService); oauthService = spectator.inject(OAuthService); }); it('should add headers to http request', done => { oauthService.getAccessToken.andReturn('ey892mkwa8^2jk'); - store.selectSnapshot.andReturn({ id: 'test' }); + sessionState.getLanguage.andReturn('tr'); + sessionState.getTenant.andReturn({ id: 'Volosoft', name: 'Volosoft' }); const request = new HttpRequest('GET', 'https://abp.io'); const handleRes$ = new Subject(); @@ -35,8 +39,8 @@ describe('ApiInterceptor', () => { const handler = { handle: (req: HttpRequest) => { expect(req.headers.get('Authorization')).toEqual('Bearer ey892mkwa8^2jk'); - expect(req.headers.get('Accept-Language')).toEqual({ id: 'test' } as any); - expect(req.headers.get('__tenant')).toEqual('test'); + expect(req.headers.get('Accept-Language')).toEqual('tr'); + expect(req.headers.get('__tenant')).toEqual('Volosoft'); done(); return handleRes$; }, diff --git a/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts index 1660f916c1..8a81dec2dd 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts @@ -2,10 +2,13 @@ import { HttpClient } from '@angular/common/http'; import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest'; import { Store } from '@ngxs/store'; import { of, ReplaySubject, timer } from 'rxjs'; -import { SetLanguage } from '../actions'; import { ApplicationConfiguration } from '../models/application-configuration'; import { Config } from '../models/config'; -import { ApplicationConfigurationService, ConfigStateService } from '../services'; +import { + ApplicationConfigurationService, + ConfigStateService, + SessionStateService, +} from '../services'; import { ConfigState } from '../states'; export const CONFIG_STATE_DATA = ({ @@ -116,7 +119,11 @@ describe('ConfigState', () => { spectator = createService(); store = spectator.inject(Store); service = spectator.service; - state = new ConfigState(spectator.inject(HttpClient), store); + state = new ConfigState( + spectator.inject(HttpClient), + store, + spectator.inject(SessionStateService), + ); }); describe('#getAll', () => { @@ -268,8 +275,6 @@ describe('ConfigState', () => { timer(0).subscribe(() => { expect(patchStateArg).toEqual(configuration); - expect(dispatchArg instanceof SetLanguage).toBeTruthy(); - expect(dispatchArg).toEqual({ payload: 'en', dispatchAppConfiguration: false }); done(); }); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/initial-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/initial-utils.spec.ts index de1e27f756..628a685789 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/initial-utils.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/initial-utils.spec.ts @@ -4,6 +4,7 @@ import { Store } from '@ngxs/store'; import { OAuthService } from 'angular-oauth2-oidc'; import { of } from 'rxjs'; import { GetAppConfiguration } from '../actions'; +import { SessionStateService } from '../services'; import * as AuthFlowStrategy from '../strategies/auth-flow.strategy'; import { CORE_OPTIONS } from '../tokens/options.token'; import { checkAccessToken, getInitialData, localeInitializer } from '../utils'; @@ -79,9 +80,8 @@ describe('InitialUtils', () => { test('should resolve registerLocale', async () => { const injector = spectator.inject(Injector); const injectorSpy = jest.spyOn(injector, 'get'); - const store = spectator.inject(Store); - store.selectSnapshot.andCallFake(selector => selector({ SessionState: { language: 'tr' } })); - injectorSpy.mockReturnValueOnce(store); + const sessionState = spectator.inject(SessionStateService); + injectorSpy.mockReturnValueOnce(sessionState); injectorSpy.mockReturnValueOnce({ registerLocaleFn: () => Promise.resolve() }); expect(typeof localeInitializer(injector)).toBe('function'); expect(await localeInitializer(injector)()).toBe('resolved'); diff --git a/npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts index ca6b28acd7..1d9e857f8a 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts @@ -2,12 +2,14 @@ import { CORE_OPTIONS } from '../tokens/options.token'; import { Router } from '@angular/router'; import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest'; import { Actions, Store } from '@ngxs/store'; -import { of, Subject } from 'rxjs'; +import { BehaviorSubject, of, Subject } from 'rxjs'; import { LocalizationService } from '../services/localization.service'; +import { SessionStateService } from '../services'; describe('LocalizationService', () => { let spectator: SpectatorService; let store: SpyObject; + let router: SpyObject; let service: LocalizationService; const createService = createServiceFactory({ @@ -26,16 +28,21 @@ describe('LocalizationService', () => { beforeEach(() => { spectator = createService(); store = spectator.inject(Store); + store.dispatch.mockReturnValue(new BehaviorSubject('tr')); + router = spectator.inject(Router); + router.routeReuseStrategy = {} as any; service = spectator.service; + + const sessionState = spectator.inject(SessionStateService); + sessionState.setLanguage('tr'); }); describe('#currentLang', () => { - it('should be tr', () => { - store.selectSnapshot.andCallFake((selector: (state: any, ...states: any[]) => string) => { - return selector({ SessionState: { language: 'tr' } }); - }); - - expect(service.currentLang).toBe('tr'); + it('should be tr', done => { + setTimeout(() => { + expect(service.currentLang).toBe('tr'); + done(); + }, 0); }); }); @@ -76,7 +83,13 @@ describe('LocalizationService', () => { it('should throw an error message when service have an otherInstance', async () => { try { - const instance = new LocalizationService(new Subject(), null, null, null, {} as any); + const instance = new LocalizationService( + { getLanguage: () => {} } as any, + null, + null, + null, + {} as any, + ); } catch (error) { expect((error as Error).message).toBe('LocalizationService should have only one instance.'); } diff --git a/npm/ng-packs/packages/core/src/lib/tests/session-state.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/session-state.service.spec.ts deleted file mode 100644 index d1c4dd5024..0000000000 --- a/npm/ng-packs/packages/core/src/lib/tests/session-state.service.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest'; -import { SessionStateService } from '../services/session-state.service'; -import { SessionState } from '../states/session.state'; -import { Store } from '@ngxs/store'; -import * as SessionActions from '../actions'; -import { OAuthService } from 'angular-oauth2-oidc'; - -describe('SessionStateService', () => { - let service: SessionStateService; - let spectator: SpectatorService; - let store: SpyObject; - - const createService = createServiceFactory({ - service: SessionStateService, - mocks: [Store, OAuthService], - }); - beforeEach(() => { - spectator = createService(); - service = spectator.service; - store = spectator.inject(Store); - }); - test('should have the all SessionState static methods', () => { - const reg = /(?<=static )(.*)(?=\()/gm; - SessionState.toString() - .match(reg) - .forEach(fnName => { - expect(service[fnName]).toBeTruthy(); - - const spy = jest.spyOn(store, 'selectSnapshot'); - spy.mockClear(); - - const isDynamicSelector = SessionState[fnName].name !== 'memoized'; - - if (isDynamicSelector) { - SessionState[fnName] = jest.fn((...args) => args); - service[fnName]('test', 0, {}); - expect(SessionState[fnName]).toHaveBeenCalledWith('test', 0, {}); - } else { - service[fnName](); - expect(spy).toHaveBeenCalledWith(SessionState[fnName]); - } - }); - }); - - test('should have a dispatch method for every sessionState action', () => { - const reg = /(?<=dispatch)(\w+)(?=\()/gm; - SessionStateService.toString() - .match(reg) - .forEach(fnName => { - expect(SessionActions[fnName]).toBeTruthy(); - - const spy = jest.spyOn(store, 'dispatch'); - spy.mockClear(); - - const params = Array.from(new Array(SessionActions[fnName].length)); - - service[`dispatch${fnName}`](...params); - expect(spy).toHaveBeenCalledWith(new SessionActions[fnName](...params)); - }); - }); -}); diff --git a/npm/ng-packs/packages/core/src/lib/tests/session.state.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/session.state.spec.ts deleted file mode 100644 index 8a7953dd16..0000000000 --- a/npm/ng-packs/packages/core/src/lib/tests/session.state.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; -import { Session } from '../models/session'; -import { LocalizationService, AuthService } from '../services'; -import { SessionState } from '../states'; -import { GetAppConfiguration } from '../actions/config.actions'; -import { of, Subject } from 'rxjs'; -import { Store, Actions } from '@ngxs/store'; -import { OAuthService } from 'angular-oauth2-oidc'; - -export class DummyClass {} - -export const SESSION_STATE_DATA = { - language: 'tr', - tenant: { id: 'd5692aef-2ac6-49cd-9f3e-394c0bd4f8b3', name: 'Test' }, -} as Session.State; - -describe('SessionState', () => { - let spectator: SpectatorService; - let state: SessionState; - - const createService = createServiceFactory({ - service: DummyClass, - mocks: [LocalizationService, Store, Actions, OAuthService], - }); - - beforeEach(() => { - spectator = createService(); - state = new SessionState(null, null, new Subject()); - }); - - describe('#getLanguage', () => { - it('should return the current language', () => { - expect(SessionState.getLanguage(SESSION_STATE_DATA)).toEqual(SESSION_STATE_DATA.language); - }); - }); - - describe('#getTenant', () => { - it('should return the tenant object', () => { - expect(SessionState.getTenant(SESSION_STATE_DATA)).toEqual(SESSION_STATE_DATA.tenant); - }); - }); - - describe('#SetLanguage', () => { - it('should set the language and dispatch the GetAppConfiguration action', () => { - let patchedData; - let dispatchedData; - const patchState = jest.fn(data => (patchedData = data)); - const dispatch = jest.fn(action => { - dispatchedData = action; - return of({}); - }); - - state.setLanguage({ patchState, dispatch } as any, { payload: 'en' }).subscribe(); - - expect(patchedData).toEqual({ language: 'en' }); - expect(dispatchedData instanceof GetAppConfiguration).toBeTruthy(); - }); - }); - - describe('#setTenantId', () => { - it('should set the tenant', () => { - let patchedData; - const patchState = jest.fn(data => (patchedData = data)); - const testTenant = { id: '54ae02ba-9289-4c1b-8521-0ea437756288', name: 'Test Tenant' }; - state.setTenant({ patchState } as any, { payload: testTenant }); - - expect(patchedData).toEqual({ tenant: testTenant }); - }); - }); -}); diff --git a/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts index cf7771c094..f34f46e6f0 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts @@ -4,8 +4,10 @@ import { Store } from '@ngxs/store'; import { OAuthService } from 'angular-oauth2-oidc'; import { tap } from 'rxjs/operators'; import { GetAppConfiguration } from '../actions/config.actions'; +import { ApplicationConfiguration } from '../models/application-configuration'; import { ABP } from '../models/common'; import { AuthService } from '../services/auth.service'; +import { SessionStateService } from '../services/session-state.service'; import { ConfigState } from '../states/config.state'; import { clearOAuthStorage } from '../strategies/auth-flow.strategy'; import { CORE_OPTIONS } from '../tokens/options.token'; @@ -25,7 +27,17 @@ export function getInitialData(injector: Injector) { return store .dispatch(new GetAppConfiguration()) - .pipe(tap(res => checkAccessToken(store, injector))) + .pipe( + tap(() => checkAccessToken(store, injector)), + tap(() => { + const currentTenant = store.selectSnapshot( + ConfigState.getDeep('currentTenant'), + ) as ApplicationConfiguration.CurrentTenant; + if (!currentTenant?.id) return; + + injector.get(SessionStateService).setTenant(currentTenant); + }), + ) .toPromise(); }; @@ -41,10 +53,10 @@ export function checkAccessToken(store: Store, injector: Injector) { export function localeInitializer(injector: Injector) { const fn = () => { - const store: Store = injector.get(Store); + const sessionState = injector.get(SessionStateService); const { registerLocaleFn }: ABP.Root = injector.get(CORE_OPTIONS); - const lang = store.selectSnapshot(state => state.SessionState.language) || 'en'; + const lang = sessionState.getLanguage() || 'en'; return new Promise((resolve, reject) => { registerLocaleFn(lang).then(module => { diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/languages.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/languages.component.ts index 115e9e563e..fae3fa0cf4 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/languages.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/languages.component.ts @@ -1,6 +1,6 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { Store, Select } from '@ngxs/store'; -import { SetLanguage, ConfigState, ApplicationConfiguration, SessionState } from '@abp/ng.core'; +import { ApplicationConfiguration, ConfigState, SessionStateService } from '@abp/ng.core'; +import { Component, OnInit } from '@angular/core'; +import { Select } from '@ngxs/store'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import snq from 'snq'; @@ -75,14 +75,14 @@ export class LanguagesComponent implements OnInit { } get selectedLangCulture(): string { - return this.store.selectSnapshot(SessionState.getLanguage); + return this.sessionState.getLanguage(); } - constructor(private store: Store) {} + constructor(private sessionState: SessionStateService) {} ngOnInit() {} onChangeLang(cultureName: string) { - this.store.dispatch(new SetLanguage(cultureName)); + this.sessionState.setLanguage(cultureName); } } diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/enum.util.ts b/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/enum.util.ts index 869b9a8c80..6e9985d0f6 100644 --- a/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/enum.util.ts +++ b/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/enum.util.ts @@ -49,7 +49,7 @@ export function createEnumOptions( } function createLocalizationStream(l10n: LocalizationService, mapTarget: any) { - return merge(of(null), l10n.languageChange).pipe(map(() => mapTarget)); + return merge(of(null), l10n.languageChange$).pipe(map(() => mapTarget)); } function createEnumLocalizer( diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/tests/enum.util.spec.ts b/npm/ng-packs/packages/theme-shared/extensions/src/tests/enum.util.spec.ts index 68f7df2fb5..12f520b12a 100644 --- a/npm/ng-packs/packages/theme-shared/extensions/src/tests/enum.util.spec.ts +++ b/npm/ng-packs/packages/theme-shared/extensions/src/tests/enum.util.spec.ts @@ -1,10 +1,16 @@ import { LocalizationService } from '@abp/ng.core'; import { Store } from '@ngxs/store'; -import { Subject } from 'rxjs'; +import { BehaviorSubject, Subject } from 'rxjs'; import { take } from 'rxjs/operators'; import { PropData } from '../lib/models/props'; import { createEnum, createEnumOptions, createEnumValueResolver } from '../lib/utils/enum.util'; +const mockSessionState = { + languageChange$: new BehaviorSubject('tr'), + getLanguage: () => 'tr', + onLanguageChange$: () => new BehaviorSubject('tr'), +} as any; + const fields = [ { name: 'foo', value: 1 }, { name: 'bar', value: 2 }, @@ -38,7 +44,7 @@ describe('Enum Utils', () => { describe('#createEnumValueResolver', () => { const service = new LocalizationService( - new Subject().asObservable(), + mockSessionState, ({ selectSnapshot: () => ({ values: { @@ -78,9 +84,7 @@ describe('Enum Utils', () => { const propData = new MockPropData({ extraProperties: { EnumProp: value } }); propData.getInjected = () => service as any; - const resolved = await valueResolver(propData) - .pipe(take(1)) - .toPromise(); + const resolved = await valueResolver(propData).pipe(take(1)).toPromise(); expect(resolved).toBe(expected); }, @@ -89,7 +93,7 @@ describe('Enum Utils', () => { describe('#createEnumOptions', () => { const service = new LocalizationService( - new Subject().asObservable(), + mockSessionState, ({ selectSnapshot: () => ({ values: { @@ -119,9 +123,7 @@ describe('Enum Utils', () => { const propData = new MockPropData({}); propData.getInjected = () => service as any; - const resolved = await options(propData) - .pipe(take(1)) - .toPromise(); + const resolved = await options(propData).pipe(take(1)).toPromise(); expect(resolved).toEqual([ { key: 'Foo', value: 1 }, diff --git a/npm/ng-packs/packages/theme-shared/src/lib/handlers/lazy-style.handler.ts b/npm/ng-packs/packages/theme-shared/src/lib/handlers/lazy-style.handler.ts index c52214bf95..79bed70563 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/handlers/lazy-style.handler.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/handlers/lazy-style.handler.ts @@ -57,14 +57,9 @@ export class LazyStyleHandler { const l10n = injector.get(LocalizationService); // will always listen, no need to unsubscribe - l10n.languageChange - .pipe( - map(({ payload }) => payload), - startWith(l10n.currentLang), - ) - .subscribe(locale => { - this.dir = getLocaleDirection(locale); - }); + l10n.languageChange$.pipe(startWith(l10n.currentLang)).subscribe(locale => { + this.dir = getLocaleDirection(locale); + }); } private setBodyDir(dir: LocaleDirection) { diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/lazy-style.handler.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/lazy-style.handler.spec.ts index 5836bee474..ea02893f90 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/lazy-style.handler.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/lazy-style.handler.spec.ts @@ -4,7 +4,7 @@ import { EMPTY, of } from 'rxjs'; import { BOOTSTRAP } from '../constants/styles'; import { createLazyStyleHref, initLazyStyleHandler, LazyStyleHandler } from '../handlers'; -const languageChange = of({ payload: 'en' }); +const languageChange$ = of({ payload: 'en' }); describe('LazyStyleHandler', () => { let spectator: SpectatorService; @@ -16,7 +16,7 @@ describe('LazyStyleHandler', () => { providers: [ { provide: LocalizationService, - useValue: { currentLang: 'en', languageChange }, + useValue: { currentLang: 'en', languageChange$ }, }, ], }); @@ -50,10 +50,10 @@ describe('LazyStyleHandler', () => { describe('initLazyStyleHandler', () => { it('should return a LazyStyleHandler factory', () => { - const generator = (function*() { + const generator = (function* () { yield undefined; // LAZY_STYLES yield { loaded: new Map() }; // LazyLoadService - yield { currentLang: 'en', languageChange: EMPTY }; // LocalizationService + yield { currentLang: 'en', languageChange$: EMPTY }; // LocalizationService })(); const injector = { diff --git a/npm/ng-packs/yarn.lock b/npm/ng-packs/yarn.lock index 98afa1cf3a..32320d2877 100644 --- a/npm/ng-packs/yarn.lock +++ b/npm/ng-packs/yarn.lock @@ -102,7 +102,7 @@ chart.js "^2.9.3" tslib "^2.0.0" -"@abp/utils@^3.3.0", "@abp/utils@^3.3.0-rc.2": +"@abp/utils@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.3.0.tgz#44ccacf4c415227e94981d907017de1c6f7225bb" integrity sha512-/JIcygVJu/Ob1G5IicL2XgOVjIIePC2t3QzgEbwaq6NDGMHiOm2quyS8Lj7TfjBeUFTJhCFEbXJMq0lGQ+KMww==