Skip to content

Commit

Permalink
feat: add order templates widget on MyAccount overview page (#502)
Browse files Browse the repository at this point in the history
  • Loading branch information
SGrueber committed Jan 28, 2021
1 parent 2302a53 commit 6eff141
Show file tree
Hide file tree
Showing 20 changed files with 245 additions and 50 deletions.
2 changes: 1 addition & 1 deletion e2e/cypress/integration/pages/account/my-account.page.ts
Expand Up @@ -30,7 +30,7 @@ export class MyAccountPage {
}

navigateToOrderTemplates() {
cy.get('a[href="/account/order-templates"]').click();
cy.get('a[href="/account/order-templates"]').first().click();
}

navigateToPayments() {
Expand Down
Expand Up @@ -3,7 +3,7 @@
<div class="loading-container">
<div *ngIf="!(budgetLoading$ | async); else loading">
<div class="row">
<div class="col-xs-12 col-md-8">
<div class="col-12">
<ish-user-budget progressBarClass="background-inverse" [budget]="userBudget$ | async"></ish-user-budget>
</div>
</div>
Expand Down
Expand Up @@ -4,6 +4,7 @@ import { FeatureToggleModule } from 'ish-core/feature-toggle.module';
import { LAZY_FEATURE_MODULE } from 'ish-core/utils/module-loader/module-loader.service';

import { LazyBasketCreateOrderTemplateComponent } from './lazy-basket-create-order-template/lazy-basket-create-order-template.component';
import { LazyOrderTemplateWidgetComponent } from './lazy-order-template-widget/lazy-order-template-widget.component';
import { LazyProductAddToOrderTemplateComponent } from './lazy-product-add-to-order-template/lazy-product-add-to-order-template.component';

@NgModule({
Expand All @@ -15,7 +16,15 @@ import { LazyProductAddToOrderTemplateComponent } from './lazy-product-add-to-or
multi: true,
},
],
declarations: [LazyBasketCreateOrderTemplateComponent, LazyProductAddToOrderTemplateComponent],
exports: [LazyBasketCreateOrderTemplateComponent, LazyProductAddToOrderTemplateComponent],
declarations: [
LazyBasketCreateOrderTemplateComponent,
LazyOrderTemplateWidgetComponent,
LazyProductAddToOrderTemplateComponent,
],
exports: [
LazyBasketCreateOrderTemplateComponent,
LazyOrderTemplateWidgetComponent,
LazyProductAddToOrderTemplateComponent,
],
})
export class OrderTemplatesExportsModule {}
Expand Up @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';

import { ShoppingFacade } from 'ish-core/facades/shopping.facade';
import { HttpError } from 'ish-core/models/http-error/http-error.model';

import { OrderTemplate, OrderTemplateHeader } from '../models/order-template/order-template.model';
Expand All @@ -22,7 +23,7 @@ import {

@Injectable({ providedIn: 'root' })
export class OrderTemplatesFacade {
constructor(private store: Store) {}
constructor(private productFacade: ShoppingFacade, private store: Store) {}

orderTemplates$: Observable<OrderTemplate[]> = this.store.pipe(select(getAllOrderTemplates));
currentOrderTemplate$: Observable<OrderTemplate> = this.store.pipe(select(getSelectedOrderTemplateDetails));
Expand All @@ -37,6 +38,14 @@ export class OrderTemplatesFacade {
this.store.dispatch(addBasketToNewOrderTemplate({ orderTemplate }));
}

addOrderTemplateToBasket(orderTemplate: OrderTemplate) {
if (orderTemplate?.items?.length) {
orderTemplate.items.forEach(item => {
this.productFacade.addProductToBasket(item.sku, item.desiredQuantity.value);
});
}
}

deleteOrderTemplate(id: string): void {
this.store.dispatch(deleteOrderTemplate({ orderTemplateId: id }));
}
Expand Down
4 changes: 3 additions & 1 deletion src/app/extensions/order-templates/order-templates.module.ts
Expand Up @@ -4,6 +4,7 @@ import { SharedModule } from 'ish-shared/shared.module';

import { BasketCreateOrderTemplateComponent } from './shared/basket-create-order-template/basket-create-order-template.component';
import { OrderTemplatePreferencesDialogComponent } from './shared/order-template-preferences-dialog/order-template-preferences-dialog.component';
import { OrderTemplateWidgetComponent } from './shared/order-template-widget/order-template-widget.component';
import { ProductAddToOrderTemplateComponent } from './shared/product-add-to-order-template/product-add-to-order-template.component';
import { SelectOrderTemplateModalComponent } from './shared/select-order-template-modal/select-order-template-modal.component';

Expand All @@ -12,9 +13,10 @@ import { SelectOrderTemplateModalComponent } from './shared/select-order-templat
declarations: [
BasketCreateOrderTemplateComponent,
OrderTemplatePreferencesDialogComponent,
OrderTemplateWidgetComponent,
ProductAddToOrderTemplateComponent,
SelectOrderTemplateModalComponent,
],
exports: [OrderTemplatePreferencesDialogComponent, SelectOrderTemplateModalComponent],
exports: [OrderTemplatePreferencesDialogComponent, OrderTemplateWidgetComponent, SelectOrderTemplateModalComponent],
})
export class OrderTemplatesModule {}
Expand Up @@ -31,7 +31,7 @@
displayType="icon"
[product]="dummyProduct"
class="text-primary p-0"
(productToBasket)="addTemplateToCart(orderTemplate.id)"
(productToBasket)="addTemplateToBasket(orderTemplate)"
></ish-product-add-to-basket>
</a>
<a
Expand Down
Expand Up @@ -3,20 +3,21 @@ import { RouterTestingModule } from '@angular/router/testing';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent, MockPipe } from 'ng-mocks';
import { anything, capture, instance, mock, spy, verify } from 'ts-mockito';
import { anything, capture, instance, mock, spy, verify, when } from 'ts-mockito';

import { ShoppingFacade } from 'ish-core/facades/shopping.facade';
import { DatePipe } from 'ish-core/pipes/date.pipe';
import { ModalDialogComponent } from 'ish-shared/components/common/modal-dialog/modal-dialog.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 { AccountOrderTemplateListComponent } from './account-order-template-list.component';

describe('Account Order Template List Component', () => {
let component: AccountOrderTemplateListComponent;
let fixture: ComponentFixture<AccountOrderTemplateListComponent>;
let element: HTMLElement;
let shoppingFacadeMock: ShoppingFacade;
let orderTemplateFacadeMock: OrderTemplatesFacade;

const orderTemplateDetails = [
{
Expand Down Expand Up @@ -50,7 +51,8 @@ describe('Account Order Template List Component', () => {
];

beforeEach(async () => {
shoppingFacadeMock = mock(ShoppingFacade);
orderTemplateFacadeMock = mock(OrderTemplatesFacade);
when(orderTemplateFacadeMock.addOrderTemplateToBasket(anything())).thenReturn();
await TestBed.configureTestingModule({
declarations: [
AccountOrderTemplateListComponent,
Expand All @@ -60,7 +62,7 @@ describe('Account Order Template List Component', () => {
MockPipe(DatePipe),
],
imports: [RouterTestingModule, TranslateModule.forRoot()],
providers: [{ provide: ShoppingFacade, useFactory: () => instance(shoppingFacadeMock) }],
providers: [{ provide: OrderTemplatesFacade, useFactory: () => instance(orderTemplateFacadeMock) }],
}).compileComponents();
});

Expand All @@ -84,25 +86,31 @@ describe('Account Order Template List Component', () => {
verify(emitter.emit('deleteId')).once();
});

it('should trigger add product to cart with right sku', () => {
it('should trigger add product to basket with right order template', () => {
expect(() => fixture.detectChanges()).not.toThrow();
component.orderTemplates = orderTemplateDetails;
component.addTemplateToCart('.SKsEQAE4FIAAAFuNiUBWx0d');
component.addTemplateToBasket(orderTemplateDetails[0]);

verify(shoppingFacadeMock.addProductToBasket(anything(), anything())).once();
expect(capture(shoppingFacadeMock.addProductToBasket).last()).toMatchInlineSnapshot(`
verify(orderTemplateFacadeMock.addOrderTemplateToBasket(anything())).once();
expect(capture(orderTemplateFacadeMock.addOrderTemplateToBasket).last()).toMatchInlineSnapshot(`
Array [
"1234",
1,
Object {
"id": ".SKsEQAE4FIAAAFuNiUBWx0d",
"items": Array [
Object {
"creationDate": 123124125,
"desiredQuantity": Object {
"value": 1,
},
"id": "12345",
"sku": "1234",
},
],
"itemsCount": 1,
"public": false,
"title": "testing order template",
},
]
`);
});

it('should not trigger add to product if template doesnt have items', () => {
expect(() => fixture.detectChanges()).not.toThrow();
component.orderTemplates = orderTemplateDetails;
component.addTemplateToCart('.AsdHS18FIAAAFuNiUBWx0d');

verify(shoppingFacadeMock.addProductToBasket(anything(), anything())).never();
});
});
Expand Up @@ -3,9 +3,9 @@ import { TranslateService } from '@ngx-translate/core';
import { Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';

import { ShoppingFacade } from 'ish-core/facades/shopping.facade';
import { ModalDialogComponent } from 'ish-shared/components/common/modal-dialog/modal-dialog.component';

import { OrderTemplatesFacade } from '../../../facades/order-templates.facade';
import { OrderTemplate } from '../../../models/order-template/order-template.model';

@Component({
Expand All @@ -27,18 +27,10 @@ export class AccountOrderTemplateListComponent implements OnDestroy {

private destroy$ = new Subject();

constructor(private translate: TranslateService, private productFacade: ShoppingFacade) {}
constructor(private translate: TranslateService, private facade: OrderTemplatesFacade) {}

addTemplateToCart(orderTemplateId: string) {
const products = this.orderTemplates.find(t => t.id === orderTemplateId).items
? this.orderTemplates.find(t => t.id === orderTemplateId).items
: [];

if (products.length > 0) {
products.forEach(product => {
this.productFacade.addProductToBasket(product.sku, product.desiredQuantity.value);
});
}
addTemplateToBasket(orderTemplate: OrderTemplate) {
this.facade.addOrderTemplateToBasket(orderTemplate);
}

/** Emits the id of the order template to delete. */
Expand Down
@@ -0,0 +1,38 @@
<ish-info-box
heading="account.ordertemplates.widget.heading"
class="infobox-wrapper h-100"
cssClass="infobox-color-widget d-flex flex-column"
>
<div class="loading-container">
<div *ngIf="!(loading$ | async); else loading" class="pb-2">
<ng-container *ngIf="orderTemplates$ | async as orderTemplates; else emptyList">
<ng-container *ngIf="orderTemplates.length; else emptyList">
<div *ngFor="let orderTemplate of orderTemplates" class="mb-2">
{{ orderTemplate.title }}
<a *ngIf="orderTemplate.items?.length" class="align-top float-right">
<ish-product-add-to-basket
displayType="icon"
[product]="dummyProduct"
class="p-0 mb-0"
(productToBasket)="addTemplateToBasket(orderTemplate)"
></ish-product-add-to-basket>
</a>
</div>
</ng-container>
</ng-container>
<ng-template #emptyList>
{{ 'account.order_template.list.no_templates.text' | translate }}
</ng-template>
</div>
</div>

<div class="mt-auto">
<a routerLink="/account/order-templates" data-testing-id="order-templates-list-link">
{{ 'account.ordertemplates.widget.view_all.link' | translate }}
</a>
</div>
</ish-info-box>

<ng-template #loading>
<ish-loading></ish-loading>
</ng-template>
@@ -0,0 +1,83 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';
import { of } from 'rxjs';
import { anything, capture, instance, mock, verify, when } from 'ts-mockito';

import { InfoBoxComponent } from 'ish-shared/components/common/info-box/info-box.component';
import { LoadingComponent } from 'ish-shared/components/common/loading/loading.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 { OrderTemplateWidgetComponent } from './order-template-widget.component';

describe('Order Template Widget Component', () => {
let component: OrderTemplateWidgetComponent;
let fixture: ComponentFixture<OrderTemplateWidgetComponent>;
let element: HTMLElement;
let orderTemplatesFacade: OrderTemplatesFacade;

const orderTemplates = [
{ id: '1', title: 'order template' },
{ id: '2', title: 'order template 2' },
] as OrderTemplate[];

beforeEach(async () => {
orderTemplatesFacade = mock(OrderTemplatesFacade);
when(orderTemplatesFacade.orderTemplates$).thenReturn(of(orderTemplates));
when(orderTemplatesFacade.addOrderTemplateToBasket(anything())).thenReturn();
await TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [
MockComponent(InfoBoxComponent),
MockComponent(LoadingComponent),
MockComponent(ProductAddToBasketComponent),
OrderTemplateWidgetComponent,
],
providers: [{ provide: OrderTemplatesFacade, useFactory: () => instance(orderTemplatesFacade) }],
}).compileComponents();
});

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

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

it('should render loading component if order templates are loading', () => {
when(orderTemplatesFacade.orderTemplateLoading$).thenReturn(of(true));
fixture.detectChanges();
expect(element.querySelector('ish-loading')).toBeTruthy();
});

it('should render order template list after creation', () => {
fixture.detectChanges();
expect(element.querySelector('.loading-container').textContent.trim()).toMatchInlineSnapshot(
`"order template order template 2"`
);
});

it('should trigger add product to basket with right order template', () => {
expect(() => fixture.detectChanges()).not.toThrow();

component.addTemplateToBasket(orderTemplates[1]);

verify(orderTemplatesFacade.addOrderTemplateToBasket(anything())).once();
expect(capture(orderTemplatesFacade.addOrderTemplateToBasket).last()).toMatchInlineSnapshot(`
Array [
Object {
"id": "2",
"title": "order template 2",
},
]
`);
});
});
@@ -0,0 +1,36 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { GenerateLazyComponent } from 'ish-core/utils/module-loader/generate-lazy-component.decorator';
import { whenTruthy } from 'ish-core/utils/operators';

import { OrderTemplatesFacade } from '../../facades/order-templates.facade';
import { OrderTemplate } from '../../models/order-template/order-template.model';

@Component({
selector: 'ish-order-template-widget',
templateUrl: './order-template-widget.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
@GenerateLazyComponent()
export class OrderTemplateWidgetComponent implements OnInit {
loading$: Observable<boolean>;
orderTemplates$: Observable<OrderTemplate[]>;

dummyProduct = { sku: 'dummy', available: true };

constructor(private facade: OrderTemplatesFacade) {}

ngOnInit() {
this.loading$ = this.facade.orderTemplateLoading$;
this.orderTemplates$ = this.facade.orderTemplates$.pipe(
whenTruthy(),
map(orderTemplates => orderTemplates.slice(0, 3))
);
}

addTemplateToBasket(orderTemplate: OrderTemplate) {
this.facade.addOrderTemplateToBasket(orderTemplate);
}
}
@@ -1,5 +1,5 @@
<div class="section">
<h2>{{ 'account.wishlists.widget.heading' | translate }}</h2>
<h3>{{ 'account.wishlists.widget.heading' | translate }}</h3>
<ng-container *ngIf="allWishlistsItemsSkus$ | async as itemSku">
<ng-container *ngIf="itemSku.length > 0; else noItems" class="section">
<div class="product-list">
Expand Down

0 comments on commit 6eff141

Please sign in to comment.