From fbc5e3e613ca6aef289d849f7ac35cb054ba1c0f Mon Sep 17 00:00:00 2001 From: Marcel Eisentraut Date: Mon, 26 Apr 2021 08:31:36 +0200 Subject: [PATCH] feat: add support for CMS component "Product List (Filter)" (#673) --- src/app/core/facades/cms.facade.ts | 38 +++++++ src/app/shared/cms/cms.module.ts | 9 ++ .../cms-product-list-filter.component.html | 13 +++ .../cms-product-list-filter.component.spec.ts | 99 +++++++++++++++++++ .../cms-product-list-filter.component.ts | 40 ++++++++ src/app/shared/shared.module.ts | 2 + 6 files changed, 201 insertions(+) create mode 100644 src/app/shared/cms/components/cms-product-list-filter/cms-product-list-filter.component.html create mode 100644 src/app/shared/cms/components/cms-product-list-filter/cms-product-list-filter.component.spec.ts create mode 100644 src/app/shared/cms/components/cms-product-list-filter/cms-product-list-filter.component.ts diff --git a/src/app/core/facades/cms.facade.ts b/src/app/core/facades/cms.facade.ts index b597c135eb..6c218793e2 100644 --- a/src/app/core/facades/cms.facade.ts +++ b/src/app/core/facades/cms.facade.ts @@ -4,13 +4,16 @@ import { Observable, combineLatest } from 'rxjs'; import { delay, filter, map, switchMap, tap } from 'rxjs/operators'; import { CallParameters } from 'ish-core/models/call-parameters/call-parameters.model'; +import { CategoryHelper } from 'ish-core/models/category/category.helper'; import { getContentInclude, loadContentInclude } from 'ish-core/store/content/includes'; import { getContentPageTree, loadContentPageTree } from 'ish-core/store/content/page-tree'; import { getContentPagelet } from 'ish-core/store/content/pagelets'; import { getContentPageLoading, getSelectedContentPage } from 'ish-core/store/content/pages'; +import { getParametersProductList, loadParametersProductListFilter } from 'ish-core/store/content/parameters'; import { getViewContext, loadViewContextEntrypoint } from 'ish-core/store/content/viewcontexts'; import { getPGID } from 'ish-core/store/customer/user'; import { whenTruthy } from 'ish-core/utils/operators'; +import { URLFormParams } from 'ish-core/utils/url-form-params'; import { SfeAdapterService } from 'ish-shared/cms/sfe-adapter/sfe-adapter.service'; import { SfeMapper } from 'ish-shared/cms/sfe-adapter/sfe.mapper'; @@ -50,4 +53,39 @@ export class CMSFacade { this.store.dispatch(loadContentPageTree({ rootId, depth })); return this.store.pipe(select(getContentPageTree(rootId))); } + + parameterProductListFilter$(categoryId?: string, productFilter?: string, scope?: string, amount?: number) { + const listConfiguration = this.getProductListConfiguration(categoryId, productFilter, scope, amount); + this.store.dispatch( + loadParametersProductListFilter({ + id: listConfiguration.id, + searchParameter: listConfiguration.searchParameter, + amount, + }) + ); + return this.store.pipe(select(getParametersProductList(listConfiguration.id))); + } + + private getProductListConfiguration( + categoryId?: string, + productFilter?: string, + scope?: string, + amount?: number + ): { id: string; searchParameter: URLFormParams } { + let id = ''; + const searchParameter: URLFormParams = {}; + + id = categoryId ? `${id}@${categoryId}` : id; + id = productFilter ? `${id}@${productFilter}` : id; + id = scope ? `${id}@${scope}` : id; + id = amount ? `${id}@${amount}` : id; + + if (categoryId && scope !== 'Global') { + searchParameter.category = [CategoryHelper.getCategoryPath(categoryId)]; + } + + searchParameter.productFilter = productFilter ? [productFilter] : ['fallback_searchquerydefinition']; + + return { id, searchParameter }; + } } diff --git a/src/app/shared/cms/cms.module.ts b/src/app/shared/cms/cms.module.ts index 63852cd3c9..203d566a4b 100644 --- a/src/app/shared/cms/cms.module.ts +++ b/src/app/shared/cms/cms.module.ts @@ -7,6 +7,7 @@ import { CMSFreestyleComponent } from './components/cms-freestyle/cms-freestyle. import { CMSImageEnhancedComponent } from './components/cms-image-enhanced/cms-image-enhanced.component'; import { CMSImageComponent } from './components/cms-image/cms-image.component'; import { CMSLandingPageComponent } from './components/cms-landing-page/cms-landing-page.component'; +import { CMSProductListFilterComponent } from './components/cms-product-list-filter/cms-product-list-filter.component'; import { CMSProductListComponent } from './components/cms-product-list/cms-product-list.component'; import { CMSStandardPageComponent } from './components/cms-standard-page/cms-standard-page.component'; import { CMSStaticPageComponent } from './components/cms-static-page/cms-static-page.component'; @@ -73,6 +74,14 @@ import { SfeAdapterService } from './sfe-adapter/sfe-adapter.service'; }, multi: true, }, + { + provide: CMS_COMPONENT, + useValue: { + definitionQualifiedName: 'app_sf_base_cm:component.common.productListFilter.pagelet2-Component', + class: CMSProductListFilterComponent, + }, + multi: true, + }, { provide: CMS_COMPONENT, useValue: { diff --git a/src/app/shared/cms/components/cms-product-list-filter/cms-product-list-filter.component.html b/src/app/shared/cms/components/cms-product-list-filter/cms-product-list-filter.component.html new file mode 100644 index 0000000000..ea93a81b0e --- /dev/null +++ b/src/app/shared/cms/components/cms-product-list-filter/cms-product-list-filter.component.html @@ -0,0 +1,13 @@ + +
+

{{ pagelet.stringParam('Title') }}

+ + +
+
diff --git a/src/app/shared/cms/components/cms-product-list-filter/cms-product-list-filter.component.spec.ts b/src/app/shared/cms/components/cms-product-list-filter/cms-product-list-filter.component.spec.ts new file mode 100644 index 0000000000..a1a44bc9a2 --- /dev/null +++ b/src/app/shared/cms/components/cms-product-list-filter/cms-product-list-filter.component.spec.ts @@ -0,0 +1,99 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; +import { anyNumber, instance, mock, when } from 'ts-mockito'; + +import { CMSFacade } from 'ish-core/facades/cms.facade'; +import { ShoppingFacade } from 'ish-core/facades/shopping.facade'; +import { ContentPagelet } from 'ish-core/models/content-pagelet/content-pagelet.model'; +import { createContentPageletView } from 'ish-core/models/content-view/content-view.model'; +import { ProductsListComponent } from 'ish-shared/components/product/products-list/products-list.component'; + +import { CMSProductListFilterComponent } from './cms-product-list-filter.component'; + +describe('Cms Product List Filter Component', () => { + let component: CMSProductListFilterComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + let cmsFacade: CMSFacade; + let shoppingFacade: ShoppingFacade; + let pagelet: ContentPagelet; + + beforeEach(async () => { + cmsFacade = mock(CMSFacade); + shoppingFacade = mock(ShoppingFacade); + + await TestBed.configureTestingModule({ + declarations: [CMSProductListFilterComponent, MockComponent(ProductsListComponent)], + providers: [ + { provide: CMSFacade, useFactory: () => instance(cmsFacade) }, + { provide: ShoppingFacade, useFactory: () => instance(shoppingFacade) }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CMSProductListFilterComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + + pagelet = { + definitionQualifiedName: 'fq', + id: 'id', + displayName: 'name', + domain: 'domain', + configurationParameters: { + ListStyle: 'Carousel', + MaxNumberOfProducts: 6, + ShowViewAll: 'true', + SlideItems: 4, + Scope: 'GlobalScope', + Filter: 'top_seller_products', + ListItemStyle: 'tile', + Title: 'Filter Products Pagelet', + CSSClass: 'container', + ListItemCSSClass: 'col-12 col-sm-6 col-lg-3', + }, + }; + component.pagelet = createContentPageletView(pagelet); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); + + describe('getProductSKUs$', () => { + beforeEach(() => { + when( + cmsFacade.parameterProductListFilter$('categoryId', 'top_seller_products', 'GlobalScope', anyNumber()) + ).thenReturn(of(['id-1', 'id-2', 'id-3'])); + when(shoppingFacade.selectedCategoryId$).thenReturn(of('categoryId')); + }); + + it('should get all available products for given product filter', done => { + component.pagelet = createContentPageletView({ + ...pagelet, + configurationParameters: { + ...pagelet.configurationParameters, + MaxNumberOfProducts: 6, + }, + }); + + component.ngOnChanges(); + + component.productSKUs$.subscribe(productSKUs => { + expect(productSKUs).toHaveLength(3); + expect(productSKUs).toMatchInlineSnapshot(` + Array [ + "id-1", + "id-2", + "id-3", + ] + `); + done(); + }); + }); + }); +}); diff --git a/src/app/shared/cms/components/cms-product-list-filter/cms-product-list-filter.component.ts b/src/app/shared/cms/components/cms-product-list-filter/cms-product-list-filter.component.ts new file mode 100644 index 0000000000..3084ce5fd5 --- /dev/null +++ b/src/app/shared/cms/components/cms-product-list-filter/cms-product-list-filter.component.ts @@ -0,0 +1,40 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; +import { Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { CMSFacade } from 'ish-core/facades/cms.facade'; +import { ShoppingFacade } from 'ish-core/facades/shopping.facade'; +import { ContentPageletView } from 'ish-core/models/content-view/content-view.model'; +import { CMSComponent } from 'ish-shared/cms/models/cms-component/cms-component.model'; + +@Component({ + selector: 'ish-cms-product-list-filter', + templateUrl: './cms-product-list-filter.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CMSProductListFilterComponent implements CMSComponent, OnChanges { + @Input() pagelet: ContentPageletView; + + productSKUs$: Observable; + + constructor(private cmsFacade: CMSFacade, private shoppingFacade: ShoppingFacade) {} + + ngOnChanges() { + if (this.pagelet.hasParam('Filter')) { + this.productSKUs$ = this.getProductSKUs$(); + } + } + + getProductSKUs$(): Observable { + return this.shoppingFacade.selectedCategoryId$.pipe( + switchMap(categoryId => + this.cmsFacade.parameterProductListFilter$( + categoryId, + this.pagelet.stringParam('Filter'), + this.pagelet.stringParam('Scope'), + this.pagelet.numberParam('MaxNumberOfProducts') + ) + ) + ); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 2983cd076f..141ef0650d 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -37,6 +37,7 @@ import { CMSFreestyleComponent } from './cms/components/cms-freestyle/cms-freest import { CMSImageEnhancedComponent } from './cms/components/cms-image-enhanced/cms-image-enhanced.component'; import { CMSImageComponent } from './cms/components/cms-image/cms-image.component'; import { CMSLandingPageComponent } from './cms/components/cms-landing-page/cms-landing-page.component'; +import { CMSProductListFilterComponent } from './cms/components/cms-product-list-filter/cms-product-list-filter.component'; import { CMSProductListComponent } from './cms/components/cms-product-list/cms-product-list.component'; import { CMSStandardPageComponent } from './cms/components/cms-standard-page/cms-standard-page.component'; import { CMSStaticPageComponent } from './cms/components/cms-static-page/cms-static-page.component'; @@ -171,6 +172,7 @@ const declaredComponents = [ CMSImageEnhancedComponent, CMSLandingPageComponent, CMSProductListComponent, + CMSProductListFilterComponent, CMSStandardPageComponent, CMSStaticPageComponent, CMSTextComponent,