Merged PR 1472: Branch Selector Update

Branch Selector Update
This commit is contained in:
Nino Righi
2023-01-26 09:26:34 +00:00
committed by Lorenz Hilpert
parent 872db4085c
commit fdc1dadd36
22 changed files with 329 additions and 294 deletions

View File

@@ -1,5 +1,7 @@
import { Injectable } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { Store } from '@ngrx/store';
import { BranchDTO } from '@swagger/oms';
import { isBoolean, isNumber } from '@utils/common';
import { BehaviorSubject, Observable } from 'rxjs';
import { first, map } from 'rxjs/operators';
@@ -31,7 +33,7 @@ export class ApplicationService {
return this.activatedProcessIdSubject.asObservable();
}
constructor(private store: Store) {}
constructor(private store: Store, private _availability: DomainAvailabilityService) {}
getProcesses$(section?: 'customer' | 'branch') {
const processes$ = this.store.select(selectProcesses);
@@ -68,8 +70,8 @@ export class ApplicationService {
this.store.dispatch(patchProcessData({ processId, data }));
}
getSelectedBranchId$(processId: number): Observable<number> {
return this.getProcessById$(processId).pipe(map((process) => process?.data?.selectedBranchId));
getSelectedBranch$(processId: number): Observable<BranchDTO> {
return this.getProcessById$(processId).pipe(map((process) => process?.data?.selectedBranch));
}
async createProcess(process: ApplicationProcess) {
@@ -90,6 +92,13 @@ export class ApplicationService {
process.confirmClosing = true;
}
if (process.type === 'cart') {
const currentBranch = await this._availability.getCurrentBranch().pipe(first()).toPromise();
process.data = {
selectedBranch: currentBranch,
};
}
process.created = this._createTimestamp();
process.activated = 0;
this.store.dispatch(addProcess({ process }));

View File

@@ -1,4 +1,4 @@
import { NgModule } from '@angular/core';
import { isDevMode, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {
CanActivateCartGuard,
@@ -15,6 +15,7 @@ import {
IsAuthenticatedGuard,
} from './guards';
import { CanActivatePackageInspectionGuard } from './guards/can-activate-package-inspection.guard';
import { PreviewComponent } from './preview';
import { BranchSectionResolver, CustomerSectionResolver, ProcessIdResolver } from './resolvers';
import { ShellComponent, ShellModule } from './shell';
import { TokenLoginComponent, TokenLoginModule } from './token-login';
@@ -125,6 +126,13 @@ const routes: Routes = [
},
];
if (isDevMode()) {
routes.unshift({
path: 'preview',
component: PreviewComponent,
});
}
@NgModule({
imports: [RouterModule.forRoot(routes), ShellModule, TokenLoginModule],
exports: [RouterModule],

View File

@@ -33,7 +33,7 @@ import { ScanAdapterModule, ScanAdapterService, ScanditScanAdapterModule } from
import { RootStateService } from './store/root-state.service';
import * as Commands from './commands';
import { UiIconModule } from '@ui/icon';
import { UserStateService } from '@swagger/isa';
import { PreviewComponent } from './preview';
registerLocaleData(localeDe, localeDeExtra);
registerLocaleData(localeDe, 'de', localeDeExtra);
@@ -79,6 +79,7 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
CoreCommandModule.forRoot(Object.values(Commands)),
CoreLoggerModule.forRoot(),
AppStoreModule,
PreviewComponent,
AuthModule.forRoot(),
CoreApplicationModule.forRoot(),
UiModalModule.forRoot(),

View File

@@ -0,0 +1,3 @@
// start:ng42.barrel
export * from './preview.component';
// end:ng42.barrel

View File

@@ -0,0 +1,3 @@
:host {
@apply grid min-h-screen content-center justify-center;
}

View File

@@ -0,0 +1 @@
<shared-branch-selector [value]="selectedBranch$ | async" (valueChange)="setNewBranch($event)"></shared-branch-selector>

View File

@@ -0,0 +1,24 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { BranchSelectorComponent } from '@shared/components/branch-selector';
import { BranchDTO } from '@swagger/oms';
import { BehaviorSubject } from 'rxjs';
@Component({
selector: 'app-preview',
templateUrl: 'preview.component.html',
styleUrls: ['preview.component.css'],
imports: [CommonModule, BranchSelectorComponent],
standalone: true,
})
export class PreviewComponent implements OnInit {
selectedBranch$ = new BehaviorSubject<BranchDTO>({});
constructor() {}
ngOnInit() {}
setNewBranch(branch: BranchDTO) {
this.selectedBranch$.next(branch);
}
}

View File

@@ -137,7 +137,7 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
withLatestFrom(this.filter$, this.items$, this._appService.getProcesses$('customer')),
switchMap(([options, filter, items, processes]) => {
const process = processes?.find((process) => process.id === this.processId);
const stockId = process?.data?.selectedBranchId;
const stockId = process?.data?.selectedBranch?.id;
return this.searchRequest({
...filter.getQueryToken(),
skip: items.length,

View File

@@ -1,8 +1,4 @@
<shared-breadcrumb class="my-4" [key]="activatedProcessId$ | async" [tags]="['catalog']"
><shared-branch-selector
class="shared-branch-selector-breadcrumb"
[value]="selectedBranchId$ | async"
(valueChange)="patchProcessData($event)"
></shared-branch-selector
><shared-branch-selector [value]="selectedBranch$ | async" (valueChange)="patchProcessData($event)"></shared-branch-selector
></shared-breadcrumb>
<router-outlet></router-outlet>

View File

@@ -5,3 +5,8 @@
shell-breadcrumb {
@apply sticky z-sticky top-0;
}
shared-branch-selector.shared-branch-selector-opend {
@apply absolute top-0 right-0 w-full;
max-width: 814px;
}

View File

@@ -1,7 +1,9 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ApplicationService } from '@core/application';
import { BranchDTO } from '@swagger/oms';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { Observable } from 'rxjs';
import { first, map, switchMap, tap } from 'rxjs/operators';
import { first, map, switchMap } from 'rxjs/operators';
@Component({
selector: 'page-catalog',
@@ -11,20 +13,28 @@ import { first, map, switchMap, tap } from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PageCatalogComponent implements OnInit {
activatedProcessId$ = this.application.activatedProcessId$.pipe(map((processId) => String(processId)));
activatedProcessId$: Observable<string>;
selectedBranch$: Observable<BranchDTO>;
selectedBranchId$: Observable<number>;
constructor(public application: ApplicationService, private _uiModal: UiModalService) {}
constructor(public application: ApplicationService) {}
ngOnInit() {
this.activatedProcessId$ = this.application.activatedProcessId$.pipe(map((processId) => String(processId)));
ngOnInit(): void {
this.selectedBranchId$ = this.activatedProcessId$.pipe(
switchMap((processId) => this.application.getSelectedBranchId$(Number(processId)))
);
this.selectedBranch$ = this.activatedProcessId$.pipe(switchMap((processId) => this.application.getSelectedBranch$(Number(processId))));
}
async patchProcessData(selectedBranchId: number) {
const processId = await this.activatedProcessId$.pipe(first()).toPromise();
this.application.patchProcessData(Number(processId), { selectedBranchId });
async patchProcessData(selectedBranch: BranchDTO) {
try {
const processId = await this.activatedProcessId$.pipe(first()).toPromise();
this.application.patchProcessData(Number(processId), { selectedBranch });
} catch (error) {
this._uiModal.open({
title: 'Fehler beim aktualisieren der Daten des Prozesses',
content: UiErrorModalComponent,
data: error,
config: { showScrollbarY: false },
});
}
}
}

View File

@@ -1,6 +1,8 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs/operators';
import { ApplicationService } from '@core/application';
import { DomainAvailabilityService } from '@domain/availability';
import { map, withLatestFrom } from 'rxjs/operators';
@Component({
selector: 'page-goods-out',
@@ -9,7 +11,18 @@ import { map } from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GoodsOutComponent {
processId$ = this._activatedRoute.data.pipe(map((data) => String(data.processId)));
// #3359 Set Default selectedBranch if navigate to goods-out
processId$ = this._activatedRoute.data.pipe(
withLatestFrom(this._availability.getCurrentBranch()),
map(([data, currentBranch]) => {
this._application.patchProcessData(Number(data.processId), { selectedBranch: currentBranch });
return String(data.processId);
})
);
constructor(private _activatedRoute: ActivatedRoute) {}
constructor(
private _activatedRoute: ActivatedRoute,
private _application: ApplicationService,
private _availability: DomainAvailabilityService
) {}
}

View File

@@ -1,12 +1,10 @@
<div
[class.shared-branch-selector-expand]="autocompleteComponent?.opend"
class="shared-branch-selector-container flex flex-row w-72 z-modal"
>
<ui-svg-icon class="mr-2 text-inactive-customer z-modal" icon="magnify" [size]="32"></ui-svg-icon>
<button class="shared-branch-selector-input-container" (click)="branchInput.focus(); openComplete()">
<button class="shared-branch-selector-input-icon">
<ui-svg-icon class="text-inactive-customer" icon="magnify" [size]="32"></ui-svg-icon>
</button>
<input
(click)="autocompleteComponent.open()"
class="shared-branch-selector-input text-ellipsis whitespace-nowrap mr-2 font-bold text-black z-modal"
[class.shared-branch-selector-expand]="autocompleteComponent?.opend"
#branchInput
class="shared-branch-selector-input"
uiInput
type="text"
[placeholder]="'Filiale suchen'"
@@ -14,19 +12,25 @@
(ngModelChange)="onQueryChange($event)"
(keyup)="onKeyup($event)"
/>
<button class="shared-branch-selector-clear-input mr-2 z-modal" *ngIf="(query$ | async).length > 0" type="button" (click)="clear()">
<button
class="shared-branch-selector-clear-input-icon"
*ngIf="(query$ | async)?.length > 0"
type="button"
(click)="clear(); $event.stopPropagation()"
>
<ui-svg-icon class="text-black" icon="close" [size]="32"></ui-svg-icon>
</button>
</div>
<hr class="mt-3 mx-4 text-active-customer z-modal" *ngIf="autocompleteComponent?.opend" />
</button>
<ui-autocomplete class="shared-branch-selector-autocomplete z-modal w-full">
<hr class="ml-3 text-active-customer" *ngIf="autocompleteComponent?.opend" uiAutocompleteSeparator />
<p *ngIf="(branches$ | async)?.length > 0" class="text-base p-4 font-normal" uiAutocompleteLabel>Filialvorschläge</p>
<button
class="shared-branch-selector-autocomplete-option"
(click)="setBranch(branch.id)"
[class.shared-branch-selector-selected]="value && value.id === branch.id"
(click)="setBranch(branch)"
*ngFor="let branch of branches$ | async"
[uiAutocompleteItem]="branch"
>
<span class="text-lg font-semibold">{{ store.formatBranchById(branch.id) }}</span>
<span class="text-lg font-semibold">{{ store.formatBranch(branch) }}</span>
</button>
</ui-autocomplete>

View File

@@ -1,21 +1,24 @@
.shared-branch-selector-expand {
@apply w-full;
.shared-branch-selector-input-container {
@apply w-full flex flex-row items-center z-modal bg-white p-2 min-w-[21rem];
}
.shared-branch-selector-input-icon {
@apply mr-1;
}
.shared-branch-selector-input {
@apply text-ellipsis whitespace-nowrap px-4 py-1 font-bold text-black w-full;
}
.shared-branch-selector-input::placeholder {
@apply text-black;
}
.shared-branch-selector-breadcrumb {
@apply justify-self-end bg-white z-modal py-2 rounded-t-md;
max-width: 814px;
.shared-branch-selector-clear-input-icon {
@apply ml-1;
}
.shared-branch-selector-breadcrumb.shared-branch-selector-opend {
@apply absolute mt-3 w-full;
}
.shared-branch-selector ui-autocomplete .ui-autocomplete-output-wrapper {
::ng-deep shared-branch-selector ui-autocomplete .ui-autocomplete-output-wrapper {
@apply overflow-hidden overflow-y-auto max-w-content rounded-b-md;
max-height: 500px;
width: 100%;
@@ -26,3 +29,7 @@
@apply py-2;
}
}
.shared-branch-selector-autocomplete-option.shared-branch-selector-selected {
@apply bg-customer;
}

View File

@@ -1,8 +1,11 @@
// import { CommonModule } from '@angular/common';
// import { FormsModule } from '@angular/forms';
// import { ActivatedRoute } from '@angular/router';
// import { DomainAvailabilityService } from '@domain/availability';
// import { createComponentFactory, createSpyObject, mockProvider, Spectator, SpyObject } from '@ngneat/spectator';
// import { provideComponentStore } from '@ngrx/component-store';
// import { UiAutocompleteModule } from '@ui/autocomplete';
// import { UiCommonModule } from '@ui/common';
// import { IconRegistry, UiIconModule } from '@ui/icon';
// import { from, of } from 'rxjs';
@@ -11,26 +14,18 @@
// describe('BranchSelectorComponent', () => {
// let spectator: Spectator<BranchSelectorComponent>;
// let activatedRouteMock: SpyObject<ActivatedRoute>;
// let branchSelectorStoreMock: jasmine.SpyObj<BranchSelectorStore>;
// const createComponent = createComponentFactory({
// component: BranchSelectorComponent,
// imports: [CommonModule, FormsModule, UiIconModule, UiAutocompleteModule],
// mocks: [IconRegistry],
// componentProviders: [
// mockProvider(BranchSelectorStore, {
// loadBranches: async () => {},
// query$: of('test'),
// imports: [UiCommonModule, CommonModule, FormsModule, UiIconModule, UiAutocompleteModule],
// mocks: [IconRegistry, DomainAvailabilityService],
// }),
// ],
// });
// beforeEach(() => {
// activatedRouteMock = createSpyObject(ActivatedRoute);
// spectator = createComponent({
// providers: [{ provide: ActivatedRoute, useValue: { params: from([{ id: 1 }]), parent: { data: from([{ processId: 123 }]) } } }],
// });
// spectator = createComponent();
// branchSelectorStoreMock = spectator.inject(BranchSelectorStore, true);
// });
@@ -39,62 +34,37 @@
// expect(spectator.component).toBeTruthy();
// });
// describe('ngOnInit()', () => {
// it('should call', () => {
// // branchSelectorStoreMock.query$;
// // branchSelectorStoreMock.query$.and
// // branchSelectorStoreMock.query$ = of('test')
// // spectator.component.query$.subscribe((q) => {
// // expect(q).toBe('test');
// // done();
// // });
// });
// // describe('ngAfterViewInit()', () => {
// // it('should call initAutocomplete()', () => {
// // const initAutocompleteSpy = spyOn(spectator.component, 'initAutocomplete');
// // spectator.component.ngAfterViewInit();
// // expect(initAutocompleteSpy).toHaveBeenCalled();
// // });
// // });
// it('should return an observable that completes when ref.dismissed$ emits', () => {
// // snackbarRefMock.options = {
// // duration: 50
// // };
// // const expectedMarble = '10ms |';
// // const expectedValues = { };
// // testScheduler.run(({ cold, expectObservable }) => {
// // spectator.component['_onDestroy$'].next();
// // const timer$ = spectator.component.createTimer$();
// // expectObservable(timer$).toBe(expectedMarble, expectedValues);
// // })
// });
// });
// // describe('ngOnDestroy()', () => {
// // it('should emit _onDestroy$', () => {
// // spyOn(spectator.component['_onDestroy$'], 'next');
// // spectator.component.ngOnDestroy();
// // expect(spectator.component['_onDestroy$'].next).toHaveBeenCalled();
// // });
// describe('ngAfterViewInit()', () => {
// it('should call initAutocomplete()', () => {
// const initAutocompleteSpy = spyOn(spectator.component, 'initAutocomplete');
// spectator.component.ngAfterViewInit();
// expect(initAutocompleteSpy).toHaveBeenCalled();
// });
// });
// // it('should call _onDestroy$.complete()', () => {
// // spyOn(spectator.component['_onDestroy$'], 'complete');
// // spectator.component.ngOnDestroy();
// // expect(spectator.component['_onDestroy$'].complete).toHaveBeenCalled();
// // });
// // });
// describe('ngOnDestroy()', () => {
// it('should emit _onDestroy$', () => {
// spyOn(spectator.component['_onDestroy$'], 'next');
// spectator.component.ngOnDestroy();
// expect(spectator.component['_onDestroy$'].next).toHaveBeenCalled();
// });
// // describe('initAutocomplete()', () => {
// // it('should call complete.asObservable()', () => {
// // spyOn(spectator.component.complete, 'asObservable').and.returnValue(of());
// // spectator.component.initAutocomplete();
// // expect(spectator.component.complete.asObservable).toHaveBeenCalled();
// // });
// // });
// it('should call _onDestroy$.complete()', () => {
// spyOn(spectator.component['_onDestroy$'], 'complete');
// spectator.component.ngOnDestroy();
// expect(spectator.component['_onDestroy$'].complete).toHaveBeenCalled();
// });
// });
// describe('initAutocomplete()', () => {
// it('should call complete.asObservable()', () => {
// spyOn(spectator.component.complete, 'asObservable').and.returnValue(of());
// spectator.component.initAutocomplete();
// expect(spectator.component.complete.asObservable).toHaveBeenCalled();
// });
// });
// describe('filterAutocompleteFn()', () => {});
// // describe('filterAutocompleteFn()', () => {});
// // it('should not emit search with the current query when key is ArrowUp or ArrowDown', () => {
// // spectator.component.query = 'my query';

View File

@@ -2,16 +2,17 @@ import { CommonModule } from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
forwardRef,
HostBinding,
HostListener,
Input,
Output,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import { ControlValueAccessor, FormsModule } from '@angular/forms';
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { provideComponentStore } from '@ngrx/component-store';
import { BranchDTO } from '@swagger/oms';
import { UiAutocompleteComponent, UiAutocompleteModule } from '@ui/autocomplete';
@@ -26,9 +27,15 @@ import { BranchSelectorStore } from './branch-selector.store';
templateUrl: 'branch-selector.component.html',
styleUrls: ['branch-selector.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: { class: 'shared-branch-selector', tabindex: '0' },
providers: [provideComponentStore(BranchSelectorStore)],
host: { tabindex: '0' },
providers: [
provideComponentStore(BranchSelectorStore),
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BranchSelectorComponent),
multi: true,
},
],
imports: [CommonModule, FormsModule, UiIconModule, UiAutocompleteModule, UiCommonModule],
standalone: true,
})
@@ -44,23 +51,23 @@ export class BranchSelectorComponent implements AfterViewInit, ControlValueAcces
query$ = this.store.query$;
@Input()
get value(): number {
get value(): BranchDTO {
return this._value;
}
set value(value: number) {
set value(value: BranchDTO) {
this.setBranch(value);
}
private _value: number;
private _value: BranchDTO;
@Output() valueChange = new EventEmitter<number>();
@Output() valueChange = new EventEmitter<BranchDTO>();
@Input()
disabled = false;
onChange = (value: number) => {};
onChange = (value: BranchDTO) => {};
onTouched = () => {};
constructor(public store: BranchSelectorStore) {}
constructor(public store: BranchSelectorStore, private _cdr: ChangeDetectorRef) {}
writeValue(obj: any): void {
this.value = obj;
@@ -85,45 +92,57 @@ export class BranchSelectorComponent implements AfterViewInit, ControlValueAcces
initAutocomplete() {
this.branches$ = this.complete.asObservable().pipe(
withLatestFrom(this.store.branches$),
map(([query, branches]) => this.initAutocompleteFn({ query, branches }))
map(([query, branches]) => this.initAutocompleteFn({ query, branches })),
map(this.sortAutocompleteFn)
);
}
initAutocompleteFn = ({ query, branches }: { query: string; branches: BranchDTO[] }) =>
query.length > 1 ? this.filterAutocompleteFn({ query, branches }) : [];
query?.length > 1 ? this.filterAutocompleteFn({ query, branches }) : branches;
filterAutocompleteFn({ query, branches }: { query: string; branches: BranchDTO[] }): BranchDTO[] {
return branches?.filter(
(branch) =>
(!!branch.key && branch.key.toLowerCase().includes(query.toLowerCase())) ||
(!!branch.name && branch.name.toLowerCase().includes(query.toLowerCase()))
branch?.name?.toLowerCase()?.indexOf(query.toLowerCase()) >= 0 ||
branch?.key?.toLowerCase()?.indexOf(query.toLowerCase()) >= 0 ||
branch?.address?.city?.toLowerCase()?.indexOf(query.toLowerCase()) >= 0 ||
branch?.address?.zipCode?.indexOf(query) >= 0
);
}
sortAutocompleteFn = (branches: BranchDTO[]): BranchDTO[] =>
branches?.sort((branchA, branchB) => branchA?.name?.localeCompare(branchB?.name));
onQueryChange(query: string) {
this.store.setQuery(query);
this.complete.next(query);
}
setBranch(branchId?: number) {
if (this.value !== branchId) {
this._value = branchId;
this.store.setSelectedBranchId(branchId);
this.emitValues(branchId);
openComplete() {
this.autocompleteComponent?.open();
this.store.setQuery('');
this.complete.next('');
}
setBranch(branch?: BranchDTO) {
if (this.value !== branch) {
this._value = branch;
this.store.setSelectedBranch(branch);
this.emitValues(branch);
}
this.closeAutocomplete();
}
clear() {
this.store.setSelectedBranchId();
this.store.setSelectedBranch();
this.emitValues();
this.closeAndClearAutocomplete();
}
emitValues(branchId?: number) {
this.onChange(branchId);
emitValues(branch?: BranchDTO) {
this.onChange(branch);
this.onTouched();
this.valueChange.emit(branchId);
this.valueChange.emit(branch);
}
closeAndClearAutocomplete() {
@@ -131,18 +150,11 @@ export class BranchSelectorComponent implements AfterViewInit, ControlValueAcces
this.complete.next('');
}
@HostListener('focusin', ['$event'])
clearAutocomplete(event: FocusEvent) {
if (!(event?.target as HTMLElement)?.classList.contains('shared-branch-selector-clear-input')) {
this.store.setQuery('');
}
}
@HostListener('focusout', ['$event'])
closeAutocomplete(event?: FocusEvent) {
const isAutocompleteOption = (event?.relatedTarget as HTMLElement)?.classList.contains('shared-branch-selector-autocomplete-option');
if (!isAutocompleteOption) {
this.store.setQuery(this.store.formatBranchById(this.value));
this.store.setQuery(this.store.formatBranch(this.value));
this.closeAndClearAutocomplete();
}
}
@@ -157,18 +169,18 @@ export class BranchSelectorComponent implements AfterViewInit, ControlValueAcces
handleEnterEvent() {
if (this.autocompleteComponent?.opend && this.autocompleteComponent?.activeItem) {
this.setBranch(this.autocompleteComponent?.activeItem?.item?.id);
this.setBranch(this.autocompleteComponent?.activeItem?.item);
}
}
handleArrowUpDownEvent(event: KeyboardEvent) {
this.autocompleteComponent?.handleKeyboardEvent(event);
if (this.autocompleteComponent?.activeItem) {
this.store.setQuery(this.store.formatBranchById(this.autocompleteComponent.activeItem.item.id));
this.store.setQuery(this.store.formatBranch(this.autocompleteComponent.activeItem.item));
}
}
@HostBinding('class.shared-branch-selector-opend') get autocompleteOpend() {
return this.autocompleteComponent?.opend;
@HostBinding('class.shared-branch-selector-opend') get class() {
return !!this.autocompleteComponent?.opend;
}
}

View File

@@ -1,11 +1,13 @@
import { DomainAvailabilityService } from '@domain/availability';
import { createServiceFactory, createSpyObject, SpectatorService, SpyObject } from '@ngneat/spectator';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { of, throwError } from 'rxjs';
import { BranchSelectorStore } from './branch-selector.store';
describe('BranchSelectorComponent', () => {
let spectator: SpectatorService<BranchSelectorStore>;
let availabilityServiceMock: SpyObject<DomainAvailabilityService>;
let uiModalServiceMock: SpyObject<UiModalService>;
const createService = createServiceFactory({
service: BranchSelectorStore,
@@ -16,8 +18,13 @@ describe('BranchSelectorComponent', () => {
availabilityServiceMock.getBranches.and.returnValue(of([]));
availabilityServiceMock.getCurrentBranch.and.returnValue(of({}));
uiModalServiceMock = createSpyObject(UiModalService);
spectator = createService({
providers: [{ provide: DomainAvailabilityService, useValue: availabilityServiceMock }],
providers: [
{ provide: DomainAvailabilityService, useValue: availabilityServiceMock },
{ provide: UiModalService, useValue: uiModalServiceMock },
],
});
});
@@ -88,33 +95,22 @@ describe('BranchSelectorComponent', () => {
});
});
describe('get selectedBranchId', () => {
it('should return the selectedBranchId', () => {
const selectedBranchId = 123;
spectator.service.setSelectedBranchId(selectedBranchId);
expect(spectator.service.selectedBranchId).toBe(selectedBranchId);
describe('get selectedBranch', () => {
it('should return the selectedBranch', () => {
const selectedBranch = { id: 123 };
spectator.service.setSelectedBranch(selectedBranch);
expect(spectator.service.selectedBranch).toEqual(selectedBranch);
});
});
describe('get selectedBranchId$', () => {
it('should return the selectedBranchId$', () => {
const selectedBranchId = 123;
spectator.service.setSelectedBranchId(selectedBranchId);
describe('get selectedBranch$', () => {
it('should return the selectedBranch$', () => {
const selectedBranch = { id: 123 };
spectator.service.setSelectedBranch(selectedBranch);
spectator.service.selectedBranchId$
.subscribe((sbid) => {
expect(sbid).toBe(selectedBranchId);
})
.unsubscribe();
});
});
describe('get currentBranch$', () => {
it('should call _availabilityService.getCurrentBranch()', (done) => {
spectator.service.currentBranch$
.subscribe(() => {
expect(availabilityServiceMock.getCurrentBranch).toHaveBeenCalled();
done();
spectator.service.selectedBranch$
.subscribe((sb) => {
expect(sb).toEqual(selectedBranch);
})
.unsubscribe();
});
@@ -130,11 +126,9 @@ describe('BranchSelectorComponent', () => {
describe('loadBranches', () => {
const branches = [{ id: 1 }, { id: 2 }];
const currentBranch = { id: 2 };
beforeEach(() => {
availabilityServiceMock.getBranches.and.returnValue(of(branches));
availabilityServiceMock.getCurrentBranch.and.returnValue(of(currentBranch));
});
it('should call setFetching(true)', () => {
@@ -148,12 +142,12 @@ describe('BranchSelectorComponent', () => {
expect(availabilityServiceMock.getBranches).toHaveBeenCalled();
});
it('should call loadBranchesResponseFn({ response, currentBranch, selectedBranchId })', () => {
const selectedBranchId = 123;
spectator.service.setSelectedBranchId(selectedBranchId);
it('should call loadBranchesResponseFn({ response, selectedBranch })', () => {
const selectedBranch = { id: 123 };
spectator.service.setSelectedBranch(selectedBranch);
const loadBranchesResponseFnSpy = spyOn(spectator.service, 'loadBranchesResponseFn');
spectator.service.loadBranches();
expect(loadBranchesResponseFnSpy).toHaveBeenCalledWith({ response: branches, currentBranch, selectedBranchId });
expect(loadBranchesResponseFnSpy).toHaveBeenCalledWith({ response: branches, selectedBranch });
});
it('should call loadBranchesErrorFn(error) if error got thrown', () => {
@@ -167,83 +161,85 @@ describe('BranchSelectorComponent', () => {
describe('loadBranchesResponseFn()', () => {
let branches = [];
let currentBranch = {};
beforeEach(() => {
branches = [{ id: 1 }, { id: 2 }];
currentBranch = { id: 2 };
});
it('should call _filterBranches(response, currentBranch)', () => {
it('should call _filterBranches(response)', () => {
const filterBranchesSpy = spyOn<any>(spectator.service, '_filterBranches');
spectator.service.loadBranchesResponseFn({ response: branches, currentBranch });
expect(filterBranchesSpy).toHaveBeenCalledWith(branches, currentBranch);
spectator.service.loadBranchesResponseFn({ response: branches });
expect(filterBranchesSpy).toHaveBeenCalledWith(branches);
});
it('should call setBranches() with branches after filtering', () => {
spyOn<any>(spectator.service, '_filterBranches').and.returnValue([{ id: 1 }]);
const setBranchesSpy = spyOn(spectator.service, 'setBranches');
spectator.service.loadBranchesResponseFn({ response: branches, currentBranch });
spectator.service.loadBranchesResponseFn({ response: branches });
expect(setBranchesSpy).toHaveBeenCalledWith([{ id: 1 }]);
});
it('should call setBranches() with default [] if getBranches() return undefined', () => {
spyOn<any>(spectator.service, '_filterBranches').and.returnValue([]);
const setBranchesSpy = spyOn(spectator.service, 'setBranches');
spectator.service.loadBranchesResponseFn({ response: branches, currentBranch });
spectator.service.loadBranchesResponseFn({ response: branches });
expect(setBranchesSpy).toHaveBeenCalledWith([]);
});
it('should call setSelectedBranchId(selectedBranchId) if selectedBranchId is set', () => {
const selectedBranchId = 2;
const setSelectedBranchIdSpy = spyOn(spectator.service, 'setSelectedBranchId');
spectator.service.loadBranchesResponseFn({ response: branches, currentBranch, selectedBranchId });
expect(setSelectedBranchIdSpy).toHaveBeenCalledWith(selectedBranchId);
it('should call setSelectedBranchId(selectedBranchId) if selectedBranch is set', () => {
const selectedBranch = { id: 123 };
const setSelectedBranchSpy = spyOn(spectator.service, 'setSelectedBranch');
spectator.service.loadBranchesResponseFn({ response: branches, selectedBranch });
expect(setSelectedBranchSpy).toHaveBeenCalledWith(selectedBranch);
});
it('should not call setSelectedBranchId(selectedBranchId) if selectedBranchId is not set', () => {
const setSelectedBranchIdSpy = spyOn(spectator.service, 'setSelectedBranchId');
spectator.service.loadBranchesResponseFn({ response: branches, currentBranch });
expect(setSelectedBranchIdSpy).not.toHaveBeenCalled();
it('should not call setSelectedBranch(selectedBranch) if selectedBranch is not set', () => {
const setSelectedBranchSpy = spyOn(spectator.service, 'setSelectedBranch');
spectator.service.loadBranchesResponseFn({ response: branches });
expect(setSelectedBranchSpy).not.toHaveBeenCalled();
});
it('should call setFetching(false)', () => {
const setFetchingSpy = spyOn(spectator.service, 'setFetching');
spectator.service.loadBranchesResponseFn({ response: branches, currentBranch });
spectator.service.loadBranchesResponseFn({ response: branches });
expect(setFetchingSpy).toHaveBeenCalledWith(false);
});
});
describe('loadBranchesErrorFn()', () => {
it('should call loadBranches()', () => {
it('should call uiModalServiceMock.open with appropriate UiErrorModal data', () => {
const error = new Error('test');
spyOn(console, 'error');
spectator.service.loadBranchesErrorFn(error);
expect(console.error).toHaveBeenCalledWith('BranchSelectorStore.loadBranches()', error);
expect(uiModalServiceMock.open).toHaveBeenCalledWith({
title: 'Fehler beim Laden der Filialen',
content: UiErrorModalComponent,
data: error,
config: { showScrollbarY: false },
});
});
});
describe('setSelectedBranchId()', () => {
it('should call patchState with empty query and undefined selectedBranchId if function call without arguments', () => {
describe('setSelectedBranch()', () => {
it('should call patchState with empty query and undefined selectedBranch if function call without arguments', () => {
const patchStateSpy = spyOn(spectator.service, 'patchState');
spectator.service.setSelectedBranchId();
expect(patchStateSpy).toHaveBeenCalledWith({ selectedBranchId: undefined, query: '' });
spectator.service.setSelectedBranch();
expect(patchStateSpy).toHaveBeenCalledWith({ selectedBranch: undefined, query: '' });
});
it('should call formatBranchById(selectedBranchId) if function call with selectedBranchId', () => {
const selectedBranchId = 123;
const formatBranchByIdSpy = spyOn(spectator.service, 'formatBranchById');
spectator.service.setSelectedBranchId(selectedBranchId);
expect(formatBranchByIdSpy).toHaveBeenCalledWith(selectedBranchId);
it('should call formatBranch(selectedBranch) if function call with selectedBranch', () => {
const selectedBranch = { id: 123 };
const formatBranchBySpy = spyOn(spectator.service, 'formatBranch');
spectator.service.setSelectedBranch(selectedBranch);
expect(formatBranchBySpy).toHaveBeenCalledWith(selectedBranch);
});
it('should call patchState({ selectedBranchId, query: this.formatBranchById(selectedBranchId) }) if function call with selectedBranchId', () => {
const selectedBranchId = 123;
it('should call patchState({ selectedBranch, query: this.formatBranch(selectedBranch) }) if function call with selectedBranch', () => {
const selectedBranch = { id: 123 };
const query = 'test-branch';
spyOn(spectator.service, 'formatBranchById').and.returnValue(query);
spyOn(spectator.service, 'formatBranch').and.returnValue(query);
const patchStateSpy = spyOn(spectator.service, 'patchState');
spectator.service.setSelectedBranchId(selectedBranchId);
expect(patchStateSpy).toHaveBeenCalledWith({ selectedBranchId, query });
spectator.service.setSelectedBranch(selectedBranch);
expect(patchStateSpy).toHaveBeenCalledWith({ selectedBranch, query });
});
});
@@ -274,41 +270,29 @@ describe('BranchSelectorComponent', () => {
});
});
describe('formatBranchById()', () => {
it('should call _getBranchById with branchId', () => {
const branchId = 123;
const getBranchByIdSpy = spyOn<any>(spectator.service, '_getBranchById');
spectator.service.formatBranchById(branchId);
expect(getBranchByIdSpy).toHaveBeenCalledWith(branchId);
describe('formatBranch()', () => {
it('should return a formatted branch with key', () => {
const branch = {
id: 1,
name: 'Test',
key: 'T',
};
const formattedString = spectator.service.formatBranch(branch);
expect(formattedString).toBe('T - Test');
});
it('should call _formatBranch(branch)', () => {
const branchId = 123;
const branch = { id: branchId };
spyOn<any>(spectator.service, '_getBranchById').and.returnValue(branch);
const formatBranchSpy = spyOn<any>(spectator.service, '_formatBranch');
spectator.service.formatBranchById(branchId);
expect(formatBranchSpy).toHaveBeenCalledWith(branch);
it('should return a formatted branch without key', () => {
const branch = {
id: 1,
name: 'Test',
};
const formattedString = spectator.service.formatBranch(branch);
expect(formattedString).toBe('Test');
});
});
describe('_getBranchById()', () => {
it('should find and return the branch by id', () => {
const branchId = 2;
const branches = [{ id: 1 }, { id: 2 }];
spectator.service.setBranches(branches);
const branch = spectator.service['_getBranchById'](branchId);
expect(branch).toEqual(branches[1]);
});
});
describe('_formatBranch()', () => {
it('should find and return the branch by id', () => {
const branchId = 2;
const branches = [{ id: 1 }, { id: 2 }];
spectator.service.setBranches(branches);
const branch = spectator.service['_getBranchById'](branchId);
expect(branch).toEqual(branches[1]);
it('should return an empty string if called without branch', () => {
const formattedString = spectator.service.formatBranch();
expect(formattedString).toBe('');
});
});
@@ -326,11 +310,7 @@ describe('BranchSelectorComponent', () => {
},
];
const currentBranch = {
id: 1,
};
expect(spectator.service['_filterBranches'](branches, currentBranch)).toEqual([branches[1]]);
expect(spectator.service['_filterBranches'](branches)).toEqual([branches[1]]);
});
});
});

View File

@@ -2,13 +2,14 @@ import { Injectable } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { ComponentStore, OnStateInit, tapResponse } from '@ngrx/component-store';
import { BranchDTO } from '@swagger/oms';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { switchMap, tap, withLatestFrom } from 'rxjs/operators';
export interface BranchSelectorState {
query: string;
fetching: boolean;
branches: BranchDTO[];
selectedBranchId?: number;
selectedBranch?: BranchDTO;
}
@Injectable()
@@ -31,17 +32,13 @@ export class BranchSelectorStore extends ComponentStore<BranchSelectorState> imp
readonly branches$ = this.select((s) => s.branches);
get selectedBranchId() {
return this.get((s) => s.selectedBranchId);
get selectedBranch() {
return this.get((s) => s.selectedBranch);
}
readonly selectedBranchId$ = this.select((s) => s.selectedBranchId);
readonly selectedBranch$ = this.select((s) => s.selectedBranch);
get currentBranch$() {
return this._availabilityService.getCurrentBranch();
}
constructor(private _availabilityService: DomainAvailabilityService) {
constructor(private _availabilityService: DomainAvailabilityService, private _uiModal: UiModalService) {
super({
query: '',
fetching: false,
@@ -58,9 +55,9 @@ export class BranchSelectorStore extends ComponentStore<BranchSelectorState> imp
tap((_) => this.setFetching(true)),
switchMap(() =>
this._availabilityService.getBranches().pipe(
withLatestFrom(this.currentBranch$, this.selectedBranchId$),
withLatestFrom(this.selectedBranch$),
tapResponse(
([response, currentBranch, selectedBranchId]) => this.loadBranchesResponseFn({ response, currentBranch, selectedBranchId }),
([response, selectedBranch]) => this.loadBranchesResponseFn({ response, selectedBranch }),
(error: Error) => this.loadBranchesErrorFn(error)
)
)
@@ -68,38 +65,36 @@ export class BranchSelectorStore extends ComponentStore<BranchSelectorState> imp
)
);
loadBranchesResponseFn = ({
response,
currentBranch,
selectedBranchId,
}: {
response: BranchDTO[];
currentBranch: BranchDTO;
selectedBranchId?: number;
}) => {
const branches = this._filterBranches(response, currentBranch);
loadBranchesResponseFn = ({ response, selectedBranch }: { response: BranchDTO[]; selectedBranch?: BranchDTO }) => {
const branches = this._filterBranches(response);
this.setBranches(branches ?? []);
if (selectedBranchId) {
this.setSelectedBranchId(selectedBranchId);
if (selectedBranch) {
this.setSelectedBranch(selectedBranch);
}
this.setFetching(false);
};
loadBranchesErrorFn = (error: Error) => console.error('BranchSelectorStore.loadBranches()', error);
loadBranchesErrorFn = (error: Error) =>
this._uiModal.open({
title: 'Fehler beim Laden der Filialen',
content: UiErrorModalComponent,
data: error,
config: { showScrollbarY: false },
});
setBranches(branches: BranchDTO[]) {
this.patchState({ branches });
}
setSelectedBranchId(selectedBranchId?: number) {
if (selectedBranchId) {
setSelectedBranch(selectedBranch?: BranchDTO) {
if (selectedBranch) {
this.patchState({
selectedBranchId,
query: this.formatBranchById(selectedBranchId),
selectedBranch,
query: this.formatBranch(selectedBranch),
});
} else {
this.patchState({
selectedBranchId,
selectedBranch,
query: '',
});
}
@@ -113,23 +108,12 @@ export class BranchSelectorStore extends ComponentStore<BranchSelectorState> imp
this.patchState({ fetching });
}
formatBranchById(branchId: number) {
const branch = this._getBranchById(branchId);
return this._formatBranch(branch);
}
private _getBranchById(branchId: number) {
return this.branches.find((branch) => branch.id === branchId);
}
private _formatBranch(branch?: BranchDTO) {
formatBranch(branch?: BranchDTO) {
return branch ? (branch.key ? branch.key + ' - ' + branch.name : branch.name) : '';
}
// Frontend-Seitige Filterung der Branches vom Backend (Filterkriterien wie bei PDP - Weitere Verfügbarkeiten Modal)
private _filterBranches(branches: BranchDTO[], currentBranch: BranchDTO): BranchDTO[] {
return branches?.filter(
(branch) => branch && branch?.isOnline && branch?.isShippingEnabled && branch?.isOrderingEnabled && branch.id !== currentBranch.id
);
private _filterBranches(branches: BranchDTO[]): BranchDTO[] {
return branches?.filter((branch) => branch && branch?.isOnline && branch?.isShippingEnabled && branch?.isOrderingEnabled);
}
}

View File

@@ -1,5 +1,5 @@
import { Highlightable } from '@angular/cdk/a11y';
import { Component, ChangeDetectionStrategy, Input, ViewEncapsulation, HostBinding, HostListener } from '@angular/core';
import { Component, ChangeDetectionStrategy, Input, ViewEncapsulation, HostBinding, HostListener, ElementRef } from '@angular/core';
@Component({
selector: '[uiAutocompleteItem]',
@@ -22,7 +22,7 @@ export class UiAutocompleteItemComponent implements Highlightable {
onClick = (item: any) => {};
constructor() {}
constructor(private _elementRef: ElementRef<any>) {}
@HostListener('click')
handleClickEvent() {
@@ -40,4 +40,8 @@ export class UiAutocompleteItemComponent implements Highlightable {
setInactiveStyles(): void {
this.isActive = false;
}
scrollIntoView() {
this._elementRef?.nativeElement?.scrollIntoView();
}
}

View File

@@ -1,4 +1,5 @@
<div class="ui-autocomplete-output-wrapper scroll-bar" *ngIf="opend">
<ng-content select="[uiAutocompleteSeparator]"></ng-content>
<ng-content select="[uiAutocompleteLabel]"></ng-content>
<ng-content select="[uiAutocompleteItem]"></ng-content>
</div>

View File

@@ -80,7 +80,7 @@ describe('UiSearchboxAutocomplete', () => {
it('should call listKeyManager.onKeyDown and not emit selectItem and not call close on ArrowDown key', () => {
spyOn(spectator.component, 'close');
spyOn(spectator.component.listKeyManager, 'onKeydown');
spyOnProperty(spectator.component, 'activeItem', 'get').and.returnValue({ item: 'Test' });
spyOnProperty(spectator.component, 'activeItem', 'get').and.returnValue({ item: 'Test', scrollIntoView: () => {} });
let selectItem;
spectator.output('selectItem').subscribe((item) => (selectItem = item));

View File

@@ -48,7 +48,6 @@ export class UiAutocompleteComponent implements AfterContentInit, OnDestroy {
this.subscriptions.add(
this.items.changes.subscribe(() => {
this.registerItemOnClick();
this.activateFirstItem();
})
);
}
@@ -75,6 +74,7 @@ export class UiAutocompleteComponent implements AfterContentInit, OnDestroy {
break;
default:
this.listKeyManager.onKeydown(event);
this.activeItem?.scrollIntoView();
}
}
}