diff --git a/src/app/core/interceptors/api.interceptor.ts b/src/app/core/interceptors/api.interceptor.ts index 34d8c7c..2e05691 100644 --- a/src/app/core/interceptors/api.interceptor.ts +++ b/src/app/core/interceptors/api.interceptor.ts @@ -9,7 +9,7 @@ export const apiInterceptor: HttpInterceptorFn = (req, next) => { return next(req); } let requestURL = req.url; - let apiURL = environment.apiUrl; + const apiURL = environment.apiUrl; if (!requestURL.startsWith(apiURL)) { requestURL = `${apiURL}/${req.url}`; } diff --git a/src/app/modules/product/product.component.spec.ts b/src/app/modules/product/product.component.spec.ts index f806fbc..d3d880e 100644 --- a/src/app/modules/product/product.component.spec.ts +++ b/src/app/modules/product/product.component.spec.ts @@ -95,4 +95,9 @@ describe('ProductComponent', () => { expect(product.name.toLowerCase()).toContain(productName); }); })); + + it('setupIntersectionObserver should not trigger when init page', () => { + component.ngAfterViewInit(); + expect(component.criteria.nextPageHref).toBeUndefined(); + }); }); diff --git a/src/app/modules/product/product.component.ts b/src/app/modules/product/product.component.ts index 979249e..46b8727 100644 --- a/src/app/modules/product/product.component.ts +++ b/src/app/modules/product/product.component.ts @@ -50,8 +50,8 @@ export class ProductComponent implements AfterViewInit, OnDestroy { type: FilterType.All_TYPES, sort: SortType.POPULARITY }; - links!: Link; - page!: Page; + responseLink!: Link; + responsePage!: Page; productService = inject(ProductService); themeService = inject(ThemeService); @@ -85,7 +85,7 @@ export class ProductComponent implements AfterViewInit, OnDestroy { onFilterChange(filterType: FilterType) { this.criteria = { ...this.criteria, - nextPageHref: undefined, + nextPageHref: '', type: filterType }; this.loadProductItems(true); @@ -94,7 +94,7 @@ export class ProductComponent implements AfterViewInit, OnDestroy { onSortChange(sortType: SortType) { this.criteria = { ...this.criteria, - nextPageHref: undefined, + nextPageHref: '', sort: sortType }; this.loadProductItems(true); @@ -104,27 +104,27 @@ export class ProductComponent implements AfterViewInit, OnDestroy { this.searchTextChanged.next(searchString); } - loadProductItems(shouldCleanData?: boolean) { + loadProductItems(shouldCleanData = false) { this.subscriptions.push( this.productService.findProductsByCriteria(this.criteria).subscribe((response: ProductApiResponse) => { - let newProducts = response._embedded.products as Product[]; + const newProducts = response._embedded.products; if (shouldCleanData) { this.products.set(newProducts); } else { this.products.update(existingProducts => existingProducts.concat(newProducts)); } - this.links = response._links; - this.page = response.page; + this.responseLink = response._links; + this.responsePage = response.page; }) ); } setupIntersectionObserver() { const options = { root: null, rootMargin: '0px', threshold: 0.1 }; - const observer = new IntersectionObserver((entries) => { + const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting && this.hasMore()) { - this.criteria.nextPageHref = this.links?.next?.href; + this.criteria.nextPageHref = this.responseLink?.next?.href; this.loadProductItems(); } }); @@ -134,11 +134,11 @@ export class ProductComponent implements AfterViewInit, OnDestroy { } hasMore() { - if (!this.page || !this.links) { + if (!this.responsePage || !this.responseLink) { return false; } - return this.page.number < this.page.totalPages - && this.links?.next != undefined; + return this.responsePage.number < this.responsePage.totalPages + && this.responseLink?.next !== undefined; } ngOnDestroy(): void { diff --git a/src/app/modules/product/product.service.spec.ts b/src/app/modules/product/product.service.spec.ts index 41845ca..1368b49 100644 --- a/src/app/modules/product/product.service.spec.ts +++ b/src/app/modules/product/product.service.spec.ts @@ -11,6 +11,7 @@ import { MOCK_PRODUCTS } from '../../shared/mocks/mock-data'; import { Criteria } from '../../shared/models/criteria.model'; import { ProductService } from './product.service'; import { Product } from '../../shared/models/product.model'; +import { catchError } from 'rxjs'; const PRODUCT_ID = 'amazon-comprehend'; const NOT_EXIST_ID = 'undefined'; @@ -48,7 +49,7 @@ describe('ProductService', () => { }); it('findProductsByCriteria with should return products properly', () => { - const searchString = 'amazon-comprehend'; + const searchString = 'Amazon Comprehend'; const criteria: Criteria = { search: searchString, sort: SortType.ALPHABETICALLY, @@ -68,7 +69,7 @@ describe('ProductService', () => { }); }); - it('getProductByCriteria with empty searchString', () => { + it('findProductsByCriteria with empty searchString', () => { const criteria: Criteria = { search: '', sort: null, @@ -79,7 +80,7 @@ describe('ProductService', () => { }); }); - it('getProductByCriteria with popularity order', () => { + it('findProductsByCriteria with popularity order', () => { const criteria: Criteria = { search: '', sort: SortType.POPULARITY, @@ -101,7 +102,7 @@ describe('ProductService', () => { }); }); - it('getProductByCriteria with default sort', () => { + it('findProductsByCriteria with default sort', () => { const criteria: Criteria = { search: '', sort: SortType.RECENT, @@ -111,4 +112,17 @@ describe('ProductService', () => { expect(response._embedded.products.length).toEqual(products.length); }); }); + + it('findProductsByCriteria by next page url', () => { + const criteria: Criteria = { + nextPageHref: 'http://localhost:8080/marketplace-service/api/product?type=all&page=1&size=20', + search: '', + sort: SortType.RECENT, + type: FilterType.All_TYPES + }; + service.findProductsByCriteria(criteria).subscribe(response => { + expect(response._embedded.products.length).toEqual(0); + expect(response.page.number).toEqual(1); + }); + }); }); diff --git a/src/app/modules/product/product.service.ts b/src/app/modules/product/product.service.ts index bbdb69b..c9e34e7 100644 --- a/src/app/modules/product/product.service.ts +++ b/src/app/modules/product/product.service.ts @@ -1,6 +1,6 @@ -import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; -import { Observable, catchError, map, of, throwError } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { MOCK_PRODUCTS } from '../../shared/mocks/mock-data'; import { Criteria } from '../../shared/models/criteria.model'; import { Product } from '../../shared/models/product.model'; @@ -24,25 +24,12 @@ export class ProductService { .set(RequestParam.SORT, `${criteria.sort}`) .set(RequestParam.KEYWORD, `${criteria.search}`); } - return this.httpClient.get(requestURL, { params: requestParams }) - .pipe( - catchError(this.handleError) - ); - } - - private handleError(error: HttpErrorResponse) { - let errorMessage = 'An unknown error occurred!'; - if (error.error instanceof ErrorEvent) { - errorMessage = `Error: ${error.error.message}`; - } else { - errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`; - } - return throwError(() => new Error(errorMessage)); + return this.httpClient.get(requestURL, { params: requestParams }); } // TODO MARP-358 getProductById(productId: string): Observable { - let products = MOCK_PRODUCTS._embedded.products as Product[]; + const products = MOCK_PRODUCTS._embedded.products; const product = products.find(p => p.id === productId); if (product) { return of(product); diff --git a/src/app/shared/mocks/mock-data.ts b/src/app/shared/mocks/mock-data.ts index aa9d1e8..dc81ec9 100644 --- a/src/app/shared/mocks/mock-data.ts +++ b/src/app/shared/mocks/mock-data.ts @@ -115,3 +115,23 @@ export const MOCK_PRODUCTS_FILTER_CONNECTOR = { } } as ProductApiResponse; + +export const MOCK_PRODUCTS_NEXT_PAGE = { + _embedded: { + products: [] + }, + _links: { + first: { + href: "http://localhost:8080/marketplace-service/api/product?type=all&page=0&size=20" + }, + self: { + href: "http://localhost:8080/marketplace-service/api/product?type=all&page=1&size=20" + } + }, + page: { + size: 20, + totalElements: 1, + totalPages: 1, + number: 1 + } +} as ProductApiResponse; \ No newline at end of file diff --git a/src/app/shared/mocks/mock-services.ts b/src/app/shared/mocks/mock-services.ts index 8a14dea..da7afc1 100644 --- a/src/app/shared/mocks/mock-services.ts +++ b/src/app/shared/mocks/mock-services.ts @@ -2,7 +2,8 @@ import { Observable, of } from 'rxjs'; import { Product } from '../models/product.model'; import { Criteria } from '../models/criteria.model'; import { FilterType } from '../enums/filter-type.enum'; -import { MOCK_PRODUCTS, MOCK_PRODUCTS_FILTER_CONNECTOR } from './mock-data'; +import { MOCK_PRODUCTS, MOCK_PRODUCTS_FILTER_CONNECTOR, MOCK_PRODUCTS_NEXT_PAGE } from './mock-data'; +import { ProductApiResponse } from '../models/apis/product-response.model'; import { ProductApiResponse } from '../models/apis/product-response.model'; const products = MOCK_PRODUCTS._embedded.products as Product[]; @@ -14,7 +15,9 @@ export class MockProductService { findProductsByCriteria(criteria: Criteria): Observable { let response = MOCK_PRODUCTS; - if (criteria.type == FilterType.CONNECTORS) { + if (criteria.nextPageHref) { + response = MOCK_PRODUCTS_NEXT_PAGE; + } else if (criteria.type == FilterType.CONNECTORS) { response = MOCK_PRODUCTS_FILTER_CONNECTOR; } return of(response);