mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
69
angular.json
69
angular.json
@@ -27,7 +27,10 @@
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": ["libs/ui/tsconfig.lib.json", "libs/ui/tsconfig.spec.json"],
|
||||
"tsConfig": [
|
||||
"libs/ui/tsconfig.lib.json",
|
||||
"libs/ui/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
}
|
||||
@@ -141,14 +144,24 @@
|
||||
"tsConfig": "apps/sales/tsconfig.spec.json",
|
||||
"karmaConfig": "apps/sales/karma.conf.js",
|
||||
"styles": ["apps/sales/src/styles.scss"],
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": ["apps/sales/src/scss"]
|
||||
},
|
||||
"scripts": [],
|
||||
"assets": ["apps/sales/src/favicon.ico", "apps/sales/src/assets", "apps/sales/src/manifest.webmanifest"]
|
||||
"assets": [
|
||||
"apps/sales/src/favicon.ico",
|
||||
"apps/sales/src/assets",
|
||||
"apps/sales/src/manifest.webmanifest"
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": ["apps/sales/tsconfig.app.json", "apps/sales/tsconfig.spec.json"],
|
||||
"tsConfig": [
|
||||
"apps/sales/tsconfig.app.json",
|
||||
"apps/sales/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
}
|
||||
@@ -207,7 +220,10 @@
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": ["libs/sso/tsconfig.lib.json", "libs/sso/tsconfig.spec.json"],
|
||||
"tsConfig": [
|
||||
"libs/sso/tsconfig.lib.json",
|
||||
"libs/sso/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
}
|
||||
@@ -237,7 +253,10 @@
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": ["apps/swagger/availability/tsconfig.lib.json", "apps/swagger/availability/tsconfig.spec.json"],
|
||||
"tsConfig": [
|
||||
"apps/swagger/availability/tsconfig.lib.json",
|
||||
"apps/swagger/availability/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
}
|
||||
@@ -267,7 +286,10 @@
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": ["apps/swagger/checkout/tsconfig.lib.json", "apps/swagger/checkout/tsconfig.spec.json"],
|
||||
"tsConfig": [
|
||||
"apps/swagger/checkout/tsconfig.lib.json",
|
||||
"apps/swagger/checkout/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
}
|
||||
@@ -297,7 +319,10 @@
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": ["apps/swagger/crm/tsconfig.lib.json", "apps/swagger/crm/tsconfig.spec.json"],
|
||||
"tsConfig": [
|
||||
"apps/swagger/crm/tsconfig.lib.json",
|
||||
"apps/swagger/crm/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
}
|
||||
@@ -327,7 +352,10 @@
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": ["apps/swagger/isa/tsconfig.lib.json", "apps/swagger/isa/tsconfig.spec.json"],
|
||||
"tsConfig": [
|
||||
"apps/swagger/isa/tsconfig.lib.json",
|
||||
"apps/swagger/isa/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
}
|
||||
@@ -357,7 +385,10 @@
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": ["apps/swagger/oms/tsconfig.lib.json", "apps/swagger/oms/tsconfig.spec.json"],
|
||||
"tsConfig": [
|
||||
"apps/swagger/oms/tsconfig.lib.json",
|
||||
"apps/swagger/oms/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
}
|
||||
@@ -387,7 +418,10 @@
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": ["apps/swagger/print/tsconfig.lib.json", "apps/swagger/print/tsconfig.spec.json"],
|
||||
"tsConfig": [
|
||||
"apps/swagger/print/tsconfig.lib.json",
|
||||
"apps/swagger/print/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
}
|
||||
@@ -417,7 +451,10 @@
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": ["apps/swagger/cat/tsconfig.lib.json", "apps/swagger/cat/tsconfig.spec.json"],
|
||||
"tsConfig": [
|
||||
"apps/swagger/cat/tsconfig.lib.json",
|
||||
"apps/swagger/cat/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
}
|
||||
@@ -447,7 +484,10 @@
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": ["apps/swagger/eis/tsconfig.lib.json", "apps/swagger/eis/tsconfig.spec.json"],
|
||||
"tsConfig": [
|
||||
"apps/swagger/eis/tsconfig.lib.json",
|
||||
"apps/swagger/eis/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
}
|
||||
@@ -477,7 +517,10 @@
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": ["apps/native-container/tsconfig.lib.json", "apps/native-container/tsconfig.spec.json"],
|
||||
"tsConfig": [
|
||||
"apps/native-container/tsconfig.lib.json",
|
||||
"apps/native-container/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,12 @@ import { RootState } from './store/root.state';
|
||||
import { rootReducer } from './store/root.reducer';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { SearchEffects } from './store/customer';
|
||||
import { HistoryEffects } from '@shelf-store/history';
|
||||
|
||||
// TODO: In Service Speichern
|
||||
export function storeInLocalStorage(reducer: ActionReducer<any>): ActionReducer<any> {
|
||||
export function storeInLocalStorage(
|
||||
reducer: ActionReducer<any>
|
||||
): ActionReducer<any> {
|
||||
const lsKey = 'ISA_NGRX_STATE';
|
||||
|
||||
return function (state, action) {
|
||||
@@ -25,12 +28,14 @@ export function storeInLocalStorage(reducer: ActionReducer<any>): ActionReducer<
|
||||
};
|
||||
}
|
||||
|
||||
export const metaReducers: MetaReducer<RootState>[] = !environment.production ? [storeFreeze, storeInLocalStorage] : [storeInLocalStorage];
|
||||
export const metaReducers: MetaReducer<RootState>[] = !environment.production
|
||||
? [storeFreeze, storeInLocalStorage]
|
||||
: [storeInLocalStorage];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
StoreModule.forRoot(rootReducer, { metaReducers }),
|
||||
EffectsModule.forRoot([SearchEffects]),
|
||||
EffectsModule.forRoot([SearchEffects, HistoryEffects]),
|
||||
StoreDevtoolsModule.instrument({ name: 'ISA Ngrx Store' }),
|
||||
],
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Observable } from 'rxjs';
|
||||
import { RemissionSelectors } from '../../core/store/selectors/remission.selectors';
|
||||
import { Select } from '@ngxs/store';
|
||||
import { ContentHeaderService } from '../../core/services/content-header.service';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-content-header',
|
||||
@@ -22,9 +23,9 @@ export class ContentHeaderComponent implements OnInit {
|
||||
constructor(private contentHeaderService: ContentHeaderService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.isFilterActive$ = this.contentHeaderService.isFilterActive$;
|
||||
this.showFilter$ = this.contentHeaderService.showFilter$;
|
||||
this.showBreadCrumbs$ = this.contentHeaderService.showBreadcrumbs$;
|
||||
this.isFilterActive$ = this.contentHeaderService.isFilterActive$;
|
||||
this.activeModule$ = this.contentHeaderService.module$;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
VATDTO,
|
||||
SupplierDTO,
|
||||
QueryTokenDTO,
|
||||
AutocompleteTokenDTO,
|
||||
} from '@swagger/oms';
|
||||
import { map, filter } from 'rxjs/operators';
|
||||
import { Observable } from 'rxjs';
|
||||
@@ -100,11 +101,13 @@ export class CollectingShelfService {
|
||||
skip = 0,
|
||||
take = 10,
|
||||
hitsOnly = false,
|
||||
selectedFilters = {},
|
||||
}): Observable<ListResponseArgsOfOrderItemListItemDTO> {
|
||||
const params = <QueryTokenDTO>{
|
||||
input: {
|
||||
qs: input,
|
||||
},
|
||||
filter: selectedFilters,
|
||||
skip,
|
||||
take,
|
||||
hitsOnly,
|
||||
@@ -120,6 +123,10 @@ export class CollectingShelfService {
|
||||
);
|
||||
}
|
||||
|
||||
searchWarenausgabeAutocomplete(autocompleteTokenDTO: AutocompleteTokenDTO) {
|
||||
return this.omsService.OrderWarenausgabeAutocomplete(autocompleteTokenDTO);
|
||||
}
|
||||
|
||||
getOrderByOrderId(orderId: number): Observable<OrderDTO> {
|
||||
return this.omsService.OrderGetOrder(orderId).pipe(
|
||||
map((response) => {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map, switchMap, filter, startWith } from 'rxjs/operators';
|
||||
import { map, switchMap, filter, tap, shareReplay } from 'rxjs/operators';
|
||||
import { LocationService } from './location-service';
|
||||
import { RemissionOverlayService } from '../../modules/remission/services';
|
||||
import { Select } from '@ngxs/store';
|
||||
|
||||
import { isNullOrUndefined } from 'util';
|
||||
import { ShelfOverlayService } from '../../modules/shelf/services';
|
||||
import { RemissionSelectors } from '../store/selectors/remission.selectors';
|
||||
import { SharedSelectors } from '../store/selectors/shared.selectors';
|
||||
import { SearchStateFacade } from '../../store/customer';
|
||||
import { isNullOrUndefined } from 'util';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ContentHeaderService {
|
||||
@@ -40,29 +40,26 @@ export class ContentHeaderService {
|
||||
}
|
||||
|
||||
get isFilterActive$(): Observable<boolean> {
|
||||
return this.activeSection$.pipe(
|
||||
return this.locationService.url$.pipe(
|
||||
filter((url) => !isNullOrUndefined(url)),
|
||||
switchMap((activeSection) => {
|
||||
let filters$: Observable<string[]> = of([]);
|
||||
|
||||
switch (activeSection) {
|
||||
case 'remission':
|
||||
filters$ = this.remissionFilters$;
|
||||
break;
|
||||
|
||||
case 'shelf':
|
||||
filters$ = this.searchStore.selectedFilter$.pipe(
|
||||
filter((filters) => !isNullOrUndefined(filters) && !filters.length),
|
||||
// To ensure if no filter is set on process that filter is inactive
|
||||
startWith([])
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
return of(false);
|
||||
if (activeSection.includes('remission')) {
|
||||
return this.remissionFilters$.pipe(
|
||||
tap((selectedFilters) => console.log({ selectedFilters })),
|
||||
map(
|
||||
(selectedFilters) =>
|
||||
selectedFilters && !!Object.entries(selectedFilters).length
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return filters$.pipe(map((selectedFilters) => selectedFilters && !!Object.entries(selectedFilters).length));
|
||||
})
|
||||
if (activeSection.includes('shelf')) {
|
||||
return this.searchStore.hasActiveFilters$;
|
||||
}
|
||||
|
||||
return of(false);
|
||||
}),
|
||||
shareReplay()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,12 +112,18 @@ export class ContentHeaderService {
|
||||
const applicableBlacklist = this.blackList[type];
|
||||
|
||||
if (type === 'filter') {
|
||||
const isOnWhiteList = !!applicableWhitelist.find((whitelistetUrl) => whitelistetUrl.includes(url) || url.includes(whitelistetUrl));
|
||||
const isOnWhiteList = !!applicableWhitelist.find(
|
||||
(whitelistetUrl) =>
|
||||
whitelistetUrl.includes(url) || url.includes(whitelistetUrl)
|
||||
);
|
||||
return isOnWhiteList;
|
||||
}
|
||||
|
||||
if (type === 'breadcrumbs') {
|
||||
const isOnBlackList = !!applicableBlacklist.find((blacklistetUrl) => blacklistetUrl.includes(url) || url.includes(blacklistetUrl));
|
||||
const isOnBlackList = !!applicableBlacklist.find(
|
||||
(blacklistetUrl) =>
|
||||
blacklistetUrl.includes(url) || url.includes(blacklistetUrl)
|
||||
);
|
||||
return !isOnBlackList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import { Injectable, OnInit } from '@angular/core';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Router, RouterEvent, ActivatedRoute } from '@angular/router';
|
||||
import { map, distinctUntilChanged, filter, share } from 'rxjs/operators';
|
||||
import {
|
||||
Router,
|
||||
RouterEvent,
|
||||
NavigationStart,
|
||||
ActivationStart,
|
||||
ActivatedRoute,
|
||||
} from '@angular/router';
|
||||
import {
|
||||
map,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
import { isNullOrUndefined } from 'util';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -19,4 +31,12 @@ export class LocationService {
|
||||
get url() {
|
||||
return this.router.url;
|
||||
}
|
||||
|
||||
get url$() {
|
||||
return this.router.events.pipe(
|
||||
map((routerEvent: RouterEvent) => routerEvent.url),
|
||||
filter((url) => !isNullOrUndefined(url)),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
@import 'variables';
|
||||
@import 'mixins/media';
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
1
apps/sales/src/app/modules/filter/models/filter-type.ts
Normal file
1
apps/sales/src/app/modules/filter/models/filter-type.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type FilterType = 'select' | 'text' | 'date' | 'number' | 'checkbox';
|
||||
@@ -4,3 +4,4 @@ export * from './filter-base';
|
||||
export * from './select-filter';
|
||||
export * from './select-option';
|
||||
export * from './filter';
|
||||
export * from './filter-type';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RemissionListFilterComponent } from './remission-list-filter.component';
|
||||
|
||||
fdescribe('RemissionListFilterComponent', () => {
|
||||
describe('RemissionListFilterComponent', () => {
|
||||
let fixture: ComponentFixture<RemissionListFilterComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -11,4 +11,6 @@
|
||||
top: 61px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Output,
|
||||
EventEmitter,
|
||||
} from '@angular/core';
|
||||
import { OrderItemListItemDTO } from '@swagger/oms/lib';
|
||||
import { AutocompleteDTO } from '@swagger/oms/lib';
|
||||
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
|
||||
import { ResultItemComponent } from './result-item';
|
||||
|
||||
@@ -22,9 +22,9 @@ import { ResultItemComponent } from './result-item';
|
||||
export class AutocompleteResultsComponent implements AfterViewInit {
|
||||
@ViewChildren(ResultItemComponent) items: QueryList<ResultItemComponent>;
|
||||
|
||||
@Input() results: OrderItemListItemDTO[];
|
||||
@Input() results: AutocompleteDTO[];
|
||||
@Output() selectItem = new EventEmitter<
|
||||
OrderItemListItemDTO & { shouldNavigate?: boolean }
|
||||
AutocompleteDTO & { shouldNavigate?: boolean }
|
||||
>();
|
||||
|
||||
private keyManager: ActiveDescendantKeyManager<ResultItemComponent>;
|
||||
@@ -44,8 +44,7 @@ export class AutocompleteResultsComponent implements AfterViewInit {
|
||||
this.keyManager = new ActiveDescendantKeyManager(this.items).withWrap();
|
||||
}
|
||||
|
||||
onItemClicked(item: OrderItemListItemDTO) {
|
||||
console.log({ item });
|
||||
onItemClicked(item: AutocompleteDTO) {
|
||||
this.keyManager.setActiveItem(this.getItemIndex(item));
|
||||
this.selectItem.emit({
|
||||
...this.keyManager.activeItem.result,
|
||||
@@ -53,7 +52,7 @@ export class AutocompleteResultsComponent implements AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
private getItemIndex(item: OrderItemListItemDTO): number {
|
||||
private getItemIndex(item: AutocompleteDTO): number {
|
||||
return this.results.indexOf(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<div class="item isa-font-lightgrey" (click)="clicked.emit(result)">
|
||||
{{ result.firstName }} {{ result.lastName }}
|
||||
{{ result.display }}
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Output,
|
||||
EventEmitter,
|
||||
} from '@angular/core';
|
||||
import { OrderItemListItemDTO } from '@swagger/oms/lib';
|
||||
import { AutocompleteDTO } from '@swagger/oms/lib';
|
||||
import { Highlightable } from '@angular/cdk/a11y';
|
||||
|
||||
@Component({
|
||||
@@ -16,8 +16,8 @@ import { Highlightable } from '@angular/cdk/a11y';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ResultItemComponent implements Highlightable {
|
||||
@Input() result: OrderItemListItemDTO;
|
||||
@Output() clicked = new EventEmitter<OrderItemListItemDTO>();
|
||||
@Input() result: AutocompleteDTO;
|
||||
@Output() clicked = new EventEmitter<AutocompleteDTO>();
|
||||
|
||||
private _isActive: boolean;
|
||||
|
||||
|
||||
@@ -1,17 +1,37 @@
|
||||
<div class="isa-card">
|
||||
<h3 class="heading">{{ group.items[0].firstName }} {{ group.items[0].lastName }} </h3>
|
||||
<ng-container *ngFor="let byOrderNumber of group.items | groupBy:byOrderNumberFn; let lastOrder = last">
|
||||
<ng-container
|
||||
*ngFor="let byProcessingStatus of byOrderNumber.items | groupBy:byProcessingStatusFn; let lastStatus = last">
|
||||
<ng-container *ngFor="let groupedBy of byProcessingStatus.items | groupBy:byCompartmentCodeFn">
|
||||
<div class="isa-mb-12 isa-mt-12">
|
||||
<app-search-result-group-item *ngFor="let item of groupedBy.items; let lastItem = last"
|
||||
[item]="item" [class.group-item-bottom-space]="!lastItem">
|
||||
</app-search-result-group-item>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="divider" *ngIf="!lastStatus"></div>
|
||||
</ng-container>
|
||||
<div class="divider" *ngIf="!lastOrder"></div>
|
||||
<h3 class="heading">
|
||||
{{ group.items[0].firstName }} {{ group.items[0].lastName }}
|
||||
</h3>
|
||||
<ng-container
|
||||
*ngFor="
|
||||
let byOrderNumber of group.items | groupBy: byOrderNumberFn;
|
||||
let lastOrder = last
|
||||
"
|
||||
>
|
||||
<ng-container
|
||||
*ngFor="
|
||||
let byProcessingStatus of byOrderNumber.items
|
||||
| groupBy: byProcessingStatusFn;
|
||||
let lastStatus = last
|
||||
"
|
||||
>
|
||||
<ng-container
|
||||
*ngFor="
|
||||
let groupedBy of byProcessingStatus.items
|
||||
| groupBy: byCompartmentCodeFn
|
||||
"
|
||||
>
|
||||
<div class="isa-mb-12 isa-mt-12">
|
||||
<app-search-result-group-item
|
||||
*ngFor="let item of groupedBy.items; let lastItem = last"
|
||||
[item]="item"
|
||||
[class.group-item-bottom-space]="!lastItem"
|
||||
>
|
||||
</app-search-result-group-item>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="divider" *ngIf="!lastStatus"></div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="divider" *ngIf="!lastOrder"></div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
} from '@angular/core';
|
||||
import { OrderItemListItemDTO } from '@swagger/oms';
|
||||
import { Group } from 'apps/sales/src/app/utils';
|
||||
import { ShelfNavigationService } from '../../shared/services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search-result-group',
|
||||
@@ -18,7 +24,11 @@ export class SearchResultGroupComponent implements OnInit {
|
||||
|
||||
byCompartmentCodeFn = (item: OrderItemListItemDTO) => item.compartmentCode;
|
||||
|
||||
constructor() {}
|
||||
constructor(private navigationFacade: ShelfNavigationService) {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
navigateToDetails(orderItem: OrderItemListItemDTO) {
|
||||
this.navigationFacade.navigateToDetails(orderItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,12 @@ export class ShelfSearchbarComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
setInputValue(queryString: string) {
|
||||
if (this.searchForm) {
|
||||
this.searchForm.setValue(queryString);
|
||||
}
|
||||
}
|
||||
|
||||
private validateSearchInputBeforeSubmit(searchInput: string): boolean {
|
||||
return !isNullOrUndefined(searchInput) && searchInput.length >= 1;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface ShelfPrimaryFilterOptions {
|
||||
allBranches: boolean;
|
||||
customerName: boolean;
|
||||
author: boolean;
|
||||
title: boolean;
|
||||
export interface PrimaryFilterOption {
|
||||
name: string;
|
||||
id: string;
|
||||
key: string;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
@@ -1,34 +1,11 @@
|
||||
<div class="container mb-40" *ngIf="primaryFilters$ | async as primaryFilters">
|
||||
<button
|
||||
*ngFor="let filter of primaryFilters"
|
||||
class="isa-chip"
|
||||
id="allBranches"
|
||||
[id]="filter.id || filter.key"
|
||||
(click)="handleClick($event.target)"
|
||||
[class.selected]="primaryFilters['allBranches']"
|
||||
[class.selected]="filter.selected"
|
||||
>
|
||||
Alle Filialen
|
||||
</button>
|
||||
<button
|
||||
class="isa-chip"
|
||||
id="customerName"
|
||||
(click)="handleClick($event.target)"
|
||||
[class.selected]="primaryFilters['customerName']"
|
||||
>
|
||||
Kundenname
|
||||
</button>
|
||||
<button
|
||||
class="isa-chip"
|
||||
id="author"
|
||||
(click)="handleClick($event.target)"
|
||||
[class.selected]="primaryFilters['author']"
|
||||
>
|
||||
Autor
|
||||
</button>
|
||||
<button
|
||||
class="isa-chip"
|
||||
id="title"
|
||||
(click)="handleClick($event.target)"
|
||||
[class.selected]="primaryFilters['title']"
|
||||
>
|
||||
Titel
|
||||
{{ filter.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Output, EventEmitter } from '@angular/core';
|
||||
import { ShelfPrimaryFilterOptions } from '../../../defs';
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
Output,
|
||||
EventEmitter,
|
||||
} from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { SearchStateFacade } from 'apps/sales/src/app/store/customer';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { PrimaryFilterOption } from '../../../defs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shelf-primary-filters',
|
||||
@@ -11,7 +17,7 @@ import { take } from 'rxjs/operators';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ShelfPrimaryFiltersComponent implements OnInit {
|
||||
primaryFilters$: Observable<ShelfPrimaryFilterOptions>;
|
||||
primaryFilters$: Observable<PrimaryFilterOption[]>;
|
||||
|
||||
@Output() change = new EventEmitter<void>();
|
||||
|
||||
@@ -32,14 +38,23 @@ export class ShelfPrimaryFiltersComponent implements OnInit {
|
||||
|
||||
private handleUpdate(params: { identifier: string }) {
|
||||
this.primaryFilters$.pipe(take(1)).subscribe((currentFilters) => {
|
||||
const updatedValue = !currentFilters[params.identifier];
|
||||
const filterToUpdate = currentFilters.find(
|
||||
(filter) => filter.id === params.identifier
|
||||
);
|
||||
|
||||
this.updateSelectedFilters({ [params.identifier]: updatedValue });
|
||||
if (!filterToUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateSelectedFilters({
|
||||
...filterToUpdate,
|
||||
selected: !filterToUpdate.selected,
|
||||
});
|
||||
this.change.emit();
|
||||
});
|
||||
}
|
||||
|
||||
private updateSelectedFilters(changes: Partial<ShelfPrimaryFilterOptions>) {
|
||||
private updateSelectedFilters(changes: PrimaryFilterOption) {
|
||||
this.searchStateFacade.setPrimaryFilters(changes);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
<app-shelf-primary-filters
|
||||
(change)="setSearchFocus()"
|
||||
></app-shelf-primary-filters>
|
||||
<app-shelf-search-input></app-shelf-search-input>
|
||||
<app-shelf-search-input
|
||||
[searchCallback]="close.bind(this)"
|
||||
></app-shelf-search-input>
|
||||
|
||||
<div class="d-flex flex-column mt-40">
|
||||
<ng-container *ngIf="pendingFilters$ | async as filters">
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
app-select-filter {
|
||||
@include mq-desktop() {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
app-selected-filter-options {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@@ -17,6 +23,14 @@ app-selected-filter-options {
|
||||
.apply-filter-wrapper {
|
||||
text-align: center;
|
||||
padding-bottom: 5px;
|
||||
|
||||
@include mq-desktop() {
|
||||
position: absolute;
|
||||
bottom: 25px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-apply-filters {
|
||||
|
||||
@@ -13,9 +13,11 @@ import { slideIn } from 'apps/sales/src/app/core/overlay';
|
||||
import { Observable, BehaviorSubject, interval } from 'rxjs';
|
||||
import { Filter, SelectFilter } from '../../../filter';
|
||||
import { ShelfFilterService } from '../../services/shelf-filter.service';
|
||||
import { startWith } from 'rxjs/operators';
|
||||
import { startWith, first } from 'rxjs/operators';
|
||||
import { isNullOrUndefined } from 'util';
|
||||
import { ShelfSearchInputComponent } from '../../pages/shelf-search/search';
|
||||
import { cloneFilter } from '../../../filter/utils';
|
||||
import { SearchStateFacade } from '@shelf-store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shelf-filter',
|
||||
@@ -34,7 +36,7 @@ export class ShelfFilterComponent implements OnInit, OnDestroy {
|
||||
searchInput: ShelfSearchInputComponent;
|
||||
|
||||
checkHeightTimerSub = interval(100)
|
||||
.pipe(startWith(0))
|
||||
.pipe(startWith(true))
|
||||
.subscribe(() => this.updateHeight());
|
||||
|
||||
filtersByGroup$: Observable<Filter[]>;
|
||||
@@ -45,6 +47,7 @@ export class ShelfFilterComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private overlayRef: IsaOverlayRef,
|
||||
private filterService: ShelfFilterService,
|
||||
private searchStateFacade: SearchStateFacade,
|
||||
private renderer: Renderer2,
|
||||
private cdr: ChangeDetectorRef
|
||||
) {}
|
||||
@@ -75,7 +78,7 @@ export class ShelfFilterComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onFiltersChange(updatedFilters: SelectFilter[]) {
|
||||
this.pendingFilters$.next(updatedFilters);
|
||||
this.pendingFilters$.next(updatedFilters.map(cloneFilter));
|
||||
this.setSearchFocus();
|
||||
}
|
||||
|
||||
@@ -93,7 +96,7 @@ export class ShelfFilterComponent implements OnInit, OnDestroy {
|
||||
if (this.pendingFilters$.value) {
|
||||
this.filterService.updateFilters(this.pendingFilters$.value);
|
||||
}
|
||||
this.close();
|
||||
this.initiateSearchAndCloseOverlay();
|
||||
}
|
||||
|
||||
close() {
|
||||
@@ -142,4 +145,26 @@ export class ShelfFilterComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private initiateSearchAndCloseOverlay() {
|
||||
if (this.searchInput) {
|
||||
const searchbarValue =
|
||||
(this.searchInput &&
|
||||
this.searchInput.searchbar &&
|
||||
this.searchInput.searchbar.searchForm.value) ||
|
||||
'';
|
||||
|
||||
this.searchStateFacade.selectedFilter$
|
||||
.pipe(first())
|
||||
.subscribe((selectedFilters) =>
|
||||
this.searchInput.triggerSearch({
|
||||
type: 'search',
|
||||
value: searchbarValue,
|
||||
selectedFilters,
|
||||
bypassValidation: true,
|
||||
closeOverlay: () => this.close(),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
<div>Order Item Id: {{ orderItemId$ | async }}</div>
|
||||
<pre>{{ history$ | async | json }}</pre>
|
||||
<pre>{{ status$ | async | json }}</pre>
|
||||
|
||||
@@ -14,7 +14,6 @@ import { OrderHistoryStatus } from '@shelf-store/defs';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ShelfHistoryComponent implements OnInit {
|
||||
orderItemId$: Observable<number>;
|
||||
history$: Observable<HistoryDTO>;
|
||||
status$: Observable<OrderHistoryStatus>;
|
||||
|
||||
@@ -29,8 +28,7 @@ export class ShelfHistoryComponent implements OnInit {
|
||||
}
|
||||
|
||||
getHistory() {
|
||||
this.orderItemId$ = this.getOrderItemId();
|
||||
return this.orderItemId$.pipe(
|
||||
return this.getOrderItemId$().pipe(
|
||||
switchMap((orderItemId) =>
|
||||
this.historyStateFacade.getHistory$(orderItemId)
|
||||
)
|
||||
@@ -38,14 +36,14 @@ export class ShelfHistoryComponent implements OnInit {
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this.orderItemId$.pipe(
|
||||
return this.getOrderItemId$().pipe(
|
||||
switchMap((orderItemId) =>
|
||||
this.historyStateFacade.getStatus$(orderItemId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private getOrderItemId(): Observable<number> {
|
||||
private getOrderItemId$(): Observable<number> {
|
||||
return this.activatedRoute.params.pipe(
|
||||
filter((params) => !isNullOrUndefined(params)),
|
||||
map((params) => Number(params.orderItemId))
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<div class="result-container" #scroll>
|
||||
<app-search-result-group class="isa-mb-10" *ngFor="let group of grouped$ | async; let i = index" [group]="group">
|
||||
<app-search-result-group
|
||||
class="isa-mb-10"
|
||||
*ngFor="let group of grouped$ | async; let i = index"
|
||||
[group]="group"
|
||||
>
|
||||
</app-search-result-group>
|
||||
<app-loading *ngIf="fetching$ | async" [style.marginTop.px]="60" [style.marginBottom.px]="60" loading="true"
|
||||
text="Inhalte werden geladen"></app-loading>
|
||||
</div>
|
||||
<app-loading
|
||||
*ngIf="fetching$ | async"
|
||||
[style.marginTop.px]="60"
|
||||
[style.marginBottom.px]="60"
|
||||
loading="true"
|
||||
text="Inhalte werden geladen"
|
||||
></app-loading>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Subject, fromEvent, combineLatest } from 'rxjs';
|
||||
|
||||
import { Component, OnDestroy, OnInit, ChangeDetectorRef, ViewChild, ElementRef } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ElementRef,
|
||||
} from '@angular/core';
|
||||
import { SearchStateFacade } from 'apps/sales/src/app/store/customer';
|
||||
import { first, takeUntil, map } from 'rxjs/operators';
|
||||
import { groupBy } from 'apps/sales/src/app/utils';
|
||||
@@ -43,11 +49,20 @@ export class ShelfSearchResultsComponent implements OnInit, OnDestroy {
|
||||
|
||||
reachedBottom() {
|
||||
const scrollContainer: HTMLElement = this.scrollContainer.nativeElement;
|
||||
return scrollContainer.scrollHeight - (scrollContainer.scrollTop + scrollContainer.clientHeight) - 100 <= 0;
|
||||
return (
|
||||
scrollContainer.scrollHeight -
|
||||
(scrollContainer.scrollTop + scrollContainer.clientHeight) -
|
||||
100 <=
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
async fetch(force = false) {
|
||||
const [hits, result, fetching] = await combineLatest([this.searchStateFacade.hits$, this.searchStateFacade.result$, this.fetching$])
|
||||
const [hits, result, fetching] = await combineLatest([
|
||||
this.searchStateFacade.hits$,
|
||||
this.searchStateFacade.result$,
|
||||
this.fetching$,
|
||||
])
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
|
||||
@@ -5,20 +5,17 @@ import {
|
||||
Input,
|
||||
OnDestroy,
|
||||
ViewChild,
|
||||
QueryList,
|
||||
ViewChildren,
|
||||
AfterViewInit,
|
||||
} from '@angular/core';
|
||||
import { Subject, BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import {
|
||||
ShelfSearchFacadeService,
|
||||
ShelfStoreFacadeService,
|
||||
ShelfNavigationService,
|
||||
} from '../../../shared/services';
|
||||
import {
|
||||
OrderItemListItemDTO,
|
||||
ListResponseArgsOfOrderItemListItemDTO,
|
||||
} from '@swagger/oms/lib';
|
||||
AutocompleteDTO,
|
||||
} from '@swagger/oms';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
debounceTime,
|
||||
@@ -27,11 +24,14 @@ import {
|
||||
takeUntil,
|
||||
filter,
|
||||
first,
|
||||
shareReplay,
|
||||
tap,
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators';
|
||||
import { isNullOrUndefined } from 'util';
|
||||
import { SHELF_SCROLL_INDEX } from 'apps/sales/src/app/core/utils/app.constants';
|
||||
import { ShelfSearchbarComponent } from '../../../components';
|
||||
import { ShelfFilterService } from '../../../services/shelf-filter.service';
|
||||
import { SearchStateFacade } from '@shelf-store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shelf-search-input',
|
||||
@@ -43,6 +43,7 @@ export class ShelfSearchInputComponent
|
||||
implements OnInit, AfterViewInit, OnDestroy {
|
||||
@Input() allowScan = false;
|
||||
@Input() hasAutocomplete = true;
|
||||
@Input() searchCallback: () => void;
|
||||
|
||||
@ViewChild(ShelfSearchbarComponent, { static: true })
|
||||
searchbar: ShelfSearchbarComponent;
|
||||
@@ -54,17 +55,17 @@ export class ShelfSearchInputComponent
|
||||
errorMessage$ = new BehaviorSubject<string>('');
|
||||
|
||||
autocompleteQueryString$ = new BehaviorSubject<string>('');
|
||||
autocompleteResult$: Observable<OrderItemListItemDTO[]>;
|
||||
autocompleteResult$: Observable<AutocompleteDTO[]>;
|
||||
|
||||
searchMode$: Observable<'standalone' | 'autocomplete'>;
|
||||
|
||||
selectedItem$ = new BehaviorSubject<
|
||||
OrderItemListItemDTO & { shouldNavigate?: boolean }
|
||||
AutocompleteDTO & { shouldNavigate?: boolean }
|
||||
>(null);
|
||||
|
||||
constructor(
|
||||
private shelfSearchService: ShelfSearchFacadeService,
|
||||
private shelfStoreFacade: ShelfStoreFacadeService,
|
||||
private searchStateFacade: SearchStateFacade,
|
||||
private shelfNavigationService: ShelfNavigationService,
|
||||
private filterService: ShelfFilterService
|
||||
) {}
|
||||
@@ -75,7 +76,6 @@ export class ShelfSearchInputComponent
|
||||
}
|
||||
|
||||
this.setUpSearchMode();
|
||||
this.setUpNavigationToDetailsPage();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
@@ -94,39 +94,66 @@ export class ShelfSearchInputComponent
|
||||
}
|
||||
|
||||
triggerBarcodeSearch(barcode: string) {
|
||||
this.triggerSearch({ type: 'scan', value: barcode, target: 'click' });
|
||||
this.triggerSearch({ type: 'scan', value: barcode });
|
||||
}
|
||||
|
||||
triggerSearch({
|
||||
type,
|
||||
value,
|
||||
target,
|
||||
}: {
|
||||
type: 'search' | 'scan';
|
||||
value: string;
|
||||
target: 'click' | 'enter';
|
||||
}) {
|
||||
triggerSearch(
|
||||
{
|
||||
type,
|
||||
value,
|
||||
selectedFilters,
|
||||
bypassValidation,
|
||||
closeOverlay,
|
||||
}: {
|
||||
type: 'search' | 'scan';
|
||||
value: string;
|
||||
selectedFilters?: { [key: string]: string };
|
||||
bypassValidation?: boolean;
|
||||
closeOverlay?: () => void;
|
||||
} = {
|
||||
type: 'search',
|
||||
value: '',
|
||||
bypassValidation: false,
|
||||
selectedFilters: {},
|
||||
}
|
||||
) {
|
||||
let result$: Observable<ListResponseArgsOfOrderItemListItemDTO>;
|
||||
let searchQuery = value;
|
||||
this.setIsFetchingData(true);
|
||||
this.resetError();
|
||||
|
||||
if (this.shouldNavigateToSelectedItem(target)) {
|
||||
return this.shelfNavigationService.navigateToDetails(
|
||||
this.selectedItem$.value
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'search') {
|
||||
result$ = this.shelfSearchService.search(value);
|
||||
if (this.isAutocompleteSelected()) {
|
||||
searchQuery = this.selectedItem$.value.query;
|
||||
result$ = this.shelfSearchService.search(searchQuery, {
|
||||
hitsOnly: true,
|
||||
bypassValidation,
|
||||
selectedFilters,
|
||||
});
|
||||
this.updateSearchbarValue(searchQuery);
|
||||
} else if (type === 'search') {
|
||||
result$ = this.shelfSearchService.search(value, {
|
||||
hitsOnly: true,
|
||||
bypassValidation,
|
||||
selectedFilters,
|
||||
});
|
||||
} else if (type === 'scan') {
|
||||
result$ = this.shelfSearchService.searchWithBarcode(value);
|
||||
}
|
||||
|
||||
result$
|
||||
.pipe(takeUntil(this.destroy$), first())
|
||||
.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
first(),
|
||||
tap(() => this.searchStateFacade.setInput(searchQuery))
|
||||
)
|
||||
.subscribe(
|
||||
(result) => this.handleSearchResult(result, value),
|
||||
this.handleSearchResultError
|
||||
(result) =>
|
||||
this.handleSearchResult(
|
||||
result,
|
||||
searchQuery,
|
||||
closeOverlay || this.searchCallback
|
||||
),
|
||||
() => this.handleSearchResultError()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,13 +163,13 @@ export class ShelfSearchInputComponent
|
||||
|
||||
private handleSearchResult(
|
||||
result: ListResponseArgsOfOrderItemListItemDTO,
|
||||
value: string
|
||||
value: string,
|
||||
successCB?: () => void
|
||||
) {
|
||||
this.setIsFetchingData(false);
|
||||
if (this.hasNoResult(result)) {
|
||||
this.setErrorMessage('Ergibt keine Suchergebnisse');
|
||||
return this.setErrorMessage('Ergibt keine Suchergebnisse');
|
||||
} else if (this.hasMultipleSearchResults(result)) {
|
||||
this.shelfStoreFacade.setShelfSearch(value);
|
||||
this.shelfNavigationService.navigateToResultList({
|
||||
searchQuery: value,
|
||||
numberOfHits: result.hits,
|
||||
@@ -151,6 +178,10 @@ export class ShelfSearchInputComponent
|
||||
} else {
|
||||
this.shelfNavigationService.navigateToDetails(this.getDetails(result));
|
||||
}
|
||||
|
||||
if (successCB) {
|
||||
successCB();
|
||||
}
|
||||
}
|
||||
|
||||
private hasNoResult(result: ListResponseArgsOfOrderItemListItemDTO): boolean {
|
||||
@@ -167,24 +198,33 @@ export class ShelfSearchInputComponent
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
|
||||
private handleSearchResultError() {
|
||||
private handleSearchResultError(errorCB?: () => void) {
|
||||
this.setErrorMessage('Ein Fehler ist aufgetreten');
|
||||
this.setIsFetchingData(false);
|
||||
|
||||
if (errorCB) {
|
||||
errorCB();
|
||||
}
|
||||
}
|
||||
|
||||
private setupAutocompletion() {
|
||||
this.autocompleteResult$ = this.autocompleteQueryString$.pipe(
|
||||
distinctUntilChanged(),
|
||||
debounceTime(250),
|
||||
switchMap((queryString: string) => {
|
||||
filter((queryString) => typeof queryString === 'string'),
|
||||
distinctUntilChanged(this.filterDistinctStrings),
|
||||
withLatestFrom(this.searchStateFacade.selectedFilter$),
|
||||
switchMap(([queryString, selectedFilters]) => {
|
||||
if (this.isValidQuery(queryString)) {
|
||||
return this.shelfSearchService
|
||||
.search(queryString)
|
||||
.pipe(map((result) => result.result && result.result.slice(0, 5)));
|
||||
.searchForAutocomplete(queryString, {
|
||||
selectedFilters: selectedFilters,
|
||||
})
|
||||
.pipe(map((result) => result.result));
|
||||
}
|
||||
|
||||
return of([]);
|
||||
})
|
||||
}),
|
||||
shareReplay()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -212,18 +252,6 @@ export class ShelfSearchInputComponent
|
||||
}
|
||||
}
|
||||
|
||||
private setUpNavigationToDetailsPage() {
|
||||
this.selectedItem$
|
||||
.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
filter((item) => !isNullOrUndefined(item)),
|
||||
distinctUntilChanged(),
|
||||
filter(({ shouldNavigate }) => !!shouldNavigate),
|
||||
map(({ shouldNavigate, ...item }) => item)
|
||||
)
|
||||
.subscribe((item) => this.shelfNavigationService.navigateToDetails(item));
|
||||
}
|
||||
|
||||
private resetQueryString() {
|
||||
this.autocompleteQueryString$.next('');
|
||||
}
|
||||
@@ -240,11 +268,21 @@ export class ShelfSearchInputComponent
|
||||
this.errorMessage$.next(message);
|
||||
}
|
||||
|
||||
private isValidQuery(queryString): boolean {
|
||||
return !!queryString && queryString.length > 3;
|
||||
private isValidQuery(queryString: string): boolean {
|
||||
return !!queryString && queryString.length >= 3;
|
||||
}
|
||||
|
||||
private shouldNavigateToSelectedItem(target: 'click' | 'enter'): boolean {
|
||||
return !!this.selectedItem$.value && target === 'enter';
|
||||
private filterDistinctStrings(oldQuery: string, newQuery: string): boolean {
|
||||
return (oldQuery || '').toLowerCase() === (newQuery || '').toLowerCase();
|
||||
}
|
||||
|
||||
private isAutocompleteSelected() {
|
||||
return !!this.selectedItem$.value;
|
||||
}
|
||||
|
||||
private updateSearchbarValue(queryString: string) {
|
||||
if (this.searchbar) {
|
||||
this.searchbar.setInputValue(queryString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { Observable, BehaviorSubject, of, Subject, combineLatest } from 'rxjs';
|
||||
import { Observable, BehaviorSubject, Subject, combineLatest } from 'rxjs';
|
||||
import { SelectFilter, Filter, SelectFilterOption } from '../../filter';
|
||||
import { mockFilters } from '../shared/mockdata/filters.mock';
|
||||
import { tap, switchMap, startWith, map, withLatestFrom, debounceTime, filter, first } from 'rxjs/operators';
|
||||
import { tap, startWith, map, filter } from 'rxjs/operators';
|
||||
import { SearchStateFacade } from '../../../store/customer';
|
||||
import { flatten } from '../../../shared/utils';
|
||||
import { isNullOrUndefined } from 'util';
|
||||
import { cloneFilter } from '../../filter/utils';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ShelfFilterService implements OnDestroy {
|
||||
@@ -18,12 +18,14 @@ export class ShelfFilterService implements OnDestroy {
|
||||
public overlayClosed$ = new Subject<void>();
|
||||
|
||||
constructor(private searchStateFacade: SearchStateFacade) {
|
||||
this.filters$ = this.searchStateFacade.currentFilter$;
|
||||
this.filters$ = this.searchStateFacade.selectFilters$;
|
||||
this.initPendingFilters();
|
||||
}
|
||||
|
||||
get hasSelectedFilters$(): Observable<boolean> {
|
||||
return this.searchStateFacade.currentFilter$.pipe(map(this.computeHasSelectedFilters));
|
||||
return this.searchStateFacade.selectFilters$.pipe(
|
||||
map(this.computeHasSelectedFilters)
|
||||
);
|
||||
}
|
||||
|
||||
get hasSelectedPendingFilters$(): Observable<boolean> {
|
||||
@@ -43,7 +45,7 @@ export class ShelfFilterService implements OnDestroy {
|
||||
tap(() => this.pendingFilters$.next(null))
|
||||
),
|
||||
]).subscribe(([filters]) => {
|
||||
this.pendingFilters$.next(filters);
|
||||
this.pendingFilters$.next(filters.map(cloneFilter));
|
||||
this.setInitialFilterGroupLastChanged(filters);
|
||||
});
|
||||
}
|
||||
@@ -64,7 +66,10 @@ export class ShelfFilterService implements OnDestroy {
|
||||
}
|
||||
|
||||
private computeHasSelectedFilters(filters: SelectFilter[]): boolean {
|
||||
const flattenedFilters = (flatten(filters, 'options') as unknown) as SelectFilterOption[];
|
||||
const flattenedFilters = (flatten(
|
||||
filters,
|
||||
'options'
|
||||
) as unknown) as SelectFilterOption[];
|
||||
|
||||
if (flattenedFilters.some((f) => !!f.selected)) {
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// start:ng42.barrel
|
||||
export * from './filters.mock';
|
||||
export * from './primary-filters.mock';
|
||||
// end:ng42.barrel
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { PrimaryFilterOption } from '../../defs';
|
||||
|
||||
export const primaryFiltersMock: PrimaryFilterOption[] = [
|
||||
{
|
||||
name: 'Primary Filter One',
|
||||
id: 'filter1',
|
||||
key: 'One',
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
name: 'Primary Filter Two',
|
||||
id: 'filter2',
|
||||
key: 'Two',
|
||||
selected: false,
|
||||
},
|
||||
];
|
||||
@@ -1,6 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './shelf-navigation.service';
|
||||
export * from './shelf-search.facade.service';
|
||||
export * from './shelf-store.facade.service';
|
||||
// end:ng42.barrel
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ import { Process } from 'apps/sales/src/app/core/models/process.model';
|
||||
export class ShelfNavigationService {
|
||||
constructor(private store: Store, private router: Router) {}
|
||||
|
||||
// TODO Add Navigation Logic for History Page
|
||||
|
||||
navigateToDetails(order: OrderItemListItemDTO) {
|
||||
this.createTab();
|
||||
const path = this.getDetailsPath(order);
|
||||
@@ -38,6 +36,10 @@ export class ShelfNavigationService {
|
||||
this.navigateToRoute(path, breadcrumb);
|
||||
}
|
||||
|
||||
navigateToHistory() {
|
||||
// TODO Add Navigation Logic for History Page
|
||||
}
|
||||
|
||||
private navigateToRoute(route: string, breadcrumbName: string) {
|
||||
this.store.dispatch(
|
||||
new AddBreadcrumb(
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store, Select } from '@ngxs/store';
|
||||
import { Select } from '@ngxs/store';
|
||||
import { BranchSelectors } from 'apps/sales/src/app/core/store/selectors/branch.selector';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, switchMap, map } from 'rxjs/operators';
|
||||
import { isNullOrUndefined } from 'util';
|
||||
import { CollectingShelfService } from 'apps/sales/src/app/core/services/collecting-shelf.service';
|
||||
import { ListResponseArgsOfOrderItemListItemDTO } from '@swagger/oms/lib';
|
||||
import {
|
||||
ListResponseArgsOfOrderItemListItemDTO,
|
||||
AutocompleteTokenDTO,
|
||||
ResponseArgsOfIEnumerableOfAutocompleteDTO,
|
||||
} from '@swagger/oms/lib';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ShelfSearchFacadeService {
|
||||
@@ -15,14 +19,29 @@ export class ShelfSearchFacadeService {
|
||||
|
||||
constructor(private collectingShelfService: CollectingShelfService) {}
|
||||
|
||||
search(queryString: string) {
|
||||
search(
|
||||
queryString: string,
|
||||
options: {
|
||||
hitsOnly: boolean;
|
||||
bypassValidation: boolean;
|
||||
selectedFilters: { [key: string]: string };
|
||||
} = {
|
||||
hitsOnly: false,
|
||||
bypassValidation: false,
|
||||
selectedFilters: {},
|
||||
}
|
||||
) {
|
||||
const searchParams = queryString.trim();
|
||||
const { hitsOnly, bypassValidation, selectedFilters } = options;
|
||||
|
||||
if (!this.isValidSearchQuery(searchParams)) {
|
||||
if (!bypassValidation && !this.isValidSearchQuery(searchParams)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.requestSearch(searchParams).pipe(map(this.handleSearchResult));
|
||||
return this.requestSearch(searchParams, {
|
||||
hitsOnly,
|
||||
selectedFilters,
|
||||
}).pipe(map(this.handleSearchResult));
|
||||
}
|
||||
|
||||
searchWithBarcode(barcode: string) {
|
||||
@@ -35,13 +54,64 @@ export class ShelfSearchFacadeService {
|
||||
return this.requestSearch(searchParams).pipe(map(this.handleSearchResult));
|
||||
}
|
||||
|
||||
searchForAutocomplete(
|
||||
queryString: string,
|
||||
options: { selectedFilters?: { [key: string]: string } } = {}
|
||||
) {
|
||||
const searchParams = queryString.trim();
|
||||
const autoCompleteQuery: AutocompleteTokenDTO = this.generateAutocompleteToken(
|
||||
{ queryString, filter: options.selectedFilters || {}, take: 5 }
|
||||
);
|
||||
|
||||
if (!this.isValidSearchQuery(searchParams)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.requestAutocompleteSearch(autoCompleteQuery);
|
||||
}
|
||||
|
||||
private requestSearch(
|
||||
input: string
|
||||
input: string,
|
||||
options?: {
|
||||
hitsOnly: boolean;
|
||||
selectedFilters?: { [key: string]: string };
|
||||
}
|
||||
): Observable<ListResponseArgsOfOrderItemListItemDTO> {
|
||||
return this.currentUserBranchId$.pipe(
|
||||
filter((branchNumber) => !isNullOrUndefined(branchNumber)),
|
||||
switchMap((branchNumber) =>
|
||||
this.collectingShelfService.searchWarenausgabe({ input, branchNumber })
|
||||
this.collectingShelfService.searchWarenausgabe({
|
||||
input,
|
||||
branchNumber,
|
||||
selectedFilters: options.selectedFilters,
|
||||
hitsOnly: options.hitsOnly,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private generateAutocompleteToken(params: {
|
||||
queryString: string;
|
||||
take?: number;
|
||||
filter?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}): AutocompleteTokenDTO {
|
||||
return {
|
||||
input: params.queryString,
|
||||
take: params.take || 5,
|
||||
filter: params.filter || {},
|
||||
};
|
||||
}
|
||||
|
||||
private requestAutocompleteSearch(
|
||||
autocompleteToken: AutocompleteTokenDTO
|
||||
): Observable<ResponseArgsOfIEnumerableOfAutocompleteDTO> {
|
||||
return this.currentUserBranchId$.pipe(
|
||||
switchMap(() =>
|
||||
this.collectingShelfService.searchWarenausgabeAutocomplete(
|
||||
autocompleteToken
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store as NgxsStore } from '@ngxs/store';
|
||||
import { SetShelfSearch } from 'apps/sales/src/app/core/store/actions/process.actions';
|
||||
import { ShelfSearch } from 'apps/sales/src/app/core/models/shelf-search.modal';
|
||||
import { BranchSelectors } from 'apps/sales/src/app/core/store/selectors/branch.selector';
|
||||
import { SearchStateFacade } from 'apps/sales/src/app/store/customer';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ShelfStoreFacadeService {
|
||||
constructor(
|
||||
private ngxsStore: NgxsStore,
|
||||
private storeFacade: SearchStateFacade
|
||||
) {}
|
||||
|
||||
get branchnumber(): string {
|
||||
return this.ngxsStore.selectSnapshot(BranchSelectors.getUserBranch);
|
||||
}
|
||||
|
||||
setShelfSearch(input: string) {
|
||||
/** NGXXS Store */
|
||||
this.ngxsStore.dispatch(
|
||||
new SetShelfSearch(<ShelfSearch>{
|
||||
input,
|
||||
branchnumber: this.branchnumber,
|
||||
})
|
||||
);
|
||||
|
||||
/** NEW NgRx Store */
|
||||
this.storeFacade.setInput(input);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ describe('CalendarComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CalendarModule]
|
||||
imports: [CalendarModule],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CalendarComponent);
|
||||
@@ -232,18 +232,21 @@ describe('CalendarComponent', () => {
|
||||
{
|
||||
color: 'red',
|
||||
date: new Date(2019, 5, 4),
|
||||
title: 'test 1'
|
||||
title: 'test 1',
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
date: new Date(2019, 5, 23),
|
||||
title: 'test 4'
|
||||
title: 'test 4',
|
||||
id: 2,
|
||||
},
|
||||
{
|
||||
color: 'white',
|
||||
date: new Date(2019, 5, 23),
|
||||
title: 'test 5'
|
||||
}
|
||||
title: 'test 5',
|
||||
id: 3,
|
||||
},
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
@@ -281,18 +284,21 @@ describe('CalendarComponent', () => {
|
||||
{
|
||||
color: 'red',
|
||||
date: new Date(2019, 5, 4),
|
||||
title: 'test 1'
|
||||
title: 'test 1',
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
date: new Date(2019, 5, 6),
|
||||
title: 'test 4'
|
||||
title: 'test 4',
|
||||
id: 2,
|
||||
},
|
||||
{
|
||||
color: 'white',
|
||||
date: new Date(2019, 5, 6),
|
||||
title: 'test 5'
|
||||
}
|
||||
title: 'test 5',
|
||||
id: 3,
|
||||
},
|
||||
];
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -2,5 +2,5 @@ import { HistoryDTO } from '@cmf/trade-api';
|
||||
import { OrderHistoryStatus } from './order-history-status';
|
||||
|
||||
export interface OrderHistory extends HistoryDTO {
|
||||
status?: OrderHistoryStatus;
|
||||
status: OrderHistoryStatus;
|
||||
}
|
||||
|
||||
@@ -1,39 +1,38 @@
|
||||
import { OrderHistory, OrderHistoryStatus } from '../defs';
|
||||
import { props, createAction } from '@ngrx/store';
|
||||
import { StrictHttpResponse, ResponseArgs } from '@swagger/oms';
|
||||
import { HistoryDTO } from '@cmf/trade-api';
|
||||
import { StrictHttpResponse, ResponseArgsOfHistoryDTO } from '@swagger/oms';
|
||||
|
||||
const prefix = '[CUSTOMER] [SHELF] [HISTORY]';
|
||||
|
||||
export const initOrderHistory = createAction(
|
||||
`${prefix} Init Order History`,
|
||||
props<{ orderItemId: number }>()
|
||||
props<{ id: number }>()
|
||||
);
|
||||
|
||||
export const addOrderHistory = createAction(
|
||||
`${prefix} Add Order History`,
|
||||
props<{ orderItemId: number; history: OrderHistory }>()
|
||||
props<{ id: number; history: OrderHistory }>()
|
||||
);
|
||||
|
||||
export const setStatus = createAction(
|
||||
`${prefix} Add Order History`,
|
||||
props<{ orderItemId: number; status: OrderHistoryStatus }>()
|
||||
`${prefix} Set Order Status`,
|
||||
props<{ id: number; status: OrderHistoryStatus }>()
|
||||
);
|
||||
|
||||
export const fetchHistory = createAction(
|
||||
`${prefix} Fetch Order History`,
|
||||
props<{ orderItemId: number }>()
|
||||
props<{ id: number }>()
|
||||
);
|
||||
|
||||
export const fetchHistoryDone = createAction(
|
||||
`${prefix} Fetch Order History Done`,
|
||||
props<{
|
||||
orderItemId: number;
|
||||
response: StrictHttpResponse<ResponseArgs & { result: HistoryDTO }>;
|
||||
id: number;
|
||||
response: StrictHttpResponse<ResponseArgsOfHistoryDTO>;
|
||||
}>()
|
||||
);
|
||||
|
||||
export const updateOrderHistory = createAction(
|
||||
`${prefix} Update Order History`,
|
||||
props<{ orderItemId: number; history: Partial<OrderHistory> }>()
|
||||
props<{ id: number; history: Partial<OrderHistory> }>()
|
||||
);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
import * as actions from './history.actions';
|
||||
import { switchMap, map, catchError, flatMap } from 'rxjs/operators';
|
||||
import { OrderService, StrictHttpResponse, ResponseArgs } from '@swagger/oms';
|
||||
import {
|
||||
OrderService,
|
||||
StrictHttpResponse,
|
||||
ResponseArgs,
|
||||
ResponseArgsOfHistoryDTO,
|
||||
} from '@swagger/oms';
|
||||
import { HistoryDTO } from '@cmf/trade-api';
|
||||
import { of, NEVER } from 'rxjs';
|
||||
import { OrderHistoryStatus } from '../defs';
|
||||
@@ -12,26 +16,28 @@ import { OrderHistoryStatus } from '../defs';
|
||||
export class HistoryEffects {
|
||||
constructor(private actions$: Actions, private orderService: OrderService) {}
|
||||
|
||||
initHistory$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(actions.initOrderHistory),
|
||||
map((action) => actions.fetchHistory({ id: action.id }))
|
||||
)
|
||||
);
|
||||
|
||||
fetchHistory$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(actions.fetchHistory),
|
||||
switchMap((action) =>
|
||||
this.orderService
|
||||
.OrderGetOrderItemHistoryResponse({ orderItemId: action.orderItemId })
|
||||
.OrderGetOrderItemHistoryResponse({ orderItemId: action.id })
|
||||
.pipe(
|
||||
catchError((err) =>
|
||||
of<StrictHttpResponse<ResponseArgs & { result: HistoryDTO }>>(err)
|
||||
of<StrictHttpResponse<ResponseArgsOfHistoryDTO>>(err)
|
||||
),
|
||||
map(
|
||||
(
|
||||
response: StrictHttpResponse<
|
||||
ResponseArgs & { result: HistoryDTO }
|
||||
>
|
||||
) =>
|
||||
actions.fetchHistoryDone({
|
||||
orderItemId: action.orderItemId,
|
||||
response,
|
||||
})
|
||||
map((response: StrictHttpResponse<ResponseArgsOfHistoryDTO>) =>
|
||||
actions.fetchHistoryDone({
|
||||
id: action.id,
|
||||
response,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -43,12 +49,17 @@ export class HistoryEffects {
|
||||
ofType(actions.fetchHistoryDone),
|
||||
flatMap((action) => {
|
||||
if (action.response.ok) {
|
||||
const history = action.response.body.result;
|
||||
actions.addOrderHistory({ orderItemId: action.orderItemId, history });
|
||||
const history = action.response.body.result[0];
|
||||
return [
|
||||
actions.addOrderHistory({
|
||||
id: action.id,
|
||||
history,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
actions.setStatus({
|
||||
orderItemId: action.orderItemId,
|
||||
id: action.id,
|
||||
status: OrderHistoryStatus.ERROR,
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ import { Store } from '@ngrx/store';
|
||||
import { Dictionary } from '@ngrx/entity';
|
||||
import { Observable } from 'rxjs';
|
||||
import * as selectors from './history.selectors';
|
||||
import * as actions from './history.actions';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { isNullOrUndefined } from 'util';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class HistoryStateFacade {
|
||||
@@ -13,15 +16,17 @@ export class HistoryStateFacade {
|
||||
|
||||
constructor(private store: Store<any>) {}
|
||||
|
||||
private getHistories$(): Observable<Dictionary<OrderHistory>> {
|
||||
return this.store.select(selectors.selectHistories);
|
||||
}
|
||||
|
||||
public getHistory$(orderItemId: number): Observable<OrderHistory> {
|
||||
this.store.dispatch(actions.initOrderHistory({ id: orderItemId }));
|
||||
|
||||
return this.store.select(selectors.selectHistory, orderItemId);
|
||||
}
|
||||
|
||||
public getStatus$(orderItemId: number): Observable<OrderHistoryStatus> {
|
||||
return this.store.select(selectors.selectStatus, orderItemId);
|
||||
}
|
||||
|
||||
private getHistories$(): Observable<Dictionary<OrderHistory>> {
|
||||
return this.store.select(selectors.selectHistories);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
INITIAL_HISTORY_STATE,
|
||||
INITIAL_ORDER_HISTORY,
|
||||
HistoryState,
|
||||
} from './history.state';
|
||||
import { historyReducer } from './history.reducer';
|
||||
import * as actions from './history.actions';
|
||||
import { OrderHistory } from '@shelf-store/defs';
|
||||
|
||||
fdescribe('#HistoryStateReducer', () => {
|
||||
const id = 123;
|
||||
const mockOrderHistory: OrderHistory = {
|
||||
...INITIAL_ORDER_HISTORY,
|
||||
name: 'Fake History',
|
||||
id,
|
||||
values: [],
|
||||
};
|
||||
|
||||
it('should return the initial state if on Init Order action is dispatched', () => {
|
||||
const initialState = INITIAL_HISTORY_STATE;
|
||||
const action = actions.initOrderHistory({ id });
|
||||
|
||||
const state = historyReducer(initialState, action);
|
||||
const entity = state.entities[id];
|
||||
|
||||
expect(entity).toEqual({
|
||||
...initialState.entities[id],
|
||||
...INITIAL_ORDER_HISTORY,
|
||||
id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should add history and set status to available (2)', () => {
|
||||
let state: HistoryState;
|
||||
const initialState = INITIAL_HISTORY_STATE;
|
||||
|
||||
const initAction = actions.initOrderHistory({ id });
|
||||
state = historyReducer(initialState, initAction);
|
||||
|
||||
const action = actions.addOrderHistory({ id, history: mockOrderHistory });
|
||||
state = historyReducer(state, action);
|
||||
|
||||
const entity = state.entities[id];
|
||||
|
||||
expect(entity.status).toBe(2);
|
||||
expect(entity).toEqual({ ...mockOrderHistory, status: 2 });
|
||||
});
|
||||
});
|
||||
@@ -12,24 +12,39 @@ export const _historyReducer = createReducer(
|
||||
INITIAL_HISTORY_STATE,
|
||||
on(actions.initOrderHistory, (s, a) =>
|
||||
historyStateAdapter.addOne(
|
||||
{ id: a.orderItemId, ...INITIAL_ORDER_HISTORY },
|
||||
{
|
||||
...INITIAL_ORDER_HISTORY,
|
||||
id: a.id,
|
||||
},
|
||||
s
|
||||
)
|
||||
),
|
||||
on(actions.updateOrderHistory, (s, a) =>
|
||||
historyStateAdapter.updateOne(
|
||||
{
|
||||
id: a.orderItemId,
|
||||
changes: { ...a.history, status: OrderHistoryStatus.AVAILABLE },
|
||||
id: a.id,
|
||||
changes: {
|
||||
...a.history,
|
||||
status: OrderHistoryStatus.AVAILABLE,
|
||||
},
|
||||
},
|
||||
s
|
||||
)
|
||||
),
|
||||
on(actions.addOrderHistory, (s, a) =>
|
||||
on(actions.addOrderHistory, (s, a) => {
|
||||
return historyStateAdapter.updateOne(
|
||||
{
|
||||
id: a.id,
|
||||
changes: { ...a.history, status: OrderHistoryStatus.AVAILABLE },
|
||||
},
|
||||
s
|
||||
);
|
||||
}),
|
||||
on(actions.setStatus, (s, a) =>
|
||||
historyStateAdapter.updateOne(
|
||||
{
|
||||
id: a.orderItemId,
|
||||
changes: { ...a.history, status: OrderHistoryStatus.AVAILABLE },
|
||||
id: a.id,
|
||||
changes: { status: a.status },
|
||||
},
|
||||
s
|
||||
)
|
||||
|
||||
@@ -25,5 +25,6 @@ export const selectHistory = createSelector(
|
||||
|
||||
export const selectStatus = createSelector(
|
||||
selectEntities,
|
||||
(entities: Dictionary<OrderHistory>, id: number) => entities[id].status
|
||||
(entities: Dictionary<OrderHistory>, id: number) =>
|
||||
entities[id] && entities[id].status
|
||||
);
|
||||
|
||||
@@ -9,6 +9,6 @@ export const INITIAL_HISTORY_STATE: HistoryState = {
|
||||
...historyStateAdapter.getInitialState(),
|
||||
};
|
||||
|
||||
export const INITIAL_ORDER_HISTORY = {
|
||||
export const INITIAL_ORDER_HISTORY: OrderHistory = {
|
||||
status: OrderHistoryStatus.INIT,
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { OrderItemListItemDTO } from '@swagger/oms';
|
||||
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
|
||||
import { ShelfPrimaryFilterOptions } from 'apps/sales/src/app/modules/shelf/defs';
|
||||
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
|
||||
|
||||
export interface SearchProcess {
|
||||
id: number; // Prozess ID;
|
||||
input?: string;
|
||||
filters?: {
|
||||
selectedFilters?: SelectFilter[];
|
||||
primaryFilters?: ShelfPrimaryFilterOptions;
|
||||
primaryFilters?: PrimaryFilterOption[];
|
||||
};
|
||||
result: OrderItemListItemDTO[];
|
||||
hits?: number;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// start:ng42.barrel
|
||||
export * from './input-dto-to-primary-filters.mapper';
|
||||
export * from './input-dto-to-selected-filters.mapper';
|
||||
export * from './primary-filters-to-filters-dictionary.mapper';
|
||||
export * from './select-filters-to-filters-dictionary.mapper';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
|
||||
import { InputDTO } from '@swagger/oms';
|
||||
|
||||
export function inputDtoToPrimaryFilterOption(
|
||||
inputDTO: InputDTO
|
||||
): PrimaryFilterOption {
|
||||
return {
|
||||
id: inputDTO.key,
|
||||
key: inputDTO.key,
|
||||
name: inputDTO.label,
|
||||
selected: false,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
SelectFilter,
|
||||
SelectFilterOption,
|
||||
FilterType,
|
||||
} from 'apps/sales/src/app/modules/filter';
|
||||
import { InputDTO, OptionDTO, InputType } from '@swagger/oms';
|
||||
|
||||
export function inputDtoToSelectedFilters(inputDTO: InputDTO): SelectFilter {
|
||||
return {
|
||||
key: inputDTO.key,
|
||||
name: inputDTO.label,
|
||||
max: inputDTO.options && inputDTO.options.max,
|
||||
type: inputTypeToType(inputDTO.type),
|
||||
options:
|
||||
inputDTO.options &&
|
||||
inputDTO.options.values.map(optionDtoToSelectFilterOption),
|
||||
} as SelectFilter;
|
||||
}
|
||||
|
||||
export function optionDtoToSelectFilterOption(
|
||||
optionDto: OptionDTO
|
||||
): SelectFilterOption {
|
||||
return {
|
||||
name: optionDto.label,
|
||||
id: optionDto.key || optionDto.value,
|
||||
selected: optionDto.selected || false,
|
||||
expanded: false,
|
||||
options:
|
||||
optionDto.values && optionDto.values.map(optionDtoToSelectFilterOption),
|
||||
} as SelectFilterOption;
|
||||
}
|
||||
|
||||
export function inputTypeToType(type: InputType): FilterType {
|
||||
switch (type) {
|
||||
case 1:
|
||||
return 'text';
|
||||
|
||||
case 2:
|
||||
return 'select';
|
||||
|
||||
case 4:
|
||||
return 'checkbox';
|
||||
|
||||
case 8 || 16:
|
||||
return 'date';
|
||||
|
||||
case 32 || 64:
|
||||
return 'number';
|
||||
|
||||
default:
|
||||
return 'select';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Dictionary } from '@cmf/core';
|
||||
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
|
||||
|
||||
export function primaryFiltersToFiltersDictionary(
|
||||
primaryFilters: PrimaryFilterOption[]
|
||||
): { [key: string]: string[] } {
|
||||
if (!primaryFilters) {
|
||||
return {};
|
||||
}
|
||||
return primaryFilters.reduce((acc, curr) => {
|
||||
if (!curr.selected) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return { ...acc, [curr.key]: String(curr.selected) };
|
||||
}, {});
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
|
||||
import { flatten } from 'apps/sales/src/app/shared/utils';
|
||||
|
||||
export function selectFiltersToFiltersDictionary(
|
||||
selectFilters: SelectFilter[]
|
||||
): { [key: string]: string } {
|
||||
if (!selectFilters) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const result = selectFilters
|
||||
.map((selectFilter) => {
|
||||
const flattened = flatten(selectFilter.options, 'options').filter(
|
||||
(o) => !!o.selected
|
||||
);
|
||||
return { [selectFilter.key]: flattened.map((f) => f.id).join(';') };
|
||||
})
|
||||
.reduce((acc, curr) => ({ ...acc, ...curr }), {});
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -3,9 +3,10 @@ import {
|
||||
OrderItemListItemDTO,
|
||||
StrictHttpResponse,
|
||||
ListResponseArgsOfOrderItemListItemDTO,
|
||||
ResponseArgsOfIEnumerableOfInputDTO,
|
||||
} from '@swagger/oms';
|
||||
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
|
||||
import { ShelfPrimaryFilterOptions } from 'apps/sales/src/app/modules/shelf/defs';
|
||||
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
|
||||
|
||||
const prefix = '[CUSTOMER] [SHELF] [SEARCH]';
|
||||
|
||||
@@ -28,18 +29,18 @@ export const fetchFiltersDone = createAction(
|
||||
`${prefix} Fetch Filters Done`,
|
||||
props<{
|
||||
id: number;
|
||||
response: SelectFilter[];
|
||||
response: StrictHttpResponse<ResponseArgsOfIEnumerableOfInputDTO>;
|
||||
}>()
|
||||
);
|
||||
|
||||
export const setSelectedFilters = createAction(
|
||||
export const setSelectFilters = createAction(
|
||||
`${prefix} Set Selected Filters`,
|
||||
props<{ id: number; filters: SelectFilter[] }>()
|
||||
);
|
||||
|
||||
export const setPrimaryFilters = createAction(
|
||||
`${prefix} Set Primary Filters`,
|
||||
props<{ id: number; filters: ShelfPrimaryFilterOptions }>()
|
||||
props<{ id: number; filters: PrimaryFilterOption[] }>()
|
||||
);
|
||||
|
||||
export const setInput = createAction(
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { SearchEffects } from './search.effects';
|
||||
import { provideMockStore, MockStore } from '@ngrx/store/testing';
|
||||
import { provideMockActions } from '@ngrx/effects/testing';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import {
|
||||
StrictHttpResponse,
|
||||
ResponseArgsOfIEnumerableOfInputDTO,
|
||||
OrderService,
|
||||
InputDTO,
|
||||
} from '@swagger/oms';
|
||||
import { hot, cold } from 'jasmine-marbles';
|
||||
import * as actions from './search.actions';
|
||||
import * as processActions from 'apps/sales/src/app/core/store/actions/process.actions';
|
||||
import { Store as NgxsStore, ofActionDispatched } from '@ngxs/store';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Actions, NgxsModule } from '@ngxs/store';
|
||||
import { BranchService } from '@sales/core-services';
|
||||
import { SearchStateFacade } from './search.facade';
|
||||
import {
|
||||
inputDtoToSelectedFilters,
|
||||
inputDtoToPrimaryFilterOption,
|
||||
} from './mappers';
|
||||
import { Process } from 'apps/sales/src/app/core/models/process.model';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { ProcessState } from 'apps/sales/src/app/core/store/state/process.state';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
|
||||
fdescribe('#SearchEffects', () => {
|
||||
let orderService: jasmine.SpyObj<OrderService>;
|
||||
let searchEffects: SearchEffects;
|
||||
let actions$: Observable<any>;
|
||||
let store: MockStore<any>;
|
||||
let ngxsStore: NgxsStore;
|
||||
let ngxsActions$: Observable<any>;
|
||||
let searchStateFacade: SearchStateFacade;
|
||||
|
||||
// Test Data
|
||||
const id = 123;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgxsModule.forRoot([]),
|
||||
NgxsModule.forFeature([ProcessState]),
|
||||
],
|
||||
providers: [
|
||||
SearchEffects,
|
||||
provideMockStore({}),
|
||||
provideMockActions(() => actions$),
|
||||
{
|
||||
provide: OrderService,
|
||||
useValue: jasmine.createSpyObj('orderService', [
|
||||
'OrderGetWarenausgabeFilterResponse',
|
||||
]),
|
||||
},
|
||||
{
|
||||
provide: BranchService,
|
||||
useValue: jasmine.createSpy('branchService'),
|
||||
},
|
||||
{
|
||||
provide: SearchStateFacade,
|
||||
useValue: jasmine.createSpy('searchStateFacade'),
|
||||
},
|
||||
Actions,
|
||||
SearchStateFacade,
|
||||
],
|
||||
});
|
||||
|
||||
searchEffects = TestBed.get(SearchEffects);
|
||||
orderService = TestBed.get(OrderService);
|
||||
store = TestBed.get(Store);
|
||||
ngxsStore = TestBed.get(NgxsStore);
|
||||
ngxsActions$ = TestBed.get(Actions);
|
||||
searchStateFacade = TestBed.get(SearchStateFacade);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(searchEffects).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('#FetchFilters', () => {
|
||||
it('should fetch the filters and dispatch fetch filters done with filters http response', () => {
|
||||
const mockHttpResponse = ({
|
||||
ok: true,
|
||||
body: [],
|
||||
} as unknown) as StrictHttpResponse<ResponseArgsOfIEnumerableOfInputDTO>;
|
||||
const fetchFiltersAction = actions.fetchFilters({ id });
|
||||
|
||||
const fetchFiltersDoneAction = actions.fetchFiltersDone({
|
||||
id,
|
||||
response: mockHttpResponse,
|
||||
});
|
||||
|
||||
const response = cold('-a', { a: mockHttpResponse });
|
||||
orderService.OrderGetWarenausgabeFilterResponse.and.returnValue(response);
|
||||
|
||||
actions$ = hot('--a', { a: fetchFiltersAction });
|
||||
const expected = cold('---b', { b: fetchFiltersDoneAction });
|
||||
|
||||
expect(searchEffects.fetchFilters$).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#FetchFiltersDone', () => {
|
||||
it('should dispatch setSelectedFiters and setPrimaryFilters if the filters http response is ok ', () => {
|
||||
const mockHttpResponse = ({
|
||||
ok: true,
|
||||
body: {
|
||||
result: [
|
||||
{ key: 'Key Mock 1', label: 'Label Mock 1' },
|
||||
{
|
||||
key: 'Key Mock 2',
|
||||
label: 'Label Mock 2',
|
||||
options: {
|
||||
max: 3,
|
||||
values: [
|
||||
{ key: 'Sub 1 Key', label: 'Sub 1 Label', selected: false },
|
||||
{ key: 'Sub 2 Key', label: 'Sub 2 Label', selected: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown) as StrictHttpResponse<ResponseArgsOfIEnumerableOfInputDTO>;
|
||||
const fetchFiltersDoneAction = actions.fetchFiltersDone({
|
||||
id,
|
||||
response: mockHttpResponse,
|
||||
});
|
||||
|
||||
const selectFilters = [
|
||||
{
|
||||
key: 'Key Mock 2',
|
||||
label: 'Label Mock 2',
|
||||
options: {
|
||||
max: 3,
|
||||
values: [
|
||||
{ key: 'Sub 1 Key', label: 'Sub 1 Label', selected: false },
|
||||
{ key: 'Sub 2 Key', label: 'Sub 2 Label', selected: true },
|
||||
],
|
||||
},
|
||||
} as InputDTO,
|
||||
].map((input) => inputDtoToSelectedFilters(input));
|
||||
|
||||
const primaryFilters = [
|
||||
{ key: 'Key Mock 1', label: 'Label Mock 1' } as InputDTO,
|
||||
].map((input) => inputDtoToPrimaryFilterOption(input));
|
||||
|
||||
const setSelectedFiltersAction = actions.setSelectFilters({
|
||||
id,
|
||||
filters: selectFilters,
|
||||
});
|
||||
const setPrimaryFiltersAction = actions.setPrimaryFilters({
|
||||
id,
|
||||
filters: primaryFilters,
|
||||
});
|
||||
|
||||
actions$ = hot('-a', { a: fetchFiltersDoneAction });
|
||||
const expected = cold('-(bc)', {
|
||||
b: setSelectedFiltersAction,
|
||||
c: setPrimaryFiltersAction,
|
||||
});
|
||||
|
||||
expect(searchEffects.fetchFiltersDone$).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#InitAddProcess', () => {
|
||||
beforeEach(() => {
|
||||
searchStateFacade = TestBed.get(SearchStateFacade);
|
||||
});
|
||||
it('should dispatch addSearchProcess and fetch filters', async () => {
|
||||
spyOn(ngxsStore, 'dispatch').and.callThrough();
|
||||
spyOn(store, 'dispatch').and.callThrough();
|
||||
spyOn(searchStateFacade, 'fetchFilters').and.callFake(() => null);
|
||||
|
||||
const addProcessAction = new processActions.AddProcess({
|
||||
id,
|
||||
} as Process);
|
||||
|
||||
const addSearchProcessAction = actions.addSearchProcess({ id });
|
||||
|
||||
searchEffects.initAddProcess();
|
||||
|
||||
await ngxsStore
|
||||
.dispatch(
|
||||
new processActions.AddProcess({
|
||||
id,
|
||||
} as Process)
|
||||
)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
expect(ngxsStore.dispatch).toHaveBeenCalledWith(addProcessAction);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(addSearchProcessAction);
|
||||
expect(searchStateFacade.fetchFilters).toHaveBeenCalledWith(id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -18,11 +18,18 @@ import {
|
||||
OrderService,
|
||||
StrictHttpResponse,
|
||||
ListResponseArgsOfOrderItemListItemDTO,
|
||||
ResponseArgsOfIEnumerableOfInputDTO,
|
||||
} from '@swagger/oms';
|
||||
import { BranchService } from '@sales/core-services';
|
||||
import { SearchStateFacade } from './search.facade';
|
||||
import { of, NEVER } from 'rxjs';
|
||||
import { mockFilters } from 'apps/sales/src/app/modules/shelf/shared/mockdata/filters.mock';
|
||||
import {
|
||||
inputDtoToSelectedFilters,
|
||||
inputDtoToPrimaryFilterOption,
|
||||
} from './mappers';
|
||||
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
|
||||
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
|
||||
import { isNullOrUndefined } from 'util';
|
||||
|
||||
@Injectable()
|
||||
export class SearchEffects {
|
||||
@@ -43,14 +50,20 @@ export class SearchEffects {
|
||||
ofType(actions.fetchResult),
|
||||
switchMap((a) =>
|
||||
of(a).pipe(
|
||||
withLatestFrom(this.searchStateFacade.getProcess$(a.id)),
|
||||
flatMap(([_, process]) =>
|
||||
withLatestFrom(
|
||||
this.searchStateFacade.getProcess$(a.id),
|
||||
this.searchStateFacade.selectedFilter$
|
||||
),
|
||||
flatMap(([_, process, filter]) =>
|
||||
this.orderService
|
||||
.OrderQueryOrderItemResponse({
|
||||
branchNumber: this.branchService.getCurrentBranchNumber(),
|
||||
input: {
|
||||
qs: process.input,
|
||||
},
|
||||
filter: (filter as unknown) as {
|
||||
[key: string]: string;
|
||||
},
|
||||
skip: process.result.length || 0,
|
||||
take: 20,
|
||||
})
|
||||
@@ -88,10 +101,16 @@ export class SearchEffects {
|
||||
|
||||
fetchFilters$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(actions.addSearchProcess),
|
||||
map((action) =>
|
||||
// Placeholder for getting values from Backend
|
||||
actions.fetchFiltersDone({ id: action.id, response: mockFilters })
|
||||
ofType(actions.fetchFilters),
|
||||
flatMap((action) =>
|
||||
this.orderService.OrderGetWarenausgabeFilterResponse().pipe(
|
||||
catchError((err) =>
|
||||
of<StrictHttpResponse<ResponseArgsOfIEnumerableOfInputDTO>>(err)
|
||||
),
|
||||
map((response) =>
|
||||
actions.fetchFiltersDone({ id: action.id, response })
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -99,9 +118,39 @@ export class SearchEffects {
|
||||
fetchFiltersDone$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(actions.fetchFiltersDone),
|
||||
map((action) =>
|
||||
actions.setSelectedFilters({ id: action.id, filters: action.response })
|
||||
)
|
||||
flatMap((action) => {
|
||||
if (action.response.ok) {
|
||||
const result = action.response.body;
|
||||
|
||||
const selectedFiltersRaw = result.result.filter(
|
||||
(filter) => !!filter.options
|
||||
);
|
||||
const primaryFiltersRaw = result.result.filter((filter) =>
|
||||
isNullOrUndefined(filter.options)
|
||||
);
|
||||
|
||||
const selectFilters: SelectFilter[] = selectedFiltersRaw.map(
|
||||
(input) => inputDtoToSelectedFilters(input)
|
||||
);
|
||||
|
||||
const primaryFilters: PrimaryFilterOption[] = primaryFiltersRaw.map(
|
||||
(input) => inputDtoToPrimaryFilterOption(input)
|
||||
);
|
||||
|
||||
return [
|
||||
actions.setSelectFilters({
|
||||
id: action.id,
|
||||
filters: selectFilters,
|
||||
}),
|
||||
actions.setPrimaryFilters({
|
||||
id: action.id,
|
||||
filters: primaryFilters,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
return NEVER;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
@@ -113,6 +162,8 @@ export class SearchEffects {
|
||||
this.store.dispatch(
|
||||
actions.addSearchProcess({ id: action.payload.id })
|
||||
);
|
||||
|
||||
this.searchStateFacade.fetchFilters(action.payload.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { SearchStateFacade } from './search.facade';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { Store as NgxsStore } from '@ngxs/store';
|
||||
import { primaryFiltersMock } from 'apps/sales/src/app/modules/shelf/shared/mockdata';
|
||||
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
|
||||
import { of, Observable } from 'rxjs';
|
||||
import * as actions from './search.actions';
|
||||
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
fdescribe('SearchFacade', () => {
|
||||
let facade: SearchStateFacade;
|
||||
let store: Store<any>;
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [StoreModule.forRoot({})],
|
||||
providers: [Store, { provide: NgxsStore, useValue: NgxsStore }],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
facade = TestBed.get(SearchStateFacade);
|
||||
store = TestBed.get(Store);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(facade).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('setPrimaryFilters', () => {
|
||||
const primaryFilterOption: PrimaryFilterOption = {
|
||||
...primaryFiltersMock[0],
|
||||
selected: true,
|
||||
};
|
||||
const id = 123;
|
||||
|
||||
it('should dispatch setPrimary in Store', async () => {
|
||||
spyOn(store, 'dispatch').and.callThrough();
|
||||
spyOn(facade, 'setPrimaryFilters').and.callThrough();
|
||||
spyOn(facade, 'getPrimaryFilters').and.returnValue(
|
||||
of(primaryFiltersMock)
|
||||
);
|
||||
|
||||
await facade.setPrimaryFilters(primaryFilterOption, id);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should keep the original order of filter when dispatching setPrimary in Store', async () => {
|
||||
spyOn(store, 'dispatch').and.callThrough();
|
||||
spyOn(facade, 'setPrimaryFilters').and.callThrough();
|
||||
spyOn(facade, 'getPrimaryFilters').and.returnValue(
|
||||
of(primaryFiltersMock)
|
||||
);
|
||||
|
||||
await facade.setPrimaryFilters(primaryFilterOption, id);
|
||||
|
||||
const updatedFilters = [primaryFilterOption, primaryFiltersMock[1]];
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
actions.setPrimaryFilters({ filters: updatedFilters, id })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectedFilter$', () => {
|
||||
it('should return all selected filters from store', async () => {
|
||||
const currentFilters: Observable<SelectFilter[]> = of([
|
||||
{
|
||||
type: 'select',
|
||||
key: 'key1',
|
||||
name: 'Filter Name 1',
|
||||
options: [
|
||||
{
|
||||
key: 'Option 1',
|
||||
id: 'Option 1',
|
||||
name: 'Option 1',
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
key: 'Option 2',
|
||||
id: 'Option 2',
|
||||
name: 'Option 2',
|
||||
selected: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
const primaryFilters: Observable<PrimaryFilterOption[]> = of([
|
||||
{
|
||||
id: 'primary1',
|
||||
key: 'primary1',
|
||||
name: 'Primary Filter 1',
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
id: 'primary2',
|
||||
key: 'primary2',
|
||||
name: 'Primary Filter 2',
|
||||
selected: false,
|
||||
},
|
||||
]);
|
||||
spyOnProperty(facade, 'selectFilters$', 'get').and.returnValue(
|
||||
currentFilters
|
||||
);
|
||||
spyOnProperty(facade, 'primaryFilters$', 'get').and.returnValue(
|
||||
primaryFilters
|
||||
);
|
||||
|
||||
const result = await facade.selectedFilter$.pipe(first()).toPromise();
|
||||
|
||||
expect(result).toEqual({
|
||||
key1: 'Option 1',
|
||||
primary1: 'true',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,10 +5,15 @@ import * as actions from './search.actions';
|
||||
import * as selectors from './search.selectors';
|
||||
import { switchMap, filter, map, first } from 'rxjs/operators';
|
||||
import { SharedSelectors } from 'apps/sales/src/app/core/store/selectors/shared.selectors';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import {
|
||||
selectFiltersToFiltersDictionary,
|
||||
primaryFiltersToFiltersDictionary,
|
||||
} from './mappers';
|
||||
import { Observable } from 'rxjs';
|
||||
import { isNullOrUndefined } from 'util';
|
||||
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
|
||||
import { ShelfPrimaryFilterOptions } from 'apps/sales/src/app/modules/shelf/defs';
|
||||
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SearchStateFacade {
|
||||
@@ -35,22 +40,55 @@ export class SearchStateFacade {
|
||||
return this.getProcessId$().pipe(switchMap((id) => this.getFetching$(id)));
|
||||
}
|
||||
|
||||
get primaryFilters$() {
|
||||
return this.process$.pipe(switchMap((process) => this.getPrimaryFilters(process.id)));
|
||||
get primaryFilters$(): Observable<PrimaryFilterOption[]> {
|
||||
return this.process$.pipe(
|
||||
switchMap((process) => this.getPrimaryFilters(process.id))
|
||||
);
|
||||
}
|
||||
|
||||
get currentFilter$(): Observable<SelectFilter[]> {
|
||||
get selectFilters$(): Observable<SelectFilter[]> {
|
||||
return this.process$.pipe(
|
||||
filter((process) => !isNullOrUndefined(process) && !isNullOrUndefined(process.filters)),
|
||||
filter(
|
||||
(process) =>
|
||||
!isNullOrUndefined(process) && !isNullOrUndefined(process.filters)
|
||||
),
|
||||
map((process) => process.filters.selectedFilters)
|
||||
);
|
||||
}
|
||||
|
||||
get selectedFilter$(): Observable<string[]> {
|
||||
return this.currentFilter$.pipe(
|
||||
map(() => {
|
||||
// TODO extracting all selected filters possibly required for building search query
|
||||
return [];
|
||||
get hasActiveFilters$(): Observable<boolean> {
|
||||
return this.selectedFilter$.pipe(
|
||||
map((selectedFilters) => Object.values(selectedFilters)),
|
||||
map((filterValues) =>
|
||||
filterValues.reduce((acc, curr) => [...acc, curr], [])
|
||||
),
|
||||
map((flattenedFilters) => !!flattenedFilters.length)
|
||||
);
|
||||
}
|
||||
|
||||
get selectedFilter$(): Observable<{ [key: string]: string }> {
|
||||
return combineLatest([this.selectFilters$, this.primaryFilters$]).pipe(
|
||||
map(([selectFilters, primaryFilters]) => {
|
||||
const selectFilterDictionary = selectFiltersToFiltersDictionary(
|
||||
selectFilters
|
||||
);
|
||||
const primaryFiltersDictionary = primaryFiltersToFiltersDictionary(
|
||||
primaryFilters
|
||||
);
|
||||
|
||||
const mergedFilters = {
|
||||
...selectFilterDictionary,
|
||||
...primaryFiltersDictionary,
|
||||
};
|
||||
|
||||
const selectedFilters = {};
|
||||
for (const f in mergedFilters) {
|
||||
if (!!mergedFilters[f]) {
|
||||
selectedFilters[f] = mergedFilters[f];
|
||||
}
|
||||
}
|
||||
|
||||
return selectedFilters;
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -88,7 +126,7 @@ export class SearchStateFacade {
|
||||
return this.store.select(selectors.selectFetching, id);
|
||||
}
|
||||
|
||||
getPrimaryFilters(id: number): Observable<ShelfPrimaryFilterOptions> {
|
||||
getPrimaryFilters(id: number): Observable<PrimaryFilterOption[]> {
|
||||
return this.store.select(selectors.selectPrimaryFilters, id);
|
||||
}
|
||||
|
||||
@@ -104,30 +142,42 @@ export class SearchStateFacade {
|
||||
|
||||
async setSelectedFilters(filters: SelectFilter[], id?: number) {
|
||||
if (id) {
|
||||
return this.store.dispatch(actions.setSelectedFilters({ filters, id }));
|
||||
return this.store.dispatch(actions.setSelectFilters({ filters, id }));
|
||||
}
|
||||
|
||||
const processId = await this.getProcessId();
|
||||
|
||||
this.store.dispatch(actions.setSelectedFilters({ filters, id: processId }));
|
||||
this.store.dispatch(actions.setSelectFilters({ filters, id: processId }));
|
||||
}
|
||||
|
||||
async setPrimaryFilters(filters: Partial<ShelfPrimaryFilterOptions>, id?: number) {
|
||||
let updatedFilters: ShelfPrimaryFilterOptions;
|
||||
async setPrimaryFilters(primaryFilter: PrimaryFilterOption, id?: number) {
|
||||
let updatedFilters: PrimaryFilterOption[];
|
||||
let processId = id;
|
||||
|
||||
if (!id) {
|
||||
processId = await this.getProcessId();
|
||||
}
|
||||
|
||||
const currentPrimaryFilters = await this.getPrimaryFilters(processId).pipe(first()).toPromise();
|
||||
const currentPrimaryFilters = await this.getPrimaryFilters(processId)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
updatedFilters = {
|
||||
...currentPrimaryFilters,
|
||||
...filters,
|
||||
} as ShelfPrimaryFilterOptions;
|
||||
const indexOfUpdatedFilter = currentPrimaryFilters.findIndex(
|
||||
(f) => f.id === primaryFilter.id
|
||||
);
|
||||
|
||||
return this.store.dispatch(actions.setPrimaryFilters({ filters: updatedFilters, id: processId }));
|
||||
updatedFilters = [
|
||||
...currentPrimaryFilters.slice(0, indexOfUpdatedFilter),
|
||||
primaryFilter,
|
||||
...currentPrimaryFilters.slice(
|
||||
indexOfUpdatedFilter + 1,
|
||||
currentPrimaryFilters.length + 1
|
||||
),
|
||||
];
|
||||
|
||||
return this.store.dispatch(
|
||||
actions.setPrimaryFilters({ filters: updatedFilters, id: processId })
|
||||
);
|
||||
}
|
||||
|
||||
async fetchResult(id?: number) {
|
||||
@@ -135,6 +185,7 @@ export class SearchStateFacade {
|
||||
if (typeof processId !== 'number') {
|
||||
processId = await this.getProcessId();
|
||||
}
|
||||
|
||||
this.store.dispatch(actions.fetchResult({ id: processId }));
|
||||
}
|
||||
|
||||
@@ -145,4 +196,14 @@ export class SearchStateFacade {
|
||||
}
|
||||
this.store.dispatch(actions.clearResults({ id: processId }));
|
||||
}
|
||||
|
||||
async fetchFilters(id?: number) {
|
||||
let processId = id;
|
||||
|
||||
if (typeof processId !== 'number') {
|
||||
processId = await this.getProcessId();
|
||||
}
|
||||
|
||||
this.store.dispatch(actions.fetchFilters({ id: processId }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { SearchState, INITIAL_SEARCH_STATE } from './search.state';
|
||||
import { TypedAction } from '@ngrx/store/src/models';
|
||||
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
|
||||
import { primaryFiltersMock } from 'apps/sales/src/app/modules/shelf/shared/mockdata';
|
||||
import * as actions from './search.actions';
|
||||
import { searchReducer } from './search.reducer';
|
||||
|
||||
fdescribe('#SearchStateReducer', () => {
|
||||
const id = 123;
|
||||
let initialState: SearchState;
|
||||
|
||||
beforeEach(() => {
|
||||
const action = actions.addSearchProcess({ id });
|
||||
initialState = searchReducer(INITIAL_SEARCH_STATE, action);
|
||||
});
|
||||
|
||||
describe('setPrimaryFilters', () => {
|
||||
const filters = primaryFiltersMock;
|
||||
let action: {
|
||||
id: number;
|
||||
filters: PrimaryFilterOption[];
|
||||
} & TypedAction<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
action = actions.setPrimaryFilters({ id, filters });
|
||||
});
|
||||
|
||||
it('should update the primary filters', () => {
|
||||
const state = searchReducer(initialState, action);
|
||||
|
||||
const entity = state.entities[id];
|
||||
|
||||
expect(entity).toBeTruthy();
|
||||
expect(entity.filters.primaryFilters).toEqual(primaryFiltersMock);
|
||||
});
|
||||
|
||||
it('should keep the original order of primary filter items', () => {
|
||||
const state = searchReducer(initialState, action);
|
||||
|
||||
const entity = state.entities[id];
|
||||
|
||||
expect(entity.filters.primaryFilters[0]).toEqual(primaryFiltersMock[0]);
|
||||
expect(entity.filters.primaryFilters[1]).toEqual(primaryFiltersMock[1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,24 @@
|
||||
import { createReducer, Action, on } from '@ngrx/store';
|
||||
import { INITIAL_SEARCH_STATE, SearchState, searchStateAdapter, INITIAL_SEARCH_PROCESS } from './search.state';
|
||||
import {
|
||||
INITIAL_SEARCH_STATE,
|
||||
SearchState,
|
||||
searchStateAdapter,
|
||||
INITIAL_SEARCH_PROCESS,
|
||||
} from './search.state';
|
||||
import * as actions from './search.actions';
|
||||
|
||||
const _searchReducer = createReducer(
|
||||
INITIAL_SEARCH_STATE,
|
||||
on(actions.addSearchProcess, (s, a) => searchStateAdapter.addOne({ ...INITIAL_SEARCH_PROCESS, id: a.id }, s)),
|
||||
on(actions.removeSearchProcess, (s, a) => searchStateAdapter.removeOne(a.id, s)),
|
||||
on(actions.setInput, (s, a) => searchStateAdapter.updateOne({ id: a.id, changes: { input: a.input } }, s)),
|
||||
on(actions.setSelectedFilters, (s, a) =>
|
||||
on(actions.addSearchProcess, (s, a) =>
|
||||
searchStateAdapter.addOne({ ...INITIAL_SEARCH_PROCESS, id: a.id }, s)
|
||||
),
|
||||
on(actions.removeSearchProcess, (s, a) =>
|
||||
searchStateAdapter.removeOne(a.id, s)
|
||||
),
|
||||
on(actions.setInput, (s, a) =>
|
||||
searchStateAdapter.updateOne({ id: a.id, changes: { input: a.input } }, s)
|
||||
),
|
||||
on(actions.setSelectFilters, (s, a) =>
|
||||
searchStateAdapter.updateOne(
|
||||
{
|
||||
id: a.id,
|
||||
@@ -32,7 +43,9 @@ const _searchReducer = createReducer(
|
||||
s
|
||||
)
|
||||
),
|
||||
on(actions.clearResults, (s, a) => searchStateAdapter.updateOne({ id: a.id, changes: { result: [] } }, s)),
|
||||
on(actions.clearResults, (s, a) =>
|
||||
searchStateAdapter.updateOne({ id: a.id, changes: { result: [] } }, s)
|
||||
),
|
||||
on(actions.addResult, (s, a) =>
|
||||
searchStateAdapter.updateOne(
|
||||
{
|
||||
@@ -42,10 +55,18 @@ const _searchReducer = createReducer(
|
||||
s
|
||||
)
|
||||
),
|
||||
on(actions.clearHits, (s, a) => searchStateAdapter.updateOne({ id: a.id, changes: { hits: undefined } }, s)),
|
||||
on(actions.setHits, (s, a) => searchStateAdapter.updateOne({ id: a.id, changes: { hits: a.hits } }, s)),
|
||||
on(actions.fetchResult, (s, a) => searchStateAdapter.updateOne({ id: a.id, changes: { fetching: true } }, s)),
|
||||
on(actions.fetchResultDone, (s, a) => searchStateAdapter.updateOne({ id: a.id, changes: { fetching: false } }, s))
|
||||
on(actions.clearHits, (s, a) =>
|
||||
searchStateAdapter.updateOne({ id: a.id, changes: { hits: undefined } }, s)
|
||||
),
|
||||
on(actions.setHits, (s, a) =>
|
||||
searchStateAdapter.updateOne({ id: a.id, changes: { hits: a.hits } }, s)
|
||||
),
|
||||
on(actions.fetchResult, (s, a) =>
|
||||
searchStateAdapter.updateOne({ id: a.id, changes: { fetching: true } }, s)
|
||||
),
|
||||
on(actions.fetchResultDone, (s, a) =>
|
||||
searchStateAdapter.updateOne({ id: a.id, changes: { fetching: false } }, s)
|
||||
)
|
||||
);
|
||||
|
||||
export function searchReducer(state: SearchState, action: Action) {
|
||||
|
||||
@@ -4,28 +4,58 @@ import { searchStateAdapter } from './search.state';
|
||||
import { Dictionary } from '@ngrx/entity';
|
||||
import { SearchProcess } from './defs';
|
||||
|
||||
export const selectSearchState = createSelector(selectShelfState, (s) => s.search);
|
||||
export const selectSearchState = createSelector(
|
||||
selectShelfState,
|
||||
(s) => s.search
|
||||
);
|
||||
|
||||
export const { selectAll, selectEntities } = searchStateAdapter.getSelectors(selectSearchState);
|
||||
export const { selectAll, selectEntities } = searchStateAdapter.getSelectors(
|
||||
selectSearchState
|
||||
);
|
||||
|
||||
export const selectProcess = createSelector(selectEntities, (entities: Dictionary<SearchProcess>, id: number) => entities[id]);
|
||||
export const selectProcess = createSelector(
|
||||
selectEntities,
|
||||
(entities: Dictionary<SearchProcess>, id: number) => entities[id]
|
||||
);
|
||||
|
||||
export const selectInput = createSelector(selectEntities, (entities: Dictionary<SearchProcess>, id: number) => entities[id].input);
|
||||
export const selectInput = createSelector(
|
||||
selectEntities,
|
||||
(entities: Dictionary<SearchProcess>, id: number) =>
|
||||
entities[id] && entities[id].input
|
||||
);
|
||||
|
||||
export const selectResult = createSelector(selectEntities, (entities: Dictionary<SearchProcess>, id: number) => entities[id].result);
|
||||
export const selectResult = createSelector(
|
||||
selectEntities,
|
||||
(entities: Dictionary<SearchProcess>, id: number) =>
|
||||
entities[id] && entities[id].result
|
||||
);
|
||||
|
||||
export const selectHits = createSelector(selectEntities, (entities: Dictionary<SearchProcess>, id: number) => entities[id].hits);
|
||||
export const selectHits = createSelector(
|
||||
selectEntities,
|
||||
(entities: Dictionary<SearchProcess>, id: number) =>
|
||||
entities[id] && entities[id].hits
|
||||
);
|
||||
|
||||
export const selectFetching = createSelector(selectEntities, (entities: Dictionary<SearchProcess>, id: number) => entities[id].fetching);
|
||||
export const selectFetching = createSelector(
|
||||
selectEntities,
|
||||
(entities: Dictionary<SearchProcess>, id: number) =>
|
||||
entities[id] && entities[id].fetching
|
||||
);
|
||||
|
||||
export const selectFilters = createSelector(selectEntities, (entities: Dictionary<SearchProcess>, id: number) => entities[id].filters);
|
||||
export const selectFilters = createSelector(
|
||||
selectEntities,
|
||||
(entities: Dictionary<SearchProcess>, id: number) =>
|
||||
entities[id] && entities[id].filters
|
||||
);
|
||||
|
||||
export const selectSelectedFilters = createSelector(
|
||||
selectEntities,
|
||||
(entities: Dictionary<SearchProcess>, id: number) => entities[id].filters.selectedFilters
|
||||
(entities: Dictionary<SearchProcess>, id: number) =>
|
||||
entities[id] && entities[id].filters.selectedFilters
|
||||
);
|
||||
|
||||
export const selectPrimaryFilters = createSelector(
|
||||
selectEntities,
|
||||
(entities: Dictionary<SearchProcess>, id: number) => entities[id].filters.primaryFilters
|
||||
(entities: Dictionary<SearchProcess>, id: number) =>
|
||||
entities[id] && entities[id].filters.primaryFilters
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EntityState, createEntityAdapter } from '@ngrx/entity';
|
||||
import { SearchProcess } from './defs';
|
||||
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
|
||||
import { ShelfPrimaryFilterOptions } from 'apps/sales/src/app/modules/shelf/defs';
|
||||
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
|
||||
|
||||
export interface SearchState extends EntityState<SearchProcess> {}
|
||||
|
||||
@@ -11,19 +11,12 @@ export const INITIAL_SEARCH_STATE: SearchState = {
|
||||
...searchStateAdapter.getInitialState(),
|
||||
};
|
||||
|
||||
export const INITIAL_PRIMARY_FILTERS: ShelfPrimaryFilterOptions = {
|
||||
allBranches: false,
|
||||
customerName: false,
|
||||
author: false,
|
||||
title: false,
|
||||
};
|
||||
|
||||
export const INITIAL_FILTERS: {
|
||||
selectedFilters?: SelectFilter[];
|
||||
primaryFilters?: ShelfPrimaryFilterOptions;
|
||||
primaryFilters?: PrimaryFilterOption[];
|
||||
} = {
|
||||
selectedFilters: [],
|
||||
primaryFilters: INITIAL_PRIMARY_FILTERS,
|
||||
primaryFilters: [],
|
||||
};
|
||||
|
||||
export const INITIAL_SEARCH_PROCESS: SearchProcess = {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { searchReducer } from './search';
|
||||
import { historyReducer } from './history';
|
||||
|
||||
const _shelfReducer = combineReducers<ShelfState>({
|
||||
search: searchReducer,
|
||||
history: historyReducer,
|
||||
search: searchReducer,
|
||||
});
|
||||
|
||||
export function shelfReducer(state: ShelfState, action: Action) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@import 'variables';
|
||||
|
||||
.isa-form-group {
|
||||
font-family: $font-family;
|
||||
font-weight: $font-weight;
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto 1fr;
|
||||
|
||||
@include mq-desktop() {
|
||||
position: inherit;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.isa-filter-title {
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
|
||||
17
display-addressee-dto.d.ts
vendored
Normal file
17
display-addressee-dto.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Gender } from './dist/swagger/oms/lib/models/gender';
|
||||
import { OrganisationDTO } from './dist/swagger/oms/lib/models/organisation-dto';
|
||||
import { AddressDTO } from './dist/swagger/oms/lib/models/address-dto';
|
||||
import { CommunicationDetailsDTO } from './dist/swagger/oms/lib/models/communication-details-dto';
|
||||
import { ExternalReferenceDTO } from './dist/swagger/oms/lib/models/external-reference-dto';
|
||||
export interface DisplayAddresseeDTO {
|
||||
number?: string;
|
||||
locale?: string;
|
||||
gender: Gender;
|
||||
title?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
organisation?: OrganisationDTO;
|
||||
address?: AddressDTO;
|
||||
communicationDetails?: CommunicationDetailsDTO;
|
||||
externalReference?: ExternalReferenceDTO;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Output,
|
||||
EventEmitter,
|
||||
Input,
|
||||
ChangeDetectorRef,
|
||||
} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FilterButtonComponent {
|
||||
@Input() active = false;
|
||||
@Input() active = true;
|
||||
@Input() module: 'Branch' | 'Customer' = 'Branch';
|
||||
@Output() toggleFilter = new EventEmitter();
|
||||
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -9137,6 +9137,15 @@
|
||||
"integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=",
|
||||
"dev": true
|
||||
},
|
||||
"jasmine-marbles": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://pkgs.dev.azure.com/hugendubel/_packaging/hugendubel@Local/npm/registry/jasmine-marbles/-/jasmine-marbles-0.6.0.tgz",
|
||||
"integrity": "sha1-943Bo7xFKXbeEO6LR8c9YWUyqVQ=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"jasmine-spec-reporter": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz",
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
"@types/node": "~8.9.4",
|
||||
"codelyzer": "~4.5.0",
|
||||
"jasmine-core": "~2.99.1",
|
||||
"jasmine-marbles": "^0.6.0",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "^4.0.1",
|
||||
"karma-chrome-launcher": "~2.2.0",
|
||||
|
||||
Reference in New Issue
Block a user