Merged PR 209: #667 #668 #772 #683 #685 #773 Autocomplete &Filters from Backend

Related work items: #667, #668, #683, #685, #772, #773
This commit is contained in:
Sebastian Neumair
2020-07-07 08:43:06 +00:00
committed by Lorenz Hilpert
66 changed files with 1380 additions and 353 deletions

View File

@@ -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/**"]
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,6 @@
@import 'variables';
@import 'mixins/media';
:host {
display: block;
}

View File

@@ -0,0 +1 @@
export type FilterType = 'select' | 'text' | 'date' | 'number' | 'checkbox';

View File

@@ -4,3 +4,4 @@ export * from './filter-base';
export * from './select-filter';
export * from './select-option';
export * from './filter';
export * from './filter-type';

View File

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

View File

@@ -11,4 +11,6 @@
top: 61px;
left: 0;
right: 0;
z-index: 2;
}

View File

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

View File

@@ -1,3 +1,3 @@
<div class="item isa-font-lightgrey" (click)="clicked.emit(result)">
{{ result.firstName }} {{ result.lastName }}
{{ result.display }}
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,2 @@
<div>Order Item Id: {{ orderItemId$ | async }}</div>
<pre>{{ history$ | async | json }}</pre>
<pre>{{ status$ | async | json }}</pre>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
// start:ng42.barrel
export * from './filters.mock';
export * from './primary-filters.mock';
// end:ng42.barrel

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,3 @@
@import 'variables';
.isa-form-group {
font-family: $font-family;
font-weight: $font-weight;

View File

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

View File

@@ -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
View File

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

View File

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