From 11b0871fd57a793dca4de7880a877cf36cba9fa0 Mon Sep 17 00:00:00 2001 From: Danilo Hoffmann Date: Sun, 21 Jun 2020 18:06:21 +0200 Subject: [PATCH] refactor: set breadcrumb trail consistently via store --- .../core/routing/category/category.route.ts | 12 +- .../core/store/content/pages/pages.effects.ts | 10 ++ .../shopping/categories/categories.effects.ts | 17 ++- .../categories/categories.selectors.spec.ts | 89 ++++++++++--- .../categories/categories.selectors.ts | 22 +++- .../shopping/products/products.effects.ts | 12 +- .../products/products.selectors.spec.ts | 114 ++++++++++++++++- .../shopping/products/products.selectors.ts | 32 ++++- .../shopping/search/search.effects.spec.ts | 2 + .../store/shopping/search/search.effects.ts | 18 ++- .../store/shopping/shopping-store.spec.ts | 35 ++++-- .../pages/quickorder-routing.module.ts | 2 +- .../quickorder/quickorder-page.component.html | 2 +- .../quote-request/quote-request.effects.ts | 3 +- .../pages/account/account-page.component.html | 2 +- .../pages/account/account-page.component.ts | 3 - src/app/pages/app-routing.module.ts | 2 + .../category-categories.component.html | 2 +- .../category-products.component.html | 2 +- .../pages/contact/contact-page.component.html | 2 +- .../pages/content/content-page.component.html | 2 +- .../pages/product/product-page.component.html | 2 +- .../recently/recently-page.component.html | 2 +- .../pages/search/search-page.component.html | 2 +- .../search-result.component.html | 2 +- .../breadcrumb/breadcrumb.component.html | 9 +- .../breadcrumb/breadcrumb.component.spec.ts | 119 +++--------------- .../common/breadcrumb/breadcrumb.component.ts | 51 +++----- 28 files changed, 364 insertions(+), 208 deletions(-) diff --git a/src/app/core/routing/category/category.route.ts b/src/app/core/routing/category/category.route.ts index 8d0ee81b8ea..5686c3e5b05 100644 --- a/src/app/core/routing/category/category.route.ts +++ b/src/app/core/routing/category/category.route.ts @@ -2,16 +2,12 @@ import { UrlMatchResult, UrlSegment } from '@angular/router'; import { MonoTypeOperatorFunction } from 'rxjs'; import { filter } from 'rxjs/operators'; -import { CategoryView } from 'ish-core/models/category-view/category-view.model'; +import { Category } from 'ish-core/models/category/category.model'; import { CoreState } from 'ish-core/store/core/core-store'; import { selectRouteParam } from 'ish-core/store/core/router'; -export function generateLocalizedCategorySlug(category: CategoryView) { - if (!category || !category.categoryPath.length) { - return ''; - } - const lastCat = category.pathCategories()[category.categoryPath.length - 1].name; - return lastCat ? lastCat.replace(/ /g, '-') : ''; +export function generateLocalizedCategorySlug(category: Category) { + return category?.name?.replace(/ /g, '-') || ''; } const categoryRouteFormat = /^\/(?!category\/.*$)(.*-)?cat(.*)$/; @@ -37,7 +33,7 @@ export function matchCategoryRoute(segments: UrlSegment[]): UrlMatchResult { return; } -export function generateCategoryUrl(category: CategoryView): string { +export function generateCategoryUrl(category: Category): string { if (!category) { return '/'; } diff --git a/src/app/core/store/content/pages/pages.effects.ts b/src/app/core/store/content/pages/pages.effects.ts index bfa7a5abe3b..268a91e311b 100644 --- a/src/app/core/store/content/pages/pages.effects.ts +++ b/src/app/core/store/content/pages/pages.effects.ts @@ -5,9 +5,11 @@ import { map, mergeMap } from 'rxjs/operators'; import { CMSService } from 'ish-core/services/cms/cms.service'; import { selectRouteParam } from 'ish-core/store/core/router'; +import { setBreadcrumbData } from 'ish-core/store/core/viewconf'; import { mapErrorToAction, mapToPayloadProperty, whenTruthy } from 'ish-core/utils/operators'; import { loadContentPage, loadContentPageFail, loadContentPageSuccess } from './pages.actions'; +import { getSelectedContentPage } from './pages.selectors'; @Injectable() export class PagesEffects { @@ -32,4 +34,12 @@ export class PagesEffects { map(contentPageId => loadContentPage({ contentPageId })) ) ); + + setBreadcrumbForContentPage$ = createEffect(() => + this.store.pipe( + select(getSelectedContentPage), + whenTruthy(), + map(contentPage => setBreadcrumbData({ breadcrumbData: [{ key: contentPage.displayName }] })) + ) + ); } diff --git a/src/app/core/store/shopping/categories/categories.effects.ts b/src/app/core/store/shopping/categories/categories.effects.ts index 3458e8c7ebf..31248e6ca1c 100644 --- a/src/app/core/store/shopping/categories/categories.effects.ts +++ b/src/app/core/store/shopping/categories/categories.effects.ts @@ -20,6 +20,7 @@ import { CategoryHelper } from 'ish-core/models/category/category.model'; import { ofCategoryUrl } from 'ish-core/routing/category/category.route'; import { CategoriesService } from 'ish-core/services/categories/categories.service'; import { selectRouteParam } from 'ish-core/store/core/router'; +import { setBreadcrumbData } from 'ish-core/store/core/viewconf'; import { loadMoreProducts } from 'ish-core/store/shopping/product-listing'; import { HttpStatusCodeService } from 'ish-core/utils/http-status-code/http-status-code.service'; import { mapErrorToAction, mapToPayloadProperty, mapToProperty, whenFalsy, whenTruthy } from 'ish-core/utils/operators'; @@ -32,7 +33,12 @@ import { loadTopLevelCategoriesFail, loadTopLevelCategoriesSuccess, } from './categories.actions'; -import { getCategoryEntities, getSelectedCategory, isTopLevelCategoriesLoaded } from './categories.selectors'; +import { + getBreadcrumbForCategoryPage, + getCategoryEntities, + getSelectedCategory, + isTopLevelCategoriesLoaded, +} from './categories.selectors'; @Injectable() export class CategoriesEffects { @@ -134,4 +140,13 @@ export class CategoriesEffects { ), { dispatch: false } ); + + setBreadcrumbForCategoryPage$ = createEffect(() => + this.store.pipe( + ofCategoryUrl(), + select(getBreadcrumbForCategoryPage), + whenTruthy(), + map(breadcrumbData => setBreadcrumbData({ breadcrumbData })) + ) + ); } diff --git a/src/app/core/store/shopping/categories/categories.selectors.spec.ts b/src/app/core/store/shopping/categories/categories.selectors.spec.ts index 39d20f465a9..af2081be01c 100644 --- a/src/app/core/store/shopping/categories/categories.selectors.spec.ts +++ b/src/app/core/store/shopping/categories/categories.selectors.spec.ts @@ -3,7 +3,7 @@ import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { Category } from 'ish-core/models/category/category.model'; +import { Category, CategoryCompletenessLevel } from 'ish-core/models/category/category.model'; import { HttpError } from 'ish-core/models/http-error/http-error.model'; import { Product } from 'ish-core/models/product/product.model'; import { CoreStoreModule } from 'ish-core/store/core/core-store.module'; @@ -19,6 +19,7 @@ import { loadTopLevelCategoriesSuccess, } from './categories.actions'; import { + getBreadcrumbForCategoryPage, getCategoryEntities, getCategoryLoading, getSelectedCategory, @@ -30,13 +31,24 @@ describe('Categories Selectors', () => { let store$: StoreWithSnapshots; let router: Router; - let cat: Category; + let catA: Category; + let catA1: Category; let prod: Product; beforeEach(() => { prod = { sku: 'sku' } as Product; - cat = { uniqueId: 'Aa', categoryPath: ['Aa'] } as Category; - cat.hasOnlineProducts = true; + catA = { + uniqueId: 'A', + categoryPath: ['A'], + completenessLevel: CategoryCompletenessLevel.Max, + name: 'nA', + } as Category; + catA1 = { + uniqueId: 'A.1', + categoryPath: ['A', 'A.1'], + completenessLevel: CategoryCompletenessLevel.Max, + name: 'nA1', + } as Category; @Component({ template: 'dummy' }) class DummyComponent {} @@ -82,12 +94,12 @@ describe('Categories Selectors', () => { describe('and reporting success', () => { beforeEach(() => { - store$.dispatch(loadCategorySuccess({ categories: categoryTree([cat]) })); + store$.dispatch(loadCategorySuccess({ categories: categoryTree([catA]) })); }); it('should set loading to false', () => { expect(getCategoryLoading(store$.state)).toBeFalse(); - expect(getCategoryEntities(store$.state)).toEqual({ [cat.uniqueId]: cat }); + expect(getCategoryEntities(store$.state)).toHaveProperty(catA.uniqueId); }); }); @@ -105,46 +117,89 @@ describe('Categories Selectors', () => { describe('state with a category', () => { beforeEach(() => { - store$.dispatch(loadCategorySuccess({ categories: categoryTree([cat]) })); + store$.dispatch(loadCategorySuccess({ categories: categoryTree([catA, catA1]) })); store$.dispatch(loadProductSuccess({ product: prod })); }); describe('but no current router state', () => { it('should return the category information when used', () => { - expect(getCategoryEntities(store$.state)).toEqual({ [cat.uniqueId]: cat }); + expect(getCategoryEntities(store$.state)).toHaveProperty(catA.uniqueId); expect(getCategoryLoading(store$.state)).toBeFalse(); }); it('should not select the irrelevant category when used', () => { expect(getSelectedCategory(store$.state)).toBeUndefined(); }); + + it('should not generate a breadcrumb for unselected category', () => { + expect(getBreadcrumbForCategoryPage(store$.state)).toBeUndefined(); + }); }); describe('with category route', () => { beforeEach(fakeAsync(() => { - router.navigate(['category', cat.uniqueId]); + router.navigate(['category', catA.uniqueId]); tick(500); })); it('should return the category information when used', () => { - expect(getCategoryEntities(store$.state)).toEqual({ [cat.uniqueId]: cat }); + expect(getCategoryEntities(store$.state)).toHaveProperty(catA.uniqueId); expect(getCategoryLoading(store$.state)).toBeFalse(); }); it('should select the selected category when used', () => { - expect(getSelectedCategory(store$.state).uniqueId).toEqual(cat.uniqueId); + expect(getSelectedCategory(store$.state).uniqueId).toEqual(catA.uniqueId); + }); + + it('should set a category page breadcrumb when selected', () => { + expect(getBreadcrumbForCategoryPage(store$.state)).toMatchInlineSnapshot(` + Array [ + Object { + "link": undefined, + "text": "nA", + }, + ] + `); + }); + + describe('with subcategory', () => { + beforeEach(fakeAsync(() => { + router.navigate(['category', catA1.uniqueId]); + tick(500); + })); + + it('should select the selected category when used', () => { + expect(getSelectedCategory(store$.state).uniqueId).toEqual(catA1.uniqueId); + }); + + it('should set a category page breadcrumb when selected', () => { + expect(getBreadcrumbForCategoryPage(store$.state)).toMatchInlineSnapshot(` + Array [ + Object { + "link": "/nA-catA", + "text": "nA", + }, + Object { + "link": undefined, + "text": "nA1", + }, + ] + `); + }); }); }); }); describe('loading top level categories', () => { - let catA: Category; - let catB: Category; - beforeEach(() => { - catA = { uniqueId: 'A', categoryPath: ['A'] } as Category; - catB = { uniqueId: 'B', categoryPath: ['B'] } as Category; - store$.dispatch(loadTopLevelCategoriesSuccess({ categories: categoryTree([catA, catB]) })); + store$.dispatch( + loadTopLevelCategoriesSuccess({ + categories: categoryTree([ + { uniqueId: 'A', categoryPath: ['A'] }, + { uniqueId: 'B', categoryPath: ['B'] }, + ] as Category[]), + }) + ); }); it('should select root categories when used', () => { diff --git a/src/app/core/store/shopping/categories/categories.selectors.ts b/src/app/core/store/shopping/categories/categories.selectors.ts index 001e5d6944f..191b33b3e6d 100644 --- a/src/app/core/store/shopping/categories/categories.selectors.ts +++ b/src/app/core/store/shopping/categories/categories.selectors.ts @@ -1,6 +1,10 @@ -import { createSelector } from '@ngrx/store'; +import { Dictionary } from '@ngrx/entity'; +import { createSelector, createSelectorFactory, defaultMemoize } from '@ngrx/store'; +import { isEqual } from 'lodash-es'; -import { createCategoryView } from 'ish-core/models/category-view/category-view.model'; +import { CategoryView, createCategoryView } from 'ish-core/models/category-view/category-view.model'; +import { Category, CategoryHelper } from 'ish-core/models/category/category.model'; +import { generateCategoryUrl } from 'ish-core/routing/category/category.route'; import { selectRouteParam } from 'ish-core/store/core/router'; import { ShoppingState, getShoppingState } from 'ish-core/store/shopping/shopping-store'; @@ -29,3 +33,17 @@ export const getTopLevelCategories = createSelector(getCategoryTree, tree => ); export const isTopLevelCategoriesLoaded = createSelector(getCategoriesState, state => state.topLevelLoaded); + +export const getBreadcrumbForCategoryPage = createSelectorFactory(projector => + defaultMemoize(projector, undefined, isEqual) +)(getSelectedCategory, getCategoryEntities, (category: CategoryView, entities: Dictionary) => + CategoryHelper.isCategoryCompletelyLoaded(category) + ? (category.categoryPath || []) + .map(id => entities[id]) + .filter(x => !!x) + .map((cat, idx, arr) => ({ + text: cat.name, + link: idx === arr.length - 1 ? undefined : generateCategoryUrl(cat), + })) + : undefined +); diff --git a/src/app/core/store/shopping/products/products.effects.ts b/src/app/core/store/shopping/products/products.effects.ts index 35c85da74c6..161ca3927c0 100644 --- a/src/app/core/store/shopping/products/products.effects.ts +++ b/src/app/core/store/shopping/products/products.effects.ts @@ -25,6 +25,7 @@ import { Product, ProductCompletenessLevel, ProductHelper } from 'ish-core/model import { ofProductUrl } from 'ish-core/routing/product/product.route'; import { ProductsService } from 'ish-core/services/products/products.service'; import { selectRouteParam } from 'ish-core/store/core/router'; +import { setBreadcrumbData } from 'ish-core/store/core/viewconf'; import { loadCategory } from 'ish-core/store/shopping/categories'; import { setProductListingPages } from 'ish-core/store/shopping/product-listing'; import { HttpStatusCodeService } from 'ish-core/utils/http-status-code/http-status-code.service'; @@ -52,7 +53,7 @@ import { loadProductsForCategoryFail, loadRetailSetSuccess, } from './products.actions'; -import { getProductEntities, getSelectedProduct } from './products.selectors'; +import { getBreadcrumbForProductPage, getProductEntities, getSelectedProduct } from './products.selectors'; @Injectable() export class ProductsEffects { @@ -327,5 +328,14 @@ export class ProductsEffects { ) ); + setBreadcrumbForProductPage$ = createEffect(() => + this.store.pipe( + ofProductUrl(), + select(getBreadcrumbForProductPage), + whenTruthy(), + map(breadcrumbData => setBreadcrumbData({ breadcrumbData })) + ) + ); + private throttleOnBrowser = () => (isPlatformBrowser(this.platformId) ? throttleTime(3000) : map(identity)); } diff --git a/src/app/core/store/shopping/products/products.selectors.spec.ts b/src/app/core/store/shopping/products/products.selectors.spec.ts index ffb118af0dc..6a42b38d52f 100644 --- a/src/app/core/store/shopping/products/products.selectors.spec.ts +++ b/src/app/core/store/shopping/products/products.selectors.spec.ts @@ -3,11 +3,14 @@ import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; +import { Category } from 'ish-core/models/category/category.model'; import { HttpError } from 'ish-core/models/http-error/http-error.model'; -import { Product } from 'ish-core/models/product/product.model'; +import { Product, ProductCompletenessLevel } from 'ish-core/models/product/product.model'; import { CoreStoreModule } from 'ish-core/store/core/core-store.module'; +import { loadCategorySuccess } from 'ish-core/store/shopping/categories'; import { ShoppingStoreModule } from 'ish-core/store/shopping/shopping-store.module'; import { StoreWithSnapshots, provideStoreSnapshots } from 'ish-core/utils/dev/ngrx-testing'; +import { categoryTree } from 'ish-core/utils/dev/test-data-utils'; import { loadProduct, @@ -20,7 +23,14 @@ import { loadProductVariationsSuccess, loadRetailSetSuccess, } from './products.actions'; -import { getProduct, getProductEntities, getProductLinks, getProducts, getSelectedProduct } from './products.selectors'; +import { + getBreadcrumbForProductPage, + getProduct, + getProductEntities, + getProductLinks, + getProducts, + getSelectedProduct, +} from './products.selectors'; describe('Products Selectors', () => { let store$: StoreWithSnapshots; @@ -29,7 +39,7 @@ describe('Products Selectors', () => { let prod: Product; beforeEach(() => { - prod = { sku: 'sku' } as Product; + prod = { sku: 'sku', completenessLevel: ProductCompletenessLevel.Detail, name: 'product' } as Product; @Component({ template: 'dummy' }) class DummyComponent {} @@ -101,6 +111,10 @@ describe('Products Selectors', () => { it('should not select the irrelevant product when used', () => { expect(getSelectedProduct(store$.state)).toBeUndefined(); }); + + it('should not have a breadcrumb when no product is selected', () => { + expect(getBreadcrumbForProductPage(store$.state)).toBeUndefined(); + }); }); describe('with product route', () => { @@ -116,6 +130,98 @@ describe('Products Selectors', () => { it('should select the selected product when used', () => { expect(getSelectedProduct(store$.state)).toHaveProperty('sku', prod.sku); }); + + it('should generate a breadcrumb when product is selected', () => { + expect(getBreadcrumbForProductPage(store$.state)).toMatchInlineSnapshot(` + Array [ + Object { + "link": undefined, + "text": "product", + }, + ] + `); + }); + + describe('with category', () => { + beforeEach(() => { + store$.dispatch( + loadCategorySuccess({ + categories: categoryTree([{ uniqueId: 'A', name: 'nA', categoryPath: ['A'] }] as Category[]), + }) + ); + store$.dispatch( + loadCategorySuccess({ + categories: categoryTree([{ uniqueId: 'B', name: 'nB', categoryPath: ['B'] }] as Category[]), + }) + ); + }); + + describe('as default category', () => { + beforeEach(() => { + store$.dispatch(loadProductSuccess({ product: { ...prod, defaultCategoryId: 'A' } })); + }); + + it('should generate a breadcrumb with default category when product is selected', () => { + expect(getBreadcrumbForProductPage(store$.state)).toMatchInlineSnapshot(` + Array [ + Object { + "link": "/nA-catA", + "text": "nA", + }, + Object { + "link": undefined, + "text": "product", + }, + ] + `); + }); + }); + + describe('as selected category', () => { + beforeEach(fakeAsync(() => { + router.navigateByUrl('/product;sku=sku;categoryUniqueId=B'); + tick(500); + })); + + it('should generate a breadcrumb with selected category when product is selected', () => { + expect(getBreadcrumbForProductPage(store$.state)).toMatchInlineSnapshot(` + Array [ + Object { + "link": "/nB-catB", + "text": "nB", + }, + Object { + "link": undefined, + "text": "product", + }, + ] + `); + }); + }); + + describe('both selected and default', () => { + beforeEach(fakeAsync(() => { + store$.dispatch(loadProductSuccess({ product: { ...prod, defaultCategoryId: 'A' } })); + router.navigateByUrl('/product;sku=sku;categoryUniqueId=B'); + tick(500); + })); + + it('should generate a breadcrumb with selected category even if product has default category when product is selected', () => { + expect(getBreadcrumbForProductPage(store$.state)).toMatchInlineSnapshot(` + Array [ + Object { + "link": "/nB-catB", + "text": "nB", + }, + Object { + "link": undefined, + "text": "product", + }, + ] + `); + }); + }); + }); }); }); @@ -215,7 +321,7 @@ describe('Products Selectors', () => { store$.dispatch(loadProductSuccess({ product: { sku: 'SKU3', name: 'sku3' } as Product })); }); - it('should select various products on entites selector', () => { + it('should select various products on entities selector', () => { expect(getProductEntities(store$.state)).toHaveProperty('SKU1'); expect(getProductEntities(store$.state)).toHaveProperty('SKU2'); expect(getProductEntities(store$.state)).toHaveProperty('SKU3'); diff --git a/src/app/core/store/shopping/products/products.selectors.ts b/src/app/core/store/shopping/products/products.selectors.ts index a198dfc32d7..554c522ed28 100644 --- a/src/app/core/store/shopping/products/products.selectors.ts +++ b/src/app/core/store/shopping/products/products.selectors.ts @@ -1,8 +1,10 @@ -import { createSelector } from '@ngrx/store'; -import { memoize } from 'lodash-es'; +import { Dictionary } from '@ngrx/entity'; +import { createSelector, createSelectorFactory, defaultMemoize } from '@ngrx/store'; +import { isEqual, memoize } from 'lodash-es'; import { CategoryTree } from 'ish-core/models/category-tree/category-tree.model'; -import { createCategoryView } from 'ish-core/models/category-view/category-view.model'; +import { CategoryView, createCategoryView } from 'ish-core/models/category-view/category-view.model'; +import { Category } from 'ish-core/models/category/category.model'; import { ProductLinks, ProductLinksView } from 'ish-core/models/product-links/product-links.model'; import { ProductVariationHelper } from 'ish-core/models/product-variation/product-variation.helper'; import { @@ -13,9 +15,10 @@ import { createVariationProductMasterView, createVariationProductView, } from 'ish-core/models/product-view/product-view.model'; -import { Product, ProductHelper } from 'ish-core/models/product/product.model'; +import { Product, ProductCompletenessLevel, ProductHelper } from 'ish-core/models/product/product.model'; +import { generateCategoryUrl } from 'ish-core/routing/category/category.route'; import { selectRouteParam } from 'ish-core/store/core/router'; -import { getCategoryTree } from 'ish-core/store/shopping/categories'; +import { getCategoryEntities, getCategoryTree, getSelectedCategory } from 'ish-core/store/shopping/categories'; import { getShoppingState } from 'ish-core/store/shopping/shopping-store'; import { productAdapter } from './products.reducer'; @@ -137,3 +140,22 @@ export const getProductLinks = createSelector( return acc; }, {}) ); + +export const getBreadcrumbForProductPage = createSelectorFactory(projector => + defaultMemoize(projector, undefined, isEqual) +)( + getSelectedProduct, + getSelectedCategory, + getCategoryEntities, + (product: ProductView, category: CategoryView, entities: Dictionary) => + ProductHelper.isSufficientlyLoaded(product, ProductCompletenessLevel.Detail) + ? (category?.categoryPath || product.defaultCategory()?.categoryPath || []) + .map(id => entities[id]) + .filter(x => !!x) + .map(cat => ({ + text: cat.name, + link: generateCategoryUrl(cat), + })) + .concat([{ text: product.name, link: undefined }]) + : undefined +); diff --git a/src/app/core/store/shopping/search/search.effects.spec.ts b/src/app/core/store/shopping/search/search.effects.spec.ts index 7e8f5f7f8d5..4614d80bfbe 100644 --- a/src/app/core/store/shopping/search/search.effects.spec.ts +++ b/src/app/core/store/shopping/search/search.effects.spec.ts @@ -3,6 +3,7 @@ import { Component } from '@angular/core'; import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; import { of, throwError } from 'rxjs'; import { anyNumber, anyString, anything, instance, mock, verify, when } from 'ts-mockito'; @@ -65,6 +66,7 @@ describe('Search Effects', () => { { path: 'search/:searchTerm', component: DummyComponent }, ]), ShoppingStoreModule.forTesting('productListing'), + TranslateModule.forRoot(), ], providers: [ provideStoreSnapshots(), diff --git a/src/app/core/store/shopping/search/search.effects.ts b/src/app/core/store/shopping/search/search.effects.ts index f90dca148d2..41bf9708d09 100644 --- a/src/app/core/store/shopping/search/search.effects.ts +++ b/src/app/core/store/shopping/search/search.effects.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { routerNavigatedAction } from '@ngrx/router-store'; import { Store, select } from '@ngrx/store'; +import { TranslateService } from '@ngx-translate/core'; import { isEqual } from 'lodash-es'; import { EMPTY } from 'rxjs'; import { catchError, concatMap, debounceTime, distinctUntilChanged, map, sample, switchMap, tap } from 'rxjs/operators'; @@ -10,6 +11,7 @@ import { ProductListingMapper } from 'ish-core/models/product-listing/product-li import { ProductsService } from 'ish-core/services/products/products.service'; import { SuggestService } from 'ish-core/services/suggest/suggest.service'; import { ofUrl, selectRouteParam } from 'ish-core/store/core/router'; +import { setBreadcrumbData } from 'ish-core/store/core/viewconf'; import { loadMoreProducts, setProductListingPages } from 'ish-core/store/shopping/product-listing'; import { loadProductSuccess } from 'ish-core/store/shopping/products'; import { HttpStatusCodeService } from 'ish-core/utils/http-status-code/http-status-code.service'; @@ -25,7 +27,8 @@ export class SearchEffects { private productsService: ProductsService, private suggestService: SuggestService, private httpStatusCodeService: HttpStatusCodeService, - private productListingMapper: ProductListingMapper + private productListingMapper: ProductListingMapper, + private translateService: TranslateService ) {} /** @@ -96,4 +99,17 @@ export class SearchEffects { ), { dispatch: false } ); + + setSearchBreadcrumb$ = createEffect(() => + this.store.pipe( + ofUrl(/^\/search.*/), + select(selectRouteParam('searchTerm')), + whenTruthy(), + switchMap(searchTerm => + this.translateService + .get('search.breadcrumbs.your_search.label') + .pipe(map(translation => setBreadcrumbData({ breadcrumbData: [{ text: `${translation} ${searchTerm}` }] }))) + ) + ) + ); } diff --git a/src/app/core/store/shopping/shopping-store.spec.ts b/src/app/core/store/shopping/shopping-store.spec.ts index 4faa3edc653..974dcac9f53 100644 --- a/src/app/core/store/shopping/shopping-store.spec.ts +++ b/src/app/core/store/shopping/shopping-store.spec.ts @@ -58,14 +58,15 @@ describe('Shopping Store', () => { @Component({ template: 'dummy' }) class DummyComponent {} - const catA = { uniqueId: 'A', categoryPath: ['A'] } as Category; - const catA123 = { uniqueId: 'A.123', categoryPath: ['A', 'A.123'] } as Category; + const catA = { uniqueId: 'A', categoryPath: ['A'], name: 'nA' } as Category; + const catA123 = { uniqueId: 'A.123', categoryPath: ['A', 'A.123'], name: 'nA123' } as Category; const catA123456 = { uniqueId: 'A.123.456', categoryPath: ['A', 'A.123', 'A.123.456'], hasOnlineProducts: true, + name: 'nA123456', } as Category; - const catB = { uniqueId: 'B', categoryPath: ['B'] } as Category; + const catB = { uniqueId: 'B', categoryPath: ['B'], name: 'nB' } as Category; const promotion = { id: 'PROMO_UUID', @@ -119,7 +120,7 @@ describe('Shopping Store', () => { productsServiceMock = mock(ProductsService); when(productsServiceMock.getProduct(anyString())).thenCall(sku => { if (['P1', 'P2'].find(x => x === sku)) { - return of({ sku }); + return of({ sku, name: 'n' + sku }); } else { return throwError({ message: `error loading product ${sku}` }); } @@ -269,6 +270,8 @@ describe('Shopping Store', () => { categories: tree(A.123,A.123.456) [Categories Internal] Load Category: categoryId: "A" + [Viewconf Internal] Set Breadcrumb Data: + breadcrumbData: [{"text":"nA","link":"/nA-catA"},{"text":"nA123"}] [Categories API] Load Category Success: categories: tree(A,A.123) @ngrx/router-store/navigated: @@ -315,6 +318,8 @@ describe('Shopping Store', () => { @ngrx/router-store/navigation: routerState: {"url":"/search/something","params":{"searchTerm":"something... event: {"id":2,"url":"/search/something"} + [Viewconf Internal] Set Breadcrumb Data: + breadcrumbData: [{"text":"search.breadcrumbs.your_search.label something"}] @ngrx/router-store/navigated: routerState: {"url":"/search/something","params":{"searchTerm":"something... event: {"id":2,"url":"/search/something"} @@ -366,7 +371,7 @@ describe('Shopping Store', () => { sku: "P2" group: undefined [Products API] Load Product Success: - product: {"sku":"P2"} + product: {"sku":"P2","name":"nP2"} @ngrx/router-store/navigated: routerState: {"url":"/product/P2","params":{"sku":"P2"},"queryParams":{},... event: {"id":3,"url":"/product/P2"} @@ -402,8 +407,12 @@ describe('Shopping Store', () => { categories: tree(A.123,A.123.456) [Categories Internal] Load Category: categoryId: "A" + [Viewconf Internal] Set Breadcrumb Data: + breadcrumbData: [{"text":"nA123"}] [Categories API] Load Category Success: categories: tree(A,A.123) + [Viewconf Internal] Set Breadcrumb Data: + breadcrumbData: [{"text":"nA","link":"/nA-catA"},{"text":"nA123"}] @ngrx/router-store/navigated: routerState: {"url":"/category/A.123","params":{"categoryUniqueId":"A.123... event: {"id":1,"url":"/category/A.123"} @@ -476,12 +485,16 @@ describe('Shopping Store', () => { categoryId: "A" [Categories Internal] Load Category: categoryId: "A.123" + [Viewconf Internal] Set Breadcrumb Data: + breadcrumbData: [{"text":"nA123456"}] [Categories API] Load Category Success: categories: tree(A,A.123) [Categories API] Load Category Success: categories: tree(A.123,A.123.456) [Categories Internal] Load Category: categoryId: "A.123" + [Viewconf Internal] Set Breadcrumb Data: + breadcrumbData: [{"text":"nA","link":"/nA-catA"},{"text":"nA123","link":"/nA... [Categories API] Load Category Success: categories: tree(A.123,A.123.456) @ngrx/router-store/navigated: @@ -545,7 +558,7 @@ describe('Shopping Store', () => { sku: "P1" group: undefined [Products API] Load Product Success: - product: {"sku":"P1"} + product: {"sku":"P1","name":"nP1"} @ngrx/router-store/navigated: routerState: {"url":"/category/A.123.456/product/P1","params":{"categoryU... event: {"id":2,"url":"/category/A.123.456/product/P1"} @@ -594,6 +607,8 @@ describe('Shopping Store', () => { @ngrx/router-store/navigation: routerState: {"url":"/search/something","params":{"searchTerm":"something... event: {"id":2,"url":"/search/something"} + [Viewconf Internal] Set Breadcrumb Data: + breadcrumbData: [{"text":"search.breadcrumbs.your_search.label something"}] @ngrx/router-store/navigated: routerState: {"url":"/search/something","params":{"searchTerm":"something... event: {"id":2,"url":"/search/something"} @@ -708,7 +723,7 @@ describe('Shopping Store', () => { [Categories API] Load Category Success: categories: tree(A.123.456) [Products API] Load Product Success: - product: {"sku":"P1"} + product: {"sku":"P1","name":"nP1"} [Categories Internal] Load Category: categoryId: "A" [Categories Internal] Load Category: @@ -759,6 +774,8 @@ describe('Shopping Store', () => { @ngrx/router-store/navigation: routerState: {"url":"/category/A.123.456","params":{"categoryUniqueId":"A... event: {"id":2,"url":"/category/A.123.456"} + [Viewconf Internal] Set Breadcrumb Data: + breadcrumbData: [{"text":"nA","link":"/nA-catA"},{"text":"nA123","link":"/nA... [Product Listing] Load More Products: id: {"type":"category","value":"A.123.456"} [Product Listing Internal] Load More Products For Params: @@ -853,7 +870,7 @@ describe('Shopping Store', () => { [Products Internal] Load Product: sku: "P1" [Products API] Load Product Success: - product: {"sku":"P1"} + product: {"sku":"P1","name":"nP1"} [Recently Viewed Internal] Add Product to Recently: sku: "P1" group: undefined @@ -1036,6 +1053,8 @@ describe('Shopping Store', () => { @ngrx/router-store/navigation: routerState: {"url":"/search/something","params":{"searchTerm":"something... event: {"id":1,"url":"/search/something"} + [Viewconf Internal] Set Breadcrumb Data: + breadcrumbData: [{"text":"search.breadcrumbs.your_search.label something"}] @ngrx/router-store/navigated: routerState: {"url":"/search/something","params":{"searchTerm":"something... event: {"id":1,"url":"/search/something"} diff --git a/src/app/extensions/quickorder/pages/quickorder-routing.module.ts b/src/app/extensions/quickorder/pages/quickorder-routing.module.ts index dbedcfdccd4..1421729097b 100644 --- a/src/app/extensions/quickorder/pages/quickorder-routing.module.ts +++ b/src/app/extensions/quickorder/pages/quickorder-routing.module.ts @@ -8,7 +8,7 @@ const routes: Routes = [ path: 'quick-order', loadChildren: () => import('./quickorder/quickorder-page.module').then(m => m.QuickorderPageModule), canActivate: [FeatureToggleGuard], - data: { feature: 'quickorder' }, + data: { feature: 'quickorder', breadcrumbData: [{ key: 'quickorder.page.breadcrumb' }] }, }, ]; diff --git a/src/app/extensions/quickorder/pages/quickorder/quickorder-page.component.html b/src/app/extensions/quickorder/pages/quickorder/quickorder-page.component.html index d41fe906e98..7c37cd6a8cc 100644 --- a/src/app/extensions/quickorder/pages/quickorder/quickorder-page.component.html +++ b/src/app/extensions/quickorder/pages/quickorder/quickorder-page.component.html @@ -1,5 +1,5 @@
- +

{{ 'quickorder.page.title' | translate }}

diff --git a/src/app/extensions/quoting/store/quote-request/quote-request.effects.ts b/src/app/extensions/quoting/store/quote-request/quote-request.effects.ts index e166d1d53c2..5c7d36451c2 100644 --- a/src/app/extensions/quoting/store/quote-request/quote-request.effects.ts +++ b/src/app/extensions/quoting/store/quote-request/quote-request.effects.ts @@ -26,7 +26,7 @@ import { import { LineItemUpdate } from 'ish-core/models/line-item-update/line-item-update.model'; import { ProductCompletenessLevel } from 'ish-core/models/product/product.model'; import { displaySuccessMessage } from 'ish-core/store/core/messages'; -import { selectRouteParam } from 'ish-core/store/core/router'; +import { ofUrl, selectRouteParam } from 'ish-core/store/core/router'; import { setBreadcrumbData } from 'ish-core/store/core/viewconf'; import { getCurrentBasket } from 'ish-core/store/customer/basket'; import { getUserAuthorized, loadCompanyUserSuccess } from 'ish-core/store/customer/user'; @@ -452,6 +452,7 @@ export class QuoteRequestEffects { setQuoteRequestBreadcrumb$ = createEffect(() => this.store.pipe( + ofUrl(/^\/account\/.*/), select(getSelectedQuoteRequest), whenTruthy(), withLatestFrom(this.translateService.get('quote.edit.unsubmitted.quote_request_details.text')), diff --git a/src/app/pages/account/account-page.component.html b/src/app/pages/account/account-page.component.html index e342d79e8cb..fd4881fe028 100644 --- a/src/app/pages/account/account-page.component.html +++ b/src/app/pages/account/account-page.component.html @@ -4,7 +4,7 @@
- +
diff --git a/src/app/pages/account/account-page.component.ts b/src/app/pages/account/account-page.component.ts index 92171343f9b..a8591e30ec5 100644 --- a/src/app/pages/account/account-page.component.ts +++ b/src/app/pages/account/account-page.component.ts @@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { AppFacade } from 'ish-core/facades/app.facade'; -import { BreadcrumbItem } from 'ish-core/models/breadcrumb-item/breadcrumb-item.interface'; import { DeviceType } from 'ish-core/models/viewtype/viewtype.types'; @Component({ @@ -11,13 +10,11 @@ import { DeviceType } from 'ish-core/models/viewtype/viewtype.types'; changeDetection: ChangeDetectionStrategy.Default, }) export class AccountPageComponent implements OnInit { - breadcrumbData$: Observable; deviceType$: Observable; constructor(private appFacade: AppFacade) {} ngOnInit() { - this.breadcrumbData$ = this.appFacade.breadcrumbData$; this.deviceType$ = this.appFacade.deviceType$; } } diff --git a/src/app/pages/app-routing.module.ts b/src/app/pages/app-routing.module.ts index f6dc0e1f61b..f556feeff2c 100644 --- a/src/app/pages/app-routing.module.ts +++ b/src/app/pages/app-routing.module.ts @@ -61,6 +61,7 @@ const routes: Routes = [ title: 'application.recentlyViewed.heading', robots: 'noindex, nofollow', }, + breadcrumbData: [{ key: 'application.recentlyViewed.breadcrumb.label' }], }, }, { @@ -142,6 +143,7 @@ const routes: Routes = [ title: 'helpdesk.contact_us.heading', robots: 'index, nofollow', }, + breadcrumbData: [{ key: 'helpdesk.contact_us.link' }], }, }, ]; diff --git a/src/app/pages/category/category-categories/category-categories.component.html b/src/app/pages/category/category-categories/category-categories.component.html index 8a1a20c4f52..db11a95f65b 100644 --- a/src/app/pages/category/category-categories/category-categories.component.html +++ b/src/app/pages/category/category-categories/category-categories.component.html @@ -11,7 +11,7 @@
- +

{{ category.name }}

diff --git a/src/app/pages/category/category-products/category-products.component.html b/src/app/pages/category/category-products/category-products.component.html index 0e37ce39431..57e28510095 100644 --- a/src/app/pages/category/category-products/category-products.component.html +++ b/src/app/pages/category/category-products/category-products.component.html @@ -14,7 +14,7 @@
- +

{{ category.name }}

+

{{ 'helpdesk.contact_us.heading' | translate }}

diff --git a/src/app/pages/content/content-page.component.html b/src/app/pages/content/content-page.component.html index 7f9675a639a..55be7775086 100644 --- a/src/app/pages/content/content-page.component.html +++ b/src/app/pages/content/content-page.component.html @@ -1,5 +1,5 @@ - + - + + diff --git a/src/app/pages/search/search-page.component.html b/src/app/pages/search/search-page.component.html index ca681d0c726..a66c5bd82b2 100644 --- a/src/app/pages/search/search-page.component.html +++ b/src/app/pages/search/search-page.component.html @@ -10,7 +10,7 @@ - + diff --git a/src/app/pages/search/search-result/search-result.component.html b/src/app/pages/search/search-result/search-result.component.html index 73080adf253..8d910688650 100644 --- a/src/app/pages/search/search-result/search-result.component.html +++ b/src/app/pages/search/search-result/search-result.component.html @@ -10,7 +10,7 @@
- +

{{ 'search.title.text' diff --git a/src/app/shared/components/common/breadcrumb/breadcrumb.component.html b/src/app/shared/components/common/breadcrumb/breadcrumb.component.html index f98f76e1c32..e35a4ccf720 100644 --- a/src/app/shared/components/common/breadcrumb/breadcrumb.component.html +++ b/src/app/shared/components/common/breadcrumb/breadcrumb.component.html @@ -7,13 +7,8 @@ >{{ separator }} - - -