Merge develop to feature/HIMA-15

This commit is contained in:
Peter Skrlj
2019-02-07 19:02:55 +01:00
47 changed files with 702 additions and 11816 deletions

View File

@@ -107,7 +107,7 @@ export const filterMock: Filter[] = <Filter[]>[
},
<FilterItem> {
id: 2,
name: 'Deutsche',
name: 'Deutsch',
selected: false
}
],

11706
package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,7 @@
"@ngxs/store": "^3.3.4",
"angular2-signaturepad": "^2.8.0",
"core-js": "^2.5.4",
"ngx-infinite-scroll": "^7.0.1",
"rxjs": "~6.3.3",
"tslib": "^1.9.0",
"zone.js": "~0.8.26"

View File

@@ -0,0 +1,56 @@
import { TestBed, getTestBed } from '@angular/core/testing';
import { CatAvailabilityService } from './cat-availability.service';
import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing';
import { CAT_AV_SERVICE_ENDPOINT } from './tokens';
import { AvailabilityRequestDTO } from './dtos';
describe('CatAvailabilityService', () => {
const endpoint = 'https://fake-endpoint.de';
let injector: TestBed;
let service: CatAvailabilityService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
CatAvailabilityService,
{ provide: CAT_AV_SERVICE_ENDPOINT, useValue: endpoint }
]
});
injector = getTestBed();
service = injector.get(CatAvailabilityService);
httpMock = injector.get(HttpTestingController);
});
describe('#get', () => {
it('should issue a POST request to /ola/availability', () => {
service.get([]).subscribe();
const req = httpMock.expectOne({ method: 'POST', url: `${endpoint}/ola/availability` });
});
it('should have the argument in the request body', () => {
const av: AvailabilityRequestDTO[] = [{ ean: '123456789', qty: 1, supplier: 'L', shopId: 1 }];
service.get(av).subscribe();
const req = httpMock.expectOne({ method: 'POST', url: `${endpoint}/ola/availability` });
expect(req.request.body).toBe(av);
});
it('should throw an Error if the validation fails', () => {
const av: AvailabilityRequestDTO[] = [null];
service.get(av).subscribe(
() => { console.log('fsag'); },
(error) => {
expect(error).toBeDefined();
}
);
httpMock.expectNone({ method: 'POST', url: `${endpoint}/ola/availability` });
});
});
});

View File

@@ -0,0 +1,38 @@
import { Injectable, Inject } from '@angular/core';
import { AvailabilityRequestDTO, ArticleAvailabilityDTO } from './dtos';
import { HttpClient } from '@angular/common/http';
import { CAT_AV_SERVICE_ENDPOINT } from './tokens';
import { Observable, isObservable, throwError, } from 'rxjs';
import { ApiResponse } from './response';
import { availabilityRequestValidator } from './validators';
@Injectable({ providedIn: 'root' })
export class CatAvailabilityService {
endpoint = '';
constructor(
private http: HttpClient,
@Inject(CAT_AV_SERVICE_ENDPOINT) endpoint: string | Observable<string>
) {
if (isObservable(endpoint)) {
endpoint.subscribe(e => this.endpoint = e);
} else {
this.endpoint = endpoint;
}
}
get(req: AvailabilityRequestDTO[]): Observable<ApiResponse<ArticleAvailabilityDTO[]>> {
for (const result of req.map(item => availabilityRequestValidator(item))) {
if (!result.valid) {
return throwError(new Error(result.message));
}
}
return this.http.post<ApiResponse<ArticleAvailabilityDTO[]>>(
`${this.endpoint}/ola/availability`,
req
);
}
}

View File

@@ -7,6 +7,8 @@ import { CAT_SERVICE_ENDPOINT } from './tokens';
import { QueryTokenDTO, AutocompleteTokenDTO } from './dtos/query';
import { UISettingsDTO } from './dtos/ui';
import { shareReplay } from 'rxjs/operators';
import { DEFAULT_QUERY_TOKEN_DTO } from './defaults/query-token-dto';
@Injectable({ providedIn: 'root' })
@@ -36,7 +38,7 @@ export class CatSearchService {
return this.http
.post<PagedApiResponse<ItemDTO>>(
`${this.endpoint}/s/top`,
query
{ ...DEFAULT_QUERY_TOKEN_DTO, ...query }
);
}
@@ -50,7 +52,7 @@ export class CatSearchService {
return this.http
.post<PagedApiResponse<ItemDTO>>(
`${this.endpoint}/s`,
query
{ ...DEFAULT_QUERY_TOKEN_DTO, ...query }
);
}

View File

@@ -0,0 +1,5 @@
import { QueryTokenDTO } from '../dtos';
export const DEFAULT_QUERY_TOKEN_DTO: Partial<QueryTokenDTO> = {
take: 10
};

View File

@@ -0,0 +1,84 @@
import { PriceDTO } from './price.dto';
import { AvailabilityType } from '../../enums';
export interface ArticleAvailabilityDTO {
/**
* Produkt / Artikel PK
*/
itemId?: number;
/**
* Eindeutige Referenz zur Zuordnung
*/
requestReference: string;
/**
* EAN
*/
ean: string;
/**
* Shop Id
*/
shop?: number;
/**
* Price
*/
price: PriceDTO;
/**
* Lieferant
*/
supplier: string;
/**
* Stock Status Code / Meldeschlüssel
*/
ssc: string;
/**
* Verfügbare Menge
*/
qty?: number;
/**
* Vorgemerkt
*/
isPrebooked?: boolean;
/**
* Voraussichtliches Lieferdatum
*/
at?: Date;
/**
* Alternatives Voraussichtliches Lieferdatum
*/
altAt?: Date;
/**
* Verfügbarkeitsstatus
*/
availabilityType: AvailabilityType;
/**
* Rang
*/
preferred?: number;
/**
* Zeitstemple der Anfrage
*/
requested?: Date;
/**
* StatusCode der Verfügbarkeitsanfrage
*/
requestStatusCode: string;
/**
* Beschreibung des StatusCode
*/
requestMessage: string;
}

View File

@@ -1,10 +1,12 @@
// start:ng42.barrel
export * from './article-availability.dto';
export * from './availability.dto';
export * from './item.dto';
export * from './price-value.dto';
export * from './price.dto';
export * from './product.dto';
export * from './shelf-info.dto';
export * from './shop.dto';
export * from './size.dto';
export * from './spec.dto';
export * from './stock-info.dto';

View File

@@ -0,0 +1,11 @@
export interface ShopDTO {
/**
* PK
*/
id?: number;
/**
* Name / Bezeichner
*/
name: string;
}

View File

@@ -0,0 +1,58 @@
import { PriceDTO } from '../data';
export interface AvailabilityRequestDTO {
/**
* Artikel / Produkt PK
*/
itemId?: string;
/**
* EAN
*/
ean?: string;
/**
* Menge / Stück
*/
qty: number;
/**
* Bestellzeichen
*/
orderCode?: string;
/**
* Lieferant
*/
supplier: string;
/**
* Bestellung vormerken
*/
preBook?: boolean;
/**
* Preis
*/
price?: PriceDTO;
/**
* Stock Status Code
*/
ssc?: string;
/**
* Vsl. Lieferdatum
*/
estimatedShipping?: Date;
/**
* Shop PK
*/
shopId: number;
/**
* Zuordnungs-ID
*/
availabilityReference?: string;
}

View File

@@ -1,5 +1,6 @@
// start:ng42.barrel
export * from './autocomplete-token.dto';
export * from './availability-request.dto';
export * from './query-token.dto';
// end:ng42.barrel

View File

@@ -1,5 +1,6 @@
// start:ng42.barrel
export * from './cat-image.service';
export * from './cat-availability.service';
export * from './cat-search-mock.data';
export * from './cat-search-mock.service';
export * from './cat-search.service';

View File

@@ -1,4 +1,5 @@
// start:ng42.barrel
export * from './dictionary.model';
export * from './valiation-result.model';
// end:ng42.barrel

View File

@@ -0,0 +1,27 @@
import { StringDictionary } from './dictionary.model';
export class ValidationResult {
private _valid = true;
get valid() { return this._valid; }
private _message?: string;
get message() { return this._message; }
private _invalidProperties?: StringDictionary<string>;
get invalidProperties() { return this._invalidProperties; }
setMessage(message: string) {
this._message = message;
this._valid = false;
}
addPropertyError(proeprty: string, reason: string) {
const ip = this.invalidProperties || {};
ip[proeprty] = reason;
this._invalidProperties = ip;
if (this.message == null) {
this.setMessage('Invalid property.');
}
}
}

View File

@@ -2,3 +2,4 @@ import { InjectionToken } from '@angular/core';
import { Observable } from 'rxjs';
export const CAT_SERVICE_ENDPOINT = new InjectionToken<string | Observable<string>>('cat:service:endpoint');
export const CAT_AV_SERVICE_ENDPOINT = new InjectionToken<string | Observable<string>>('cat:av:service:endpoint');

View File

@@ -0,0 +1,19 @@
import { isNil } from './common';
describe('utils common isNil', () => {
it('should return true if parameter is null', () => {
expect(isNil(null)).toBeTruthy();
});
it('should return true if parameter is undefined', () => {
expect(isNil(undefined)).toBeTruthy();
});
it('should return false if parameter is an empty string', () => {
expect(isNil('')).toBeFalsy();
});
it('should return false if parameter is 0', () => {
expect(isNil(0)).toBeFalsy();
});
});

View File

@@ -0,0 +1,3 @@
export function isNil(val: any): boolean {
return val == null;
}

View File

@@ -0,0 +1,5 @@
// start:ng42.barrel
export * from './common';
export * from './string';
// end:ng42.barrel

View File

@@ -0,0 +1,23 @@
import { stringIsNilOrEmpty } from './string';
describe('utils common stringIsNilOrEmpty', () => {
it('should return true if the parameter is null', () => {
expect(stringIsNilOrEmpty(null)).toBeTruthy();
});
it('should return true if the parameter is undefined', () => {
expect(stringIsNilOrEmpty(undefined)).toBeTruthy();
});
it('should return true if the parameter is an empty string', () => {
expect(stringIsNilOrEmpty('')).toBeTruthy();
});
it('should return true if the parameter is a string with whitespaces', () => {
expect(stringIsNilOrEmpty(' ')).toBeTruthy();
});
it('should return false if the parameter is a string with content', () => {
expect(stringIsNilOrEmpty(' Hello World ')).toBeFalsy();
});
});

View File

@@ -0,0 +1,10 @@
import { isNil } from './common';
export function stringIsNilOrEmpty(value: string) {
if (isNil(value)) {
return true;
} else if (typeof value === 'string' && value.trim() === '') {
return true;
}
return false;
}

View File

@@ -0,0 +1,28 @@
import { availabilityRequestValidator } from './availability-request.validator';
describe('Validators availabilityRequestValidator', () => {
it('should return an invalid ValidationResult when ean and itemId is empty', () => {
const result = availabilityRequestValidator({ qty: 1, shopId: 1, supplier: 'L' });
expect(result.valid).toBeFalsy();
expect(result.invalidProperties['itemId']).toBeDefined();
expect(result.invalidProperties['ean']).toBeDefined();
});
it('should return an invalid ValidationResult when qty is less than 0', () => {
const result = availabilityRequestValidator({ itemId: '123', qty: 0, shopId: 1, supplier: 'L' });
expect(result.valid).toBeFalsy();
expect(result.invalidProperties['qty']).toBeDefined();
});
it('should return an invalid ValidationResult when shopId is less than 0', () => {
const result = availabilityRequestValidator({ ean: '123', qty: 1, shopId: 0, supplier: 'L' });
expect(result.valid).toBeFalsy();
expect(result.invalidProperties['shopId']).toBeDefined();
});
it('should return an invalid ValidationResult when supplier is empty', () => {
const result = availabilityRequestValidator({ itemId: '123', qty: 1, shopId: 1, supplier: '' });
expect(result.valid).toBeFalsy();
expect(result.invalidProperties['supplier']).toBeDefined();
});
});

View File

@@ -0,0 +1,32 @@
import { AvailabilityRequestDTO } from '../dtos';
import { ValidationResult, StringDictionary } from '../models';
import { stringIsNilOrEmpty } from '../utils/string';
import { isNil } from '../utils';
export function availabilityRequestValidator(av: AvailabilityRequestDTO): ValidationResult {
const result = new ValidationResult();
if (isNil(av)) {
result.setMessage('AvailabilityRequestDTO is null or undefined.');
} else {
if (stringIsNilOrEmpty(av.itemId) && stringIsNilOrEmpty(av.ean)) {
result.addPropertyError('itemId', 'Either itemId or ean must be set.');
result.addPropertyError('ean', 'Either itemId or ean must be set.');
}
if (!(av.qty > 0)) {
result.addPropertyError('qty', 'Property qty must be grater than 0.');
}
if (stringIsNilOrEmpty(av.supplier)) {
result.addPropertyError('supplier', 'Property supplier is required.');
}
if (!(av.shopId > 0)) {
result.addPropertyError('shopId', `Value ${av.shopId} is not valid.`);
}
}
return result;
}

View File

@@ -0,0 +1,4 @@
// start:ng42.barrel
export * from './availability-request.validator';
// end:ng42.barrel

View File

@@ -2,6 +2,7 @@
* Public API Surface of cat-service
*/
export * from './lib/cat-image.service';
export * from './lib/cat-availability.service';
export * from './lib/cat-search-mock.data';
export * from './lib/cat-search-mock.service';
export * from './lib/cat-search.service';

View File

@@ -7,7 +7,7 @@ import { ComponentsModule } from './modules/components.module';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BasicAuthorizationInterceptor, BasicAuthorizationOptions } from './core/interceptors';
import { SearchResultsComponent } from './components/search-results/search-results.component';
import { CatServiceModule, CAT_SERVICE_ENDPOINT, CatSearchService } from 'cat-service';
import { CatServiceModule, CAT_SERVICE_ENDPOINT, CatSearchService, CAT_AV_SERVICE_ENDPOINT } from 'cat-service';
import { ProductCardComponent } from './components/product-card/product-card.component';
import { ConfigService } from './core/services/config.service';
import { ProductDetailsComponent } from './components/product-details/product-details.component';
@@ -17,7 +17,6 @@ import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin';
import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin';
import { FeedState } from './core/store/state/feed.state';
import { ProcessState } from './core/store/state/process.state';
import { ProductsState } from './core/store/state/products.state';
import { environment } from 'src/environments/environment';
import { BreadcrumbsState } from './core/store/state/breadcrumbs.state';
import { FilterState } from './core/store/state/filter.state';
@@ -30,7 +29,6 @@ import { CustomerSearchResultComponent } from './components/customer-search-resu
const states = [
FeedState,
ProcessState,
ProductsState,
BreadcrumbsState,
FilterState
];
@@ -45,7 +43,11 @@ export function _basicAuthorizationInterceptorFactory(conf: ConfigService) {
}
export function _catServiceEndpointProviderFactory(conf: ConfigService) {
return conf.select<string>('catService', 'endpoint');
return conf.select<string>('catService', 'endpoint', 'catService');
}
export function _catAvServiceEndpointProviderFactory(conf: ConfigService) {
return conf.select<string>('catService', 'endpoint', 'avService');
}
export function _feedServiceEndpointProviderFactory(conf: ConfigService) {
@@ -79,6 +81,7 @@ export function _feedServiceEndpointProviderFactory(conf: ConfigService) {
{ provide: APP_INITIALIZER, useFactory: _configInitializer, multi: true, deps: [ ConfigService ] },
{ provide: HTTP_INTERCEPTORS, useFactory: _basicAuthorizationInterceptorFactory, deps: [ConfigService], multi: true },
{ provide: CAT_SERVICE_ENDPOINT, useFactory: _catServiceEndpointProviderFactory, deps: [ConfigService] },
{ provide: CAT_AV_SERVICE_ENDPOINT, useFactory: _catAvServiceEndpointProviderFactory, deps: [ConfigService] },
{ provide: FEED_SERVICE_ENDPOINT, useFactory: _feedServiceEndpointProviderFactory, deps: [ConfigService] },
// { provide: CatSearchService, useClass: CatSearchMockService }, // Uncomment if u want to use the CatSearchMockService
{ provide: FeedService, useClass: FeedMockService } // Uncomment if u want to use the FeedMockService

View File

@@ -10,8 +10,8 @@ import { Process } from 'src/app/core/models/process.model';
import { getRandomPic } from 'src/app/core/utils/process.util';
import { Breadcrumb } from 'src/app/core/models/breadcrumb.model';
import { ProcessState } from 'src/app/core/store/state/process.state';
import { AddProcess, ChangeCurrentRoute, AddSearch } from 'src/app/core/store/actions/process.actions';
import { ProductsState } from 'src/app/core/store/state/products.state';
import { AddProcess, ChangeCurrentRoute, AddSearch} from 'src/app/core/store/actions/process.actions';
import { AllowProductLoad } from 'src/app/core/store/actions/process.actions';
import { LoadRecentProducts } from 'src/app/core/store/actions/product.actions';
@Component({
@@ -21,7 +21,7 @@ import { LoadRecentProducts } from 'src/app/core/store/actions/product.actions';
})
export class ArticleSearchComponent implements OnInit {
@Select(ProductsState.getRecentProducts) recentArticles$: Observable<RecentArticleSearch[]>;
@Select(ProcessState.getRecentProducts) recentArticles$: Observable<RecentArticleSearch[]>;
recentArticles: RecentArticleSearch[];
products$: Observable<Product[]>;
products: Product[];
@@ -42,10 +42,14 @@ export class ArticleSearchComponent implements OnInit {
this.loadSelectedFilters();
const search = <Search>{
query: this.searchParams,
fitlers: this.filters
fitlers: this.filters,
take: 5,
skip: 0,
firstLoad: true
};
this.store.dispatch(new AllowProductLoad());
this.store.dispatch(new AddSearch(search));
this.navigateToRoute('search-results');
this.navigateToRoute('search-results#start');
}
navigateToRoute(route: string) {

View File

@@ -2,7 +2,7 @@ import { Component, OnInit, Input } from '@angular/core';
import { Process } from 'src/app/core/models/process.model';
import { Router } from '@angular/router';
import { Store } from '@ngxs/store';
import { DeleteProcess, SelectProcess } from 'src/app/core/store/actions/process.actions';
import { DeleteProcess, SelectProcess, PreventProductLoad } from 'src/app/core/store/actions/process.actions';
import { Cart } from '../../core/models/cart.model';
@Component({
@@ -27,6 +27,7 @@ export class ProcessTabComponent implements OnInit {
selectProcess(process: Process): void {
this.store.dispatch(new SelectProcess(process));
this.store.dispatch(new PreventProductLoad());
this.router.navigate([process.currentRoute]);
}

View File

@@ -52,7 +52,7 @@
</div>
<div class="publisher-order">
<div class="publisher-serial">
<div class="publisher align-left">
<div class="publisher align-left wrap-text-more">
<span>{{product.publisher}}</span>
</div>
<div class="align-left">

View File

@@ -3,8 +3,13 @@ import { Product } from 'src/app/core/models/product.model';
import { Router } from '@angular/router';
import { CatImageService } from 'cat-service';
import { ReplaySubject, Observable, of } from 'rxjs';
import { flatMap, catchError } from 'rxjs/operators';
import { flatMap, catchError, filter, map } from 'rxjs/operators';
import { getProductTypeIcon } from 'src/app/core/utils/product.util';
import { ItemDTO } from 'projects/cat-service/src/lib';
import { Select, Store } from '@ngxs/store';
import { AddSelectedProduct } from 'src/app/core/store/actions/product.actions';
import { ChangeCurrentRoute } from 'src/app/core/store/actions/process.actions';
import { ProcessState } from 'src/app/core/store/state/process.state';
@Component({
selector: 'app-product-card',
@@ -14,6 +19,7 @@ import { getProductTypeIcon } from 'src/app/core/utils/product.util';
export class ProductCardComponent implements OnInit {
private _product: Product;
@Select(ProcessState.getProducts) items$: Observable<ItemDTO[]>;
@Input()
get product() { return this._product; }
set product(val) {
@@ -29,7 +35,11 @@ export class ProductCardComponent implements OnInit {
productTypeIcon: string;
constructor(private router: Router, private catImageService: CatImageService) {
constructor(
private router: Router,
private catImageService: CatImageService,
private store: Store
) {
this.imageUrl$ = this.eanChangedSub.pipe(
flatMap(ean => this.catImageService.getImageUrl(ean)),
catchError(() => of(''))
@@ -37,7 +47,20 @@ export class ProductCardComponent implements OnInit {
}
productDetails(product: Product) {
this.router.navigate(['product-details/' + product.id]);
// TODO: this is temporary solution for the incostency of product detail API
this.items$.pipe(
map(item => {
if (item) {
return item.find(i => i.id === product.id);
}
})
).subscribe(
(data: ItemDTO) => this.store.dispatch(new AddSelectedProduct(data))
);
const currentRoute = 'product-details/' + product.id;
this.store.dispatch(new ChangeCurrentRoute(currentRoute));
this.router.navigate([currentRoute]);
}
ngOnInit() {

View File

@@ -147,11 +147,9 @@
width: 149px;
padding: 12px;
cursor: pointer;
//position: absolute;
//bottom: 25px;
&-active, &:hover {
&-active {
background-color: #f70400;
border: none;
border-radius: 25px;

View File

@@ -4,8 +4,11 @@ import { CheckoutComponent } from '../checkout/checkout.component';
import { ProductService } from 'src/app/core/services/product.service';
import { ItemDTO, CatImageService } from 'cat-service';
import { Observable } from 'rxjs';
import { monthNames, getFormatedPublicationDate } from 'src/app/core/utils/product.util';
import { publishBehavior } from 'rxjs/operators';
import { getFormatedPublicationDate } from 'src/app/core/utils/product.util';
import { ProcessState, ProcessStateModel } from 'src/app/core/store/state/process.state';
import { Select } from '@ngxs/store';
import { stateNameErrorMessage } from '@ngxs/store/src/decorators/state';
import { Process } from 'src/app/core/models/process.model';
@Component({
selector: 'app-product-details',
@@ -17,6 +20,8 @@ export class ProductDetailsComponent implements OnInit {
id: number;
item: ItemDTO;
selectedItem: ItemDTO;
@Select(ProcessState.getSelectedProduct) selectedItem$: Observable<ItemDTO>;
readonly FULL_DESCRIPTION_LABEL = 'Klappentext';
readonly AUTOR = 'Autor';
@@ -55,6 +60,9 @@ export class ProductDetailsComponent implements OnInit {
return this.product = this.productDetailMapper(item);
}
);
this.selectedItem$.subscribe(
(data: ItemDTO) => this.selectedItem = data
);
}
productDetailMapper(item: ItemDTO) {
@@ -77,12 +85,12 @@ export class ProductDetailsComponent implements OnInit {
// product object mapping
if (item.pr) {
ean = item.pr.ean;
eanTag = 'EAN ' + ean;
eanTag = ean;
productIcon$ = this.catImageService.getImageUrl(ean, { width: 469, height: 575});
locale = item.pr.locale;
publicationDate = getFormatedPublicationDate(item.pr.publicationDate);
format = item.pr.formatDetail;
formatIcon = item.pr.format;
format = this.selectedItem ? this.selectedItem.pr.formatDetail : null;
formatIcon = this.selectedItem ? this.selectedItem.pr.format : null;
category = item.pr.productGroup;
publisher = item.pr.manufacturer;
}

View File

@@ -1,6 +1,7 @@
<div class="result-container">
<app-filter></app-filter>
<app-product-card *ngFor="let product of products"
[product]="product">
</app-product-card>
<div id="start" infiniteScroll [infiniteScrollDistance]="1" [infiniteScrollThrottle]="50" (scrolled)="onScroll()" [scrollWindow]="true">
<app-product-card *ngFor="let product of products" [product]="product">
</app-product-card>
</div>
</div>

View File

@@ -6,10 +6,11 @@ import { Router } from '@angular/router';
import { ProductMapping } from '../../core/mappings/product.mapping';
import { map, filter } from 'rxjs/operators';
import { Select, Store } from '@ngxs/store';
import { ProductsState } from 'src/app/core/store/state/products.state';
import { ItemDTO } from 'dist/cat-service/lib/dtos';
import { Observable } from 'rxjs';
import { GetProducts } from 'src/app/core/store/actions/product.actions';
import { ProcessState } from 'src/app/core/store/state/process.state';
import { AddSearch, AllowProductLoad } from 'src/app/core/store/actions/process.actions';
@Component({
selector: 'app-search-results',
@@ -20,7 +21,8 @@ export class SearchResultsComponent implements OnInit {
currentSearch: Search;
products: Product[];
@Select(ProductsState.getProducts) products$: Observable<ItemDTO[]>;
@Select(ProcessState.getProducts) products$: Observable<ItemDTO[]>;
skip = 0;
constructor(
private store: Store,
@@ -34,7 +36,7 @@ export class SearchResultsComponent implements OnInit {
this.router.navigate(['dashboard']);
return;
}
this.store.dispatch(new GetProducts(this.currentSearch.query));
this.store.dispatch(new GetProducts(this.currentSearch));
this.loadProducts();
}
@@ -50,6 +52,14 @@ export class SearchResultsComponent implements OnInit {
);
}
onScroll() {
this.store.dispatch(new AllowProductLoad());
this.skip = this.skip + 5;
const search = { ...this.currentSearch, firstLoad: false, skip: this.skip};
this.store.dispatch(new GetProducts(search));
this.store.dispatch(new AddSearch(search));
}
loadProducts() {
this.products$.pipe(
filter(f => Array.isArray(f)),

View File

@@ -2,6 +2,7 @@ import { Breadcrumb } from './breadcrumb.model';
import { Search } from './search.model';
import { User } from './user.model';
import { Cart } from './cart.model';
import { ItemDTO } from 'cat-service';
export interface Process {
id: number;
@@ -12,5 +13,8 @@ export interface Process {
breadcrumbs: Breadcrumb[];
search: Search;
users: User[];
cart: Cart[]
cart: Cart[];
itemsDTO: ItemDTO[];
selectedItem: ItemDTO;
preventLoading: boolean;
}

View File

@@ -2,5 +2,8 @@ import { Filter } from './filter.model';
export interface Search {
query: string;
skip: number;
take: number;
fitlers: Filter[];
firstLoad: boolean;
}

View File

@@ -11,6 +11,7 @@ import {
PagedApiResponse
} from 'cat-service';
import { map } from 'rxjs/operators';
import { Search } from '../models/search.model';
@Injectable({
@@ -61,10 +62,12 @@ export class ProductService {
}
// service method for calling product search API
searchItems(searchTerm: string): Observable<ItemDTO[]> {
this.persistLastSearchToLocalStorage(searchTerm);
searchItems(search: Search): Observable<ItemDTO[]> {
this.persistLastSearchToLocalStorage(search.query);
const queryToken = <QueryTokenDTO> {
input: {qs: searchTerm}
input: {qs: search.query},
skip: search.skip,
take: search.take
};
return this.searchService.search(queryToken).pipe(

View File

@@ -19,7 +19,7 @@ export class UserService {
) {
return user;
}
})
});
return of(filteredUsers);
}

View File

@@ -11,6 +11,8 @@ export const ADD_SEARCH = '[PROCESS] Add search';
export const SEARCH_USER = '[PROCESS] Search for user';
export const SET_USER_NAME = '[PROCESS] Set the users name in tab';
export const SET_CART = '[PROCESS] Set cart data for user';
export const PREVENT_PRODUCT_LOAD = '[POCESS] Prevent product load';
export const ALLOW_PRODUCT_LOAD = '[POCESS] Allow product load';
export class AddProcess {
static readonly type = ADD_PROCESS;
@@ -59,3 +61,11 @@ export class SetCartData {
constructor(public quantity: number, public payload: ItemDTO, public breadcrumb: Breadcrumb) {}
}
export class PreventProductLoad {
static readonly type = PREVENT_PRODUCT_LOAD;
}
export class AllowProductLoad {
static readonly type = ALLOW_PRODUCT_LOAD;
}

View File

@@ -1,5 +1,9 @@
import { ItemDTO } from 'projects/cat-service/src/lib';
import { Search } from '../../models/search.model';
export const LOAD_RECENT_PRODUCTS = '[PRODUCTS] Load recent';
export const GET_PRODUCTS = '[PRODUCTS] Get';
export const ADD_SELECTED_PRODUCT = '[PRODUCTS] Add selected';
export class LoadRecentProducts {
static readonly type = LOAD_RECENT_PRODUCTS;
@@ -10,5 +14,11 @@ export class LoadRecentProducts {
export class GetProducts {
static readonly type = GET_PRODUCTS;
constructor(public payload: string) {}
constructor(public payload: Search) {}
}
export class AddSelectedProduct {
static readonly type = ADD_SELECTED_PRODUCT;
constructor(public payload: ItemDTO) {}
}

View File

@@ -5,26 +5,53 @@ import { UserService } from '../../services/user.service';
import { User } from '../../models/user.model';
import { Breadcrumb } from '../../models/breadcrumb.model';
import { Cart } from '../../models/cart.model';
import { ProductService } from '../../services/product.service';
import { RecentArticleSearch } from '../../models/recent-article-search.model';
import { GetProducts, LoadRecentProducts, AddSelectedProduct } from '../actions/product.actions';
import { ItemDTO } from 'dist/cat-service/lib/dtos';
import { getCurrentProcess } from '../../utils/process.util';
export class ProcessStateModel {
processes: Process[];
recentArticles: RecentArticleSearch[];
}
@State<ProcessStateModel>({
name: 'processes',
defaults: {
processes: []
processes: [],
recentArticles: []
}
})
export class ProcessState {
constructor(private usersService: UserService) { }
constructor(private usersService: UserService, protected productService: ProductService) { }
@Selector()
static getState(state: ProcessStateModel) {
return state;
}
@Selector()
static getProcesses(state: ProcessStateModel) {
return state.processes;
}
@Selector()
static getRecentProducts(state: ProcessStateModel) {
return state.recentArticles;
}
@Selector()
static getProducts(state: ProcessStateModel) {
return state.processes.find(t => t.selected === true).itemsDTO;
}
@Selector()
static getSelectedProduct(state: ProcessStateModel) {
return state.processes.find(t => t.selected === true).selectedItem;
}
@Selector()
static getCurrentProcess(state: ProcessStateModel) {
const currenProcces = state.processes.map(
@@ -265,7 +292,7 @@ export class ProcessState {
}
@Action(actions.SetCartData)
setCartData(ctx: StateContext<ProcessStateModel>, { quantity , payload, breadcrumb }: actions.SetCartData) {
setCartData(ctx: StateContext<ProcessStateModel>, { quantity, payload, breadcrumb }: actions.SetCartData) {
const state = ctx.getState();
const newProcessState = state.processes.map(
(process: Process) => {
@@ -281,11 +308,11 @@ export class ProcessState {
book: payload
};
}
return item;
});
return {
return {
...process,
breadcrumbs: [
breadcrumb
@@ -296,7 +323,7 @@ export class ProcessState {
};
} else {
// Add new item to cart
return {
return {
...process,
breadcrumbs: [
breadcrumb
@@ -320,4 +347,121 @@ export class ProcessState {
processes: [...newProcessState]
});
}
@Action(GetProducts)
getProducts(ctx: StateContext<ProcessStateModel>, { payload }: GetProducts) {
const state = ctx.getState();
if (!state.processes) {
return;
}
const currentProcess = getCurrentProcess(state.processes);
if (currentProcess.search === payload
&& currentProcess.itemsDTO
&& currentProcess.itemsDTO.length > 0
&& currentProcess.preventLoading
) {
ctx.patchState({
...state
});
} else {
this.productService.searchItems(payload).subscribe(
(items: ItemDTO[]) => {
if (items) {
ctx.patchState({
...state,
processes: payload.skip === 0 ?
this.changeProducResultsForCurrentProcess(state.processes, items)
: this.extendProducResultsForCurrentProcess(state.processes, items)
});
}
}
);
}
}
@Action(LoadRecentProducts)
loadRecentProducts(ctx: StateContext<ProcessStateModel>) {
const state = ctx.getState();
this.productService.getRecentSearches().subscribe(
(products: RecentArticleSearch[]) => {
if (products) {
ctx.patchState({
...state,
recentArticles: products.reverse().slice(0, 5)
});
}
}
);
}
@Action(AddSelectedProduct)
AddSelectedProduct(ctx: StateContext<ProcessStateModel>, { payload }: AddSelectedProduct) {
const state = ctx.getState();
ctx.patchState({
...state,
processes: this.changeSelectedItemForCurrentProcess(state.processes, payload)
});
}
@Action(actions.PreventProductLoad)
preventProductLoad(ctx: StateContext<ProcessStateModel>) {
const state = ctx.getState();
ctx.patchState({
...state,
processes: state.processes.map(
process => {
if (process.selected === true) {
return {...process, preventLoading: true};
}
return {...process};
}
)
});
}
@Action(actions.AllowProductLoad)
allowProductLoad(ctx: StateContext<ProcessStateModel>) {
const state = ctx.getState();
ctx.patchState({
...state,
processes: state.processes.map(
process => {
if (process.selected === true) {
return {...process, preventLoading: false};
}
return {...process};
}
)
});
}
changeSelectedItemForCurrentProcess(processes: Process[], item: ItemDTO): Process[] {
const newProcessState = processes.map(process => {
if (process.selected === true) {
return { ...process, selectedItem: item };
}
return { ...process };
});
return newProcessState;
}
changeProducResultsForCurrentProcess(processes: Process[], items: ItemDTO[]): Process[] {
const newProcessState = processes.map(process => {
if (process.selected === true) {
return { ...process, itemsDTO: items };
}
return { ...process };
});
return newProcessState;
}
extendProducResultsForCurrentProcess(processes: Process[], items: ItemDTO[]): Process[] {
const newProcessState = processes.map(process => {
if (process.selected === true) {
return { ...process, itemsDTO: [...process.itemsDTO, ...items] };
}
return { ...process };
});
return newProcessState;
}
}

View File

@@ -1,63 +0,0 @@
import { RecentArticleSearch } from '../../models/recent-article-search.model';
import { State, Selector, Action, StateContext } from '@ngxs/store';
import { LoadRecentProducts, GetProducts } from '../actions/product.actions';
import { ProductService } from '../../services/product.service';
import { ItemDTO } from 'cat-service';
export class ProductsStateModel {
recentArticles: RecentArticleSearch[];
itemsDTO: ItemDTO[];
}
@State<ProductsStateModel>({
name: 'products',
defaults: {
recentArticles: [],
itemsDTO: []
}
})
export class ProductsState {
constructor(private productService: ProductService) {}
@Selector()
static getRecentProducts(state: ProductsStateModel) {
return state.recentArticles;
}
@Selector()
static getProducts(state: ProductsStateModel) {
return state.itemsDTO;
}
@Action(LoadRecentProducts)
loadRecentProducts(ctx: StateContext<ProductsStateModel>) {
const state = ctx.getState();
this.productService.getRecentSearches().subscribe(
(products: RecentArticleSearch[]) => {
if (products) {
ctx.patchState({
...state,
recentArticles: products.reverse().slice(0, 5)
});
}
}
);
}
@Action(GetProducts)
GetProducts(ctx: StateContext<ProductsStateModel>, { payload }: GetProducts) {
const state = ctx.getState();
this.productService.searchItems(payload).subscribe(
(items: ItemDTO[]) => {
if (items) {
ctx.patchState({
...state,
itemsDTO: items
});
}
}
);
}
}

View File

@@ -1,3 +1,9 @@
import { Process } from '../models/process.model';
export function getRandomPic() {
return 'Pic_' + (Math.floor(Math.random() * 6) + 1) + '-3x';
}
export function getCurrentProcess(processes: Process[]): Process {
return processes.find(t => t.selected === true);
}

View File

@@ -17,6 +17,7 @@ import { RecommandationCardComponent } from '../components/recommandation-card/r
import { FormsModule } from '@angular/forms';
import { FilterComponent } from '../components/filter/filter.component';
import { FilterItemComponent } from '../components/filter-item/filter-item.component';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
@NgModule({
declarations: [
@@ -40,6 +41,7 @@ import { FilterItemComponent } from '../components/filter-item/filter-item.compo
AppRoutingModule,
FormsModule,
NewsletterSignupModule
InfiniteScrollModule
],
exports: [
HeaderComponent,
@@ -55,7 +57,8 @@ import { FilterItemComponent } from '../components/filter-item/filter-item.compo
NewsCardComponent,
RecommandationCardComponent,
FilterComponent,
FilterItemComponent
FilterItemComponent,
InfiniteScrollModule
]
})
export class ComponentsModule {}

View File

@@ -13,7 +13,7 @@ export const routes: Routes = [
{ path: 'article-search', component: ArticleSearchComponent },
{ path: 'customer-search', component: CustomerSearchComponent },
{ path: 'customer-search-result', component: CustomerSearchResultComponent },
{ path: 'search-results', component: SearchResultsComponent },
{ path: 'search-results#start', component: SearchResultsComponent },
{ path: 'product-details/:id', component: ProductDetailsComponent },
{ path: 'newsletter', component: NewsletterSignupComponent }
];

View File

@@ -3,11 +3,15 @@
"client": "eu6YYrF3NB4CtxMTwrgC",
"password": "rf5f9JUzKW7cjwd6vb6YHv2L2knEZ6m4mNsbpLMF",
"endpoints": [
"https://catsearch.paragon-data.de"
"https://catsearch.paragon-data.de",
"https://ava.paragon-data.de"
]
},
"catService": {
"endpoint": "https://catsearch.paragon-data.de"
"endpoint": {
"catService": "https://catsearch.paragon-data.de",
"avService": "https://ava.paragon-data.de"
}
},
"feedService": {
"endpoint": "https://isa.paragon-data.de"

View File

@@ -4379,6 +4379,13 @@ ng-packagr@^4.2.0:
uglify-js "^3.0.7"
update-notifier "^2.3.0"
ngx-infinite-scroll@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/ngx-infinite-scroll/-/ngx-infinite-scroll-7.0.1.tgz#59472108f5b6960519c269d6997fe3ae0961be07"
integrity sha512-be9DAAuabV7VGI06/JUnS6pXC6mcBOzA4+SBCwOcP9WwJ2r5GjdZyOa34ls9hi1MnCOj3zrXLvPKQ/UDp6csIw==
dependencies:
opencollective "^1.0.3"
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"