Skip to content

Commit

Permalink
feat: CMS navigation category component
Browse files Browse the repository at this point in the history
* category tree data handling for CMS navigation category component
  • Loading branch information
shauke committed Mar 11, 2024
1 parent c4ff692 commit b08443c
Show file tree
Hide file tree
Showing 15 changed files with 693 additions and 25 deletions.
10 changes: 9 additions & 1 deletion src/app/core/facades/shopping.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import {
getCategory,
getCategoryIdByRefId,
getNavigationCategories,
getNavigationCategoryTree,
getSelectedCategory,
loadCategoryByRef,
loadCategoryTree,
loadTopLevelCategories,
} from 'ish-core/store/shopping/categories';
import { getAvailableFilter } from 'ish-core/store/shopping/filter';
Expand Down Expand Up @@ -69,8 +71,14 @@ export class ShoppingFacade {
}
return this.store.pipe(
select(getNavigationCategories(uniqueId)),
// prevent to display an empty navigation bar after login/logout);
filter(categories => !!categories?.length)
); // prevent to display an empty navigation bar after login/logout);
);
}

navigationCategoryTree$(categoryRef: string, depth: number) {
this.store.dispatch(loadCategoryTree({ categoryRef, depth }));
return this.store.pipe(select(getNavigationCategoryTree(categoryRef, depth)));
}

// PRODUCT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export interface NavigationCategory {
name: string;
url: string;
hasChildren: boolean;
children?: NavigationCategory[];
}
18 changes: 17 additions & 1 deletion src/app/core/services/categories/categories.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ describe('Categories Service', () => {
when(apiServiceMock.get('categories/dummyid/dummysubid', anything())).thenReturn(
of({ categoryPath: [{ id: 'blubb' }] } as CategoryData)
);
when(apiServiceMock.get('categories/dummyid@cat', anything())).thenReturn(
of({ categoryPath: [{ id: 'blubb' }] } as CategoryData)
);
TestBed.configureTestingModule({
providers: [{ provide: ApiService, useFactory: () => instance(apiServiceMock) }, provideMockStore()],
});
Expand All @@ -43,7 +46,7 @@ describe('Categories Service', () => {
expect(capture(apiServiceMock.get).last()[0].toString()).toMatchInlineSnapshot(`"categories"`);
const options: AvailableOptions = capture(apiServiceMock.get).last()[1];
expect(options.params.toString()).toMatchInlineSnapshot(
`"imageView=NO-IMAGE&view=tree&limit=1&omitHasOnlineProducts=true"`
`"view=tree&limit=1&omitHasOnlineProducts=true&imageView=NO-IMAGE"`
);
});

Expand Down Expand Up @@ -96,4 +99,17 @@ describe('Categories Service', () => {
verify(apiServiceMock.get('categories/dummyid/dummysubid', anything())).once();
});
});

describe('getCategoryTree()', () => {
it('should call ApiService "categories" when called', () => {
categoriesService.getCategoryTree('dummyid@cat', 1);
verify(apiServiceMock.get('categories/dummyid@cat', anything())).once();

expect(capture(apiServiceMock.get).last()[0].toString()).toMatchInlineSnapshot(`"categories/dummyid@cat"`);
const options: AvailableOptions = capture(apiServiceMock.get).last()[1];
expect(options.params.toString()).toMatchInlineSnapshot(
`"view=tree&limit=1&omitHasOnlineProducts=true&imageView=NO-IMAGE"`
);
});
});
});
40 changes: 34 additions & 6 deletions src/app/core/services/categories/categories.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,7 @@ export class CategoriesService {
* @returns A Sorted list of top level categories with sub categories.
*/
getTopLevelCategories(limit: number): Observable<CategoryTree> {
let params = new HttpParams().set('imageView', 'NO-IMAGE');
if (limit > 0) {
params = params.set('view', 'tree').set('limit', limit.toString()).set('omitHasOnlineProducts', 'true');
}

return this.apiService.get('categories', { sendSPGID: true, params }).pipe(
return this.apiService.get('categories', { sendSPGID: true, params: this.setTreeParams(limit) }).pipe(
unpackEnvelope<CategoryData>(),
map(categoriesData =>
categoriesData
Expand All @@ -61,4 +56,37 @@ export class CategoriesService {
)
);
}

/**
* Get the category tree data for a given categoryRef up to the given depth limit.
* If no limit is given or the limit is 0 the simple category detail REST call is made.
*
* @param categoryRef The categoryRef for the category of interest
* @param depth The number of depth levels to be returned in a hierarchical structure.
* @returns A category tree for the given category ref.
*/
getCategoryTree(categoryRef: string, depth?: number): Observable<CategoryTree> {
if (!categoryRef) {
return throwError(() => new Error('getCategoryTree() called without categoryRef'));
}
return this.apiService
.get<CategoryData>(`categories/${categoryRef}`, { sendSPGID: true, params: this.setTreeParams(depth) })
.pipe(map(categoryData => this.categoryMapper.fromData(categoryData)));
}

/**
* Set the necessary parameters for a REST call that should return a categories tree for the given depth.
* Http params for a tree REST call are only needed if a depth > 0 is set.
*/
private setTreeParams(depth?: number): HttpParams {
let params = new HttpParams();
if (depth > 0) {
params = params
.set('view', 'tree')
.set('limit', depth.toString())
.set('omitHasOnlineProducts', 'true')
.set('imageView', 'NO-IMAGE');
}
return params;
}
}
10 changes: 10 additions & 0 deletions src/app/core/store/shopping/categories/categories.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ export const loadTopLevelCategoriesSuccess = createAction(
payload<{ categories: CategoryTree }>()
);

export const loadCategoryTree = createAction(
'[Categories Internal] Load a specific category tree',
payload<{ categoryRef: string; depth: number }>()
);

export const loadCategoryTreeSuccess = createAction(
'[Categories API] Load a specific category tree success',
payload<{ categories: CategoryTree }>()
);

export const loadCategory = createAction('[Categories Internal] Load Category', payload<{ categoryId: string }>());

export const loadCategoryFail = createAction('[Categories API] Load Category Fail', httpError());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { provideMockActions } from '@ngrx/effects/testing';
import { Action, Store } from '@ngrx/store';
import { cold, hot } from 'jasmine-marbles';
import { Observable, noop, of, throwError } from 'rxjs';
import { anyNumber, anything, capture, instance, mock, spy, verify, when } from 'ts-mockito';
import { anyNumber, anyString, anything, capture, instance, mock, spy, verify, when } from 'ts-mockito';

import { MAIN_NAVIGATION_MAX_SUB_CATEGORIES_DEPTH } from 'ish-core/configurations/injection-keys';
import { CategoryView } from 'ish-core/models/category-view/category-view.model';
Expand All @@ -23,6 +23,8 @@ import {
loadCategoryByRef,
loadCategoryFail,
loadCategorySuccess,
loadCategoryTree,
loadCategoryTreeSuccess,
loadTopLevelCategories,
loadTopLevelCategoriesFail,
loadTopLevelCategoriesSuccess,
Expand All @@ -43,6 +45,12 @@ describe('Categories Effects', () => {
{ uniqueId: '456', categoryPath: ['456'] },
] as Category[]);

const CATEGORIES_TREE = categoryTree([
{ uniqueId: '123', categoryRef: '123@cat', categoryPath: ['123'] },
{ uniqueId: '123.456', categoryRef: '456@cat', categoryPath: ['123', '456'] },
{ uniqueId: '123.456.789', categoryRef: '789@cat', categoryPath: ['123', '456', '789'] },
] as Category[]);

beforeEach(() => {
categoriesServiceMock = mock(CategoriesService);
when(categoriesServiceMock.getCategory('123')).thenReturn(
Expand All @@ -55,6 +63,7 @@ describe('Categories Effects', () => {
throwError(() => makeHttpError({ message: 'invalid category' }))
);
when(categoriesServiceMock.getTopLevelCategories(anyNumber())).thenReturn(of(TOP_LEVEL_CATEGORIES));
when(categoriesServiceMock.getCategoryTree(anything(), anyNumber())).thenReturn(of(CATEGORIES_TREE));

TestBed.configureTestingModule({
imports: [
Expand Down Expand Up @@ -314,6 +323,33 @@ describe('Categories Effects', () => {
});
});

describe('loadCategoryTree$', () => {
it('should call the categoriesService for loadCategoryTree action', done => {
const action = loadCategoryTree({ categoryRef: '123@cat', depth: 1 });
actions$ = of(personalizationStatusDetermined(), action);

effects.loadCategoryTree$.subscribe(() => {
verify(categoriesServiceMock.getCategoryTree(anyString(), anyNumber())).once();
expect(capture(categoriesServiceMock.getCategoryTree).last()).toMatchInlineSnapshot(`
[
"123@cat",
1,
]
`);
done();
});
});

it('should map to action of type loadCategoryTreeSuccess', () => {
const action = loadCategoryTree({ categoryRef: '123@cat', depth: 2 });
const completion = loadCategoryTreeSuccess({ categories: CATEGORIES_TREE });
actions$ = hot('b-a-a-a', { a: action, b: personalizationStatusDetermined() });
const expected$ = cold('--c-c-c', { c: completion });

expect(effects.loadCategoryTree$).toBeObservable(expected$);
});
});

describe('redirectIfErrorInCategories$', () => {
it('should call error service if triggered', done => {
actions$ = of(loadCategoryFail({ error: makeHttpError({ status: 404 }) }));
Expand Down
15 changes: 15 additions & 0 deletions src/app/core/store/shopping/categories/categories.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { HttpStatusCodeService } from 'ish-core/utils/http-status-code/http-stat
import { InjectSingle } from 'ish-core/utils/injection';
import {
mapErrorToAction,
mapToPayload,
mapToPayloadProperty,
useCombinedObservableOnAction,
whenTruthy,
Expand All @@ -27,6 +28,8 @@ import {
loadCategoryByRef,
loadCategoryFail,
loadCategorySuccess,
loadCategoryTree,
loadCategoryTreeSuccess,
loadTopLevelCategories,
loadTopLevelCategoriesFail,
loadTopLevelCategoriesSuccess,
Expand Down Expand Up @@ -117,6 +120,18 @@ export class CategoriesEffects {
)
);

loadCategoryTree$ = createEffect(() =>
this.actions$.pipe(
useCombinedObservableOnAction(this.actions$.pipe(ofType(loadCategoryTree)), personalizationStatusDetermined),
mapToPayload(),
switchMap(({ categoryRef, depth }) =>
this.categoryService
.getCategoryTree(categoryRef, depth)
.pipe(map(categories => loadCategoryTreeSuccess({ categories })))
)
)
);

productOrCategoryChanged$ = createEffect(() =>
this.actions$.pipe(
ofType(routerNavigatedAction),
Expand Down
11 changes: 8 additions & 3 deletions src/app/core/store/shopping/categories/categories.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { createReducer, on } from '@ngrx/store';

import { CategoryTree, CategoryTreeHelper } from 'ish-core/models/category-tree/category-tree.model';

import { loadCategoryFail, loadCategorySuccess, loadTopLevelCategoriesSuccess } from './categories.actions';
import {
loadCategoryFail,
loadCategorySuccess,
loadCategoryTreeSuccess,
loadTopLevelCategoriesSuccess,
} from './categories.actions';

export interface CategoriesState {
categories: CategoryTree;
Expand All @@ -14,7 +19,7 @@ const initialState: CategoriesState = {

function mergeCategories(
state: CategoriesState,
action: ReturnType<typeof loadTopLevelCategoriesSuccess | typeof loadCategorySuccess>
action: ReturnType<typeof loadTopLevelCategoriesSuccess | typeof loadCategorySuccess | typeof loadCategoryTreeSuccess>
) {
const loadedTree = action.payload.categories;
const categories = CategoryTreeHelper.merge(state.categories, loadedTree);
Expand All @@ -32,5 +37,5 @@ export const categoriesReducer = createReducer(
...state,
})
),
on(loadCategorySuccess, loadTopLevelCategoriesSuccess, mergeCategories)
on(loadCategorySuccess, loadCategoryTreeSuccess, loadTopLevelCategoriesSuccess, mergeCategories)
);
Loading

0 comments on commit b08443c

Please sign in to comment.