Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: display feedback when product is out of stock on order templates #1067

Merged
merged 4 commits into from
Mar 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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