Skip to content

Commit

Permalink
fix: adapt Products List component to not display failed products (#1408
Browse files Browse the repository at this point in the history
)

* if a product REST call returns with an error (e.g. if the product is set to offline in ICM or if it does not exist at all) the product is marked as failed in the PWA state management
* such failed products are now filtered and no longer rendered by the generic Products List component
* the Product Tile and the Product Row component no longer render failed products as well
* Products List component now maps the ProductContextDisplayProperties configuration as listItemConfiguration
  • Loading branch information
suschneider authored and shauke committed Apr 5, 2023
1 parent f952dc9 commit ff4f2e7
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 106 deletions.
3 changes: 3 additions & 0 deletions src/app/core/facades/shopping.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
import { loadProductPrices } from 'ish-core/store/shopping/product-prices';
import { getProductPrice } from 'ish-core/store/shopping/product-prices/product-prices.selectors';
import {
getFailedProducts,
getProduct,
getProductLinks,
getProductParts,
Expand Down Expand Up @@ -108,6 +109,8 @@ export class ShoppingFacade {
);
}

failedProducts$ = this.store.pipe(select(getFailedProducts));

productPrices$(sku: string | Observable<string>, fresh = false) {
return toObservable(sku).pipe(
whenTruthy(),
Expand Down
2 changes: 2 additions & 0 deletions src/app/core/store/shopping/products/products.selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export const getProductLinks = (sku: string) => createSelector(getProductsState,

export const getProductParts = (sku: string) => createSelector(getProductsState, state => state.parts[sku]);

export const getFailedProducts = createSelector(getProductsState, state => state.failed);

export const getBreadcrumbForProductPage = createSelectorFactory<object, BreadcrumbItem[]>(projector =>
resultMemoize(projector, isEqual)
)(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<ng-container *ngIf="product$ | async">
<ish-product-tile *ngIf="isTile"></ish-product-tile>

<ish-product-row *ngIf="isRow"></ish-product-row>
</ng-container>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,70 +1,72 @@
<div *ngIf="product$ | async as product" class="product-tile-list row" [attr.data-testing-sku]="product.sku">
<div class="col-3 col-md-2">
<div class="product-image-container">
<ish-product-image imageType="S" [link]="true"></ish-product-image>
<ish-product-label></ish-product-label>
<ng-container *ngIf="product$ | async as product">
<div *ngIf="!product.failed" class="product-tile-list row" [attr.data-testing-sku]="product.sku">
<div class="col-3 col-md-2">
<div class="product-image-container">
<ish-product-image imageType="S" [link]="true"></ish-product-image>
<ish-product-label></ish-product-label>
</div>
</div>
</div>

<div class="col-9 col-md-10">
<div class="row">
<div class="col-md-7">
<ish-product-name></ish-product-name>
<div class="col-9 col-md-10">
<div class="row">
<div class="col-md-7">
<ish-product-name></ish-product-name>

<ish-lazy-product-rating [hideNumberOfReviews]="true"></ish-lazy-product-rating>
<ish-lazy-product-rating [hideNumberOfReviews]="true"></ish-lazy-product-rating>

<ish-product-id></ish-product-id>
<ish-product-id></ish-product-id>

<div
*ngIf="configuration$('description') | async"
class="product-description"
[ishServerHtml]="product.shortDescription"
></div>
<div
*ngIf="configuration$('description') | async"
class="product-description"
[ishServerHtml]="product.shortDescription"
></div>

<ish-product-promotion displayType="simpleWithDetail"></ish-product-promotion>
<ish-product-promotion displayType="simpleWithDetail"></ish-product-promotion>

<div class="product-tile-actions btn-group">
<ish-lazy-product-add-to-quote displayType="icon" cssClass="btn-link"></ish-lazy-product-add-to-quote>
<ish-lazy-product-add-to-compare displayType="icon" cssClass="btn-link"></ish-lazy-product-add-to-compare>
<ish-lazy-product-add-to-wishlist displayType="icon" cssClass="btn-link"></ish-lazy-product-add-to-wishlist>
<ish-lazy-product-add-to-order-template
displayType="icon"
cssClass="btn-link"
></ish-lazy-product-add-to-order-template>
<div class="product-tile-actions btn-group">
<ish-lazy-product-add-to-quote displayType="icon" cssClass="btn-link"></ish-lazy-product-add-to-quote>
<ish-lazy-product-add-to-compare displayType="icon" cssClass="btn-link"></ish-lazy-product-add-to-compare>
<ish-lazy-product-add-to-wishlist displayType="icon" cssClass="btn-link"></ish-lazy-product-add-to-wishlist>
<ish-lazy-product-add-to-order-template
displayType="icon"
cssClass="btn-link"
></ish-lazy-product-add-to-order-template>
</div>
</div>
</div>

<div class="col-12 col-md-5 text-md-right">
<ish-product-price [showInformationalPrice]="true"></ish-product-price>
<ish-product-inventory></ish-product-inventory>
<ish-product-shipment></ish-product-shipment>
<div class="col-12 col-md-5 text-md-right">
<ish-product-price [showInformationalPrice]="true"></ish-product-price>
<ish-product-inventory></ish-product-inventory>
<ish-product-shipment></ish-product-shipment>

<ish-product-item-variations></ish-product-item-variations>
<ish-product-item-variations></ish-product-item-variations>

<div class="product-list-actions-container">
<ish-lazy-tacton-configure-product displayType="list-button"></ish-lazy-tacton-configure-product>
<div class="product-list-actions-container">
<ish-lazy-tacton-configure-product displayType="list-button"></ish-lazy-tacton-configure-product>

<div class="product-form form-horizontal row">
<ng-container *ngIf="configuration$('readOnly') | async; else quantityInput">
<div class="action-container col-12 col-xl-7">
<span *ngIf="configuration$('quantity') | async"
>{{ 'product.quantity.label' | translate }}: {{ quantity$ | async }}</span
>
</div>
</ng-container>
<ng-template #quantityInput>
<div class="action-container col-6 offset-md-6 col-lg-5 offset-lg-0">
<ish-product-quantity></ish-product-quantity>
</div>
</ng-template>
<div class="product-form form-horizontal row">
<ng-container *ngIf="configuration$('readOnly') | async; else quantityInput">
<div class="action-container col-12 col-xl-7">
<span *ngIf="configuration$('quantity') | async"
>{{ 'product.quantity.label' | translate }}: {{ quantity$ | async }}</span
>
</div>
</ng-container>
<ng-template #quantityInput>
<div class="action-container col-6 offset-md-6 col-lg-5 offset-lg-0">
<ish-product-quantity></ish-product-quantity>
</div>
</ng-template>

<div class="action-container addtocart-container col-12 col-lg-7">
<ish-product-add-to-basket></ish-product-add-to-basket>
<ish-product-choose-variation></ish-product-choose-variation>
<div class="action-container addtocart-container col-12 col-lg-7">
<ish-product-add-to-basket></ish-product-add-to-basket>
<ish-product-choose-variation></ish-product-choose-variation>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</ng-container>
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
<div *ngIf="product$ | async as product" class="product-tile" [attr.data-testing-sku]="product.sku">
<div class="product-image-container">
<ish-product-image imageType="M" [link]="true"></ish-product-image>
<ish-product-label></ish-product-label>
</div>
<ng-container *ngIf="product$ | async as product">
<div *ngIf="!product.failed" class="product-tile" [attr.data-testing-sku]="product.sku">
<div class="product-image-container">
<ish-product-image imageType="M" [link]="true"></ish-product-image>
<ish-product-label></ish-product-label>
</div>

<ish-product-name></ish-product-name>
<ish-product-name></ish-product-name>

<ish-lazy-product-rating [hideNumberOfReviews]="true"></ish-lazy-product-rating>
<ish-lazy-product-rating [hideNumberOfReviews]="true"></ish-lazy-product-rating>

<ish-product-promotion displayType="simple"></ish-product-promotion>
<ish-product-promotion displayType="simple"></ish-product-promotion>

<div *ngIf="configuration$('price') | async" class="price-container">
<ish-product-price [showInformationalPrice]="true"></ish-product-price>
</div>
<div *ngIf="configuration$('price') | async" class="price-container">
<ish-product-price [showInformationalPrice]="true"></ish-product-price>
</div>

<ish-product-item-variations></ish-product-item-variations>
<ish-product-item-variations></ish-product-item-variations>

<div class="product-tile-actions btn-group">
<ish-lazy-tacton-configure-product displayType="icon"></ish-lazy-tacton-configure-product>
<ish-lazy-product-add-to-quote displayType="icon" cssClass="btn-link"></ish-lazy-product-add-to-quote>
<ish-lazy-product-add-to-compare displayType="icon" cssClass="btn-link"></ish-lazy-product-add-to-compare>
<ish-lazy-product-add-to-order-template
[cssClass]="'btn btn-link mr-0'"
displayType="icon"
></ish-lazy-product-add-to-order-template>
<ish-lazy-product-add-to-wishlist cssClass="btn-link" displayType="icon"></ish-lazy-product-add-to-wishlist>
<ish-product-add-to-basket displayType="icon" cssClass="btn-link"></ish-product-add-to-basket>
<div class="product-tile-actions btn-group">
<ish-lazy-tacton-configure-product displayType="icon"></ish-lazy-tacton-configure-product>
<ish-lazy-product-add-to-quote displayType="icon" cssClass="btn-link"></ish-lazy-product-add-to-quote>
<ish-lazy-product-add-to-compare displayType="icon" cssClass="btn-link"></ish-lazy-product-add-to-compare>
<ish-lazy-product-add-to-order-template
[cssClass]="'btn btn-link mr-0'"
displayType="icon"
></ish-lazy-product-add-to-order-template>
<ish-lazy-product-add-to-wishlist cssClass="btn-link" displayType="icon"></ish-lazy-product-add-to-wishlist>
<ish-product-add-to-basket displayType="icon" cssClass="btn-link"></ish-product-add-to-basket>
</div>
</div>
</div>
</ng-container>
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
<ng-container *ngIf="listStyle === 'carousel'; else plainList">
<div class="product-list">
<swiper [config]="swiperConfig">
<ng-template *ngFor="let sku of productSKUs" swiperSlide>
<div [ngClass]="listItemCSSClass">
<ish-product-item ishProductContext [sku]="sku" [displayType]="listItemStyle"></ish-product-item>
<ng-container *ngIf="productSKUs$ | async as products">
<ng-container *ngIf="products.length">
<ng-container *ngIf="listStyle === 'carousel'; else plainList">
<div class="product-list">
<swiper [config]="swiperConfig">
<ng-template *ngFor="let sku of products" swiperSlide>
<div [ngClass]="listItemCSSClass">
<ish-product-item
ishProductContext
[sku]="sku"
[configuration]="listItemConfiguration"
[displayType]="listItemStyle"
></ish-product-item>
</div>
</ng-template>
</swiper>
</div>
</ng-container>

<ng-template #plainList>
<div class="product-list row">
<div *ngFor="let sku of products" class="product-list-item" [ngClass]="listItemCSSClass">
<ish-product-item
ishProductContext
[sku]="sku"
[configuration]="listItemConfiguration"
[displayType]="listItemStyle"
></ish-product-item>
</div>
</ng-template>
</swiper>
</div>
</div>
</ng-template>
</ng-container>
</ng-container>

<ng-template #plainList>
<div class="product-list row">
<div *ngFor="let sku of productSKUs" class="product-list-item" [ngClass]="listItemCSSClass">
<ish-product-item ishProductContext [sku]="sku" [displayType]="listItemStyle"></ish-product-item>
</div>
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { MockComponent, MockDirective } from 'ng-mocks';
import { of } from 'rxjs';
import { SwiperComponent } from 'swiper/angular';
import { instance, mock, when } from 'ts-mockito';

import { ProductContextDirective } from 'ish-core/directives/product-context.directive';
import { ShoppingFacade } from 'ish-core/facades/shopping.facade';
import { ProductItemComponent } from 'ish-shared/components/product/product-item/product-item.component';

import { ProductsListComponent } from './products-list.component';
Expand All @@ -12,48 +15,50 @@ describe('Products List Component', () => {
let component: ProductsListComponent;
let fixture: ComponentFixture<ProductsListComponent>;
let element: HTMLElement;
let shoppingFacade: ShoppingFacade;

beforeEach(async () => {
shoppingFacade = mock(ShoppingFacade);

await TestBed.configureTestingModule({
declarations: [
MockComponent(ProductItemComponent),
MockComponent(SwiperComponent),
MockDirective(ProductContextDirective),
ProductsListComponent,
],
providers: [{ provide: ShoppingFacade, useFactory: () => instance(shoppingFacade) }],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(ProductsListComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;

when(shoppingFacade.failedProducts$).thenReturn(of([]));
});

it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => component.ngOnChanges()).not.toThrow();
expect(() => fixture.detectChanges()).not.toThrow();
});

describe('carousel', () => {
beforeEach(() => {
component.productSKUs = ['1', '2'];
});

it('should display a carousel when listStyle is set to carousel', () => {
component.listStyle = 'carousel';

fixture.detectChanges();
it('should display a carousel when listStyle is set to carousel', () => {
component.productSKUs = ['1', '2'];
component.listStyle = 'carousel';
component.ngOnChanges();
fixture.detectChanges();

expect(element).toMatchInlineSnapshot(`<div class="product-list"><swiper></swiper></div>`);
});
expect(element).toMatchInlineSnapshot(`<div class="product-list"><swiper></swiper></div>`);
});

it('should set displayType of product item to listItemStyle value', () => {
component.productSKUs = ['1', '2'];
component.listItemStyle = 'tile';

component.ngOnChanges();
fixture.detectChanges();

const productItem = fixture.debugElement.query(By.css('ish-product-item'))
Expand All @@ -65,7 +70,7 @@ describe('Products List Component', () => {
it('should display product items for all product skus', () => {
component.productSKUs = ['1', '2', '3'];
component.listItemStyle = 'row';

component.ngOnChanges();
fixture.detectChanges();

expect(element.querySelectorAll('ish-product-item')).toHaveLength(3);
Expand All @@ -89,4 +94,19 @@ describe('Products List Component', () => {
]
`);
});

it('should display product items only for not failed product skus', () => {
when(shoppingFacade.failedProducts$).thenReturn(of(['1']));
component.productSKUs = ['1', '2', '3'];
component.ngOnChanges();
fixture.detectChanges();

expect(element.querySelectorAll('ish-product-item')).toHaveLength(2);
expect(element.querySelectorAll('ish-product-item')).toMatchInlineSnapshot(`
NodeList [
<ish-product-item ishproductcontext="" ng-reflect-sku="2"></ish-product-item>,
<ish-product-item ishproductcontext="" ng-reflect-sku="3"></ish-product-item>,
]
`);
});
});
Loading

0 comments on commit ff4f2e7

Please sign in to comment.