mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
committed by
Nino Righi
parent
a518fc50e2
commit
f37dfd41f1
47
apps/isa-app/src/adapter/scan/dev.scan-adapter.ts
Normal file
47
apps/isa-app/src/adapter/scan/dev.scan-adapter.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Injectable, isDevMode } from '@angular/core';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { PromptModalData, UiModalService, UiPromptModalComponent } from '@ui/modal';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ScanAdapter } from './scan-adapter';
|
||||
|
||||
@Injectable()
|
||||
export class DevScanAdapter implements ScanAdapter {
|
||||
readonly name = 'Dev';
|
||||
|
||||
constructor(
|
||||
private _modal: UiModalService,
|
||||
private _environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
async init(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
// return new Promise((resolve, reject) => {
|
||||
// resolve(isDevMode());
|
||||
// });
|
||||
}
|
||||
|
||||
scan(): Observable<string> {
|
||||
return new Observable((observer) => {
|
||||
const modalRef = this._modal.open({
|
||||
content: UiPromptModalComponent,
|
||||
title: 'Scannen',
|
||||
data: {
|
||||
message: 'Diese Eingabemaske dient nur zu Entwicklungs und Testzwecken.',
|
||||
placeholder: 'Scan Code',
|
||||
confirmText: 'weiter',
|
||||
cancelText: 'abbrechen',
|
||||
} as PromptModalData,
|
||||
});
|
||||
|
||||
const sub = modalRef.afterClosed$.subscribe((result) => {
|
||||
observer.next(result.data);
|
||||
observer.complete();
|
||||
});
|
||||
|
||||
return () => {
|
||||
modalRef.close();
|
||||
sub.unsubscribe();
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
5
apps/isa-app/src/adapter/scan/dummy.spec.ts
Normal file
5
apps/isa-app/src/adapter/scan/dummy.spec.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
describe('Dummy', () => {
|
||||
it('should work', () => {
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
});
|
||||
7
apps/isa-app/src/adapter/scan/index.ts
Normal file
7
apps/isa-app/src/adapter/scan/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './dev.scan-adapter';
|
||||
export * from './native.scan-adapter';
|
||||
export * from './scan-adapter';
|
||||
export * from './scan.module';
|
||||
export * from './scan.service';
|
||||
export * from './scandit';
|
||||
export * from './tokens';
|
||||
26
apps/isa-app/src/adapter/scan/native.scan-adapter.ts
Normal file
26
apps/isa-app/src/adapter/scan/native.scan-adapter.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NativeContainerService } from '@external/native-container';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map, take } from 'rxjs/operators';
|
||||
import { ScanAdapter } from './scan-adapter';
|
||||
|
||||
@Injectable()
|
||||
export class NativeScanAdapter implements ScanAdapter {
|
||||
readonly name = 'Native';
|
||||
|
||||
constructor(private readonly nativeContainerService: NativeContainerService) {}
|
||||
|
||||
init(): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(this.nativeContainerService.isNative);
|
||||
});
|
||||
}
|
||||
|
||||
scan(): Observable<string> {
|
||||
return this.nativeContainerService.openScanner('scanBook').pipe(
|
||||
filter((result) => result.status === 'SUCCESS'),
|
||||
map((result) => result.data),
|
||||
take(1),
|
||||
);
|
||||
}
|
||||
}
|
||||
18
apps/isa-app/src/adapter/scan/scan-adapter.ts
Normal file
18
apps/isa-app/src/adapter/scan/scan-adapter.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export interface ScanAdapter {
|
||||
/**
|
||||
* Name to identify the adapter
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* @returns true if this adapter can be used
|
||||
*/
|
||||
init(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* scan for a barcode
|
||||
*/
|
||||
scan(): Observable<string>;
|
||||
}
|
||||
19
apps/isa-app/src/adapter/scan/scan.module.ts
Normal file
19
apps/isa-app/src/adapter/scan/scan.module.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { DevScanAdapter } from './dev.scan-adapter';
|
||||
import { NativeScanAdapter } from './native.scan-adapter';
|
||||
import { SCAN_ADAPTER } from './tokens';
|
||||
|
||||
@NgModule({})
|
||||
export class ScanAdapterModule {
|
||||
static forRoot() {
|
||||
return {
|
||||
ngModule: ScanAdapterModule,
|
||||
providers: [
|
||||
{ provide: SCAN_ADAPTER, useClass: NativeScanAdapter, multi: true },
|
||||
{ provide: SCAN_ADAPTER, useClass: DevScanAdapter, multi: true },
|
||||
],
|
||||
// Use for testing:
|
||||
// providers: [{ provide: SCAN_ADAPTER, useClass: dev ? DevScanAdapter : NativeScanAdapter, multi: true }],
|
||||
};
|
||||
}
|
||||
}
|
||||
52
apps/isa-app/src/adapter/scan/scan.service.ts
Normal file
52
apps/isa-app/src/adapter/scan/scan.service.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { ScanAdapter } from './scan-adapter';
|
||||
import { SCAN_ADAPTER } from './tokens';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ScanAdapterService {
|
||||
private _readyAdapters: Record<string, boolean> = {};
|
||||
|
||||
constructor(@Inject(SCAN_ADAPTER) private readonly scanAdapters: ScanAdapter[]) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
for (const adapter of this.scanAdapters) {
|
||||
const isReady = await adapter.init();
|
||||
this._readyAdapters[adapter.name] = isReady;
|
||||
}
|
||||
}
|
||||
|
||||
adapters(): ScanAdapter[] {
|
||||
return [...this.scanAdapters];
|
||||
}
|
||||
|
||||
getAdapter(name: string): ScanAdapter | undefined {
|
||||
return this._readyAdapters[name] && this.scanAdapters.find((adapter) => adapter.name === name);
|
||||
}
|
||||
|
||||
isReady(): boolean {
|
||||
return Object.values(this._readyAdapters).some((ready) => ready);
|
||||
}
|
||||
|
||||
scan(): Observable<string> {
|
||||
const adapterOrder = ['Native', 'Scandit', 'Dev'];
|
||||
|
||||
let adapter: ScanAdapter;
|
||||
|
||||
for (const name of adapterOrder) {
|
||||
adapter = this.getAdapter(name);
|
||||
|
||||
if (adapter) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!adapter) {
|
||||
return throwError('No adapter found');
|
||||
}
|
||||
|
||||
return adapter.scan();
|
||||
}
|
||||
}
|
||||
3
apps/isa-app/src/adapter/scan/scandit/index.ts
Normal file
3
apps/isa-app/src/adapter/scan/scandit/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './scandit-overlay.component';
|
||||
export * from './scandit-scan-adapter.module';
|
||||
export * from './scandit.scan-adapter';
|
||||
@@ -0,0 +1,19 @@
|
||||
:host {
|
||||
@apply block relative;
|
||||
}
|
||||
|
||||
.scanner-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.close-scanner {
|
||||
@apply absolute bottom-12 left-[50%] -translate-x-[50%] block px-6 py-4 bg-white text-brand border-2 border-solid border-brand rounded-full text-lg font-bold mx-auto mt-4;
|
||||
}
|
||||
|
||||
@screen desktop {
|
||||
.scanner-container {
|
||||
max-width: 900px;
|
||||
max-height: 900px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<div class="scanner-container" #scanContainer></div>
|
||||
<button class="close-scanner" type="button" (click)="close()">Scan abbrechen</button>
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Component, ChangeDetectionStrategy, ElementRef, ViewChild, NgZone, AfterViewInit, OnDestroy } from '@angular/core';
|
||||
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
|
||||
import { Barcode, BarcodePicker, ScanResult, ScanSettings } from 'scandit-sdk';
|
||||
|
||||
@Component({
|
||||
selector: 'app-scandit-overlay',
|
||||
templateUrl: 'scandit-overlay.component.html',
|
||||
styleUrls: ['scandit-overlay.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ScanditOverlayComponent implements AfterViewInit, OnDestroy {
|
||||
private _barcodePicker: BarcodePicker;
|
||||
|
||||
private _onScan?: (code: string) => void;
|
||||
|
||||
private _onClose?: () => void;
|
||||
|
||||
@ViewChild('scanContainer', { read: ElementRef, static: true }) scanContainer: ElementRef;
|
||||
|
||||
constructor(
|
||||
private _zone: NgZone,
|
||||
private _modal: UiModalService,
|
||||
) {}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.createBarcodePicker()
|
||||
.then(() => {
|
||||
this._barcodePicker.on('scan', (scanResult) => {
|
||||
this._zone.run(() => this.handleScanrResult(scanResult));
|
||||
});
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this._modal
|
||||
.open({
|
||||
content: UiMessageModalComponent,
|
||||
title: 'Zugriff auf Kamera verweigert',
|
||||
data: { message: 'Falls Sie den Zugriff erlauben möchten, können Sie das über die Webseiteinstellung Ihres Browsers.' },
|
||||
})
|
||||
.afterClosed$.subscribe(() => {
|
||||
this._onClose?.();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async createBarcodePicker() {
|
||||
this._barcodePicker = await BarcodePicker.create(this.scanContainer.nativeElement, {
|
||||
playSoundOnScan: true,
|
||||
vibrateOnScan: true,
|
||||
});
|
||||
|
||||
this._barcodePicker.applyScanSettings(this.getScanSettings());
|
||||
}
|
||||
|
||||
getScanSettings(): ScanSettings {
|
||||
return new ScanSettings({
|
||||
blurryRecognition: false,
|
||||
|
||||
enabledSymbologies: [
|
||||
Barcode.Symbology.EAN8,
|
||||
Barcode.Symbology.EAN13,
|
||||
Barcode.Symbology.UPCA,
|
||||
Barcode.Symbology.UPCE,
|
||||
Barcode.Symbology.CODE128,
|
||||
Barcode.Symbology.CODE39,
|
||||
Barcode.Symbology.CODE93,
|
||||
Barcode.Symbology.INTERLEAVED_2_OF_5,
|
||||
Barcode.Symbology.QR,
|
||||
],
|
||||
codeDuplicateFilter: 1000,
|
||||
});
|
||||
}
|
||||
|
||||
onScan(fn: (code: string) => void) {
|
||||
this._onScan = fn;
|
||||
}
|
||||
|
||||
onClose(fn: () => void) {
|
||||
this._onClose = fn;
|
||||
}
|
||||
|
||||
handleScanrResult(scanRestul: ScanResult) {
|
||||
let result: string | undefined;
|
||||
if (scanRestul.barcodes.length) {
|
||||
result = scanRestul.barcodes[0].data;
|
||||
} else if (scanRestul.texts.length) {
|
||||
result = scanRestul.texts[0].value;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
this._onScan?.(result);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this._onClose?.();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._zone.runOutsideAngular(() => {
|
||||
this._barcodePicker?.destroy(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { ScanditOverlayComponent } from './scandit-overlay.component';
|
||||
import { ScanditScanAdapter } from './scandit.scan-adapter';
|
||||
import { SCAN_ADAPTER } from '../tokens';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
exports: [ScanditOverlayComponent],
|
||||
declarations: [ScanditOverlayComponent],
|
||||
})
|
||||
export class ScanditScanAdapterModule {
|
||||
static forRoot() {
|
||||
return {
|
||||
ngModule: ScanditScanAdapterModule,
|
||||
providers: [{ provide: SCAN_ADAPTER, useClass: ScanditScanAdapter, multi: true }],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, Subscriber } from 'rxjs';
|
||||
import { ScanAdapter } from '../scan-adapter';
|
||||
import { Overlay } from '@angular/cdk/overlay';
|
||||
|
||||
import { configure } from 'scandit-sdk';
|
||||
// import { ScanditModalComponent } from './scandit-modal';
|
||||
import { Config } from '@core/config';
|
||||
import { ComponentPortal } from '@angular/cdk/portal';
|
||||
import { ScanditOverlayComponent } from './scandit-overlay.component';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { injectNetworkStatus$ } from 'apps/isa-app/src/app/services/network-status.service';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Injectable()
|
||||
export class ScanditScanAdapter implements ScanAdapter {
|
||||
readonly name = 'Scandit';
|
||||
|
||||
private $networkStatus = toSignal(injectNetworkStatus$());
|
||||
|
||||
constructor(
|
||||
private readonly _config: Config,
|
||||
private _overlay: Overlay,
|
||||
private _environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
async init(): Promise<boolean> {
|
||||
if (this._environmentService.isTablet()) {
|
||||
await configure(this._config.get('licence.scandit'), {
|
||||
engineLocation: '/scandit/',
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
scan(): Observable<string> {
|
||||
return new Observable((observer) => {
|
||||
if (this.$networkStatus() === 'offline') {
|
||||
observer.error(new Error('No network connection'));
|
||||
return;
|
||||
}
|
||||
|
||||
const overlay = this.createOverlay();
|
||||
|
||||
const portal = this.createPortal();
|
||||
|
||||
const ref = overlay.attach(portal);
|
||||
|
||||
const sub = new Subscriber();
|
||||
|
||||
const complete = () => {
|
||||
overlay.detach();
|
||||
ref.destroy();
|
||||
sub.unsubscribe();
|
||||
sub.complete();
|
||||
observer.complete();
|
||||
};
|
||||
|
||||
sub.add(
|
||||
overlay.backdropClick().subscribe(() => {
|
||||
complete();
|
||||
}),
|
||||
);
|
||||
|
||||
ref.instance.onScan((code) => {
|
||||
observer.next(code);
|
||||
complete();
|
||||
});
|
||||
|
||||
ref.instance.onClose(() => {
|
||||
complete();
|
||||
});
|
||||
|
||||
return complete;
|
||||
});
|
||||
}
|
||||
|
||||
createOverlay() {
|
||||
const overlay = this._overlay.create({
|
||||
positionStrategy: this._overlay.position().global().centerHorizontally().centerVertically(),
|
||||
hasBackdrop: true,
|
||||
});
|
||||
|
||||
return overlay;
|
||||
}
|
||||
|
||||
createPortal() {
|
||||
const portal = new ComponentPortal(ScanditOverlayComponent);
|
||||
|
||||
return portal;
|
||||
}
|
||||
}
|
||||
4
apps/isa-app/src/adapter/scan/tokens.ts
Normal file
4
apps/isa-app/src/adapter/scan/tokens.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { ScanAdapter } from './scan-adapter';
|
||||
|
||||
export const SCAN_ADAPTER = new InjectionToken<ScanAdapter>('SCAN_ADAPTER');
|
||||
@@ -3,7 +3,7 @@ import { EffectsModule } from '@ngrx/effects';
|
||||
import { ActionReducer, MetaReducer, StoreModule } from '@ngrx/store';
|
||||
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||
import { storeFreeze } from 'ngrx-store-freeze';
|
||||
import packageInfo from 'package';
|
||||
import packageInfo from 'packageJson';
|
||||
import { environment } from '../environments/environment';
|
||||
import { RootStateService } from './store/root-state.service';
|
||||
import { rootReducer } from './store/root.reducer';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SwUpdate } from '@angular/service-worker';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { Config } from '@core/config';
|
||||
import { NotificationsHub } from '@hub/notifications';
|
||||
import packageInfo from 'package';
|
||||
import packageInfo from 'packageJson';
|
||||
import { asapScheduler, interval, Subscription } from 'rxjs';
|
||||
import { UserStateService } from '@swagger/isa';
|
||||
import { IsaLogProvider } from './providers';
|
||||
|
||||
@@ -33,7 +33,7 @@ import { ScanAdapterModule, ScanAdapterService, ScanditScanAdapterModule } from
|
||||
import { RootStateService } from './store/root-state.service';
|
||||
import * as Commands from './commands';
|
||||
import { PreviewComponent } from './preview';
|
||||
import { NativeContainerService } from 'native-container';
|
||||
import { NativeContainerService } from '@external/native-container';
|
||||
import { ShellModule } from '@shared/shell';
|
||||
import { MainComponent } from './main.component';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
|
||||
@@ -3,12 +3,15 @@ import { Router } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { ActionHandler } from '@core/command';
|
||||
import { Result } from '@domain/defs';
|
||||
import { encodeFormData, mapCustomerInfoDtoToCustomerCreateFormData } from '@page/customer';
|
||||
import { CustomerInfoDTO } from '@swagger/crm';
|
||||
import { encodeFormData, mapCustomerInfoDtoToCustomerCreateFormData } from 'apps/page/customer/src/lib/create-customer';
|
||||
|
||||
@Injectable()
|
||||
export class CreateKubiCustomerCommand extends ActionHandler<Result<CustomerInfoDTO[]>> {
|
||||
constructor(private _router: Router, private _application: ApplicationService) {
|
||||
constructor(
|
||||
private _router: Router,
|
||||
private _application: ApplicationService,
|
||||
) {
|
||||
super('CREATE_KUBI_CUSTOMER');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { CheckoutNavigationService } from '@shared/services';
|
||||
import { CheckoutNavigationService } from '@shared/services/navigation';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CanActivateCartGuard {
|
||||
constructor(private readonly _applicationService: ApplicationService, private _checkoutNavigationService: CheckoutNavigationService) {}
|
||||
constructor(
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private _checkoutNavigationService: CheckoutNavigationService,
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { CustomerOrdersNavigationService } from '@shared/services';
|
||||
import { CustomerOrdersNavigationService } from '@shared/services/navigation';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -10,7 +10,7 @@ export class CanActivateCustomerOrdersGuard {
|
||||
constructor(
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private readonly _checkoutService: DomainCheckoutService,
|
||||
private readonly _navigationService: CustomerOrdersNavigationService
|
||||
private readonly _navigationService: CustomerOrdersNavigationService,
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { CustomerSearchNavigation } from '@shared/services';
|
||||
import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -11,7 +11,7 @@ export class CanActivateCustomerGuard {
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private readonly _checkoutService: DomainCheckoutService,
|
||||
private readonly _router: Router,
|
||||
private readonly _navigation: CustomerSearchNavigation
|
||||
private readonly _navigation: CustomerSearchNavigation,
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { ProductCatalogNavigationService } from '@shared/services';
|
||||
import { ProductCatalogNavigationService } from '@shared/services/navigation';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -10,7 +10,7 @@ export class CanActivateProductGuard {
|
||||
constructor(
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private readonly _checkoutService: DomainCheckoutService,
|
||||
private readonly _navigationService: ProductCatalogNavigationService
|
||||
private readonly _navigationService: ProductCatalogNavigationService,
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Store } from '@ngrx/store';
|
||||
import { UserStateService } from '@swagger/isa';
|
||||
import { debounceTime, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { RootState } from './root.state';
|
||||
import packageInfo from 'package';
|
||||
import packageInfo from 'packageJson';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@@ -14,7 +14,11 @@ export class RootStateService {
|
||||
|
||||
private _cancelSave = new Subject<void>();
|
||||
|
||||
constructor(private readonly _userStateService: UserStateService, private _logger: Logger, private _store: Store) {
|
||||
constructor(
|
||||
private readonly _userStateService: UserStateService,
|
||||
private _logger: Logger,
|
||||
private _store: Store,
|
||||
) {
|
||||
if (!environment.production) {
|
||||
console.log('Die UserState kann in der Konsole mit der Funktion "clearUserState()" geleert werden.');
|
||||
}
|
||||
@@ -40,7 +44,7 @@ export class RootStateService {
|
||||
const raw = JSON.stringify({ ...state, version: packageInfo.version });
|
||||
RootStateService.SaveToLocalStorageRaw(raw);
|
||||
return this._userStateService.UserStateSetUserState({ content: raw });
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
5
apps/isa-app/src/cdn/product-image/index.ts
Normal file
5
apps/isa-app/src/cdn/product-image/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './product-image-navigation.directive';
|
||||
export * from './product-image.module';
|
||||
export * from './product-image.pipe';
|
||||
export * from './product-image.service';
|
||||
export * from './tokens';
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Directive, HostListener, Input } from '@angular/core';
|
||||
import { ProductCatalogNavigationService } from '@shared/services/navigation';
|
||||
|
||||
@Directive({
|
||||
selector: '[productImageNavigation]',
|
||||
standalone: true,
|
||||
})
|
||||
export class NavigateOnClickDirective {
|
||||
@Input('productImageNavigation') ean: string;
|
||||
|
||||
constructor(private readonly _productCatalogNavigation: ProductCatalogNavigationService) {}
|
||||
|
||||
@HostListener('click', ['$event'])
|
||||
async onClick(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (this.ean) {
|
||||
await this._navigateToProductSearchDetails();
|
||||
}
|
||||
}
|
||||
|
||||
private async _navigateToProductSearchDetails() {
|
||||
await this._productCatalogNavigation
|
||||
.getArticleDetailsPathByEan({
|
||||
processId: Date.now(),
|
||||
ean: this.ean,
|
||||
extras: { queryParams: { main_qs: this.ean } },
|
||||
})
|
||||
.navigate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ProductImagePipe } from './product-image.pipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [ProductImagePipe],
|
||||
exports: [ProductImagePipe],
|
||||
})
|
||||
export class ProductImageModule {}
|
||||
14
apps/isa-app/src/cdn/product-image/product-image.pipe.ts
Normal file
14
apps/isa-app/src/cdn/product-image/product-image.pipe.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { ProductImageService } from './product-image.service';
|
||||
|
||||
@Pipe({
|
||||
name: 'productImage',
|
||||
standalone: true,
|
||||
pure: true,
|
||||
})
|
||||
export class ProductImagePipe implements PipeTransform {
|
||||
constructor(private imageService: ProductImageService) {}
|
||||
transform(imageId: string, width?: number, height?: number, showDummy?: boolean): any {
|
||||
return this.imageService.getImageUrl({ imageId, width, height, showDummy });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Config } from '@core/config';
|
||||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator';
|
||||
|
||||
import { ProductImageService } from './product-image.service';
|
||||
|
||||
describe('ProductImageService', () => {
|
||||
let spectator: SpectatorService<ProductImageService>;
|
||||
const createService = createServiceFactory({
|
||||
service: ProductImageService,
|
||||
mocks: [Config],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createService();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(spectator.service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
24
apps/isa-app/src/cdn/product-image/product-image.service.ts
Normal file
24
apps/isa-app/src/cdn/product-image/product-image.service.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { Config } from '@core/config';
|
||||
import { CDN_PRODUCT_IMAGE } from './tokens';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ProductImageService {
|
||||
constructor(private readonly _config: Config) {}
|
||||
|
||||
getImageUrl({
|
||||
imageId,
|
||||
width = 150,
|
||||
height = 150,
|
||||
showDummy = true,
|
||||
}: {
|
||||
imageId: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
showDummy?: boolean;
|
||||
}): string {
|
||||
return `${this._config.get('@cdn/product-image.url')}/${imageId}_${width}x${height}.jpg?showDummy=${showDummy}`;
|
||||
}
|
||||
}
|
||||
3
apps/isa-app/src/cdn/product-image/tokens.ts
Normal file
3
apps/isa-app/src/cdn/product-image/tokens.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
|
||||
export const CDN_PRODUCT_IMAGE = new InjectionToken<string>('cdn.product.image');
|
||||
23
apps/isa-app/src/core/application/application.module.ts
Normal file
23
apps/isa-app/src/core/application/application.module.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NgModule, ModuleWithProviders } from '@angular/core';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { applicationReducer } from './store';
|
||||
import { ApplicationService } from './application.service';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [],
|
||||
exports: [],
|
||||
})
|
||||
export class CoreApplicationModule {
|
||||
static forRoot(): ModuleWithProviders<CoreApplicationModule> {
|
||||
return {
|
||||
ngModule: RootCoreApplicationModule,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [StoreModule.forFeature('core-application', applicationReducer)],
|
||||
providers: [ApplicationService],
|
||||
})
|
||||
export class RootCoreApplicationModule {}
|
||||
233
apps/isa-app/src/core/application/application.service.spec.ts
Normal file
233
apps/isa-app/src/core/application/application.service.spec.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
// import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator';
|
||||
// import { Store } from '@ngrx/store';
|
||||
// import { Observable, of } from 'rxjs';
|
||||
// import { first } from 'rxjs/operators';
|
||||
// import { ApplicationProcess } from './defs';
|
||||
|
||||
// import { ApplicationService } from './application.service';
|
||||
// import * as actions from './store/application.actions';
|
||||
|
||||
// describe('ApplicationService', () => {
|
||||
// let spectator: SpectatorService<ApplicationService>;
|
||||
// let store: SpyObject<Store>;
|
||||
// const createService = createServiceFactory({
|
||||
// service: ApplicationService,
|
||||
// mocks: [Store],
|
||||
// });
|
||||
|
||||
// beforeEach(() => {
|
||||
// spectator = createService({});
|
||||
// store = spectator.inject(Store);
|
||||
// });
|
||||
|
||||
// it('should be created', () => {
|
||||
// expect(spectator.service).toBeTruthy();
|
||||
// });
|
||||
|
||||
// describe('activatedProcessId$', () => {
|
||||
// it('should return an observable', () => {
|
||||
// expect(spectator.service.activatedProcessId$).toBeInstanceOf(Observable);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('activatedProcessId', () => {
|
||||
// it('should return the process id as a number', () => {
|
||||
// spyOnProperty(spectator.service['activatedProcessIdSubject'] as any, 'value').and.returnValue(2);
|
||||
// expect(spectator.service.activatedProcessId).toBe(2);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('getProcesses$()', () => {
|
||||
// it('should call select on store and return all selected processes', async () => {
|
||||
// const processes: ApplicationProcess[] = [
|
||||
// { id: 1, name: 'Vorgang', type: 'cart', section: 'customer', data: { count: 1 } },
|
||||
// { id: 2, name: 'Vorgang', type: 'task-calendar', section: 'branch' },
|
||||
// ];
|
||||
// store.select.and.returnValue(of(processes));
|
||||
// const result = await spectator.service.getProcesses$().pipe(first()).toPromise();
|
||||
// expect(result).toEqual(processes);
|
||||
// expect(store.select).toHaveBeenCalled();
|
||||
// });
|
||||
|
||||
// it('should call select on store and return all section customer processes', async () => {
|
||||
// const processes: ApplicationProcess[] = [
|
||||
// { id: 1, name: 'Vorgang', type: 'cart', section: 'customer', data: { count: 1 } },
|
||||
// { id: 2, name: 'Vorgang', type: 'task-calendar', section: 'branch' },
|
||||
// ];
|
||||
// store.select.and.returnValue(of(processes));
|
||||
// const result = await spectator.service.getProcesses$('customer').pipe(first()).toPromise();
|
||||
// expect(result).toEqual([processes[0]]);
|
||||
// expect(store.select).toHaveBeenCalled();
|
||||
// });
|
||||
|
||||
// it('should call select on store and return all section branch processes', async () => {
|
||||
// const processes: ApplicationProcess[] = [
|
||||
// { id: 1, name: 'Vorgang', type: 'cart', section: 'customer', data: { count: 1 } },
|
||||
// { id: 2, name: 'Vorgang', type: 'task-calendar', section: 'branch' },
|
||||
// ];
|
||||
// store.select.and.returnValue(of(processes));
|
||||
// const result = await spectator.service.getProcesses$('branch').pipe(first()).toPromise();
|
||||
// expect(result).toEqual([processes[1]]);
|
||||
// expect(store.select).toHaveBeenCalled();
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('getProcessById$()', () => {
|
||||
// it('should return the process by id', async () => {
|
||||
// const processes: ApplicationProcess[] = [
|
||||
// { id: 1, name: 'Vorgang 1', section: 'customer' },
|
||||
// { id: 2, name: 'Vorgang 2', section: 'customer' },
|
||||
// ];
|
||||
// spyOn(spectator.service, 'getProcesses$').and.returnValue(of(processes));
|
||||
|
||||
// const process = await spectator.service.getProcessById$(1).toPromise();
|
||||
// expect(process.id).toBe(1);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('getSection$()', () => {
|
||||
// it('should return the selected section branch', async () => {
|
||||
// const section = 'branch';
|
||||
// store.select.and.returnValue(of(section));
|
||||
// const result = await spectator.service.getSection$().pipe(first()).toPromise();
|
||||
// expect(result).toEqual(section);
|
||||
// expect(store.select).toHaveBeenCalled();
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('getActivatedProcessId$', () => {
|
||||
// it('should return the current selected activated process id', async () => {
|
||||
// const activatedProcessId = 2;
|
||||
// store.select.and.returnValue(of({ id: activatedProcessId }));
|
||||
// const result = await spectator.service.getActivatedProcessId$().pipe(first()).toPromise();
|
||||
// expect(result).toEqual(activatedProcessId);
|
||||
// expect(store.select).toHaveBeenCalled();
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('activateProcess()', () => {
|
||||
// it('should dispatch action setActivatedProcess with argument activatedProcessId and action type', () => {
|
||||
// const activatedProcessId = 2;
|
||||
// spectator.service.activateProcess(activatedProcessId);
|
||||
// expect(store.dispatch).toHaveBeenCalledWith({ activatedProcessId, type: actions.setActivatedProcess.type });
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('removeProcess()', () => {
|
||||
// it('should dispatch action removeProcess with argument processId and action type', () => {
|
||||
// const processId = 2;
|
||||
// spectator.service.removeProcess(processId);
|
||||
// expect(store.dispatch).toHaveBeenCalledWith({ processId, type: actions.removeProcess.type });
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('createProcess()', () => {
|
||||
// it('should dispatch action addProcess with process', async () => {
|
||||
// const process: ApplicationProcess = {
|
||||
// id: 1,
|
||||
// name: 'Vorgang 1',
|
||||
// section: 'customer',
|
||||
// type: 'cart',
|
||||
// };
|
||||
|
||||
// const timestamp = 100;
|
||||
// spyOn(spectator.service as any, '_createTimestamp').and.returnValue(timestamp);
|
||||
// spyOn(spectator.service, 'getProcessById$').and.returnValue(of(undefined));
|
||||
// await spectator.service.createProcess(process);
|
||||
|
||||
// expect(store.dispatch).toHaveBeenCalledWith({
|
||||
// type: actions.addProcess.type,
|
||||
// process: {
|
||||
// ...process,
|
||||
// activated: 0,
|
||||
// created: timestamp,
|
||||
// },
|
||||
// });
|
||||
// });
|
||||
|
||||
// it('should throw an error if the process id is already existing', async () => {
|
||||
// const process: ApplicationProcess = {
|
||||
// id: 1,
|
||||
// name: 'Vorgang 1',
|
||||
// section: 'customer',
|
||||
// type: 'cart',
|
||||
// };
|
||||
// spyOn(spectator.service, 'getProcessById$').and.returnValue(of(process));
|
||||
// await expectAsync(spectator.service.createProcess(process)).toBeRejectedWithError('Process Id existiert bereits');
|
||||
// });
|
||||
|
||||
// it('should throw an error if the process id is not a number', async () => {
|
||||
// const process: ApplicationProcess = {
|
||||
// id: undefined,
|
||||
// name: 'Vorgang 1',
|
||||
// section: 'customer',
|
||||
// type: 'cart',
|
||||
// };
|
||||
// spyOn(spectator.service, 'getProcessById$').and.returnValue(of({ id: 5, name: 'Vorgang 2', section: 'customer' }));
|
||||
// await expectAsync(spectator.service.createProcess(process)).toBeRejectedWithError('Process Id nicht gesetzt');
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('patchProcess', () => {
|
||||
// it('should dispatch action patchProcess with changes', async () => {
|
||||
// const process: ApplicationProcess = {
|
||||
// id: 1,
|
||||
// name: 'Vorgang 1',
|
||||
// section: 'customer',
|
||||
// type: 'cart',
|
||||
// };
|
||||
|
||||
// await spectator.service.patchProcess(process.id, process);
|
||||
|
||||
// expect(store.dispatch).toHaveBeenCalledWith({
|
||||
// type: actions.patchProcess.type,
|
||||
// processId: process.id,
|
||||
// changes: {
|
||||
// ...process,
|
||||
// },
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('setSection()', () => {
|
||||
// it('should dispatch action setSection with argument section and action type', () => {
|
||||
// const section = 'customer';
|
||||
// spectator.service.setSection(section);
|
||||
// expect(store.dispatch).toHaveBeenCalledWith({ section, type: actions.setSection.type });
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('getLastActivatedProcessWithSectionAndType()', () => {
|
||||
// it('should return the last activated process by section and type', async () => {
|
||||
// const processes: ApplicationProcess[] = [
|
||||
// { id: 1, name: 'Vorgang 1', section: 'customer', type: 'cart', activated: 100 },
|
||||
// { id: 2, name: 'Vorgang 2', section: 'customer', type: 'cart', activated: 200 },
|
||||
// { id: 3, name: 'Vorgang 3', section: 'customer', type: 'goodsOut', activated: 300 },
|
||||
// ];
|
||||
// spyOn(spectator.service, 'getProcesses$').and.returnValue(of(processes));
|
||||
|
||||
// expect(await spectator.service.getLastActivatedProcessWithSectionAndType$('customer', 'cart').pipe(first()).toPromise()).toBe(
|
||||
// processes[1]
|
||||
// );
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('getLastActivatedProcessWithSection()', () => {
|
||||
// it('should return the last activated process by section', async () => {
|
||||
// const processes: ApplicationProcess[] = [
|
||||
// { id: 1, name: 'Vorgang 1', section: 'customer', activated: 100 },
|
||||
// { id: 2, name: 'Vorgang 2', section: 'customer', activated: 200 },
|
||||
// { id: 3, name: 'Vorgang 3', section: 'customer', activated: 300 },
|
||||
// ];
|
||||
// spyOn(spectator.service, 'getProcesses$').and.returnValue(of(processes));
|
||||
|
||||
// expect(await spectator.service.getLastActivatedProcessWithSection$('customer').pipe(first()).toPromise()).toBe(processes[2]);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('_createTimestamp', () => {
|
||||
// it('should return the current timestamp in ms', () => {
|
||||
// expect(spectator.service['_createTimestamp']()).toBeCloseTo(Date.now());
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
169
apps/isa-app/src/core/application/application.service.ts
Normal file
169
apps/isa-app/src/core/application/application.service.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { BranchDTO } from '@swagger/checkout';
|
||||
import { isBoolean, isNumber } from '@utils/common';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { first, map, switchMap } from 'rxjs/operators';
|
||||
import { ApplicationProcess } from './defs';
|
||||
import {
|
||||
removeProcess,
|
||||
selectSection,
|
||||
selectProcesses,
|
||||
setSection,
|
||||
addProcess,
|
||||
setActivatedProcess,
|
||||
selectActivatedProcess,
|
||||
patchProcess,
|
||||
patchProcessData,
|
||||
selectTitle,
|
||||
setTitle,
|
||||
} from './store';
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationService {
|
||||
private activatedProcessIdSubject = new BehaviorSubject<number>(undefined);
|
||||
|
||||
get activatedProcessId() {
|
||||
return this.activatedProcessIdSubject.value;
|
||||
}
|
||||
|
||||
get activatedProcessId$() {
|
||||
return this.activatedProcessIdSubject.asObservable();
|
||||
}
|
||||
|
||||
constructor(private store: Store) {}
|
||||
|
||||
getProcesses$(section?: 'customer' | 'branch') {
|
||||
const processes$ = this.store.select(selectProcesses);
|
||||
return processes$.pipe(map((processes) => processes.filter((process) => (section ? process.section === section : true))));
|
||||
}
|
||||
|
||||
getProcessById$(processId: number): Observable<ApplicationProcess> {
|
||||
return this.getProcesses$().pipe(map((processes) => processes.find((process) => process.id === processId)));
|
||||
}
|
||||
|
||||
getSection$() {
|
||||
return this.store.select(selectSection);
|
||||
}
|
||||
|
||||
getTitle$() {
|
||||
return this.getSection$().pipe(
|
||||
map((section) => {
|
||||
return section === 'customer' ? 'Kundenbereich' : 'Filialbereich';
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
getActivatedProcessId$() {
|
||||
return this.store.select(selectActivatedProcess).pipe(map((process) => process?.id));
|
||||
}
|
||||
|
||||
activateProcess(activatedProcessId: number) {
|
||||
this.store.dispatch(setActivatedProcess({ activatedProcessId }));
|
||||
this.activatedProcessIdSubject.next(activatedProcessId);
|
||||
}
|
||||
|
||||
removeProcess(processId: number) {
|
||||
this.store.dispatch(removeProcess({ processId }));
|
||||
}
|
||||
|
||||
patchProcess(processId: number, changes: Partial<ApplicationProcess>) {
|
||||
this.store.dispatch(patchProcess({ processId, changes }));
|
||||
}
|
||||
|
||||
patchProcessData(processId: number, data: Record<string, any>) {
|
||||
this.store.dispatch(patchProcessData({ processId, data }));
|
||||
}
|
||||
|
||||
getSelectedBranch$(processId?: number): Observable<BranchDTO> {
|
||||
if (!processId) {
|
||||
return this.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this.getProcessById$(processId).pipe(map((process) => process?.data?.selectedBranch))),
|
||||
);
|
||||
}
|
||||
|
||||
return this.getProcessById$(processId).pipe(map((process) => process?.data?.selectedBranch));
|
||||
}
|
||||
|
||||
readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;
|
||||
|
||||
async createCustomerProcess(processId?: number): Promise<ApplicationProcess> {
|
||||
const processes = await this.getProcesses$('customer').pipe(first()).toPromise();
|
||||
|
||||
const processIds = processes.filter((x) => this.REGEX_PROCESS_NAME.test(x.name)).map((x) => +x.name.split(' ')[1]);
|
||||
|
||||
const maxId = processIds.length > 0 ? Math.max(...processIds) : 0;
|
||||
|
||||
const process: ApplicationProcess = {
|
||||
id: processId ?? Date.now(),
|
||||
type: 'cart',
|
||||
name: `Vorgang ${maxId + 1}`,
|
||||
section: 'customer',
|
||||
closeable: true,
|
||||
};
|
||||
|
||||
await this.createProcess(process);
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
async createProcess(process: ApplicationProcess) {
|
||||
const existingProcess = await this.getProcessById$(process?.id).pipe(first()).toPromise();
|
||||
if (existingProcess?.id === process?.id) {
|
||||
throw new Error('Process Id existiert bereits');
|
||||
}
|
||||
|
||||
if (!isNumber(process.id)) {
|
||||
throw new Error('Process Id nicht gesetzt');
|
||||
}
|
||||
|
||||
if (!isBoolean(process.closeable)) {
|
||||
process.closeable = true;
|
||||
}
|
||||
|
||||
if (!isBoolean(process.confirmClosing)) {
|
||||
process.confirmClosing = true;
|
||||
}
|
||||
|
||||
process.created = this._createTimestamp();
|
||||
process.activated = 0;
|
||||
this.store.dispatch(addProcess({ process }));
|
||||
}
|
||||
|
||||
setSection(section: 'customer' | 'branch') {
|
||||
this.store.dispatch(setSection({ section }));
|
||||
}
|
||||
|
||||
getLastActivatedProcessWithSectionAndType$(section: 'customer' | 'branch', type: string): Observable<ApplicationProcess> {
|
||||
return this.getProcesses$(section).pipe(
|
||||
map((processes) =>
|
||||
processes
|
||||
?.filter((process) => process.type === type)
|
||||
?.reduce((latest, current) => {
|
||||
if (!latest) {
|
||||
return current;
|
||||
}
|
||||
return latest?.activated > current?.activated ? latest : current;
|
||||
}, undefined),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getLastActivatedProcessWithSection$(section: 'customer' | 'branch'): Observable<ApplicationProcess> {
|
||||
return this.getProcesses$(section).pipe(
|
||||
map((processes) =>
|
||||
processes?.reduce((latest, current) => {
|
||||
if (!latest) {
|
||||
return current;
|
||||
}
|
||||
return latest?.activated > current?.activated ? latest : current;
|
||||
}, undefined),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private _createTimestamp() {
|
||||
return Date.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface ApplicationProcess {
|
||||
id: number;
|
||||
created?: number;
|
||||
activated?: number;
|
||||
name: string;
|
||||
section: 'customer' | 'branch';
|
||||
type?: string;
|
||||
data?: { [key: string]: any };
|
||||
closeable?: boolean;
|
||||
confirmClosing?: boolean;
|
||||
}
|
||||
3
apps/isa-app/src/core/application/defs/index.ts
Normal file
3
apps/isa-app/src/core/application/defs/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// start:ng42.barrel
|
||||
export * from './application-process';
|
||||
// end:ng42.barrel
|
||||
4
apps/isa-app/src/core/application/index.ts
Normal file
4
apps/isa-app/src/core/application/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './application.module';
|
||||
export * from './application.service';
|
||||
export * from './defs';
|
||||
export * from './store';
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createAction, props } from '@ngrx/store';
|
||||
import { ApplicationProcess } from '..';
|
||||
|
||||
const prefix = '[CORE-APPLICATION]';
|
||||
|
||||
export const setTitle = createAction(`${prefix} Set Title`, props<{ title: string }>());
|
||||
|
||||
export const setSection = createAction(`${prefix} Set Section`, props<{ section: 'customer' | 'branch' }>());
|
||||
|
||||
export const addProcess = createAction(`${prefix} Add Process`, props<{ process: ApplicationProcess }>());
|
||||
|
||||
export const removeProcess = createAction(`${prefix} Remove Process`, props<{ processId: number }>());
|
||||
|
||||
export const setActivatedProcess = createAction(`${prefix} Set Activated Process`, props<{ activatedProcessId: number }>());
|
||||
|
||||
export const patchProcess = createAction(`${prefix} Patch Process`, props<{ processId: number; changes: Partial<ApplicationProcess> }>());
|
||||
|
||||
export const patchProcessData = createAction(`${prefix} Patch Process Data`, props<{ processId: number; data: Record<string, any> }>());
|
||||
@@ -0,0 +1,200 @@
|
||||
import { INITIAL_APPLICATION_STATE } from './application.state';
|
||||
import * as actions from './application.actions';
|
||||
import { applicationReducer } from './application.reducer';
|
||||
import { ApplicationProcess } from '../defs';
|
||||
import { ApplicationState } from './application.state';
|
||||
|
||||
describe('applicationReducer', () => {
|
||||
describe('setSection()', () => {
|
||||
it('should return modified state with section customer', () => {
|
||||
const initialState = INITIAL_APPLICATION_STATE;
|
||||
|
||||
const action = actions.setSection({ section: 'customer' });
|
||||
const state = applicationReducer(initialState, action);
|
||||
|
||||
expect(state).toEqual({
|
||||
...initialState,
|
||||
section: 'customer',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return modified state with section branch', () => {
|
||||
const initialState = INITIAL_APPLICATION_STATE;
|
||||
|
||||
const action = actions.setSection({ section: 'branch' });
|
||||
const state = applicationReducer(initialState, action);
|
||||
|
||||
expect(state).toEqual({
|
||||
...initialState,
|
||||
section: 'branch',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addProcess()', () => {
|
||||
it('should return modified state with new process if no processes are registered in the state', () => {
|
||||
const initialState = INITIAL_APPLICATION_STATE;
|
||||
|
||||
const process: ApplicationProcess = {
|
||||
id: 1,
|
||||
name: 'Vorgang',
|
||||
section: 'customer',
|
||||
type: 'cart',
|
||||
data: {},
|
||||
};
|
||||
|
||||
const action = actions.addProcess({ process });
|
||||
const state = applicationReducer(initialState, action);
|
||||
expect(state.processes[0]).toEqual(process);
|
||||
});
|
||||
});
|
||||
|
||||
describe('patchProcess()', () => {
|
||||
it('should return modified state with updated process when id is found', () => {
|
||||
const initialState = INITIAL_APPLICATION_STATE;
|
||||
|
||||
const process: ApplicationProcess = {
|
||||
id: 1,
|
||||
name: 'Vorgang',
|
||||
section: 'customer',
|
||||
type: 'cart',
|
||||
};
|
||||
|
||||
const action = actions.patchProcess({ processId: process.id, changes: { ...process, name: 'Test' } });
|
||||
const state = applicationReducer(
|
||||
{
|
||||
...initialState,
|
||||
processes: [process],
|
||||
},
|
||||
action,
|
||||
);
|
||||
expect(state.processes[0].name).toEqual('Test');
|
||||
});
|
||||
|
||||
it('should return unmodified state when id is not existing', () => {
|
||||
const initialState = INITIAL_APPLICATION_STATE;
|
||||
|
||||
const process: ApplicationProcess = {
|
||||
id: 1,
|
||||
name: 'Vorgang',
|
||||
section: 'customer',
|
||||
type: 'cart',
|
||||
};
|
||||
|
||||
const action = actions.patchProcess({ processId: process.id, changes: { ...process, id: 2 } });
|
||||
const state = applicationReducer(
|
||||
{
|
||||
...initialState,
|
||||
processes: [process],
|
||||
},
|
||||
action,
|
||||
);
|
||||
expect(state.processes).toEqual([process]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeProcess()', () => {
|
||||
it('should return initial state if no processes are registered in the state', () => {
|
||||
const initialState = INITIAL_APPLICATION_STATE;
|
||||
|
||||
const action = actions.removeProcess({ processId: 2 });
|
||||
const state = applicationReducer(initialState, action);
|
||||
expect(state).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('should return the unmodified state if processId not found', () => {
|
||||
const initialState = INITIAL_APPLICATION_STATE;
|
||||
const modifiedState: ApplicationState = {
|
||||
...initialState,
|
||||
section: 'customer',
|
||||
processes: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Vorgang',
|
||||
section: 'customer',
|
||||
type: 'cart',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Vorgang',
|
||||
section: 'customer',
|
||||
type: 'goods-out',
|
||||
},
|
||||
] as ApplicationProcess[],
|
||||
};
|
||||
|
||||
const action = actions.removeProcess({ processId: 2 });
|
||||
const state = applicationReducer(modifiedState, action);
|
||||
expect(state).toEqual(modifiedState);
|
||||
});
|
||||
|
||||
it('should return modified state, after process gets removed', () => {
|
||||
const initialState = INITIAL_APPLICATION_STATE;
|
||||
const modifiedState: ApplicationState = {
|
||||
...initialState,
|
||||
section: 'customer',
|
||||
processes: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Vorgang',
|
||||
section: 'customer',
|
||||
type: 'cart',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Vorgang',
|
||||
section: 'customer',
|
||||
type: 'goods-out',
|
||||
},
|
||||
] as ApplicationProcess[],
|
||||
};
|
||||
|
||||
const action = actions.removeProcess({ processId: 2 });
|
||||
const state = applicationReducer(modifiedState, action);
|
||||
expect(state.processes).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Vorgang',
|
||||
section: 'customer',
|
||||
type: 'cart',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setActivatedProcess()', () => {
|
||||
it('should return modified state with process.activated value', () => {
|
||||
const process: ApplicationProcess = {
|
||||
id: 3,
|
||||
name: 'Vorgang 3',
|
||||
section: 'customer',
|
||||
};
|
||||
const initialState: ApplicationState = {
|
||||
...INITIAL_APPLICATION_STATE,
|
||||
processes: [process],
|
||||
};
|
||||
|
||||
const action = actions.setActivatedProcess({ activatedProcessId: 3 });
|
||||
const state = applicationReducer(initialState, action);
|
||||
|
||||
expect(state.processes[0].activated).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return modified state without process.activated value when activatedProcessId doesnt exist', () => {
|
||||
const process: ApplicationProcess = {
|
||||
id: 1,
|
||||
name: 'Vorgang 3',
|
||||
section: 'customer',
|
||||
};
|
||||
const initialState: ApplicationState = {
|
||||
...INITIAL_APPLICATION_STATE,
|
||||
processes: [process],
|
||||
};
|
||||
|
||||
const action = actions.setActivatedProcess({ activatedProcessId: 3 });
|
||||
const state = applicationReducer(initialState, action);
|
||||
|
||||
expect(state.processes[0].activated).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Action, createReducer, on } from '@ngrx/store';
|
||||
import {
|
||||
setSection,
|
||||
addProcess,
|
||||
removeProcess,
|
||||
setActivatedProcess,
|
||||
patchProcess,
|
||||
patchProcessData,
|
||||
setTitle,
|
||||
} from './application.actions';
|
||||
import { ApplicationState, INITIAL_APPLICATION_STATE } from './application.state';
|
||||
|
||||
const _applicationReducer = createReducer(
|
||||
INITIAL_APPLICATION_STATE,
|
||||
on(setTitle, (state, { title }) => ({ ...state, title })),
|
||||
on(setSection, (state, { section }) => ({ ...state, section })),
|
||||
on(addProcess, (state, { process }) => ({ ...state, processes: [...state.processes, { data: {}, ...process }] })),
|
||||
on(removeProcess, (state, { processId }) => {
|
||||
const processes = state?.processes?.filter((process) => process.id !== processId) || [];
|
||||
return { ...state, processes };
|
||||
}),
|
||||
on(setActivatedProcess, (state, { activatedProcessId }) => {
|
||||
const processes = state.processes.map((process) => {
|
||||
if (process.id === activatedProcessId) {
|
||||
return { ...process, activated: Date.now() };
|
||||
}
|
||||
return process;
|
||||
});
|
||||
|
||||
return { ...state, processes };
|
||||
}),
|
||||
on(patchProcess, (state, { processId, changes }) => {
|
||||
const processes = state.processes.map((process) => {
|
||||
if (process.id === processId) {
|
||||
return { ...process, ...changes, id: processId };
|
||||
}
|
||||
return process;
|
||||
});
|
||||
|
||||
return { ...state, processes };
|
||||
}),
|
||||
on(patchProcessData, (state, { processId, data }) => {
|
||||
const processes = state.processes.map((process) => {
|
||||
if (process.id === processId) {
|
||||
return { ...process, data: { ...(process.data || {}), ...data } };
|
||||
}
|
||||
return process;
|
||||
});
|
||||
|
||||
return { ...state, processes };
|
||||
}),
|
||||
);
|
||||
|
||||
export function applicationReducer(state: ApplicationState, action: Action) {
|
||||
return _applicationReducer(state, action);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// import { ApplicationState } from './application.state';
|
||||
// import { ApplicationProcess } from '../defs';
|
||||
// import * as selectors from './application.selectors';
|
||||
|
||||
// describe('applicationSelectors', () => {
|
||||
// it('should select the processes', () => {
|
||||
// const processes: ApplicationProcess[] = [{ id: 1, name: 'Vorgang 1', section: 'customer' }];
|
||||
// const state: ApplicationState = {
|
||||
// processes,
|
||||
// section: 'customer',
|
||||
// };
|
||||
// expect(selectors.selectProcesses.projector(state)).toEqual(processes);
|
||||
// });
|
||||
|
||||
// it('should select the section', () => {
|
||||
// const state: ApplicationState = {
|
||||
// processes: [],
|
||||
// section: 'customer',
|
||||
// };
|
||||
// expect(selectors.selectSection.projector(state)).toEqual('customer');
|
||||
// });
|
||||
|
||||
// it('should select the activatedProcess', () => {
|
||||
// const processes: ApplicationProcess[] = [
|
||||
// { id: 1, name: 'Vorgang 1', section: 'customer', activated: 100 },
|
||||
// { id: 2, name: 'Vorgang 2', section: 'customer', activated: 300 },
|
||||
// { id: 3, name: 'Vorgang 3', section: 'customer', activated: 200 },
|
||||
// ];
|
||||
// const state: ApplicationState = {
|
||||
// processes,
|
||||
// section: 'customer',
|
||||
// };
|
||||
// expect(selectors.selectActivatedProcess.projector(state)).toEqual(processes[1]);
|
||||
// });
|
||||
// });
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||
import { ApplicationState } from './application.state';
|
||||
export const selectApplicationState = createFeatureSelector<ApplicationState>('core-application');
|
||||
|
||||
export const selectTitle = createSelector(selectApplicationState, (s) => s.title);
|
||||
|
||||
export const selectSection = createSelector(selectApplicationState, (s) => s.section);
|
||||
|
||||
export const selectProcesses = createSelector(selectApplicationState, (s) => s.processes);
|
||||
|
||||
export const selectActivatedProcess = createSelector(selectApplicationState, (s) =>
|
||||
s?.processes?.reduce((process, current) => {
|
||||
if (!process) {
|
||||
return current;
|
||||
}
|
||||
return process.activated > current.activated ? process : current;
|
||||
}, undefined),
|
||||
);
|
||||
13
apps/isa-app/src/core/application/store/application.state.ts
Normal file
13
apps/isa-app/src/core/application/store/application.state.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ApplicationProcess } from '../defs';
|
||||
|
||||
export interface ApplicationState {
|
||||
title: string;
|
||||
processes: ApplicationProcess[];
|
||||
section: 'customer' | 'branch';
|
||||
}
|
||||
|
||||
export const INITIAL_APPLICATION_STATE: ApplicationState = {
|
||||
title: '',
|
||||
processes: [],
|
||||
section: 'customer',
|
||||
};
|
||||
6
apps/isa-app/src/core/application/store/index.ts
Normal file
6
apps/isa-app/src/core/application/store/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// start:ng42.barrel
|
||||
export * from './application.actions';
|
||||
export * from './application.reducer';
|
||||
export * from './application.selectors';
|
||||
export * from './application.state';
|
||||
// end:ng42.barrel
|
||||
21
apps/isa-app/src/core/auth/auth.module.ts
Normal file
21
apps/isa-app/src/core/auth/auth.module.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { AuthService } from './auth.service';
|
||||
import { OAuthModule } from 'angular-oauth2-oidc';
|
||||
import { IfRoleDirective } from './if-role.directive';
|
||||
@NgModule({
|
||||
declarations: [IfRoleDirective],
|
||||
exports: [IfRoleDirective],
|
||||
})
|
||||
export class AuthModule {
|
||||
static forRoot(): ModuleWithProviders<AuthModule> {
|
||||
return {
|
||||
ngModule: AuthModule,
|
||||
providers: [
|
||||
AuthService,
|
||||
OAuthModule.forRoot({
|
||||
resourceServer: { sendAccessToken: true },
|
||||
}).providers,
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
151
apps/isa-app/src/core/auth/auth.service.spec.ts
Normal file
151
apps/isa-app/src/core/auth/auth.service.spec.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { Config } from '@core/config';
|
||||
import { SpectatorService, createServiceFactory, SpyObject } from '@ngneat/spectator';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
describe('AuthService', () => {
|
||||
let spectator: SpectatorService<AuthService>;
|
||||
const createService = createServiceFactory({
|
||||
service: AuthService,
|
||||
mocks: [Config, OAuthService],
|
||||
});
|
||||
let config: SpyObject<Config>;
|
||||
let oAuthService: SpyObject<OAuthService>;
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createService();
|
||||
config = spectator.inject(Config);
|
||||
oAuthService = spectator.inject(OAuthService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(spectator.service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('init()', () => {
|
||||
it('should configure the oAuthService', () => {
|
||||
config.get.and.returnValue({});
|
||||
spectator.service.init();
|
||||
expect(oAuthService.configure).toHaveBeenCalledWith({
|
||||
redirectUri: window.location.origin,
|
||||
silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html',
|
||||
useSilentRefresh: true,
|
||||
});
|
||||
expect(oAuthService.tokenValidationHandler).toBeInstanceOf(JwksValidationHandler);
|
||||
expect(oAuthService.setupAutomaticSilentRefresh).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should load the discovery document', async () => {
|
||||
config.get.and.returnValue({});
|
||||
oAuthService.loadDiscoveryDocumentAndTryLogin.and.returnValue(Promise.resolve(true));
|
||||
|
||||
spyOn(spectator.service['_initialized'], 'next');
|
||||
await spectator.service.init();
|
||||
|
||||
expect(oAuthService.loadDiscoveryDocumentAndTryLogin).toHaveBeenCalled();
|
||||
expect(spectator.service['_initialized'].next).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should throw an error if its already initialized', async () => {
|
||||
spyOn(spectator.service['_initialized'], 'getValue').and.returnValue(true);
|
||||
await expectAsync(spectator.service.init()).toBeRejectedWithError('AuthService is already initialized');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAuthenticated()', () => {
|
||||
it('should call hasValidIdToken() and return its value', () => {
|
||||
oAuthService.hasValidIdToken.and.returnValue(true);
|
||||
expect(spectator.service.isAuthenticated()).toBeTrue();
|
||||
expect(oAuthService.hasValidIdToken).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getToken()', () => {
|
||||
it('should call getAccessToken() and return its value', () => {
|
||||
oAuthService.getAccessToken.and.returnValue('token');
|
||||
expect(spectator.service.getToken()).toEqual('token');
|
||||
expect(oAuthService.getAccessToken).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getClaims()', () => {
|
||||
it('should call getAccessToken() and return its value', () => {
|
||||
oAuthService.getAccessToken.and.returnValue('token');
|
||||
const claims = {
|
||||
claim1: 'value',
|
||||
claim2: 'value2',
|
||||
};
|
||||
spyOn(spectator.service, 'parseJwt').and.returnValue(claims);
|
||||
expect(spectator.service.getClaims()).toEqual(claims);
|
||||
expect(spectator.service.parseJwt).toHaveBeenCalledWith('token');
|
||||
expect(oAuthService.getAccessToken).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getClaimByKey()', () => {
|
||||
it('should call getClaims() and return its key value', () => {
|
||||
spyOn(spectator.service, 'getClaims').and.returnValue({
|
||||
claim1: 'value',
|
||||
claim2: 'value2',
|
||||
});
|
||||
|
||||
expect(spectator.service.getClaimByKey('claim1')).toEqual('value');
|
||||
expect(spectator.service.getClaims).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return null if getClaims() returns null or undefined', () => {
|
||||
spyOn(spectator.service, 'getClaims').and.returnValue(null);
|
||||
|
||||
expect(spectator.service.getClaimByKey('claim1')).toBeNull();
|
||||
expect(spectator.service.getClaims).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseJwt()', () => {
|
||||
it('should return null if the token is null or undefined', () => {
|
||||
expect(spectator.service.parseJwt(null)).toBeNull();
|
||||
expect(spectator.service.parseJwt(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the value of the key', () => {
|
||||
const token =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
|
||||
expect(spectator.service.parseJwt(token)).toEqual({
|
||||
sub: '1234567890',
|
||||
name: 'John Doe',
|
||||
admin: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('login()', () => {
|
||||
it('should call initLoginFlow()', () => {
|
||||
spectator.service.login();
|
||||
expect(oAuthService.initLoginFlow).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout()', () => {
|
||||
it('should call revokeTokenAndLogout()', async () => {
|
||||
await spectator.service.logout();
|
||||
expect(oAuthService.revokeTokenAndLogout).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getToken()', () => {
|
||||
it('should return getAccessToken()', () => {
|
||||
spectator.service.getToken();
|
||||
expect(oAuthService.getAccessToken).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialized', () => {
|
||||
it('should return _initialized as Observable', () => {
|
||||
spyOn(spectator.service['_initialized'], 'asObservable').and.callThrough();
|
||||
expect(spectator.service.initialized$).toBeInstanceOf(Observable);
|
||||
expect(spectator.service['_initialized'].asObservable).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
130
apps/isa-app/src/core/auth/auth.service.ts
Normal file
130
apps/isa-app/src/core/auth/auth.service.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { coerceArray, coerceStringArray } from '@angular/cdk/coercion';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Config } from '@core/config';
|
||||
import { isNullOrUndefined } from '@utils/common';
|
||||
import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
|
||||
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
|
||||
import { asapScheduler, BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthService {
|
||||
private readonly _initialized = new BehaviorSubject<boolean>(false);
|
||||
get initialized$() {
|
||||
return this._initialized.asObservable();
|
||||
}
|
||||
|
||||
private _authConfig: AuthConfig;
|
||||
|
||||
constructor(
|
||||
private _config: Config,
|
||||
private readonly _oAuthService: OAuthService,
|
||||
) {
|
||||
this._oAuthService.events?.subscribe((event) => {
|
||||
if (event.type === 'token_received') {
|
||||
console.log('SSO Token Expiration:', new Date(this._oAuthService.getAccessTokenExpiration()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this._initialized.getValue()) {
|
||||
throw new Error('AuthService is already initialized');
|
||||
}
|
||||
|
||||
this._authConfig = this._config.get('@core/auth');
|
||||
|
||||
this._authConfig.redirectUri = window.location.origin;
|
||||
|
||||
this._authConfig.silentRefreshRedirectUri = window.location.origin + '/silent-refresh.html';
|
||||
this._authConfig.useSilentRefresh = true;
|
||||
|
||||
this._oAuthService.configure(this._authConfig);
|
||||
this._oAuthService.tokenValidationHandler = new JwksValidationHandler();
|
||||
|
||||
this._oAuthService.setupAutomaticSilentRefresh();
|
||||
try {
|
||||
await this._oAuthService.loadDiscoveryDocumentAndTryLogin();
|
||||
} catch (error) {
|
||||
this.login();
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
this._initialized.next(true);
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
return this._oAuthService.hasValidIdToken();
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return this._oAuthService.getAccessToken();
|
||||
}
|
||||
|
||||
getClaims() {
|
||||
const token = this._oAuthService.getAccessToken();
|
||||
return this.parseJwt(token);
|
||||
}
|
||||
|
||||
getClaimByKey(key: string) {
|
||||
const claims = this.getClaims();
|
||||
if (isNullOrUndefined(claims)) {
|
||||
return null;
|
||||
}
|
||||
return claims[key];
|
||||
}
|
||||
|
||||
parseJwt(token: string) {
|
||||
if (isNullOrUndefined(token)) {
|
||||
return null;
|
||||
}
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
|
||||
const encoded = window.atob(base64);
|
||||
return JSON.parse(encoded);
|
||||
}
|
||||
|
||||
login() {
|
||||
this._oAuthService.initLoginFlow();
|
||||
}
|
||||
|
||||
setKeyCardToken(token: string) {
|
||||
this._oAuthService.customQueryParams = {
|
||||
temp_token: token,
|
||||
};
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await this._oAuthService.revokeTokenAndLogout();
|
||||
// asapScheduler.schedule(() => {
|
||||
// window.location.reload();
|
||||
// }, 250);
|
||||
}
|
||||
|
||||
hasRole(role: string | string[]) {
|
||||
const roles = coerceArray(role);
|
||||
|
||||
const userRoles = this.getClaimByKey('role');
|
||||
|
||||
if (isNullOrUndefined(userRoles)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return roles.every((r) => userRoles.includes(r));
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
try {
|
||||
if (this._authConfig.responseType.includes('code') && this._authConfig.scope.includes('offline_access')) {
|
||||
await this._oAuthService.refreshToken();
|
||||
} else {
|
||||
await this._oAuthService.silentRefresh();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
apps/isa-app/src/core/auth/if-role.directive.spec.ts
Normal file
65
apps/isa-app/src/core/auth/if-role.directive.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
// import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator';
|
||||
// import { IfRoleDirective } from './if-role.directive';
|
||||
// import { AuthService } from './auth.service';
|
||||
// import { TemplateRef, ViewContainerRef } from '@angular/core';
|
||||
|
||||
// describe('IfRoleDirective', () => {
|
||||
// let spectator: SpectatorDirective<IfRoleDirective>;
|
||||
// const createDirective = createDirectiveFactory({
|
||||
// directive: IfRoleDirective,
|
||||
// mocks: [AuthService],
|
||||
// });
|
||||
|
||||
// it('should create an instance', () => {
|
||||
// spectator = createDirective(`<div *ifRole="'admin'"></div>`);
|
||||
// expect(spectator.directive).toBeTruthy();
|
||||
// });
|
||||
|
||||
// it('should render template when user has the role', () => {
|
||||
// spectator = createDirective(`<div *ifRole="'admin'"></div>`);
|
||||
// const authService = spectator.inject(AuthService);
|
||||
// const viewContainerRef = spectator.inject(ViewContainerRef);
|
||||
// const templateRef = spectator.inject(TemplateRef);
|
||||
// authService.hasRole.and.returnValue(true);
|
||||
// spectator.directive.ngOnChanges();
|
||||
// expect(viewContainerRef.createEmbeddedView).toHaveBeenCalledWith(templateRef, spectator.directive.getContext());
|
||||
// });
|
||||
|
||||
// it('should render else template when user does not have the role', () => {
|
||||
// authService.hasRole.and.returnValue(false);
|
||||
// const elseTemplateRef = {} as TemplateRef<any>;
|
||||
// spectator = createDirective(`<ng-template #elseTemplateRef></ng-template><div *ifRole="'admin'; else elseTemplateRef"></div>`, {
|
||||
// hostProps: {
|
||||
// elseTemplateRef,
|
||||
// },
|
||||
// });
|
||||
// spectator.directive.ngOnChanges();
|
||||
// expect(viewContainerRef.createEmbeddedView).toHaveBeenCalledWith(elseTemplateRef, spectator.directive.getContext());
|
||||
// });
|
||||
|
||||
// it('should render else template when user does not have the role using ifNotRole input', () => {
|
||||
// authService.hasRole.and.returnValue(false);
|
||||
// const elseTemplateRef = {} as TemplateRef<any>;
|
||||
// spectator = createDirective(`<ng-template #elseTemplateRef></ng-template><div *ifNotRole="'admin'; else elseTemplateRef"></div>`, {
|
||||
// hostProps: {
|
||||
// elseTemplateRef,
|
||||
// },
|
||||
// });
|
||||
// spectator.directive.ngOnChanges();
|
||||
// expect(viewContainerRef.createEmbeddedView).toHaveBeenCalledWith(elseTemplateRef, spectator.directive.getContext());
|
||||
// });
|
||||
|
||||
// it('should clear view when user does not have the role and elseTemplateRef is not defined', () => {
|
||||
// authService.hasRole.and.returnValue(false);
|
||||
// spectator = createDirective(`<div *ifRole="'admin'"></div>`);
|
||||
// spectator.directive.ngOnChanges();
|
||||
// expect(viewContainerRef.clear).toHaveBeenCalled();
|
||||
// });
|
||||
|
||||
// it('should set $implicit to ifRole or ifNotRole input', () => {
|
||||
// spectator = createDirective(`<div *ifRole="'admin'"></div>`);
|
||||
// expect(spectator.directive.getContext().$implicit).toEqual('admin');
|
||||
// spectator.setInput('ifNotRole', 'user');
|
||||
// expect(spectator.directive.getContext().$implicit).toEqual('user');
|
||||
// });
|
||||
// });
|
||||
63
apps/isa-app/src/core/auth/if-role.directive.ts
Normal file
63
apps/isa-app/src/core/auth/if-role.directive.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Directive, Input, OnChanges, TemplateRef, ViewContainerRef } from '@angular/core';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[ifRole],[ifRoleElse],[ifNotRole],[ifNotRoleElse]',
|
||||
})
|
||||
export class IfRoleDirective implements OnChanges {
|
||||
@Input()
|
||||
ifRole: string | string[];
|
||||
|
||||
@Input()
|
||||
ifRoleElse: TemplateRef<any>;
|
||||
|
||||
@Input()
|
||||
ifNotRole: string | string[];
|
||||
|
||||
@Input()
|
||||
ifNotRoleElse: TemplateRef<any>;
|
||||
|
||||
get renderTemplateRef() {
|
||||
if (this.ifRole) {
|
||||
return this._authService.hasRole(this.ifRole);
|
||||
}
|
||||
if (this.ifNotRole) {
|
||||
return !this._authService.hasRole(this.ifNotRole);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
get elseTemplateRef() {
|
||||
return this.ifRoleElse || this.ifNotRoleElse;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _templateRef: TemplateRef<any>,
|
||||
private _viewContainer: ViewContainerRef,
|
||||
private _authService: AuthService,
|
||||
) {}
|
||||
|
||||
ngOnChanges() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.renderTemplateRef) {
|
||||
this._viewContainer.createEmbeddedView(this._templateRef, this.getContext());
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.elseTemplateRef) {
|
||||
this._viewContainer.createEmbeddedView(this.elseTemplateRef, this.getContext());
|
||||
return;
|
||||
}
|
||||
|
||||
this._viewContainer.clear();
|
||||
}
|
||||
|
||||
getContext(): { $implicit: string | string[] } {
|
||||
return {
|
||||
$implicit: this.ifRole || this.ifNotRole,
|
||||
};
|
||||
}
|
||||
}
|
||||
3
apps/isa-app/src/core/auth/index.ts
Normal file
3
apps/isa-app/src/core/auth/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './auth.module';
|
||||
export * from './auth.service';
|
||||
export * from './if-role.directive';
|
||||
34
apps/isa-app/src/core/breadcrumb/breadcrumb.service.spec.ts
Normal file
34
apps/isa-app/src/core/breadcrumb/breadcrumb.service.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { isNumber } from '@utils/common';
|
||||
import { of } from 'rxjs';
|
||||
import { BreadcrumbService } from './breadcrumb.service';
|
||||
import { Breadcrumb } from './defs';
|
||||
|
||||
import * as actions from './store/breadcrumb.actions';
|
||||
import * as selectors from './store/breadcrumb.selectors';
|
||||
|
||||
describe('Breadcrumb Service', () => {
|
||||
let store: jasmine.SpyObj<Store<any>>;
|
||||
let service: BreadcrumbService;
|
||||
|
||||
beforeEach(() => {
|
||||
store = jasmine.createSpyObj<Store<any>>('Store', ['select', 'dispatch']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [BreadcrumbService, { provide: Store, useValue: store }],
|
||||
});
|
||||
|
||||
service = TestBed.inject(BreadcrumbService);
|
||||
});
|
||||
|
||||
describe('addBreadcrumb', () => {
|
||||
it('should call store.dispatch with the addBreadecrumb action and retuns a breadcrumb with an id', () => {
|
||||
let breadcrumb: Breadcrumb = { name: 'unit-test', key: 'hello-key', path: 'Run The Test', section: 'customer' };
|
||||
breadcrumb = service.addBreadcrumb(breadcrumb);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(actions.addBreadcrumb({ breadcrumb }));
|
||||
expect(isNumber(breadcrumb.id)).toBeTruthy();
|
||||
expect(isNumber(breadcrumb.timestamp)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
143
apps/isa-app/src/core/breadcrumb/breadcrumb.service.ts
Normal file
143
apps/isa-app/src/core/breadcrumb/breadcrumb.service.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { getNumberId } from '@utils/id';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { first, map, take } from 'rxjs/operators';
|
||||
import { Breadcrumb } from './defs';
|
||||
|
||||
import * as actions from './store/breadcrumb.actions';
|
||||
import * as selectors from './store/breadcrumb.selectors';
|
||||
|
||||
@Injectable()
|
||||
export class BreadcrumbService {
|
||||
constructor(private store: Store<any>) {}
|
||||
|
||||
getAll$() {
|
||||
return this.store.select(selectors.selectBreadcrumbs);
|
||||
}
|
||||
|
||||
getByKey$(key: string): Observable<Breadcrumb[]> {
|
||||
return this.store.select(selectors.selectBreadcrumbsByKey, key);
|
||||
}
|
||||
|
||||
getBreadcrumbById$(id: number): Observable<Breadcrumb> {
|
||||
return this.store.select(selectors.selectBreadcrumbById, id);
|
||||
}
|
||||
|
||||
getBreadcrumbByKey$(key: string | number): Observable<Breadcrumb[]> {
|
||||
return this.store.select(selectors.selectBreadcrumbsByKey, key);
|
||||
}
|
||||
|
||||
getLastActivatedBreadcrumbByKey$(key: string | number): Observable<Breadcrumb> {
|
||||
return this.getBreadcrumbByKey$(key).pipe(
|
||||
map((crumbs) =>
|
||||
crumbs.reduce((latest, current) => {
|
||||
if (!latest) {
|
||||
return current;
|
||||
}
|
||||
return latest.timestamp > current.timestamp ? latest : current;
|
||||
}, undefined),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
addBreadcrumb(breadcrumb: Breadcrumb): Breadcrumb {
|
||||
const newBreadcrumb: Breadcrumb = { ...breadcrumb, id: getNumberId(), timestamp: Date.now(), changed: Date.now() };
|
||||
this.store.dispatch(actions.addBreadcrumb({ breadcrumb: newBreadcrumb }));
|
||||
return newBreadcrumb;
|
||||
}
|
||||
|
||||
patchBreadcrumb(breadcrumbId: number, changes: Partial<Breadcrumb>) {
|
||||
this.store.dispatch(actions.updateBreadcrumb({ id: breadcrumbId, changes: { ...changes, changed: Date.now() } }));
|
||||
}
|
||||
|
||||
async patchBreadcrumbByKeyAndTags(key: string | number, tags: string[], changes: Partial<Breadcrumb>) {
|
||||
const crumbs = await this.getBreadcrumbsByKeyAndTags$(key, tags).pipe(first()).toPromise();
|
||||
crumbs.forEach((crumb) => this.patchBreadcrumb(crumb.id, changes));
|
||||
}
|
||||
|
||||
async addBreadcrumbIfNotExists(breadcrumb: Breadcrumb) {
|
||||
const crumbs = await this.getBreadcrumbsByKeyAndTags$(breadcrumb.key, breadcrumb.tags).pipe(take(1)).toPromise();
|
||||
if (crumbs.length === 0) {
|
||||
return this.addBreadcrumb(breadcrumb);
|
||||
}
|
||||
return crumbs[0];
|
||||
}
|
||||
|
||||
async addOrUpdateBreadcrumbIfNotExists(breadcrumb: Breadcrumb) {
|
||||
const crumbs = await this.getBreadcrumbsByKeyAndTags$(breadcrumb.key, breadcrumb.tags).pipe(take(1)).toPromise();
|
||||
if (crumbs.length === 0) {
|
||||
return this.addBreadcrumb(breadcrumb);
|
||||
}
|
||||
return this.patchBreadcrumb(crumbs[0].id, breadcrumb);
|
||||
}
|
||||
|
||||
getBreadcrumbsByKeyAndTag$(key: string | number, tag: string): Observable<Breadcrumb[]> {
|
||||
return this.store.select(selectors.selectBreadcrumbsByKeyAndTag, { key, tag });
|
||||
}
|
||||
|
||||
getBreadcrumbsByKeyAndTags$(key: string | number, tags: string[]): Observable<Breadcrumb[]> {
|
||||
return this.store.select(selectors.selectBreadcrumbsByKeyAndTags, { key, tags });
|
||||
}
|
||||
|
||||
async removeBreadcrumbsAfter(breadcrumbId: number, withTags: string[] = []) {
|
||||
const breadcrumb = await this.getBreadcrumbById$(breadcrumbId).pipe(take(1)).toPromise();
|
||||
|
||||
if (!breadcrumb) {
|
||||
return;
|
||||
}
|
||||
|
||||
let breadcrumbs: Breadcrumb[];
|
||||
|
||||
if (withTags?.length > 0) {
|
||||
breadcrumbs = await this.getBreadcrumbsByKeyAndTags$(breadcrumb.key, withTags).pipe(take(1)).toPromise();
|
||||
} else {
|
||||
breadcrumbs = await this.getBreadcrumbByKey$(breadcrumb.key).pipe(take(1)).toPromise();
|
||||
}
|
||||
|
||||
if (!breadcrumbs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const breadcrumbsToRemove = breadcrumbs.filter((crumb) => crumb.timestamp > breadcrumb.timestamp);
|
||||
|
||||
if (!breadcrumbsToRemove.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.store.dispatch(actions.removeManyBreadcrumb({ ids: breadcrumbsToRemove.map((crumb) => crumb.id) }));
|
||||
}
|
||||
|
||||
async removeBreadcrumb(breadcrumbId: number, recursive: boolean = true) {
|
||||
const breadcrumb = await this.getBreadcrumbById$(breadcrumbId).pipe(take(1)).toPromise();
|
||||
|
||||
if (!breadcrumb) {
|
||||
return;
|
||||
}
|
||||
|
||||
let breadcrumbsToRemove = [breadcrumb];
|
||||
|
||||
if (recursive) {
|
||||
const breadcrumbs = await this.getBreadcrumbByKey$(breadcrumb.key).pipe(take(1)).toPromise();
|
||||
breadcrumbsToRemove = [...breadcrumbsToRemove, ...breadcrumbs.filter((crumb) => crumb.timestamp > breadcrumb.timestamp)];
|
||||
}
|
||||
|
||||
if (!breadcrumbsToRemove.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.store.dispatch(actions.removeManyBreadcrumb({ ids: breadcrumbsToRemove.map((crumb) => crumb.id) }));
|
||||
}
|
||||
|
||||
async removeBreadcrumbsByKeyAndTags(key: number | string, tags: string[]) {
|
||||
const crumbs = await this.getBreadcrumbsByKeyAndTags$(key, tags).pipe(first()).toPromise();
|
||||
crumbs.forEach((crumb) => this.removeBreadcrumb(crumb.id));
|
||||
}
|
||||
|
||||
getLatestBreadcrumbForSection(section: 'customer' | 'branch', predicate: (crumb: Breadcrumb) => boolean = (_) => true) {
|
||||
return this.store
|
||||
.select(selectors.selectBreadcrumbsBySection, { section })
|
||||
.pipe(map((crumbs) => crumbs.sort((a, b) => b.timestamp - a.timestamp).find((f) => predicate(f))));
|
||||
}
|
||||
}
|
||||
22
apps/isa-app/src/core/breadcrumb/core-breadcrumb.module.ts
Normal file
22
apps/isa-app/src/core/breadcrumb/core-breadcrumb.module.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { BreadcrumbService } from './breadcrumb.service';
|
||||
import { BreadcrumbEffects } from './store/breadcrumb.effect';
|
||||
import { breadcrumbReducer } from './store/breadcrumb.reducer';
|
||||
import { featureName } from './store/breadcrumb.state';
|
||||
|
||||
@NgModule()
|
||||
export class CoreBreadcrumbModule {
|
||||
static forRoot(): ModuleWithProviders<CoreBreadcrumbModule> {
|
||||
return {
|
||||
ngModule: CoreBreadcrumbForRootModule,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [StoreModule.forFeature(featureName, breadcrumbReducer), EffectsModule.forFeature([BreadcrumbEffects])],
|
||||
providers: [BreadcrumbService],
|
||||
})
|
||||
export class CoreBreadcrumbForRootModule {}
|
||||
46
apps/isa-app/src/core/breadcrumb/defs/breadcrumb.model.ts
Normal file
46
apps/isa-app/src/core/breadcrumb/defs/breadcrumb.model.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
export interface Breadcrumb {
|
||||
/**
|
||||
* Eindeutige ID für die Entity
|
||||
*/
|
||||
id?: number;
|
||||
|
||||
/**
|
||||
* Identifier für ein Teilbereich/ProzessId der Applikation
|
||||
*/
|
||||
key: number | string;
|
||||
|
||||
/**
|
||||
* Tags
|
||||
*/
|
||||
tags?: string[];
|
||||
|
||||
/**
|
||||
* Anzeigename
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Url
|
||||
*/
|
||||
path: string | any[];
|
||||
|
||||
/**
|
||||
* Query Parameter
|
||||
*/
|
||||
params?: Object;
|
||||
|
||||
/**
|
||||
* Timestamp
|
||||
*/
|
||||
timestamp?: number;
|
||||
|
||||
/**
|
||||
* Cahnged
|
||||
*/
|
||||
changed?: number;
|
||||
|
||||
/**
|
||||
* Applicatiuon Section
|
||||
*/
|
||||
section: string;
|
||||
}
|
||||
3
apps/isa-app/src/core/breadcrumb/defs/index.ts
Normal file
3
apps/isa-app/src/core/breadcrumb/defs/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// start:ng42.barrel
|
||||
export * from './breadcrumb.model';
|
||||
// end:ng42.barrel
|
||||
3
apps/isa-app/src/core/breadcrumb/index.ts
Normal file
3
apps/isa-app/src/core/breadcrumb/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './breadcrumb.service';
|
||||
export * from './core-breadcrumb.module';
|
||||
export * from './defs';
|
||||
@@ -0,0 +1 @@
|
||||
describe('Breadcrumb Actions', () => {});
|
||||
24
apps/isa-app/src/core/breadcrumb/store/breadcrumb.actions.ts
Normal file
24
apps/isa-app/src/core/breadcrumb/store/breadcrumb.actions.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createAction, props } from '@ngrx/store';
|
||||
import { Breadcrumb } from '../defs';
|
||||
|
||||
const prefix = '[CORE-BREADCRUMB]';
|
||||
|
||||
/**
|
||||
* Action um Breadcrumb zum State hinzufügen
|
||||
*/
|
||||
export const addBreadcrumb = createAction(`${prefix} Add Breadcrumb`, props<{ breadcrumb: Breadcrumb }>());
|
||||
|
||||
/**
|
||||
* Action um Breadcrumb im State zu ändern
|
||||
*/
|
||||
export const updateBreadcrumb = createAction(`${prefix} Update Breadcrumb`, props<{ id: number; changes: Partial<Breadcrumb> }>());
|
||||
|
||||
/**
|
||||
* Action um Breadcrumb im State zu entfernen
|
||||
*/
|
||||
export const removeBreadcrumb = createAction(`${prefix} Remove Breadcrumb`, props<{ id: number }>());
|
||||
|
||||
/**
|
||||
* Action um mehrere Breadcrumbs im State zu entfernen
|
||||
*/
|
||||
export const removeManyBreadcrumb = createAction(`${prefix} Remove Many Breadcrumb`, props<{ ids: number[] }>());
|
||||
30
apps/isa-app/src/core/breadcrumb/store/breadcrumb.effect.ts
Normal file
30
apps/isa-app/src/core/breadcrumb/store/breadcrumb.effect.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { removeProcess } from '@core/application';
|
||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
import { NEVER } from 'rxjs';
|
||||
import { mergeMap, tap, first, map } from 'rxjs/operators';
|
||||
import { BreadcrumbService } from '../breadcrumb.service';
|
||||
|
||||
@Injectable()
|
||||
export class BreadcrumbEffects {
|
||||
removeProcess$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(removeProcess),
|
||||
mergeMap((action) =>
|
||||
this.breadcrumb.getBreadcrumbByKey$(action.processId).pipe(
|
||||
first(),
|
||||
tap((breadcrumbs) => {
|
||||
breadcrumbs?.forEach((crumb) => this.breadcrumb.removeBreadcrumb(crumb.id));
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
{ dispatch: false },
|
||||
);
|
||||
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private breadcrumb: BreadcrumbService,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Breadcrumb } from '../defs';
|
||||
import * as action from './breadcrumb.actions';
|
||||
import { breadcrumbReducer } from './breadcrumb.reducer';
|
||||
import { INIT } from './breadcrumb.state';
|
||||
|
||||
describe('Breadcrumb Reducer', () => {
|
||||
it('should return the initial state if the current state is empty', () => {
|
||||
const fixture = breadcrumbReducer(undefined, { type: '' });
|
||||
|
||||
expect(fixture).toEqual(INIT);
|
||||
});
|
||||
|
||||
describe('addBreadcrumb', () => {
|
||||
it('should add the breadcrumb to the state', () => {
|
||||
const breadcrumb: Breadcrumb = {
|
||||
id: 1,
|
||||
key: 'unit-test',
|
||||
name: 'Test Name',
|
||||
path: 'Test Patch',
|
||||
section: 'customer',
|
||||
};
|
||||
|
||||
const fixture = breadcrumbReducer(INIT, action.addBreadcrumb({ breadcrumb }));
|
||||
|
||||
expect(fixture.entities[1]).toEqual(breadcrumb);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateBreadcrumb', () => {
|
||||
it('should update an existing breadcrumb', () => {
|
||||
const breadcrumb: Breadcrumb = {
|
||||
id: 1,
|
||||
key: 'unit-test',
|
||||
name: 'Test Name',
|
||||
path: 'Test Patch',
|
||||
section: 'customer',
|
||||
};
|
||||
|
||||
const expected = {
|
||||
...breadcrumb,
|
||||
name: 'Test Name 2',
|
||||
};
|
||||
|
||||
const state = breadcrumbReducer(INIT, action.addBreadcrumb({ breadcrumb }));
|
||||
|
||||
const fixture = breadcrumbReducer(state, action.updateBreadcrumb({ id: breadcrumb.id, changes: { name: 'Test Name 2' } }));
|
||||
|
||||
expect(fixture.entities[breadcrumb.id]).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeBreadcrumb', () => {
|
||||
it('should remove the breadcrumb', () => {
|
||||
const breadcrumb: Breadcrumb = {
|
||||
id: 1,
|
||||
key: 'unit-test',
|
||||
name: 'Test Name',
|
||||
path: 'Test Patch',
|
||||
section: 'customer',
|
||||
};
|
||||
|
||||
const state = breadcrumbReducer(INIT, action.addBreadcrumb({ breadcrumb }));
|
||||
|
||||
const fixture = breadcrumbReducer(state, action.removeBreadcrumb({ id: breadcrumb.id }));
|
||||
|
||||
expect(fixture.entities[breadcrumb.id]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeManyBreadcrumb', () => {
|
||||
it('should remove all breadcrumbs for the given ids', () => {
|
||||
const breadcrumb1: Breadcrumb = {
|
||||
id: 1,
|
||||
key: 'unit-test',
|
||||
name: 'Test Name',
|
||||
path: 'Test Patch',
|
||||
section: 'customer',
|
||||
};
|
||||
const breadcrumb2 = { ...breadcrumb1, id: 2 };
|
||||
const breadcrumb3 = { ...breadcrumb1, id: 3 };
|
||||
|
||||
let state = breadcrumbReducer(INIT, action.addBreadcrumb({ breadcrumb: breadcrumb1 }));
|
||||
state = breadcrumbReducer(state, action.addBreadcrumb({ breadcrumb: breadcrumb2 }));
|
||||
state = breadcrumbReducer(state, action.addBreadcrumb({ breadcrumb: breadcrumb3 }));
|
||||
|
||||
const fixture = breadcrumbReducer(state, action.removeManyBreadcrumb({ ids: [1, 3] }));
|
||||
|
||||
expect(Object.keys(fixture.entities).length).toEqual(1);
|
||||
expect(fixture.entities[breadcrumb2.id]).toEqual(breadcrumb2);
|
||||
});
|
||||
});
|
||||
});
|
||||
16
apps/isa-app/src/core/breadcrumb/store/breadcrumb.reducer.ts
Normal file
16
apps/isa-app/src/core/breadcrumb/store/breadcrumb.reducer.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Action, createReducer, on } from '@ngrx/store';
|
||||
import { breadcrumbAdapter, BreadcrumbState, INIT } from './breadcrumb.state';
|
||||
|
||||
import * as actions from './breadcrumb.actions';
|
||||
|
||||
const _breadcrumbReducer = createReducer(
|
||||
INIT,
|
||||
on(actions.addBreadcrumb, (s, a) => breadcrumbAdapter.addOne(a.breadcrumb, s)),
|
||||
on(actions.updateBreadcrumb, (s, a) => breadcrumbAdapter.updateOne(a, s)),
|
||||
on(actions.removeBreadcrumb, (s, a) => breadcrumbAdapter.removeOne(a.id, s)),
|
||||
on(actions.removeManyBreadcrumb, (s, a) => breadcrumbAdapter.removeMany(a.ids, s)),
|
||||
);
|
||||
|
||||
export function breadcrumbReducer(state: BreadcrumbState, action: Action) {
|
||||
return _breadcrumbReducer(state, action);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import * as selector from './breadcrumb.selectors';
|
||||
import * as action from './breadcrumb.actions';
|
||||
import { breadcrumbReducer } from './breadcrumb.reducer';
|
||||
|
||||
import { BreadcrumbState, INIT } from './breadcrumb.state';
|
||||
|
||||
describe('Breadcrumb Selectors', () => {
|
||||
let state: BreadcrumbState;
|
||||
|
||||
beforeEach(() => {
|
||||
state = breadcrumbReducer(
|
||||
INIT,
|
||||
action.addBreadcrumb({ breadcrumb: { id: 1, key: 'unit-test-1', path: '', name: 'Unit Test 1', section: 'customer' } }),
|
||||
);
|
||||
state = breadcrumbReducer(
|
||||
state,
|
||||
action.addBreadcrumb({
|
||||
breadcrumb: { id: 2, key: 'unit-test-1', path: '', name: 'Unit Test 1', tags: ['details'], section: 'customer' },
|
||||
}),
|
||||
);
|
||||
state = breadcrumbReducer(
|
||||
state,
|
||||
action.addBreadcrumb({ breadcrumb: { id: 3, key: 'unit-test-2', path: '', name: 'Unit Test 1', section: 'customer' } }),
|
||||
);
|
||||
state = breadcrumbReducer(
|
||||
state,
|
||||
action.addBreadcrumb({ breadcrumb: { id: 4, key: 'unit-test-3', path: '', name: 'Unit Test 1', section: 'customer' } }),
|
||||
);
|
||||
state = breadcrumbReducer(
|
||||
state,
|
||||
action.addBreadcrumb({
|
||||
breadcrumb: { id: 5, key: 'unit-test-3', path: '', name: 'Unit Test 1', tags: ['details'], section: 'customer' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('selectBreadcrumbsByKey', () => {
|
||||
it('should return all breadcrumbs with the key unit-test-1', () => {
|
||||
const fixture = selector.selectBreadcrumbsByKey.projector(Object.values(state.entities), 'unit-test-1');
|
||||
expect(fixture.length).toBe(2);
|
||||
expect(fixture[0].key).toBe('unit-test-1');
|
||||
expect(fixture[1].key).toBe('unit-test-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectBreadcrumbsByKeyAndTag', () => {
|
||||
it('should return all breadcrumbs with the key unit-test-3 and tag details', () => {
|
||||
const fixture = selector.selectBreadcrumbsByKeyAndTag.projector(Object.values(state.entities), {
|
||||
key: 'unit-test-3',
|
||||
tag: 'details',
|
||||
});
|
||||
expect(fixture.length).toBe(1);
|
||||
expect(fixture[0].key).toBe('unit-test-3');
|
||||
expect(fixture[0].tags).toContain('details');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { createSelector, createFeatureSelector } from '@ngrx/store';
|
||||
import { Breadcrumb } from '../defs';
|
||||
import { breadcrumbAdapter, BreadcrumbState, featureName } from './breadcrumb.state';
|
||||
import { isArray } from '@utils/common';
|
||||
|
||||
const selectFeature = createFeatureSelector<BreadcrumbState>(featureName);
|
||||
|
||||
const { selectAll, selectEntities } = breadcrumbAdapter.getSelectors(selectFeature);
|
||||
|
||||
/**
|
||||
* Gibt alle Breadcrumb Entities als Array zurück
|
||||
*/
|
||||
export const selectBreadcrumbs = selectAll;
|
||||
|
||||
/**
|
||||
* Gibt alle Breadcrumb Entities als Array zurück die den key enthalten
|
||||
*/
|
||||
export const selectBreadcrumbById = createSelector(selectEntities, (entities, id: number) => entities[id]);
|
||||
|
||||
/**
|
||||
* Gibt alle Breadcrumb Entities als Array zurück die den key enthalten
|
||||
*/
|
||||
export const selectBreadcrumbsByKey = createSelector(selectAll, (entities, key: string) => entities.filter((crumb) => crumb.key == key));
|
||||
|
||||
/**
|
||||
* Gibt alle Breadcrumb Entities als Array zurück die den key und tag enthalten
|
||||
*/
|
||||
export const selectBreadcrumbsByKeyAndTag = createSelector(
|
||||
selectAll,
|
||||
(entities: Breadcrumb[], { key, tag }: { key: string; tag: string }) =>
|
||||
entities.filter((crumb) => crumb.key == key && isArray(crumb.tags) && crumb.tags.includes(tag)),
|
||||
);
|
||||
|
||||
/**
|
||||
* Gibt alle Breadcrumb Entities als Array zurück die den key und tags enthalten
|
||||
*/
|
||||
export const selectBreadcrumbsByKeyAndTags = createSelector(
|
||||
selectAll,
|
||||
(entities: Breadcrumb[], { key, tags }: { key: string; tags: string[] }) =>
|
||||
entities.filter((crumb) => crumb.key == key && isArray(crumb.tags) && tags.every((tag) => crumb.tags.includes(tag))),
|
||||
);
|
||||
|
||||
/**
|
||||
* Gibt alle Breadcrumb Entities als Array zurück die die tags enthalten
|
||||
*/
|
||||
export const selectBreadcrumbsBySection = createSelector(selectAll, (entities: Breadcrumb[], { section }: { section: string }) =>
|
||||
entities.filter((crumb) => crumb.section === section),
|
||||
);
|
||||
12
apps/isa-app/src/core/breadcrumb/store/breadcrumb.state.ts
Normal file
12
apps/isa-app/src/core/breadcrumb/store/breadcrumb.state.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createEntityAdapter, EntityState } from '@ngrx/entity';
|
||||
import { Breadcrumb } from '../defs';
|
||||
|
||||
export interface BreadcrumbState extends EntityState<Breadcrumb> {}
|
||||
|
||||
export const featureName = 'core-breadcrumb';
|
||||
|
||||
export const breadcrumbAdapter = createEntityAdapter<Breadcrumb>();
|
||||
|
||||
export const INIT: BreadcrumbState = {
|
||||
...breadcrumbAdapter.getInitialState(),
|
||||
};
|
||||
3
apps/isa-app/src/core/cache/cache-options.ts
vendored
Normal file
3
apps/isa-app/src/core/cache/cache-options.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface CacheOptions {
|
||||
ttl?: number;
|
||||
}
|
||||
8
apps/isa-app/src/core/cache/cache.module.ts
vendored
Normal file
8
apps/isa-app/src/core/cache/cache.module.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [],
|
||||
exports: [],
|
||||
})
|
||||
export class CacheModule {}
|
||||
184
apps/isa-app/src/core/cache/cache.service.ts
vendored
Normal file
184
apps/isa-app/src/core/cache/cache.service.ts
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
import { effect, inject, Injectable } from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { memorize } from '@utils/common';
|
||||
import { interval } from 'rxjs';
|
||||
|
||||
interface DbEntry<T> {
|
||||
key: string;
|
||||
token: object | string;
|
||||
data: T;
|
||||
ttl: number;
|
||||
}
|
||||
|
||||
const H12INMS = 1000 * 60 * 60 * 12;
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CacheService {
|
||||
private auth = inject(AuthService);
|
||||
|
||||
private db!: IDBDatabase;
|
||||
|
||||
get sub() {
|
||||
return Number(this.auth.getClaimByKey('sub'));
|
||||
}
|
||||
|
||||
_cleanupInterval = toSignal(interval(1000 * 60));
|
||||
|
||||
_cleanupIntervalEffect = effect(() => {
|
||||
this._cleanupInterval();
|
||||
this.cleanup();
|
||||
});
|
||||
|
||||
get storeName() {
|
||||
return 'cache';
|
||||
}
|
||||
|
||||
private getKey(token: Object | string): string {
|
||||
if (typeof token === 'string') {
|
||||
return this.hash(token);
|
||||
}
|
||||
|
||||
return this.hash(JSON.stringify(token));
|
||||
}
|
||||
|
||||
private hash(data: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
hash = data.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return (this.sub + hash).toString(16);
|
||||
}
|
||||
|
||||
@memorize()
|
||||
private async openDB(): Promise<IDBDatabase> {
|
||||
if (this.db) {
|
||||
return this.db; // Datenbank bereits geöffnet, bestehende Verbindung zurückgeben
|
||||
}
|
||||
|
||||
return new Promise<IDBDatabase>((resolve, reject) => {
|
||||
const request = indexedDB.open('isa-cache', 1);
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject(event);
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
this.db = (event.target as IDBOpenDBRequest).result;
|
||||
|
||||
if (!this.db.objectStoreNames.contains(this.storeName)) {
|
||||
this.db.createObjectStore(this.storeName, { keyPath: 'key' });
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
this.db = (event.target as IDBOpenDBRequest).result;
|
||||
resolve(this.db);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async getObjectStore(mode: IDBTransactionMode = 'readonly'): Promise<IDBObjectStore> {
|
||||
const db = await this.openDB(); // Datenbankverbindung öffnen oder wiederverwenden
|
||||
const transaction = db.transaction(this.storeName, mode);
|
||||
return transaction.objectStore(this.storeName);
|
||||
}
|
||||
|
||||
async set<T>(token: object | string, data: T, options: { ttl?: number } = {}): Promise<string> {
|
||||
const store = await this.getObjectStore('readwrite');
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const key = this.getKey(token);
|
||||
const entry: DbEntry<T> = {
|
||||
key,
|
||||
data,
|
||||
token,
|
||||
ttl: Date.now() + (options.ttl || H12INMS),
|
||||
};
|
||||
|
||||
const request = store.put(entry);
|
||||
request.onsuccess = (event) => {
|
||||
resolve(key);
|
||||
};
|
||||
request.onerror = (event) => {
|
||||
reject(event);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async cached(token: Object | string): Promise<DbEntry<any> | undefined> {
|
||||
const store = await this.getObjectStore();
|
||||
return new Promise<DbEntry<any> | undefined>((resolve, reject) => {
|
||||
const request = store.get(this.getKey(token));
|
||||
request.onsuccess = (event) => {
|
||||
resolve((event.target as IDBRequest).result);
|
||||
};
|
||||
request.onerror = (event) => {
|
||||
reject(event);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async get<T = any>(token: Object | string): Promise<T | undefined> {
|
||||
const cached = await this.cached(token);
|
||||
|
||||
if (!cached) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (cached.ttl < Date.now()) {
|
||||
this.delete(token);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
async delete(token: Object | string): Promise<void> {
|
||||
const store = await this.getObjectStore('readwrite');
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const request = store.delete(this.getKey(token));
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
request.onerror = (event) => {
|
||||
reject(event);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
const store = await this.getObjectStore('readwrite');
|
||||
|
||||
store.openCursor().onsuccess = (event) => {
|
||||
const cursor = (event.target as IDBRequest).result;
|
||||
if (cursor) {
|
||||
if (cursor.value.ttl < Date.now()) {
|
||||
store.delete(cursor.key);
|
||||
}
|
||||
cursor.continue();
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
store.transaction.oncomplete = () => {
|
||||
resolve();
|
||||
};
|
||||
store.transaction.onerror = (event) => {
|
||||
reject(event);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async clear() {
|
||||
const store = await this.getObjectStore('readwrite');
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const request = store.clear();
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
request.onerror = (event) => {
|
||||
reject(event);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
3
apps/isa-app/src/core/cache/index.ts
vendored
Normal file
3
apps/isa-app/src/core/cache/index.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './cache-options';
|
||||
export * from './cache.module';
|
||||
export * from './cache.service';
|
||||
@@ -0,0 +1,6 @@
|
||||
import { CommandService } from './command.service';
|
||||
|
||||
export abstract class ActionHandler<T = any> {
|
||||
constructor(readonly action: string) {}
|
||||
abstract handler(data: T, service?: CommandService): Promise<T>;
|
||||
}
|
||||
25
apps/isa-app/src/core/command/command.module.ts
Normal file
25
apps/isa-app/src/core/command/command.module.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ModuleWithProviders, NgModule, Provider, Type } from '@angular/core';
|
||||
import { ActionHandler } from './action-handler.interface';
|
||||
import { CommandService } from './command.service';
|
||||
import { FEATURE_ACTION_HANDLERS, ROOT_ACTION_HANDLERS } from './tokens';
|
||||
|
||||
export function provideActionHandlers(actionHandlers: Type<ActionHandler>[]): Provider[] {
|
||||
return [CommandService, actionHandlers.map((handler) => ({ provide: FEATURE_ACTION_HANDLERS, useClass: handler, multi: true }))];
|
||||
}
|
||||
|
||||
@NgModule({})
|
||||
export class CoreCommandModule {
|
||||
static forRoot(actionHandlers: Type<ActionHandler>[]): ModuleWithProviders<CoreCommandModule> {
|
||||
return {
|
||||
ngModule: CoreCommandModule,
|
||||
providers: [CommandService, actionHandlers.map((handler) => ({ provide: ROOT_ACTION_HANDLERS, useClass: handler, multi: true }))],
|
||||
};
|
||||
}
|
||||
|
||||
static forChild(actionHandlers: Type<ActionHandler>[]): ModuleWithProviders<CoreCommandModule> {
|
||||
return {
|
||||
ngModule: CoreCommandModule,
|
||||
providers: [CommandService, actionHandlers.map((handler) => ({ provide: FEATURE_ACTION_HANDLERS, useClass: handler, multi: true }))],
|
||||
};
|
||||
}
|
||||
}
|
||||
16
apps/isa-app/src/core/command/command.service.spec.ts
Normal file
16
apps/isa-app/src/core/command/command.service.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// import { TestBed } from '@angular/core/testing';
|
||||
|
||||
// import { CommandService } from './command.service';
|
||||
|
||||
// describe('CommandService', () => {
|
||||
// let service: CommandService;
|
||||
|
||||
// beforeEach(() => {
|
||||
// TestBed.configureTestingModule({});
|
||||
// service = TestBed.inject(CommandService);
|
||||
// });
|
||||
|
||||
// it('should be created', () => {
|
||||
// expect(service).toBeTruthy();
|
||||
// });
|
||||
// });
|
||||
43
apps/isa-app/src/core/command/command.service.ts
Normal file
43
apps/isa-app/src/core/command/command.service.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Injectable, Injector, Optional, SkipSelf } from '@angular/core';
|
||||
import { ActionHandler } from './action-handler.interface';
|
||||
import { FEATURE_ACTION_HANDLERS, ROOT_ACTION_HANDLERS } from './tokens';
|
||||
|
||||
@Injectable()
|
||||
export class CommandService {
|
||||
constructor(
|
||||
private injector: Injector,
|
||||
@Optional() @SkipSelf() private _parent: CommandService,
|
||||
) {}
|
||||
|
||||
async handleCommand<T>(command: string, data?: T): Promise<T> {
|
||||
const actions = this.getActions(command);
|
||||
|
||||
for (const action of actions) {
|
||||
const handler = this.getActionHandler(action);
|
||||
if (!handler) {
|
||||
console.error('CommandService.handleCommand', 'Action Handler does not exist', { action });
|
||||
throw new Error('Action Handler does not exist');
|
||||
}
|
||||
|
||||
data = await handler.handler(data, this);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
getActions(command: string) {
|
||||
return command?.split('|') || [];
|
||||
}
|
||||
|
||||
getActionHandler(action: string): ActionHandler | undefined {
|
||||
const featureActionHandlers: ActionHandler[] = this.injector.get(FEATURE_ACTION_HANDLERS, []);
|
||||
const rootActionHandlers: ActionHandler[] = this.injector.get(ROOT_ACTION_HANDLERS, []);
|
||||
|
||||
let handler = [...featureActionHandlers, ...rootActionHandlers].find((handler) => handler.action === action);
|
||||
|
||||
if (this._parent && !handler) {
|
||||
handler = this._parent.getActionHandler(action);
|
||||
}
|
||||
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
4
apps/isa-app/src/core/command/index.ts
Normal file
4
apps/isa-app/src/core/command/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './action-handler.interface';
|
||||
export * from './command.module';
|
||||
export * from './command.service';
|
||||
export * from './tokens';
|
||||
6
apps/isa-app/src/core/command/tokens.ts
Normal file
6
apps/isa-app/src/core/command/tokens.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { ActionHandler } from './action-handler.interface';
|
||||
|
||||
export const ROOT_ACTION_HANDLERS = new InjectionToken<ActionHandler[]>('@core/domain ROOT_ACTION_HANDLER');
|
||||
|
||||
export const FEATURE_ACTION_HANDLERS = new InjectionToken<ActionHandler[]>('@core/domain FEATURE_ACTION_HANDLER');
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Config loader interface for loading configurations
|
||||
*/
|
||||
export interface ConfigLoader {
|
||||
load(): Promise<any>;
|
||||
}
|
||||
4
apps/isa-app/src/core/config/config-loaders/index.ts
Normal file
4
apps/isa-app/src/core/config/config-loaders/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './config-loader';
|
||||
export * from './json.config-loader';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,36 @@
|
||||
// unit test JsonConfigLoader
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator';
|
||||
import { CORE_JSON_CONFIG_LOADER_URL } from '../tokens';
|
||||
import { JsonConfigLoader } from './json.config-loader';
|
||||
|
||||
describe('JsonConfigLoader', () => {
|
||||
let spectator: SpectatorService<JsonConfigLoader>;
|
||||
const createService = createServiceFactory({
|
||||
imports: [HttpClientTestingModule],
|
||||
service: JsonConfigLoader,
|
||||
mocks: [],
|
||||
providers: [{ provide: CORE_JSON_CONFIG_LOADER_URL, useValue: '/assets/config.json' }],
|
||||
});
|
||||
let httpTestingController: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createService();
|
||||
httpTestingController = spectator.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('load', () => {
|
||||
it('should call the provided url', async () => {
|
||||
const reqPromise = spectator.service.load();
|
||||
const req = httpTestingController.expectOne('/assets/config.json');
|
||||
req.flush({ unit: 'test' });
|
||||
const result = await reqPromise;
|
||||
httpTestingController.verify();
|
||||
expect(result).toEqual({ unit: 'test' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { ConfigLoader } from './config-loader';
|
||||
import { CORE_JSON_CONFIG_LOADER_URL } from '../tokens';
|
||||
|
||||
@Injectable()
|
||||
export class JsonConfigLoader implements ConfigLoader {
|
||||
constructor(
|
||||
@Inject(CORE_JSON_CONFIG_LOADER_URL) private url: string,
|
||||
private http: HttpClient,
|
||||
) {}
|
||||
|
||||
load(): Promise<any> {
|
||||
return this.http.get(this.url).toPromise();
|
||||
}
|
||||
}
|
||||
7
apps/isa-app/src/core/config/config-module-options.ts
Normal file
7
apps/isa-app/src/core/config/config-module-options.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Type } from '@angular/core';
|
||||
import { ConfigLoader } from './config-loaders';
|
||||
|
||||
export interface ConfigModuleOptions {
|
||||
useConfigLoader: Type<ConfigLoader>;
|
||||
jsonConfigLoaderUrl?: string;
|
||||
}
|
||||
28
apps/isa-app/src/core/config/config.module.ts
Normal file
28
apps/isa-app/src/core/config/config.module.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { CORE_CONFIG_LOADER } from '@core/config';
|
||||
import { Config } from './config';
|
||||
import { ConfigModuleOptions } from './config-module-options';
|
||||
import { CORE_JSON_CONFIG_LOADER_URL } from './tokens';
|
||||
|
||||
export function _initializeConfigFactory(config: Config) {
|
||||
return () => config.init();
|
||||
}
|
||||
|
||||
@NgModule({})
|
||||
export class ConfigModule {
|
||||
static forRoot(options: ConfigModuleOptions): ModuleWithProviders<ConfigModule> {
|
||||
const configLoaderProvider = {
|
||||
provide: CORE_CONFIG_LOADER,
|
||||
useClass: options.useConfigLoader,
|
||||
};
|
||||
|
||||
return {
|
||||
ngModule: ConfigModule,
|
||||
providers: [
|
||||
Config,
|
||||
configLoaderProvider,
|
||||
options.jsonConfigLoaderUrl ? { provide: CORE_JSON_CONFIG_LOADER_URL, useValue: options.jsonConfigLoaderUrl } : null,
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
45
apps/isa-app/src/core/config/config.spec.ts
Normal file
45
apps/isa-app/src/core/config/config.spec.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator';
|
||||
import { Config } from './config';
|
||||
import { ConfigLoader } from './config-loaders';
|
||||
import { CORE_CONFIG_LOADER } from './tokens';
|
||||
|
||||
class TestConfigLoader implements ConfigLoader {
|
||||
load() {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
}
|
||||
|
||||
// Unit test Config
|
||||
describe('Config', () => {
|
||||
let spectator: SpectatorService<Config>;
|
||||
const createService = createServiceFactory({
|
||||
service: Config,
|
||||
providers: [{ provide: CORE_CONFIG_LOADER, useClass: TestConfigLoader }],
|
||||
});
|
||||
let configLoader: ConfigLoader;
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createService();
|
||||
configLoader = spectator.inject(CORE_CONFIG_LOADER);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('init()', () => {
|
||||
it('should load config and assigns it to _config', async () => {
|
||||
const config = { unit: 'test' };
|
||||
spyOn(configLoader, 'load').and.returnValue(Promise.resolve(config));
|
||||
await spectator.service.init();
|
||||
expect(spectator.service['_config']).toEqual(config);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get()', () => {
|
||||
it('should return config value', () => {
|
||||
spectator.service['_config'] = { test: 'test' };
|
||||
expect(spectator.service.get('test')).toEqual('test');
|
||||
});
|
||||
});
|
||||
});
|
||||
27
apps/isa-app/src/core/config/config.ts
Normal file
27
apps/isa-app/src/core/config/config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import { ConfigLoader } from './config-loaders';
|
||||
import { CORE_CONFIG_LOADER } from './tokens';
|
||||
import { pick } from './utils';
|
||||
|
||||
@Injectable()
|
||||
export class Config {
|
||||
private _config: any;
|
||||
|
||||
private readonly _initilized = new ReplaySubject<void>(1);
|
||||
get initialized() {
|
||||
return this._initilized.asObservable();
|
||||
}
|
||||
|
||||
constructor(@Inject(CORE_CONFIG_LOADER) private readonly _configLoader: ConfigLoader) {}
|
||||
|
||||
// load config and assign it to this._config
|
||||
async init() {
|
||||
this._config = await this._configLoader.load();
|
||||
this._initilized.next();
|
||||
}
|
||||
|
||||
get(path: string) {
|
||||
return pick(path, this._config);
|
||||
}
|
||||
}
|
||||
6
apps/isa-app/src/core/config/index.ts
Normal file
6
apps/isa-app/src/core/config/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './config-loaders';
|
||||
export * from './config-module-options';
|
||||
export * from './config.module';
|
||||
export * from './config';
|
||||
export * from './tokens';
|
||||
export * from './utils';
|
||||
6
apps/isa-app/src/core/config/tokens.ts
Normal file
6
apps/isa-app/src/core/config/tokens.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { ConfigLoader } from './config-loaders';
|
||||
|
||||
export const CORE_CONFIG_LOADER = new InjectionToken<ConfigLoader>('core.config.loader');
|
||||
|
||||
export const CORE_JSON_CONFIG_LOADER_URL = new InjectionToken<ConfigLoader>('core.json.config.loader.url');
|
||||
3
apps/isa-app/src/core/config/utils/index.ts
Normal file
3
apps/isa-app/src/core/config/utils/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// start:ng42.barrel
|
||||
export * from './pick';
|
||||
// end:ng42.barrel
|
||||
41
apps/isa-app/src/core/config/utils/pick.spec.ts
Normal file
41
apps/isa-app/src/core/config/utils/pick.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { pick } from './pick';
|
||||
|
||||
describe('pick', () => {
|
||||
it('should pick properties from the 1st level from the object', () => {
|
||||
const obj = {
|
||||
foo: 'bar',
|
||||
};
|
||||
expect(pick('foo', obj)).toEqual('bar');
|
||||
});
|
||||
|
||||
it('should pick properties from the 2nd level from the object', () => {
|
||||
const obj = {
|
||||
foo: {
|
||||
bar: 'baz',
|
||||
},
|
||||
};
|
||||
expect(pick('foo.bar', obj)).toEqual('baz');
|
||||
});
|
||||
|
||||
it('should pick properties from the 3rd level from the object', () => {
|
||||
const obj = {
|
||||
foo: {
|
||||
bar: {
|
||||
baz: 'qux',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(pick('foo.bar.baz', obj)).toEqual('qux');
|
||||
});
|
||||
|
||||
it('should throw an error of obj is not an object', () => {
|
||||
expect(() => pick('foo', 'bar')).toThrowError(`bar is not an object`);
|
||||
});
|
||||
|
||||
it('should return undefined if the property is not found', () => {
|
||||
const obj = {
|
||||
foo: 'bar',
|
||||
};
|
||||
expect(pick('bar', obj)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
33
apps/isa-app/src/core/config/utils/pick.ts
Normal file
33
apps/isa-app/src/core/config/utils/pick.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Pick a value from an object at a given path.
|
||||
* @param path path of the value to pick
|
||||
* @param obj object to pick from
|
||||
* @returns the value at the path or undefined
|
||||
* @throws if obj is not an object
|
||||
*/
|
||||
export function pick<T = any>(path: string, obj: Object): T {
|
||||
const paths = path.split('.');
|
||||
|
||||
// check if obj is null or undefined
|
||||
if (obj == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// check if obj is of type object and not an array
|
||||
// and throw an error if not
|
||||
if (typeof obj !== 'object' || Array.isArray(obj)) {
|
||||
throw new Error(`${obj} is not an object`);
|
||||
}
|
||||
|
||||
let result = obj;
|
||||
|
||||
// loop through the path and pick the value
|
||||
// early exit if the path is empty
|
||||
for (const path of paths) {
|
||||
result = result[path];
|
||||
if (result == null) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return result as T;
|
||||
}
|
||||
8
apps/isa-app/src/core/environment/environment.module.ts
Normal file
8
apps/isa-app/src/core/environment/environment.module.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [],
|
||||
exports: [],
|
||||
})
|
||||
export class EnvironmentModule {}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EnvironmentService } from './environment.service';
|
||||
|
||||
describe('EnvironmentService', () => {
|
||||
let service: EnvironmentService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(EnvironmentService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
86
apps/isa-app/src/core/environment/environment.service.ts
Normal file
86
apps/isa-app/src/core/environment/environment.service.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { NativeContainerService } from '@external/native-container';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
const MATCH_TABLET = '(max-width: 1023px)';
|
||||
|
||||
const MATCH_DESKTOP_SMALL = '(min-width: 1024px) and (max-width: 1279px)';
|
||||
|
||||
const MATCH_DESKTOP = '(min-width: 1280px)';
|
||||
|
||||
const MATCH_DESKTOP_LARGE = '(min-width: 1440px)';
|
||||
|
||||
const MATCH_DESKTOP_XLARGE = '(min-width: 1920px)';
|
||||
|
||||
const MATCH_DESKTOP_XXLARGE = '(min-width: 2736px)';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EnvironmentService {
|
||||
constructor(
|
||||
private _platform: Platform,
|
||||
private _nativeContainer: NativeContainerService,
|
||||
private _breakpointObserver: BreakpointObserver,
|
||||
) {}
|
||||
|
||||
matchTablet(): boolean {
|
||||
return this._breakpointObserver.isMatched(MATCH_TABLET);
|
||||
}
|
||||
|
||||
matchTablet$ = this._breakpointObserver.observe(MATCH_TABLET).pipe(map((result) => result.matches));
|
||||
|
||||
matchDesktopSmall(): boolean {
|
||||
return this._breakpointObserver.isMatched(MATCH_DESKTOP_SMALL);
|
||||
}
|
||||
|
||||
matchDesktopSmall$ = this._breakpointObserver.observe(MATCH_DESKTOP_SMALL).pipe(map((result) => result.matches));
|
||||
|
||||
matchDesktop(): boolean {
|
||||
return this._breakpointObserver.isMatched(MATCH_DESKTOP);
|
||||
}
|
||||
|
||||
matchDesktop$ = this._breakpointObserver.observe(MATCH_DESKTOP).pipe(map((result) => result.matches));
|
||||
|
||||
matchDesktopLarge(): boolean {
|
||||
return this._breakpointObserver.isMatched(MATCH_DESKTOP_LARGE);
|
||||
}
|
||||
|
||||
matchDesktopLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_LARGE).pipe(map((result) => result.matches));
|
||||
|
||||
matchDesktopXLarge(): boolean {
|
||||
return this._breakpointObserver.isMatched(MATCH_DESKTOP_XLARGE);
|
||||
}
|
||||
|
||||
matchDesktopXLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_XLARGE).pipe(map((result) => result.matches));
|
||||
|
||||
matchDesktopXXLarge(): boolean {
|
||||
return this._breakpointObserver.isMatched(MATCH_DESKTOP_XXLARGE);
|
||||
}
|
||||
|
||||
matchDesktopXXLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_XXLARGE).pipe(map((result) => result.matches));
|
||||
|
||||
/**
|
||||
* @deprecated Use `matchDesktopSmall` or 'matchDesktop' instead.
|
||||
*/
|
||||
isDesktop(): boolean {
|
||||
return !this.isTablet();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `matchTablet` instead.
|
||||
*/
|
||||
isTablet(): boolean {
|
||||
return this.isNative() || this.isSafari();
|
||||
}
|
||||
|
||||
isNative(): boolean {
|
||||
return this._nativeContainer.isNative;
|
||||
}
|
||||
|
||||
isSafari(): boolean {
|
||||
return this._platform.IOS && this._platform.SAFARI;
|
||||
}
|
||||
}
|
||||
2
apps/isa-app/src/core/environment/index.ts
Normal file
2
apps/isa-app/src/core/environment/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './environment.module';
|
||||
export * from './environment.service';
|
||||
51
apps/isa-app/src/core/logger/console-log.provider.spec.ts
Normal file
51
apps/isa-app/src/core/logger/console-log.provider.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator';
|
||||
import { ConsoleLogProvider } from './console-log.provider';
|
||||
import { LogLevel } from './log-level';
|
||||
|
||||
describe('ConsoleLogProvider', () => {
|
||||
let spectator: SpectatorService<ConsoleLogProvider>;
|
||||
|
||||
const createService = createServiceFactory({
|
||||
service: ConsoleLogProvider,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createService();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('log', () => {
|
||||
it('should call console.debug', () => {
|
||||
spyOn(console, 'debug');
|
||||
spectator.service.log(LogLevel.DEBUG, 'test');
|
||||
expect(console.debug).toHaveBeenCalledWith('test');
|
||||
});
|
||||
|
||||
it('should call console.info', () => {
|
||||
spyOn(console, 'info');
|
||||
spectator.service.log(LogLevel.INFO, 'test');
|
||||
expect(console.info).toHaveBeenCalledWith('test');
|
||||
});
|
||||
|
||||
it('should call console.warn', () => {
|
||||
spyOn(console, 'warn');
|
||||
spectator.service.log(LogLevel.WARN, 'test');
|
||||
expect(console.warn).toHaveBeenCalledWith('test');
|
||||
});
|
||||
|
||||
it('should call console.error', () => {
|
||||
spyOn(console, 'error');
|
||||
spectator.service.log(LogLevel.ERROR, 'test');
|
||||
expect(console.error).toHaveBeenCalledWith('test');
|
||||
});
|
||||
|
||||
it('should not call console.log', () => {
|
||||
spyOn(console, 'log');
|
||||
spectator.service.log(LogLevel.OFF, 'test');
|
||||
expect(console.log).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
25
apps/isa-app/src/core/logger/console-log.provider.ts
Normal file
25
apps/isa-app/src/core/logger/console-log.provider.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { LogLevel } from './log-level';
|
||||
import { LogProvider } from './log.provider';
|
||||
|
||||
@Injectable()
|
||||
export class ConsoleLogProvider implements LogProvider {
|
||||
log(logLevel: LogLevel, message: string, ...optionalParams: any[]): void {
|
||||
switch (logLevel) {
|
||||
case LogLevel.DEBUG:
|
||||
console.debug(message, ...optionalParams);
|
||||
break;
|
||||
case LogLevel.INFO:
|
||||
console.info(message, ...optionalParams);
|
||||
break;
|
||||
case LogLevel.WARN:
|
||||
console.warn(message, ...optionalParams);
|
||||
break;
|
||||
case LogLevel.ERROR:
|
||||
console.error(message, ...optionalParams);
|
||||
break;
|
||||
case LogLevel.OFF:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
6
apps/isa-app/src/core/logger/index.ts
Normal file
6
apps/isa-app/src/core/logger/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './console-log.provider';
|
||||
export * from './log-level';
|
||||
export * from './log.provider';
|
||||
export * from './logger.module';
|
||||
export * from './logger.service';
|
||||
export * from './tokens';
|
||||
14
apps/isa-app/src/core/logger/log-level.ts
Normal file
14
apps/isa-app/src/core/logger/log-level.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export enum LogLevel {
|
||||
DEBUG = 0,
|
||||
INFO = 1,
|
||||
WARN = 2,
|
||||
ERROR = 3,
|
||||
OFF = 4,
|
||||
|
||||
// aliases
|
||||
debug = 0,
|
||||
info = 1,
|
||||
warn = 2,
|
||||
error = 3,
|
||||
off = 4,
|
||||
}
|
||||
6
apps/isa-app/src/core/logger/log.provider.ts
Normal file
6
apps/isa-app/src/core/logger/log.provider.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { LogLevel } from './log-level';
|
||||
|
||||
export interface LogProvider {
|
||||
log(logLevel: LogLevel, message: string, ...optionalParams: any[]): void;
|
||||
}
|
||||
31
apps/isa-app/src/core/logger/logger.module.ts
Normal file
31
apps/isa-app/src/core/logger/logger.module.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { Config } from '@core/config';
|
||||
import { ConsoleLogProvider } from './console-log.provider';
|
||||
import { Logger } from './logger.service';
|
||||
import { LOG_PROVIDER, LOG_LEVEL } from './tokens';
|
||||
|
||||
export function _logLevelProviderFactory(config: Config) {
|
||||
return config.get('@core/logger.logLevel');
|
||||
}
|
||||
|
||||
@NgModule({})
|
||||
export class CoreLoggerModule {
|
||||
static forRoot(): ModuleWithProviders<CoreLoggerModule> {
|
||||
return {
|
||||
ngModule: CoreLoggerModule,
|
||||
providers: [
|
||||
Logger,
|
||||
{
|
||||
provide: LOG_PROVIDER,
|
||||
useClass: ConsoleLogProvider,
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: LOG_LEVEL,
|
||||
useFactory: _logLevelProviderFactory,
|
||||
deps: [Config],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
140
apps/isa-app/src/core/logger/logger.service.spec.ts
Normal file
140
apps/isa-app/src/core/logger/logger.service.spec.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { SpectatorService, createServiceFactory } from '@ngneat/spectator';
|
||||
import { LogLevel } from './log-level';
|
||||
import { Logger } from './logger.service';
|
||||
import { LOG_PROVIDER } from './tokens';
|
||||
import { LOG_LEVEL } from '.';
|
||||
|
||||
describe('LoggerService', () => {
|
||||
let spectator: SpectatorService<Logger>;
|
||||
let logProviderMock1 = jasmine.createSpyObj('LogProvider', ['log']);
|
||||
let logProviderMock2 = jasmine.createSpyObj('LogProvider', ['log']);
|
||||
|
||||
const createService = createServiceFactory({
|
||||
service: Logger,
|
||||
mocks: [Logger],
|
||||
providers: [
|
||||
{
|
||||
provide: LOG_PROVIDER,
|
||||
useValue: logProviderMock1,
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: LOG_PROVIDER,
|
||||
useValue: logProviderMock2,
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: LOG_LEVEL,
|
||||
useValue: LogLevel.DEBUG,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createService();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.service).toBeTruthy();
|
||||
});
|
||||
|
||||
const testData: {
|
||||
serviceLogLevel: LogLevel;
|
||||
callLogLevel: LogLevel;
|
||||
shoudCall: boolean;
|
||||
}[] = [
|
||||
{ serviceLogLevel: LogLevel.DEBUG, callLogLevel: LogLevel.DEBUG, shoudCall: true },
|
||||
{ serviceLogLevel: LogLevel.DEBUG, callLogLevel: LogLevel.INFO, shoudCall: true },
|
||||
{ serviceLogLevel: LogLevel.DEBUG, callLogLevel: LogLevel.WARN, shoudCall: true },
|
||||
{ serviceLogLevel: LogLevel.DEBUG, callLogLevel: LogLevel.ERROR, shoudCall: true },
|
||||
|
||||
{ serviceLogLevel: LogLevel.INFO, callLogLevel: LogLevel.DEBUG, shoudCall: false },
|
||||
{ serviceLogLevel: LogLevel.INFO, callLogLevel: LogLevel.INFO, shoudCall: true },
|
||||
{ serviceLogLevel: LogLevel.INFO, callLogLevel: LogLevel.WARN, shoudCall: true },
|
||||
{ serviceLogLevel: LogLevel.INFO, callLogLevel: LogLevel.ERROR, shoudCall: true },
|
||||
|
||||
{ serviceLogLevel: LogLevel.WARN, callLogLevel: LogLevel.DEBUG, shoudCall: false },
|
||||
{ serviceLogLevel: LogLevel.WARN, callLogLevel: LogLevel.INFO, shoudCall: false },
|
||||
{ serviceLogLevel: LogLevel.WARN, callLogLevel: LogLevel.WARN, shoudCall: true },
|
||||
{ serviceLogLevel: LogLevel.WARN, callLogLevel: LogLevel.ERROR, shoudCall: true },
|
||||
|
||||
{ serviceLogLevel: LogLevel.ERROR, callLogLevel: LogLevel.DEBUG, shoudCall: false },
|
||||
{ serviceLogLevel: LogLevel.ERROR, callLogLevel: LogLevel.INFO, shoudCall: false },
|
||||
{ serviceLogLevel: LogLevel.ERROR, callLogLevel: LogLevel.WARN, shoudCall: false },
|
||||
{ serviceLogLevel: LogLevel.ERROR, callLogLevel: LogLevel.ERROR, shoudCall: true },
|
||||
|
||||
{ serviceLogLevel: LogLevel.OFF, callLogLevel: LogLevel.DEBUG, shoudCall: false },
|
||||
{ serviceLogLevel: LogLevel.OFF, callLogLevel: LogLevel.INFO, shoudCall: false },
|
||||
{ serviceLogLevel: LogLevel.OFF, callLogLevel: LogLevel.WARN, shoudCall: false },
|
||||
{ serviceLogLevel: LogLevel.OFF, callLogLevel: LogLevel.ERROR, shoudCall: false },
|
||||
];
|
||||
|
||||
describe('log()', () => {
|
||||
beforeEach(() => {
|
||||
logProviderMock1.log.calls.reset();
|
||||
logProviderMock2.log.calls.reset();
|
||||
});
|
||||
|
||||
for (const test of testData) {
|
||||
if (test.shoudCall) {
|
||||
it(`should call logProvider if logLevel is ${LogLevel[test.callLogLevel]} and serviceLogLevel is ${
|
||||
LogLevel[test.serviceLogLevel]
|
||||
}`, () => {
|
||||
spectator.service['_logLevel'] = test.serviceLogLevel;
|
||||
spectator.service.log(test.callLogLevel, 'test');
|
||||
expect(logProviderMock1.log).toHaveBeenCalledTimes(1);
|
||||
expect(logProviderMock2.log).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
} else {
|
||||
it(`should not call logProvider if logLevel is ${LogLevel[test.callLogLevel]} and serviceLogLevel is ${
|
||||
LogLevel[test.serviceLogLevel]
|
||||
}`, () => {
|
||||
spectator.service['_logLevel'] = test.serviceLogLevel;
|
||||
spectator.service.log(test.callLogLevel, 'test');
|
||||
expect(logProviderMock1.log).not.toHaveBeenCalled();
|
||||
expect(logProviderMock2.log).not.toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe('warn()', () => {
|
||||
it('should call log and logLevel should be WARN', () => {
|
||||
spyOn(spectator.service, 'log');
|
||||
|
||||
spectator.service.warn('test', 'data');
|
||||
|
||||
expect(spectator.service.log).toHaveBeenCalledWith(LogLevel.WARN, 'test', 'data');
|
||||
});
|
||||
});
|
||||
|
||||
describe('info()', () => {
|
||||
it('should call log and logLevel should be INFO', () => {
|
||||
spyOn(spectator.service, 'log');
|
||||
|
||||
spectator.service.info('test', 'data');
|
||||
|
||||
expect(spectator.service.log).toHaveBeenCalledWith(LogLevel.INFO, 'test', 'data');
|
||||
});
|
||||
});
|
||||
|
||||
describe('debug()', () => {
|
||||
it('should call log and logLevel should be DEBUG', () => {
|
||||
spyOn(spectator.service, 'log');
|
||||
|
||||
spectator.service.debug('test', 'data');
|
||||
|
||||
expect(spectator.service.log).toHaveBeenCalledWith(LogLevel.DEBUG, 'test', 'data');
|
||||
});
|
||||
});
|
||||
|
||||
describe('error()', () => {
|
||||
it('should call log and logLevel should be ERROR', () => {
|
||||
spyOn(spectator.service, 'log');
|
||||
|
||||
spectator.service.error('test', 'data');
|
||||
|
||||
expect(spectator.service.log).toHaveBeenCalledWith(LogLevel.ERROR, 'test', 'data');
|
||||
});
|
||||
});
|
||||
});
|
||||
42
apps/isa-app/src/core/logger/logger.service.ts
Normal file
42
apps/isa-app/src/core/logger/logger.service.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
|
||||
import { LogLevel } from './log-level';
|
||||
import { LogProvider } from './log.provider';
|
||||
import { LOG_LEVEL, LOG_PROVIDER } from './tokens';
|
||||
|
||||
@Injectable()
|
||||
export class Logger {
|
||||
constructor(
|
||||
@Inject(LOG_PROVIDER) private readonly _loggerProviders: LogProvider[],
|
||||
@Inject(LOG_LEVEL) private _logLevel: LogLevel,
|
||||
) {}
|
||||
|
||||
log(logLevel: LogLevel, message: string, ...optionalParams: any[]): void {
|
||||
if (this._logLevel === LogLevel.OFF) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._logLevel <= logLevel) {
|
||||
console.log(this._logLevel, logLevel, this._logLevel <= logLevel);
|
||||
this._loggerProviders.forEach((provider) => {
|
||||
provider.log(logLevel, message, ...optionalParams);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
warn(message: string, ...optionalParams: any[]): void {
|
||||
this.log(LogLevel.WARN, message, ...optionalParams);
|
||||
}
|
||||
|
||||
info(message: string, ...optionalParams: any[]): void {
|
||||
this.log(LogLevel.INFO, message, ...optionalParams);
|
||||
}
|
||||
|
||||
debug(message: string, ...optionalParams: any[]): void {
|
||||
this.log(LogLevel.DEBUG, message, ...optionalParams);
|
||||
}
|
||||
|
||||
error(message: string, ...optionalParams: any[]): void {
|
||||
this.log(LogLevel.ERROR, message, ...optionalParams);
|
||||
}
|
||||
}
|
||||
6
apps/isa-app/src/core/logger/tokens.ts
Normal file
6
apps/isa-app/src/core/logger/tokens.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { LogProvider } from './log.provider';
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { LogLevel } from './log-level';
|
||||
|
||||
export const LOG_PROVIDER = new InjectionToken<LogProvider[]>('LOG_PROVIDER');
|
||||
export const LOG_LEVEL = new InjectionToken<LogLevel>('LOG_LEVEL');
|
||||
2
apps/isa-app/src/core/signalr/index.ts
Normal file
2
apps/isa-app/src/core/signalr/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './signalr-hub-options';
|
||||
export * from './signalr.hub';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user