Merged PR 215: #816 Remove HitsOnly Call & Retrieve first Order Results on SearchPage

Fix Order Service Call to Get Warenausgabe Results

Related work items: #816
This commit is contained in:
Sebastian Neumair
2020-07-09 01:28:44 +00:00
committed by Lorenz Hilpert
20 changed files with 365 additions and 199 deletions

View File

@@ -31,7 +31,9 @@
*ngSwitchDefault
#submitButton
class="isa-input-submit"
[class.scan]="!errorMessage && !(searchQuery$ | async) && isIPad"
[class.scan]="
isIPad | appShowScanButton: (searchQuery$ | async):errorMessage
"
type="submit"
(click)="handleBtnClick(submitButton)"
></button>

View File

@@ -3,7 +3,11 @@ import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { SearchInputModule } from '@libs/ui';
import { FocusDirective } from '../../shared/directives';
import { TruncateTextPipe, ShowSearchResetPipe } from '../../shared/pipes';
import {
TruncateTextPipe,
ShowSearchResetPipe,
ShowScanButtonPipe,
} from '../../shared/pipes';
import { ShelfSearchbarComponent } from './searchbar.component';
@NgModule({
@@ -12,12 +16,14 @@ import { ShelfSearchbarComponent } from './searchbar.component';
ShelfSearchbarComponent,
ShowSearchResetPipe,
TruncateTextPipe,
ShowScanButtonPipe,
FocusDirective,
],
declarations: [
ShelfSearchbarComponent,
ShowSearchResetPipe,
TruncateTextPipe,
ShowScanButtonPipe,
FocusDirective,
],
providers: [],

View File

@@ -156,7 +156,6 @@ export class ShelfFilterComponent implements OnInit, OnDestroy {
type: 'search',
value: searchbarValue,
bypassValidation: true,
closeOverlay: () => this.close(),
});
}
}

View File

@@ -6,6 +6,7 @@ import {
OnInit,
ViewChild,
ElementRef,
ChangeDetectionStrategy,
} from '@angular/core';
import { SearchStateFacade } from 'apps/sales/src/app/store/customer';
import { first, takeUntil, map } from 'rxjs/operators';
@@ -15,7 +16,7 @@ import { groupBy } from 'apps/sales/src/app/utils';
selector: 'app-shelf-search-results',
templateUrl: './shelf-search-results.component.html',
styleUrls: ['./shelf-search-results.component.scss'],
// changeDetection: ChangeDetectionStrategy.OnPush,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShelfSearchResultsComponent implements OnInit, OnDestroy {
@ViewChild('scroll', { static: true })
@@ -23,17 +24,17 @@ export class ShelfSearchResultsComponent implements OnInit, OnDestroy {
destroy$ = new Subject();
grouped$ = this.searchStateFacade.result$.pipe(map((results) => groupBy(results, (item) => item.buyerNumber)));
grouped$ = this.searchStateFacade.result$.pipe(
map((results) => groupBy(results, (item) => item.buyerNumber))
);
fetching$ = this.searchStateFacade.fetching$;
constructor(private searchStateFacade: SearchStateFacade) {
this.searchStateFacade.clearResult();
}
constructor(private searchStateFacade: SearchStateFacade) {}
ngOnInit() {
this.initScrollContainer();
this.fetch(true);
this.fetch();
}
initScrollContainer() {
@@ -66,7 +67,7 @@ export class ShelfSearchResultsComponent implements OnInit, OnDestroy {
.pipe(first())
.toPromise();
if (force || (hits > result.length && !fetching)) {
if (force || !hits || (!result.length && !fetching)) {
this.searchStateFacade.fetchResult();
}
}

View File

@@ -1,6 +1,6 @@
<div class="search-container">
<app-shelf-searchbar
[isFetchingData]="isFetchingData$ | async"
[isFetchingData]="isFetching$ | async"
[errorMessage]="errorMessage$ | async"
[isIPad]="allowScan"
[mode]="searchMode$ | async"

View File

@@ -7,15 +7,19 @@ import {
ViewChild,
AfterViewInit,
} from '@angular/core';
import { Subject, BehaviorSubject, Observable, of, NEVER } from 'rxjs';
import {
Subject,
BehaviorSubject,
Observable,
of,
NEVER,
Subscription,
} from 'rxjs';
import {
ShelfSearchFacadeService,
ShelfNavigationService,
} from '../../../shared/services';
import {
ListResponseArgsOfOrderItemListItemDTO,
AutocompleteDTO,
} from '@swagger/oms';
import { AutocompleteDTO, OrderItemListItemDTO } from '@swagger/oms';
import {
distinctUntilChanged,
debounceTime,
@@ -23,10 +27,10 @@ import {
map,
takeUntil,
filter,
first,
shareReplay,
tap,
withLatestFrom,
startWith,
take,
} from 'rxjs/operators';
import { SHELF_SCROLL_INDEX } from 'apps/sales/src/app/core/utils/app.constants';
import { ShelfSearchbarComponent } from '../../../components';
@@ -34,7 +38,6 @@ import { ShelfFilterService } from '../../../services/shelf-filter.service';
import { SearchStateFacade } from '@shelf-store';
import { CollectingShelfScannerScanditComponent } from 'shared/public_api';
import { AutocompleteOptions } from '../../../defs';
import { ClickOutsideDirective } from 'apps/sales/src/app/shared/directives';
@Component({
selector: 'app-shelf-search-input',
@@ -44,6 +47,9 @@ import { ClickOutsideDirective } from 'apps/sales/src/app/shared/directives';
})
export class ShelfSearchInputComponent
implements OnInit, AfterViewInit, OnDestroy {
NO_RESULT_ERROR_MESSAGE = 'Ergibt keine Suchergebnisse';
GENERAL_ERROR_MESSAGE = 'Ein Fehler ist aufgetreten';
@Input() allowScan = false;
@Input() hasAutocomplete = true;
@Input() searchCallback: () => void;
@@ -55,9 +61,9 @@ export class ShelfSearchInputComponent
destroy$ = new Subject();
isFetchingData$ = new BehaviorSubject<boolean>(false);
errorMessage$ = new BehaviorSubject<string>('');
isFetching$: Observable<boolean>;
errorMessage$: Observable<string>;
hits$ = new Observable();
autocompleteQueryString$ = new BehaviorSubject<string>('');
autocompleteResult$: Observable<AutocompleteDTO[]>;
@@ -79,12 +85,14 @@ export class ShelfSearchInputComponent
) {}
ngOnInit() {
this.clearPreviousResults();
if (this.hasAutocomplete) {
this.setupAutocompletion();
}
this.setUpSearchMode();
this.initSelectedFilters();
this.initStore();
}
ngAfterViewInit() {
@@ -97,9 +105,8 @@ export class ShelfSearchInputComponent
}
reset() {
this.setIsFetchingData(false);
this.resetQueryString();
this.resetError();
this.searchStateFacade.clearError();
}
triggerBarcodeSearch(barcode: string) {
@@ -111,70 +118,27 @@ export class ShelfSearchInputComponent
type,
value,
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.isAutocompleteSelected()) {
searchQuery = this.selectedItem$.value.query;
result$ = this.selectedFilters$.pipe(
debounceTime(250),
first(),
switchMap((selectedFilters) =>
this.shelfSearchService.search(searchQuery, {
hitsOnly: true,
bypassValidation,
selectedFilters,
})
)
);
this.shelfSearchService.search(searchQuery, { bypassValidation });
this.updateSearchbarValue(searchQuery);
} else if (type === 'search') {
result$ = this.selectedFilters$.pipe(
debounceTime(250),
first(),
switchMap((selectedFilters) =>
this.shelfSearchService.search(value, {
hitsOnly: true,
bypassValidation,
selectedFilters,
})
)
);
this.shelfSearchService.search(value, { bypassValidation });
} else if (type === 'scan') {
result$ = this.shelfSearchService.searchWithBarcode(value);
this.shelfSearchService.searchWithBarcode(value);
}
result$
.pipe(
takeUntil(this.destroy$),
first(),
tap(() => this.searchStateFacade.setInput(searchQuery))
)
.subscribe(
(result) =>
this.handleSearchResult(
result,
searchQuery,
closeOverlay || this.searchCallback
),
() => this.handleSearchResultError()
);
}
openScanner() {
@@ -198,11 +162,6 @@ export class ShelfSearchInputComponent
handleAutocompleteAction(selectItem: AutocompleteDTO & AutocompleteOptions) {
const oldValue = this.selectedItem$.value && this.selectedItem$.value.query;
const newValue = selectItem.query;
console.log({
oldValue,
newValue,
shouldOverwrite: this.isAutocompleteOverwritten(oldValue, newValue),
});
if (this.isAutocompleteOverwritten(oldValue, newValue)) {
return this.setSelectedAutocompleteItem(null);
@@ -214,52 +173,14 @@ export class ShelfSearchInputComponent
}
}
private handleSearchResult(
result: ListResponseArgsOfOrderItemListItemDTO,
value: string,
successCB?: () => void
) {
this.setIsFetchingData(false);
if (this.hasNoResult(result)) {
return this.setErrorMessage('Ergibt keine Suchergebnisse');
} else if (this.hasMultipleSearchResults(result)) {
this.shelfNavigationService.navigateToResultList({
searchQuery: value,
numberOfHits: result.hits,
});
this.resetSessionStorage();
} else {
this.shelfNavigationService.navigateToDetails(this.getDetails(result));
}
if (successCB) {
successCB();
}
}
private hasNoResult(result: ListResponseArgsOfOrderItemListItemDTO): boolean {
return !!result.result && !result.result.length;
}
private hasMultipleSearchResults(
result: ListResponseArgsOfOrderItemListItemDTO
): boolean {
return !!result.result && result.result.length > 1;
private clearPreviousResults() {
this.searchStateFacade.clearResult();
}
private resetSessionStorage(key: string = SHELF_SCROLL_INDEX) {
sessionStorage.removeItem(key);
}
private handleSearchResultError(errorCB?: () => void) {
this.setErrorMessage('Ein Fehler ist aufgetreten');
this.setIsFetchingData(false);
if (errorCB) {
errorCB();
}
}
private setupAutocompletion() {
this.autocompleteResult$ = this.autocompleteQueryString$.pipe(
debounceTime(250),
@@ -297,16 +218,70 @@ export class ShelfSearchInputComponent
this.selectedFilters$ = this.searchStateFacade.selectedFilter$;
}
private initStore() {
this.isFetching$ = this.searchStateFacade.fetching$.pipe(debounceTime(25));
this.errorMessage$ = this.searchStateFacade.showNoResultError$.pipe(
map((showError) => (showError ? this.NO_RESULT_ERROR_MESSAGE : '')),
startWith('')
);
this.setUpNavigation();
this.setUpErrorCleaner();
}
private setUpNavigation() {
this.searchStateFacade.hasResults$
.pipe(
takeUntil(this.destroy$),
debounceTime(25),
filter((hasResults) => !!hasResults),
withLatestFrom(
this.searchStateFacade.hits$,
this.searchStateFacade.input$,
this.searchStateFacade.result$
),
take(1)
)
.subscribe(([_, numberOfHits, searchQuery, result]) => {
if (this.shouldNavigateToResultList(numberOfHits)) {
this.resetSessionStorage();
this.shelfNavigationService.navigateToResultList({
searchQuery,
numberOfHits,
});
}
if (this.shouldNavigateToDetails(numberOfHits)) {
this.shelfNavigationService.navigateToDetails(
this.getDetails(result)
);
}
if (this.searchCallback) {
this.searchCallback();
}
});
}
private setUpErrorCleaner() {
this.autocompleteQueryString$
.pipe(
takeUntil(this.destroy$),
withLatestFrom(this.isFetching$, this.errorMessage$),
filter(([_, isFetching, errorMessage]) =>
this.shouldClearError(isFetching, errorMessage)
)
)
.subscribe(() => this.searchStateFacade.clearError());
}
private setUpInputFocus() {
this.filterService.overlayClosed$
.pipe(takeUntil(this.destroy$))
.subscribe(() => this.setFocus());
}
private getDetails(result: ListResponseArgsOfOrderItemListItemDTO) {
return result.result && result.result[0];
}
private setFocus() {
if (this.searchbar) {
this.searchbar.setFocus('input');
@@ -317,18 +292,6 @@ export class ShelfSearchInputComponent
this.autocompleteQueryString$.next('');
}
private setIsFetchingData(isFetching: boolean) {
this.isFetchingData$.next(isFetching);
}
private resetError() {
this.setErrorMessage('');
}
private setErrorMessage(message: string) {
this.errorMessage$.next(message);
}
private setSelectedAutocompleteItem(
item: (AutocompleteDTO & AutocompleteOptions) | null
) {
@@ -382,4 +345,20 @@ export class ShelfSearchInputComponent
this.setFocus();
}
}
private shouldNavigateToResultList(hits: number): boolean {
return hits > 1;
}
private shouldNavigateToDetails(hits: number): boolean {
return hits === 1;
}
private shouldClearError(isFetching: boolean, errorMessage: string): boolean {
return !isFetching && !!errorMessage;
}
private getDetails(result: OrderItemListItemDTO[]) {
return result && result[0];
}
}

View File

@@ -1,5 +1,5 @@
// start:ng42.barrel
export * from './show-search-reset.pipe';
export * from './truncate.pipe';
export * from './show-scan-button.pipe';
// end:ng42.barrel

View File

@@ -0,0 +1,49 @@
import { ShowScanButtonPipe } from './show-scan-button.pipe';
fdescribe('#ShowScanButtonPipe', () => {
let pipe: ShowScanButtonPipe;
let isIpad: boolean;
let searchQuery: string;
let errorMessage: string;
beforeEach(() => {
pipe = new ShowScanButtonPipe();
});
it('it should not be shown on desktop', () => {
isIpad = false;
searchQuery = '';
errorMessage = '';
const result = pipe.transform(isIpad, searchQuery, errorMessage);
expect(result).toBe(false);
});
it('it should shown on iPad if no input is yet made and no error is present', () => {
isIpad = true;
searchQuery = '';
errorMessage = '';
const result = pipe.transform(isIpad, searchQuery, errorMessage);
expect(result).toBe(true);
});
it('it should not be shown on iPad if an input is made', () => {
isIpad = true;
searchQuery = 'Testkunde';
errorMessage = '';
const result = pipe.transform(isIpad, searchQuery, errorMessage);
expect(result).toBe(false);
});
it('it should not be shown on iPad if an input is made', () => {
isIpad = true;
searchQuery = '';
errorMessage = 'Unbekannter Fehler';
const result = pipe.transform(isIpad, searchQuery, errorMessage);
expect(result).toBe(false);
});
});

View File

@@ -0,0 +1,22 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'appShowScanButton',
})
export class ShowScanButtonPipe implements PipeTransform {
transform(
isIpad: boolean,
searchQuery: string,
errorMessage: string
): boolean {
if (!isIpad) {
return false;
}
if (!!searchQuery || !!errorMessage) {
return false;
}
return true;
}
}

View File

@@ -2,14 +2,14 @@ import { Injectable } from '@angular/core';
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 { switchMap } from 'rxjs/operators';
import { CollectingShelfService } from 'apps/sales/src/app/core/services/collecting-shelf.service';
import {
ListResponseArgsOfOrderItemListItemDTO,
AutocompleteTokenDTO,
ResponseArgsOfIEnumerableOfAutocompleteDTO,
} from '@swagger/oms/lib';
import { SearchStateFacade } from '@shelf-store';
@Injectable({ providedIn: 'root' })
export class ShelfSearchFacadeService {
@@ -17,77 +17,63 @@ export class ShelfSearchFacadeService {
string
>;
constructor(private collectingShelfService: CollectingShelfService) {}
constructor(
private collectingShelfService: CollectingShelfService,
private searchStateFacade: SearchStateFacade
) {}
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;
const searchQuery = queryString.trim();
const { bypassValidation } = options;
if (!bypassValidation && !this.isValidSearchQuery(searchParams)) {
if (!bypassValidation && !this.isValidSearchQuery(searchQuery)) {
return;
}
return this.requestSearch(searchParams, {
hitsOnly,
selectedFilters,
}).pipe(map(this.handleSearchResult));
this.searchStateFacade.setInput(searchQuery);
return this.requestSearch();
}
searchWithBarcode(barcode: string) {
const searchParams = this.getBarcodeSearchParams(barcode);
const searchQuery = this.getBarcodeSearchQuery(barcode);
if (!this.isValidSearchQuery(searchParams)) {
if (!this.isValidSearchQuery(searchQuery)) {
return;
}
return this.requestSearch(searchParams).pipe(map(this.handleSearchResult));
this.searchStateFacade.setInput(searchQuery);
return this.requestSearch();
}
searchForAutocomplete(
queryString: string,
options: { selectedFilters?: { [key: string]: string } } = {}
) {
const searchParams = queryString.trim();
const searchQuery = queryString.trim();
const autoCompleteQuery: AutocompleteTokenDTO = this.generateAutocompleteToken(
{ queryString, filter: options.selectedFilters || {}, take: 5 }
{
queryString: searchQuery,
filter: options.selectedFilters || {},
take: 5,
}
);
if (!this.isValidSearchQuery(searchParams)) {
if (!this.isValidSearchQuery(searchQuery)) {
return;
}
return this.requestAutocompleteSearch(autoCompleteQuery);
}
private requestSearch(
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,
selectedFilters: options.selectedFilters,
hitsOnly: options.hitsOnly,
})
)
);
private requestSearch() {
return this.searchStateFacade.fetchResult();
}
private generateAutocompleteToken(params: {
@@ -116,21 +102,11 @@ export class ShelfSearchFacadeService {
);
}
private handleSearchResult(result: ListResponseArgsOfOrderItemListItemDTO) {
if (!result) {
return {
result: [],
} as ListResponseArgsOfOrderItemListItemDTO;
}
return result;
}
private isValidSearchQuery(queryString: string): boolean {
return !!queryString && !!queryString.length;
}
private getBarcodeSearchParams(barcode: string) {
private getBarcodeSearchQuery(barcode: string) {
return barcode.replace('ORD:', '').trim();
}
}

View File

@@ -5,8 +5,6 @@ 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 {

View File

@@ -1 +1,2 @@
export * from './search-process';
export * from './search-process.state';

View File

@@ -0,0 +1,6 @@
export enum SearchProcessState {
INIT,
FETCHING,
FETCHED,
ERROR,
}

View File

@@ -1,6 +1,7 @@
import { OrderItemListItemDTO } from '@swagger/oms';
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
import { SearchProcessState } from '@shelf-store';
export interface SearchProcess {
id: number; // Prozess ID;
@@ -11,5 +12,6 @@ export interface SearchProcess {
};
result: OrderItemListItemDTO[];
hits?: number;
fetching: boolean;
state: SearchProcessState;
timestamp?: number;
}

View File

@@ -48,11 +48,21 @@ export const setInput = createAction(
props<{ id: number; input: string }>()
);
export const clearError = createAction(
`${prefix} Clear Error`,
props<{ id: number }>()
);
export const clearResults = createAction(
`${prefix} Clear Results`,
props<{ id: number }>()
);
export const reloadResults = createAction(
`${prefix} Reload Results`,
props<{ id: number }>()
);
export const addResult = createAction(
`${prefix} Add Result`,
props<{ id: number; result: OrderItemListItemDTO[] }>()
@@ -68,9 +78,14 @@ export const setHits = createAction(
props<{ id: number; hits: number }>()
);
export const setTimestamp = createAction(
`${prefix} Set Timestamp`,
props<{ id: number }>()
);
export const fetchResult = createAction(
`${prefix} Fetch Result`,
props<{ id: number }>()
props<{ id: number; skip?: number; take?: number }>()
);
export const fetchResultDone = createAction(

View File

@@ -38,13 +38,19 @@ export class SearchEffects {
private ngxsActions$: NgxsActions,
private store: Store<any>,
private searchStateFacade: SearchStateFacade,
private orderService: OrderService,
private branchService: BranchService
private orderService: OrderService
) {
this.initAddProcess();
this.initRemoveProcess();
}
reloadResults$ = createEffect(() =>
this.actions$.pipe(
ofType(actions.reloadResults),
map((a) => actions.fetchResult({ id: a.id, skip: 0 }))
)
);
fetchResults$ = createEffect(() =>
this.actions$.pipe(
ofType(actions.fetchResult),
@@ -56,16 +62,15 @@ export class SearchEffects {
),
flatMap(([_, process, filter]) =>
this.orderService
.OrderQueryOrderItemResponse({
branchNumber: this.branchService.getCurrentBranchNumber(),
.OrderWarenausgabeResponse({
input: {
qs: process.input,
},
filter: (filter as unknown) as {
[key: string]: string;
},
skip: process.result.length || 0,
take: 20,
skip: a.skip || process.result.length || 0,
take: a.take || 20,
})
.pipe(
catchError((err) =>
@@ -89,7 +94,14 @@ export class SearchEffects {
flatMap((action) => {
if (action.response.ok) {
const result = action.response.body;
const shouldSetTimestamp = !!result.skip;
return [
...(shouldSetTimestamp
? [
actions.setTimestamp({ id: action.id }),
actions.clearResults({ id: action.id }),
]
: []),
actions.setHits({ id: action.id, hits: result.hits }),
actions.addResult({ id: action.id, result: result.result }),
];

View File

@@ -3,7 +3,7 @@ import { Store } from '@ngrx/store';
import { Store as NgxsStore } from '@ngxs/store';
import * as actions from './search.actions';
import * as selectors from './search.selectors';
import { switchMap, filter, map, first } from 'rxjs/operators';
import { switchMap, filter, map, first, withLatestFrom } from 'rxjs/operators';
import { SharedSelectors } from 'apps/sales/src/app/core/store/selectors/shared.selectors';
import { combineLatest } from 'rxjs';
import {
@@ -14,6 +14,7 @@ import { Observable } from 'rxjs';
import { isNullOrUndefined } from 'util';
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
import { SearchProcessState } from './defs';
@Injectable({ providedIn: 'root' })
export class SearchStateFacade {
@@ -40,6 +41,29 @@ export class SearchStateFacade {
return this.getProcessId$().pipe(switchMap((id) => this.getFetching$(id)));
}
get state$() {
return this.getProcessId$().pipe(switchMap((id) => this.getState$(id)));
}
get hasError$() {
return this.getProcessId$().pipe(switchMap((id) => this.getHasError$(id)));
}
get hasResults$() {
return this.getProcessId$().pipe(switchMap((id) => this.getHasResult$(id)));
}
get showNoResultError$() {
return this.getProcessId$().pipe(
switchMap((id) =>
combineLatest([this.getState$(id), this.getHasResult$(id)])
),
map(([state, hasResults]) =>
!hasResults && state === SearchProcessState.FETCHED ? true : false
)
);
}
get primaryFilters$(): Observable<PrimaryFilterOption[]> {
return this.process$.pipe(
switchMap((process) => this.getPrimaryFilters(process.id))
@@ -122,10 +146,22 @@ export class SearchStateFacade {
return this.store.select(selectors.selectHits, id);
}
getState$(id: number) {
return this.store.select(selectors.selectState, id);
}
getFetching$(id: number) {
return this.store.select(selectors.selectFetching, id);
}
getHasResult$(id: number) {
return this.store.select(selectors.selectHasResults, id);
}
getHasError$(id: number) {
return this.store.select(selectors.selectHasError, id);
}
getPrimaryFilters(id: number): Observable<PrimaryFilterOption[]> {
return this.store.select(selectors.selectPrimaryFilters, id);
}
@@ -189,6 +225,14 @@ export class SearchStateFacade {
this.store.dispatch(actions.fetchResult({ id: processId }));
}
async clearError(id?: number) {
let processId = id;
if (typeof processId !== 'number') {
processId = await this.getProcessId();
}
this.store.dispatch(actions.clearError({ id: processId }));
}
async clearResult(id?: number) {
let processId = id;
if (typeof processId !== 'number') {

View File

@@ -6,6 +6,7 @@ import {
INITIAL_SEARCH_PROCESS,
} from './search.state';
import * as actions from './search.actions';
import { SearchProcessState } from './defs';
const _searchReducer = createReducer(
INITIAL_SEARCH_STATE,
@@ -43,14 +44,31 @@ const _searchReducer = createReducer(
s
)
),
on(actions.clearError, (s, a) =>
searchStateAdapter.updateOne(
{
id: a.id,
changes: {
state: SearchProcessState.INIT,
},
},
s
)
),
on(actions.clearResults, (s, a) =>
searchStateAdapter.updateOne({ id: a.id, changes: { result: [] } }, s)
searchStateAdapter.updateOne(
{ id: a.id, changes: { result: [], state: SearchProcessState.INIT } },
s
)
),
on(actions.addResult, (s, a) =>
searchStateAdapter.updateOne(
{
id: a.id,
changes: { result: [...s.entities[a.id].result, ...a.result] },
changes: {
result: [...s.entities[a.id].result, ...a.result],
state: SearchProcessState.FETCHED,
},
},
s
)
@@ -61,11 +79,26 @@ const _searchReducer = createReducer(
on(actions.setHits, (s, a) =>
searchStateAdapter.updateOne({ id: a.id, changes: { hits: a.hits } }, s)
),
on(actions.setTimestamp, (s, a) =>
searchStateAdapter.updateOne(
{
id: a.id,
changes: { timestamp: Date.now() },
},
s
)
),
on(actions.fetchResult, (s, a) =>
searchStateAdapter.updateOne({ id: a.id, changes: { fetching: true } }, s)
searchStateAdapter.updateOne(
{ id: a.id, changes: { state: SearchProcessState.FETCHING } },
s
)
),
on(actions.fetchResultDone, (s, a) =>
searchStateAdapter.updateOne({ id: a.id, changes: { fetching: false } }, s)
searchStateAdapter.updateOne(
{ id: a.id, changes: { state: SearchProcessState.FETCHED } },
s
)
)
);

View File

@@ -2,7 +2,7 @@ import { createSelector } from '@ngrx/store';
import { selectShelfState } from '../shelf.selectors';
import { searchStateAdapter } from './search.state';
import { Dictionary } from '@ngrx/entity';
import { SearchProcess } from './defs';
import { SearchProcess, SearchProcessState } from './defs';
export const selectSearchState = createSelector(
selectShelfState,
@@ -36,10 +36,30 @@ export const selectHits = createSelector(
entities[id] && entities[id].hits
);
export const selectState = createSelector(
selectEntities,
(entities: Dictionary<SearchProcess>, id: number) =>
entities[id] && entities[id].state
);
export const selectFetching = createSelector(
selectEntities,
(entities: Dictionary<SearchProcess>, id: number) =>
entities[id] && entities[id].fetching
entities[id] && entities[id].state === SearchProcessState.FETCHING
);
export const selectHasError = createSelector(
selectEntities,
(entities: Dictionary<SearchProcess>, id: number) =>
entities[id] && entities[id].state === SearchProcessState.ERROR
);
export const selectHasResults = createSelector(
selectEntities,
(entities: Dictionary<SearchProcess>, id: number) =>
entities[id] &&
entities[id].state === SearchProcessState.FETCHED &&
!!entities[id].hits
);
export const selectFilters = createSelector(

View File

@@ -2,6 +2,7 @@ import { EntityState, createEntityAdapter } from '@ngrx/entity';
import { SearchProcess } from './defs';
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
import { SearchProcessState } from './defs';
export interface SearchState extends EntityState<SearchProcess> {}
@@ -23,6 +24,6 @@ export const INITIAL_SEARCH_PROCESS: SearchProcess = {
id: undefined,
result: [],
input: '',
fetching: false,
state: SearchProcessState.INIT,
filters: INITIAL_FILTERS,
};