Skip to content

Commit

Permalink
feat: display feedback when product is out of stock on order templates (
Browse files Browse the repository at this point in the history
#1067)

Co-authored-by: Silke <s.grueber@intershop.de>
  • Loading branch information
dhhyi and SGrueber authored Mar 30, 2022
1 parent 9a8e5cd commit 7c84791
Show file tree
Hide file tree
Showing 11 changed files with 139 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
getOrderTemplateError,
getOrderTemplateLoading,
getSelectedOrderTemplateDetails,
getSelectedOrderTemplateOutOfStockItems,
moveItemToOrderTemplate,
removeItemFromOrderTemplate,
updateOrderTemplate,
Expand All @@ -28,6 +29,9 @@ export class OrderTemplatesFacade {

orderTemplates$: Observable<OrderTemplate[]> = this.store.pipe(select(getAllOrderTemplates));
currentOrderTemplate$: Observable<OrderTemplate> = this.store.pipe(select(getSelectedOrderTemplateDetails));
currentOrderTemplateOutOfStockItems$: Observable<string[]> = this.store.pipe(
select(getSelectedOrderTemplateOutOfStockItems)
);
orderTemplateLoading$: Observable<boolean> = this.store.pipe(select(getOrderTemplateLoading));
orderTemplateError$: Observable<HttpError> = this.store.pipe(select(getOrderTemplateError));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="d-flex" data-testing-id="order-template-product">
<div class="col-1 col-md-2 list-item d-flex">
<input type="checkbox" data-testing-id="productCheckbox" [checked]="true" (click)="setActive($event.target)" />
<input type="checkbox" data-testing-id="productCheckbox" [formControl]="checkBox" />
<div class="d-none d-md-inline">
<ish-product-image imageType="S" [link]="true"></ish-product-image>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent, MockPipe } from 'ng-mocks';
import { EMPTY } from 'rxjs';
import { instance, mock, when } from 'ts-mockito';
import { anything, instance, mock, when } from 'ts-mockito';

import { ProductContextFacade } from 'ish-core/facades/product-context.facade';
import { DatePipe } from 'ish-core/pipes/date.pipe';
Expand All @@ -28,7 +29,7 @@ describe('Account Order Template Detail Line Item Component', () => {

beforeEach(async () => {
const context = mock(ProductContextFacade);
when(context.select('quantity')).thenReturn(EMPTY);
when(context.select(anything())).thenReturn(EMPTY);

await TestBed.configureTestingModule({
declarations: [
Expand All @@ -45,7 +46,7 @@ describe('Account Order Template Detail Line Item Component', () => {
MockComponent(SelectOrderTemplateModalComponent),
MockPipe(DatePipe),
],
imports: [TranslateModule.forRoot()],
imports: [ReactiveFormsModule, TranslateModule.forRoot()],
providers: [
{ provide: OrderTemplatesFacade, useFactory: () => instance(mock(OrderTemplatesFacade)) },
{ provide: ProductContextFacade, useFactory: () => instance(context) },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { map } from 'rxjs';

import { ProductContextFacade } from 'ish-core/facades/product-context.facade';

Expand All @@ -11,15 +13,28 @@ import { OrderTemplate, OrderTemplateItem } from '../../../models/order-template
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccountOrderTemplateDetailLineItemComponent implements OnInit {
constructor(private context: ProductContextFacade, private orderTemplatesFacade: OrderTemplatesFacade) {}

@Input() orderTemplateItemData: OrderTemplateItem;
@Input() currentOrderTemplate: OrderTemplate;

checkBox = new FormControl();

constructor(private context: ProductContextFacade, private orderTemplatesFacade: OrderTemplatesFacade) {}

ngOnInit() {
this.context.hold(this.context.validDebouncedQuantityUpdate$(), quantity => {
this.updateProductQuantity(this.context.get('sku'), quantity);
});

this.context.connect('propagateActive', this.checkBox.valueChanges);

this.context.hold(this.context.select('product').pipe(map(product => product.available)), available => {
this.checkBox.setValue(available);
if (available) {
this.checkBox.enable();
} else {
this.checkBox.disable();
}
});
}

moveItemToOtherOrderTemplate(sku: string, orderTemplateMoveData: { id: string; title: string }) {
Expand Down Expand Up @@ -51,8 +66,4 @@ export class AccountOrderTemplateDetailLineItemComponent implements OnInit {
removeProductFromOrderTemplate(sku: string) {
this.orderTemplatesFacade.removeProductFromOrderTemplate(this.currentOrderTemplate.id, sku);
}

setActive(target: EventTarget) {
this.context.set('propagateActive', () => (target as HTMLInputElement).checked);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<ish-error-message [error]="orderTemplateError$ | async"></ish-error-message>

<ng-container *ngIf="orderTemplate$ | async as orderTemplate" ishProductContext>
<h1>
<h1 class="clearfix">
{{ orderTemplate?.title }}
<a
(click)="editOrderTemplateDialog.show()"
Expand All @@ -13,6 +13,14 @@ <h1>
>
</h1>

<p
*ngIf="noOfUnavailableProducts$ | async as noOfUnavailableProducts"
data-testing-id="out-of-stock-warning"
class="alert alert-info"
>
{{ 'account.order_template.out_of_stock.warning' | translate: { num: noOfUnavailableProducts } }}
</p>

<div class="section">
<ng-container *ngIf="orderTemplate.itemsCount && orderTemplate.itemsCount > 0; else noItems" class="section">
<div class="list-header d-md-flex">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';
import { EMPTY } from 'rxjs';
import { MockComponent, MockDirective } from 'ng-mocks';
import { of } from 'rxjs';
import { instance, mock, when } from 'ts-mockito';

import { ProductContextDirective } from 'ish-core/directives/product-context.directive';
import { findAllCustomElements } from 'ish-core/utils/dev/html-query-utils';
import { ErrorMessageComponent } from 'ish-shared/components/common/error-message/error-message.component';
import { ProductAddToBasketComponent } from 'ish-shared/components/product/product-add-to-basket/product-add-to-basket.component';

import { OrderTemplatesFacade } from '../../facades/order-templates.facade';
import { OrderTemplate } from '../../models/order-template/order-template.model';
import { OrderTemplatePreferencesDialogComponent } from '../../shared/order-template-preferences-dialog/order-template-preferences-dialog.component';

import { AccountOrderTemplateDetailLineItemComponent } from './account-order-template-detail-line-item/account-order-template-detail-line-item.component';
import { AccountOrderTemplateDetailPageComponent } from './account-order-template-detail-page.component';

describe('Account Order Template Detail Page Component', () => {
let component: AccountOrderTemplateDetailPageComponent;
let fixture: ComponentFixture<AccountOrderTemplateDetailPageComponent>;
let element: HTMLElement;
let orderTemplatesFacade: OrderTemplatesFacade;

beforeEach(async () => {
const orderTemplatesFacade = mock(OrderTemplatesFacade);
when(orderTemplatesFacade.currentOrderTemplate$).thenReturn(EMPTY);
orderTemplatesFacade = mock(OrderTemplatesFacade);
when(orderTemplatesFacade.currentOrderTemplateOutOfStockItems$).thenReturn(of([]));

await TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [AccountOrderTemplateDetailPageComponent, MockComponent(ErrorMessageComponent)],
declarations: [
AccountOrderTemplateDetailPageComponent,
MockComponent(AccountOrderTemplateDetailLineItemComponent),
MockComponent(ErrorMessageComponent),
MockComponent(OrderTemplatePreferencesDialogComponent),
MockComponent(ProductAddToBasketComponent),
MockDirective(ProductContextDirective),
],
providers: [{ provide: OrderTemplatesFacade, useFactory: () => instance(orderTemplatesFacade) }],
}).compileComponents();
});
Expand All @@ -37,4 +51,72 @@ describe('Account Order Template Detail Page Component', () => {
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});

describe('template without items', () => {
beforeEach(() => {
when(orderTemplatesFacade.currentOrderTemplate$).thenReturn(
of({
title: 'Order Template',
items: [],
itemsCount: 0,
} as OrderTemplate)
);
});

it('should display standard elements when rendering empty template', () => {
when(orderTemplatesFacade.currentOrderTemplate$).thenReturn(
of({
title: 'Order Template',
items: [],
itemsCount: 0,
} as OrderTemplate)
);
fixture.detectChanges();

expect(findAllCustomElements(element)).toMatchInlineSnapshot(`
Array [
"ish-error-message",
"ish-order-template-preferences-dialog",
]
`);
});
});

describe('template with item', () => {
beforeEach(() => {
when(orderTemplatesFacade.currentOrderTemplate$).thenReturn(
of({
title: 'Order Template',
items: [{ sku: '123', desiredQuantity: { value: 1 } }],
itemsCount: 1,
} as OrderTemplate)
);
});

it('should display line item elements when rendering template with item', () => {
fixture.detectChanges();

expect(findAllCustomElements(element)).toMatchInlineSnapshot(`
Array [
"ish-error-message",
"ish-account-order-template-detail-line-item",
"ish-product-add-to-basket",
"ish-order-template-preferences-dialog",
]
`);
});

it('should not display out of stock warning by default', () => {
fixture.detectChanges();

expect(element.querySelector('[data-testing-id="out-of-stock-warning"]')).toBeFalsy();
});

it('should display out of stock warning when items are unavailable', () => {
when(orderTemplatesFacade.currentOrderTemplateOutOfStockItems$).thenReturn(of(['123']));
fixture.detectChanges();

expect(element.querySelector('[data-testing-id="out-of-stock-warning"]')).toBeTruthy();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';

import { HttpError } from 'ish-core/models/http-error/http-error.model';
import { mapToProperty } from 'ish-core/utils/operators';

import { OrderTemplatesFacade } from '../../facades/order-templates.facade';
import { OrderTemplate } from '../../models/order-template/order-template.model';
Expand All @@ -16,12 +17,17 @@ export class AccountOrderTemplateDetailPageComponent implements OnInit {
orderTemplateError$: Observable<HttpError>;
orderTemplateLoading$: Observable<boolean>;

noOfUnavailableProducts$: Observable<number>;

constructor(private orderTemplatesFacade: OrderTemplatesFacade) {}

ngOnInit() {
this.orderTemplate$ = this.orderTemplatesFacade.currentOrderTemplate$;
this.orderTemplateLoading$ = this.orderTemplatesFacade.orderTemplateLoading$;
this.orderTemplateError$ = this.orderTemplatesFacade.orderTemplateError$;
this.noOfUnavailableProducts$ = this.orderTemplatesFacade.currentOrderTemplateOutOfStockItems$.pipe(
mapToProperty('length')
);
}

editPreferences(orderTemplate: OrderTemplate, orderTemplateName: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { createSelector } from '@ngrx/store';

import { getProductEntities } from 'ish-core/store/shopping/products';

import { getOrderTemplatesState } from '../order-templates-store';

import { initialState, orderTemplateAdapter } from './order-template.reducer';
Expand All @@ -24,4 +26,10 @@ export const getSelectedOrderTemplateDetails = createSelector(
(entities, id) => entities[id]
);

export const getSelectedOrderTemplateOutOfStockItems = createSelector(
getSelectedOrderTemplateDetails,
getProductEntities,
(template, products) => template?.items?.map(i => i.sku)?.filter(sku => products[sku] && !products[sku].available)
);

export const getOrderTemplateDetails = (id: string) => createSelector(selectEntities, entities => entities[id]);
1 change: 1 addition & 0 deletions src/assets/i18n/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"account.order_template.new_order_template.text": "Neue Bestellvorlage",
"account.order_template.no_entries": "Derzeit befinden sich keine Artikel in der Bestellvorlage.",
"account.order_template.order_template.edit.rename": "Umbenennen",
"account.order_template.out_of_stock.warning": "{{num}} Artikel in Ihrer Bestellvorlage {{ num, plural, =1{ist} other{sind} }} nicht verfügbar. {{ num, plural, =1{Dieser Artikel kann} other{Diese Artikel können} }} nicht in den Warenkorb gelegt werden.",
"account.order_template.table.header.date_added": "Hinzugefügt am",
"account.order_template.table.header.item": "Artikelbeschreibung",
"account.order_template.table.header.price": "Preis",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"account.order_template.new_order_template.text": "New Order Template",
"account.order_template.no_entries": "There are currently no items in this order template.",
"account.order_template.order_template.edit.rename": "Rename",
"account.order_template.out_of_stock.warning": "{{ num, plural, =1{# item} other{# items} }} in your order template {{ num, plural, =1{is} other{are} }} out of stock. {{ num, plural, =1{This item} other{These items} }} cannot be added to the shopping cart.",
"account.order_template.table.header.date_added": "Added on",
"account.order_template.table.header.item": "Item Description",
"account.order_template.table.header.price": "Price",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/fr_FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"account.order_template.new_order_template.text": "Nouveau modèle de commande",
"account.order_template.no_entries": "Il n’y a actuellement aucun article dans ce modèle de commande.",
"account.order_template.order_template.edit.rename": "Renommer",
"account.order_template.out_of_stock.warning": "{{ num, plural, =1{# élément} other{# éléments} }} dans votre modèle de commande {{ num, plural, =1{est} other{sont} }} en rupture de stock. {{ num, plural, =1{Cet article ne peut pas être ajouté au panier} other{Ces articles ne peuvent pas être ajoutés au panier} }}.",
"account.order_template.table.header.date_added": "Date ajoutée",
"account.order_template.table.header.item": "Description de l’article",
"account.order_template.table.header.price": "Prix",
Expand Down

0 comments on commit 7c84791

Please sign in to comment.