Merge pull request #6116 from abpframework/feat/5606

Removed the SessionState
pull/6122/head
Levent Arman Özak 5 years ago committed by GitHub
commit 71bc369bca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -309,10 +309,12 @@ import { registerLocale } from './register-locale';
]
```
After this custom `registerLocale` function, only en and fr locale files will be created as chunks:
After this custom `registerLocale` function, since the en and fr added to the `webpackInclude`, only en and fr locale files will be created as chunks:
![locale chunks](https://user-images.githubusercontent.com/34455572/98203212-acaa2100-1f44-11eb-85af-4eb66d296326.png)
Which locale files you add to `webpackInclude` magic comment, they will be included in the bundle
## See Also

@ -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",

@ -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(() => {});
}
}

@ -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<ABP.BasicItem>;
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() {

@ -9,7 +9,6 @@
"@abp/utils",
"@angular/localize",
"@ngxs/router-plugin",
"@ngxs/storage-plugin",
"@ngxs/store",
"angular-oauth2-oidc",
"just-compare",

@ -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",

@ -3,4 +3,3 @@ export * from './loader.actions';
export * from './profile.actions';
export * from './replaceable-components.actions';
export * from './rest.actions';
export * from './session.actions';

@ -1,18 +0,0 @@
import { ABP } from '../models';
export class SetLanguage {
static readonly type = '[Session] Set Language';
constructor(public payload: string, public dispatchAppConfiguration?: boolean) {}
}
export class SetTenant {
static readonly type = '[Session] Set Tenant';
constructor(public payload: ABP.BasicItem) {}
}
export class ModifyOpenedTabCount {
static readonly type = '[Session] Modify Opened Tab Count';
constructor(public operation: 'increase' | 'decrease') {}
}
export class SetRemember {
static readonly type = '[Session] Set Remember';
constructor(public payload: boolean) {}
}

@ -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);
});

@ -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'],
},
},
],
};
}

@ -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<any>, 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;
}

@ -6,6 +6,7 @@ export namespace ApplicationConfiguration {
auth: Auth;
setting: Value;
currentUser: CurrentUser;
currentTenant: CurrentTenant;
features: Value;
}
@ -74,4 +75,10 @@ export namespace ApplicationConfiguration {
phoneNumberVerified: boolean;
surName: string;
}
export interface CurrentTenant {
id: string;
name: string;
isAvailable?: boolean;
}
}

@ -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<any>;
skipGetAppConfiguration?: boolean;
sendNullsAsQueryParam?: boolean;
ngxsStoragePluginOptions?: NgxsStoragePluginOptions & { key?: string[] };
}
export interface Test {

@ -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;
}

@ -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<any> {
const tenant = this.store.selectSnapshot(SessionState.getTenant);
const tenant = this.sessionState.getTenant();
return from(
this.oAuthService.fetchTokenUsingPasswordFlow(

@ -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 { filter, 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<string>();
/**
* Returns currently selected language
*/
get currentLang(): string {
return this.store.selectSnapshot(state => state.SessionState.language);
return this.latestLang;
}
get languageChange(): Observable<SetLanguage> {
return this.actions.pipe(
ofActionSuccessful(SetLanguage),
filter((action: SetLanguage) => action.dispatchAppConfiguration !== false),
);
get languageChange$(): Observable<string> {
return this._languageChange$.asObservable();
}
constructor(
private actions: Actions,
private sessionState: SessionStateService,
private store: Store,
private injector: Injector,
private ngZone: NgZone,
@ -44,7 +45,21 @@ export class LocalizationService {
}
private listenToSetLanguage() {
this.languageChange.subscribe(({ payload }) => this.registerLocale(payload));
this.sessionState
.onLanguageChange$()
.pipe(
filter(
lang =>
this.store.selectSnapshot(
ConfigState.getDeep('localization.currentCulture.cultureName'),
) !== lang,
),
switchMap(lang => this.store.dispatch(new GetAppConfiguration()).pipe(mapTo(lang))),
)
.subscribe(lang => {
this.registerLocale(lang);
this._languageChange$.next(lang);
});
}
registerLocale(locale: string) {
@ -57,6 +72,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);

@ -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<string>): Observable<FindTenantResultDto> {
return this.restService.request(

@ -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<typeof SetLanguage>) {
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<typeof SetTenant>) {
return this.store.dispatch(new SetTenant(...args));
getTenant$() {
return this.store.sliceState(state => state.tenant);
}
dispatchSetRemember(...args: ConstructorParameters<typeof SetRemember>) {
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<typeof ModifyOpenedTabCount>) {
return this.store.dispatch(new ModifyOpenedTabCount(...args));
setLanguage(language: string) {
if (language === this.store.state.language) return;
this.store.patch({ language });
}
}

@ -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<Config.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<Config.State>) {
@ -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));

@ -1,4 +1,3 @@
export * from './replaceable-components.state';
export * from './config.state';
export * from './profile.state';
export * from './session.state';

@ -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<Session.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<Session.State>,
{ payload, dispatchAppConfiguration = true }: SetLanguage,
) {
patchState({
language: payload,
});
if (dispatchAppConfiguration) return dispatch(new GetAppConfiguration());
}
@Action(SetTenant)
setTenant({ patchState }: StateContext<Session.State>, { payload }: SetTenant) {
patchState({
tenant: payload,
});
}
@Action(SetRemember)
setRemember(
{ getState, patchState }: StateContext<Session.State>,
{ payload: remember }: SetRemember,
) {
const { sessionDetail } = getState();
patchState({
sessionDetail: {
...sessionDetail,
remember,
},
});
}
@Action(ModifyOpenedTabCount)
modifyOpenedTabCount(
{ getState, patchState }: StateContext<Session.State>,
{ 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,
},
});
}
}

@ -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<ApiInterceptor>;
let interceptor: ApiInterceptor;
let store: SpyObject<Store>;
let oauthService: SpyObject<OAuthService>;
let sessionState: SpyObject<SessionStateService>;
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<any>) => {
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$;
},

@ -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();
});
});

@ -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');

@ -2,12 +2,16 @@ 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';
const shouldReuseRoute = () => true;
describe('LocalizationService', () => {
let spectator: SpectatorService<LocalizationService>;
let store: SpyObject<Store>;
let router: SpyObject<Router>;
let service: LocalizationService;
const createService = createServiceFactory({
@ -26,16 +30,21 @@ describe('LocalizationService', () => {
beforeEach(() => {
spectator = createService();
store = spectator.inject(Store);
store.dispatch.mockReturnValue(new BehaviorSubject('tr'));
router = spectator.inject(Router);
router.routeReuseStrategy = { shouldReuseRoute } 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);
});
});
@ -59,11 +68,6 @@ describe('LocalizationService', () => {
describe('#registerLocale', () => {
it('should return registerLocale and then call setRouteReuse', () => {
const router = spectator.inject(Router);
const shouldReuseRoute = () => true;
router.routeReuseStrategy = { shouldReuseRoute } as any;
router.navigateByUrl.andCallFake(url => {
return new Promise(resolve => resolve({ catch: () => null }));
});
@ -76,7 +80,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.');
}

@ -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<SessionStateService>;
let store: SpyObject<Store>;
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));
});
});
});

@ -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<DummyClass>;
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 });
});
});
});

@ -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 => {

@ -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);
}
}

@ -49,7 +49,7 @@ export function createEnumOptions<T = any>(
}
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(

@ -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 },

@ -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) {

@ -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<LazyStyleHandler>;
@ -16,7 +16,7 @@ describe('LazyStyleHandler', () => {
providers: [
{
provide: LocalizationService,
useValue: { currentLang: 'en', languageChange },
useValue: { currentLang: 'en', languageChange$ },
},
],
});
@ -53,7 +53,7 @@ describe('initLazyStyleHandler', () => {
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 = {

@ -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==

Loading…
Cancel
Save