Skip to content

Commit

Permalink
refactor: set breadcrumb trail consistently via store (#289)
Browse files Browse the repository at this point in the history
BREAKING CHANGE:
setting the breadcrumb trail can now only be done consistently by using route data or by dispatching the appropriate action
  • Loading branch information
dhhyi authored Jun 29, 2020
1 parent de6c3c9 commit 8282584
Show file tree
Hide file tree
Showing 32 changed files with 388 additions and 214 deletions.
12 changes: 4 additions & 8 deletions src/app/core/routing/category/category.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(.*)$/;
Expand All @@ -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 '/';
}
Expand Down
10 changes: 10 additions & 0 deletions src/app/core/store/content/pages/pages.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 }] }))
)
);
}
17 changes: 16 additions & 1 deletion src/app/core/store/shopping/categories/categories.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -134,4 +140,13 @@ export class CategoriesEffects {
),
{ dispatch: false }
);

setBreadcrumbForCategoryPage$ = createEffect(() =>
this.store.pipe(
ofCategoryUrl(),
select(getBreadcrumbForCategoryPage),
whenTruthy(),
map(breadcrumbData => setBreadcrumbData({ breadcrumbData }))
)
);
}
89 changes: 72 additions & 17 deletions src/app/core/store/shopping/categories/categories.selectors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -19,6 +19,7 @@ import {
loadTopLevelCategoriesSuccess,
} from './categories.actions';
import {
getBreadcrumbForCategoryPage,
getCategoryEntities,
getCategoryLoading,
getSelectedCategory,
Expand All @@ -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 {}
Expand Down Expand Up @@ -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);
});
});

Expand All @@ -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', () => {
Expand Down
22 changes: 20 additions & 2 deletions src/app/core/store/shopping/categories/categories.selectors.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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<Category>) =>
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
);
12 changes: 11 additions & 1 deletion src/app/core/store/shopping/products/products.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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));
}
Loading

0 comments on commit 8282584

Please sign in to comment.