From dc36c87aabf1fa826aa32f124ab3d1ce63a3c896 Mon Sep 17 00:00:00 2001 From: Stefan Hauke Date: Tue, 3 Nov 2020 11:54:40 +0100 Subject: [PATCH] feat: render view context content (#4) - introduce View Context service, state management, facade and rendering component - add 'categoryRef' to category data --- src/app/core/facades/cms.facade.ts | 7 ++ .../call-parameters/call-parameters.model.ts | 3 + .../models/category/category.interface.ts | 1 + .../core/models/category/category.mapper.ts | 1 + .../core/models/category/category.model.ts | 1 + src/app/core/services/cms/cms.service.ts | 32 ++++++++ .../store/content/content-store.module.ts | 5 +- src/app/core/store/content/content-store.ts | 2 + .../content/pagelets/pagelets.reducer.ts | 4 + .../core/store/content/viewcontexts/index.ts | 4 + .../viewcontexts/viewcontexts.actions.ts | 26 +++++++ .../viewcontexts/viewcontexts.effects.spec.ts | 57 ++++++++++++++ .../viewcontexts/viewcontexts.effects.ts | 32 ++++++++ .../viewcontexts/viewcontexts.reducer.spec.ts | 30 ++++++++ .../viewcontexts/viewcontexts.reducer.ts | 42 +++++++++++ .../viewcontexts.selectors.spec.ts | 74 +++++++++++++++++++ .../viewcontexts/viewcontexts.selectors.ts | 20 +++++ .../content-viewcontext.component.html | 3 + .../content-viewcontext.component.spec.ts | 57 ++++++++++++++ .../content-viewcontext.component.ts | 42 +++++++++++ src/app/shared/shared.module.ts | 2 + 21 files changed, 444 insertions(+), 1 deletion(-) create mode 100644 src/app/core/models/call-parameters/call-parameters.model.ts create mode 100644 src/app/core/store/content/viewcontexts/index.ts create mode 100644 src/app/core/store/content/viewcontexts/viewcontexts.actions.ts create mode 100644 src/app/core/store/content/viewcontexts/viewcontexts.effects.spec.ts create mode 100644 src/app/core/store/content/viewcontexts/viewcontexts.effects.ts create mode 100644 src/app/core/store/content/viewcontexts/viewcontexts.reducer.spec.ts create mode 100644 src/app/core/store/content/viewcontexts/viewcontexts.reducer.ts create mode 100644 src/app/core/store/content/viewcontexts/viewcontexts.selectors.spec.ts create mode 100644 src/app/core/store/content/viewcontexts/viewcontexts.selectors.ts create mode 100644 src/app/shared/cms/components/content-viewcontext/content-viewcontext.component.html create mode 100644 src/app/shared/cms/components/content-viewcontext/content-viewcontext.component.spec.ts create mode 100644 src/app/shared/cms/components/content-viewcontext/content-viewcontext.component.ts diff --git a/src/app/core/facades/cms.facade.ts b/src/app/core/facades/cms.facade.ts index ea87c26273..439b98c580 100644 --- a/src/app/core/facades/cms.facade.ts +++ b/src/app/core/facades/cms.facade.ts @@ -3,9 +3,11 @@ import { Store, select } from '@ngrx/store'; import { Observable, combineLatest } from 'rxjs'; import { filter, map, switchMap, tap } from 'rxjs/operators'; +import { CallParameters } from 'ish-core/models/call-parameters/call-parameters.model'; import { getContentInclude, loadContentInclude } from 'ish-core/store/content/includes'; import { getContentPagelet } from 'ish-core/store/content/pagelets'; import { getContentPageLoading, getSelectedContentPage } from 'ish-core/store/content/pages'; +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 { SfeAdapterService } from 'ish-shared/cms/sfe-adapter/sfe-adapter.service'; @@ -36,4 +38,9 @@ export class CMSFacade { pagelet$(id: string) { return this.store.pipe(select(getContentPagelet(id))); } + + viewContext$(viewContextId: string, callParameters: CallParameters) { + this.store.dispatch(loadViewContextEntrypoint({ viewContextId, callParameters })); + return this.store.pipe(select(getViewContext(viewContextId, callParameters))); + } } diff --git a/src/app/core/models/call-parameters/call-parameters.model.ts b/src/app/core/models/call-parameters/call-parameters.model.ts new file mode 100644 index 0000000000..271c91e297 --- /dev/null +++ b/src/app/core/models/call-parameters/call-parameters.model.ts @@ -0,0 +1,3 @@ +export interface CallParameters { + [key: string]: string; +} diff --git a/src/app/core/models/category/category.interface.ts b/src/app/core/models/category/category.interface.ts index bd29866113..8164d31590 100644 --- a/src/app/core/models/category/category.interface.ts +++ b/src/app/core/models/category/category.interface.ts @@ -9,6 +9,7 @@ export interface CategoryPathElement { } export interface CategoryData { + categoryRef: string; name: string; hasOnlineProducts: boolean; hasOnlineSubCategories: boolean; diff --git a/src/app/core/models/category/category.mapper.ts b/src/app/core/models/category/category.mapper.ts index af1f989155..9db4c4268f 100644 --- a/src/app/core/models/category/category.mapper.ts +++ b/src/app/core/models/category/category.mapper.ts @@ -100,6 +100,7 @@ export class CategoryMapper { return { uniqueId, + categoryRef: categoryData.categoryRef, categoryPath, name: categoryData.name, hasOnlineProducts: categoryData.hasOnlineProducts, diff --git a/src/app/core/models/category/category.model.ts b/src/app/core/models/category/category.model.ts index 61a91e9b4c..5fcaeef165 100644 --- a/src/app/core/models/category/category.model.ts +++ b/src/app/core/models/category/category.model.ts @@ -4,6 +4,7 @@ import { SeoAttributes } from 'ish-core/models/seo-attributes/seo-attributes.mod export interface Category { uniqueId: string; + categoryRef: string; categoryPath: string[]; name: string; diff --git a/src/app/core/services/cms/cms.service.ts b/src/app/core/services/cms/cms.service.ts index bcc9c51c7a..e1f52dd368 100644 --- a/src/app/core/services/cms/cms.service.ts +++ b/src/app/core/services/cms/cms.service.ts @@ -1,7 +1,9 @@ +import { HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, throwError } from 'rxjs'; import { map } from 'rxjs/operators'; +import { CallParameters } from 'ish-core/models/call-parameters/call-parameters.model'; import { ContentPageletEntryPointData } from 'ish-core/models/content-pagelet-entry-point/content-pagelet-entry-point.interface'; import { ContentPageletEntryPointMapper } from 'ish-core/models/content-pagelet-entry-point/content-pagelet-entry-point.mapper'; import { ContentPageletEntryPoint } from 'ish-core/models/content-pagelet-entry-point/content-pagelet-entry-point.model'; @@ -50,4 +52,34 @@ export class CMSService { map(({ pageletEntryPoint, pagelets }) => ({ page: pageletEntryPoint, pagelets })) ); } + + /** + * Get the content for the given View Context with the given context (e.g. Product or Category). + * @param viewContextId The view context ID. + * @param callParameters The call parameters to give the current context. + * @returns The view contexts entrypoint content data. + */ + getViewContextContent( + viewContextId: string, + callParameters: CallParameters + ): Observable<{ entrypoint: ContentPageletEntryPoint; pagelets: ContentPagelet[] }> { + if (!viewContextId) { + return throwError('getViewContextContent() called without a viewContextId'); + } + + let params = new HttpParams(); + if (callParameters) { + params = Object.entries(callParameters).reduce((param, [key, value]) => param.set(key, value), new HttpParams()); + } + + return this.apiService + .get(`cms/viewcontexts/${viewContextId}/entrypoint`, { + params, + skipApiErrorHandling: true, + }) + .pipe( + map(entrypoint => this.contentPageletEntryPointMapper.fromData(entrypoint)), + map(({ pageletEntryPoint, pagelets }) => ({ entrypoint: pageletEntryPoint, pagelets })) + ); + } } diff --git a/src/app/core/store/content/content-store.module.ts b/src/app/core/store/content/content-store.module.ts index 158a95a581..8e051f027d 100644 --- a/src/app/core/store/content/content-store.module.ts +++ b/src/app/core/store/content/content-store.module.ts @@ -11,14 +11,17 @@ import { includesReducer } from './includes/includes.reducer'; import { pageletsReducer } from './pagelets/pagelets.reducer'; import { PagesEffects } from './pages/pages.effects'; import { pagesReducer } from './pages/pages.reducer'; +import { ViewcontextsEffects } from './viewcontexts/viewcontexts.effects'; +import { viewcontextsReducer } from './viewcontexts/viewcontexts.reducer'; const contentReducers: ActionReducerMap = { includes: includesReducer, pagelets: pageletsReducer, pages: pagesReducer, + viewcontexts: viewcontextsReducer, }; -const contentEffects = [IncludesEffects, PagesEffects]; +const contentEffects = [IncludesEffects, PagesEffects, ViewcontextsEffects]; const metaReducers = [resetOnLogoutMeta]; diff --git a/src/app/core/store/content/content-store.ts b/src/app/core/store/content/content-store.ts index 08e6a01822..13ee4c6dd2 100644 --- a/src/app/core/store/content/content-store.ts +++ b/src/app/core/store/content/content-store.ts @@ -3,11 +3,13 @@ import { createFeatureSelector } from '@ngrx/store'; import { IncludesState } from './includes/includes.reducer'; import { PageletsState } from './pagelets/pagelets.reducer'; import { PagesState } from './pages/pages.reducer'; +import { ViewcontextsState } from './viewcontexts/viewcontexts.reducer'; export interface ContentState { includes: IncludesState; pagelets: PageletsState; pages: PagesState; + viewcontexts: ViewcontextsState; } export const getContentState = createFeatureSelector('content'); diff --git a/src/app/core/store/content/pagelets/pagelets.reducer.ts b/src/app/core/store/content/pagelets/pagelets.reducer.ts index e885021230..3b5f52a78b 100644 --- a/src/app/core/store/content/pagelets/pagelets.reducer.ts +++ b/src/app/core/store/content/pagelets/pagelets.reducer.ts @@ -4,6 +4,7 @@ import { createReducer, on } from '@ngrx/store'; import { ContentPagelet } from 'ish-core/models/content-pagelet/content-pagelet.model'; import { loadContentIncludeSuccess } from 'ish-core/store/content/includes/includes.actions'; import { loadContentPageSuccess } from 'ish-core/store/content/pages/pages.actions'; +import { loadViewContextEntrypointSuccess } from 'ish-core/store/content/viewcontexts/viewcontexts.actions'; export interface PageletsState extends EntityState {} @@ -18,5 +19,8 @@ export const pageletsReducer = createReducer( ), on(loadContentPageSuccess, (state: PageletsState, action) => pageletsAdapter.upsertMany(action.payload.pagelets, state) + ), + on(loadViewContextEntrypointSuccess, (state: PageletsState, action) => + pageletsAdapter.upsertMany(action.payload.pagelets, state) ) ); diff --git a/src/app/core/store/content/viewcontexts/index.ts b/src/app/core/store/content/viewcontexts/index.ts new file mode 100644 index 0000000000..791832afb9 --- /dev/null +++ b/src/app/core/store/content/viewcontexts/index.ts @@ -0,0 +1,4 @@ +// tslint:disable no-barrel-files +// API to access ngrx viewcontexts state +export * from './viewcontexts.actions'; +export * from './viewcontexts.selectors'; diff --git a/src/app/core/store/content/viewcontexts/viewcontexts.actions.ts b/src/app/core/store/content/viewcontexts/viewcontexts.actions.ts new file mode 100644 index 0000000000..49a4be4827 --- /dev/null +++ b/src/app/core/store/content/viewcontexts/viewcontexts.actions.ts @@ -0,0 +1,26 @@ +import { createAction } from '@ngrx/store'; + +import { CallParameters } from 'ish-core/models/call-parameters/call-parameters.model'; +import { ContentPageletEntryPoint } from 'ish-core/models/content-pagelet-entry-point/content-pagelet-entry-point.model'; +import { ContentPagelet } from 'ish-core/models/content-pagelet/content-pagelet.model'; +import { httpError, payload } from 'ish-core/utils/ngrx-creators'; + +export const loadViewContextEntrypoint = createAction( + '[Content View Context] Load Entrypoint', + payload<{ viewContextId: string; callParameters: CallParameters }>() +); + +export const loadViewContextEntrypointFail = createAction( + '[Content View Context API] Load Entrypoint Fail', + httpError() +); + +export const loadViewContextEntrypointSuccess = createAction( + '[Content View Context API] Load Entrypoint Success', + payload<{ + entrypoint: ContentPageletEntryPoint; + pagelets: ContentPagelet[]; + viewContextId: string; + callParameters: CallParameters; + }>() +); diff --git a/src/app/core/store/content/viewcontexts/viewcontexts.effects.spec.ts b/src/app/core/store/content/viewcontexts/viewcontexts.effects.spec.ts new file mode 100644 index 0000000000..94de5fd909 --- /dev/null +++ b/src/app/core/store/content/viewcontexts/viewcontexts.effects.spec.ts @@ -0,0 +1,57 @@ +import { TestBed } from '@angular/core/testing'; +import { provideMockActions } from '@ngrx/effects/testing'; +import { Action } from '@ngrx/store'; +import { cold, hot } from 'jest-marbles'; +import { Observable, of } from 'rxjs'; +import { anything, instance, mock, when } from 'ts-mockito'; + +import { ContentPageletEntryPoint } from 'ish-core/models/content-pagelet-entry-point/content-pagelet-entry-point.model'; +import { CMSService } from 'ish-core/services/cms/cms.service'; + +import { loadViewContextEntrypoint, loadViewContextEntrypointSuccess } from './viewcontexts.actions'; +import { ViewcontextsEffects } from './viewcontexts.effects'; + +describe('Viewcontexts Effects', () => { + let actions$: Observable; + let effects: ViewcontextsEffects; + let cmsServiceMock: CMSService; + + beforeEach(() => { + cmsServiceMock = mock(CMSService); + + TestBed.configureTestingModule({ + providers: [ + ViewcontextsEffects, + provideMockActions(() => actions$), + { provide: CMSService, useFactory: () => instance(cmsServiceMock) }, + ], + }); + + effects = TestBed.inject(ViewcontextsEffects); + }); + + describe('loadViewContextEntrypoint$', () => { + it('should dispatch success actions when encountering loadViewcontexts', () => { + when(cmsServiceMock.getViewContextContent(anything(), anything())).thenReturn( + of({ entrypoint: { id: 'test' } as ContentPageletEntryPoint, pagelets: [] }) + ); + + actions$ = hot('-a-a-a', { + a: loadViewContextEntrypoint({ + viewContextId: 'test', + callParameters: {}, + }), + }); + const expected$ = cold('-c-c-c', { + c: loadViewContextEntrypointSuccess({ + entrypoint: { id: 'test' } as ContentPageletEntryPoint, + pagelets: [], + viewContextId: 'test', + callParameters: {}, + }), + }); + + expect(effects.loadViewContextEntrypoint$).toBeObservable(expected$); + }); + }); +}); diff --git a/src/app/core/store/content/viewcontexts/viewcontexts.effects.ts b/src/app/core/store/content/viewcontexts/viewcontexts.effects.ts new file mode 100644 index 0000000000..83ab12c1ef --- /dev/null +++ b/src/app/core/store/content/viewcontexts/viewcontexts.effects.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { concatMap, map } from 'rxjs/operators'; + +import { CMSService } from 'ish-core/services/cms/cms.service'; +import { mapErrorToAction, mapToPayload } from 'ish-core/utils/operators'; + +import { + loadViewContextEntrypoint, + loadViewContextEntrypointFail, + loadViewContextEntrypointSuccess, +} from './viewcontexts.actions'; + +@Injectable() +export class ViewcontextsEffects { + constructor(private actions$: Actions, private cmsService: CMSService) {} + + loadViewContextEntrypoint$ = createEffect(() => + this.actions$.pipe( + ofType(loadViewContextEntrypoint), + mapToPayload(), + concatMap(({ viewContextId, callParameters }) => + this.cmsService.getViewContextContent(viewContextId, callParameters).pipe( + map(({ entrypoint, pagelets }) => + loadViewContextEntrypointSuccess({ entrypoint, pagelets, viewContextId, callParameters }) + ), + mapErrorToAction(loadViewContextEntrypointFail) + ) + ) + ) + ); +} diff --git a/src/app/core/store/content/viewcontexts/viewcontexts.reducer.spec.ts b/src/app/core/store/content/viewcontexts/viewcontexts.reducer.spec.ts new file mode 100644 index 0000000000..3c27a97f46 --- /dev/null +++ b/src/app/core/store/content/viewcontexts/viewcontexts.reducer.spec.ts @@ -0,0 +1,30 @@ +import { serializeContextSpecificViewContextId } from './viewcontexts.reducer'; + +describe('Viewcontexts Reducer', () => { + it('should serialize callParameters in a sorted manner if callParameters are given', () => { + const viewContextId = 'the_viewcontext'; + const callParameters = { Product: 'TEST', Category: 'Hello@World', Extra: 'foo', Alternative: 'bar' }; + + expect(serializeContextSpecificViewContextId(viewContextId, callParameters)).toMatchInlineSnapshot( + `"the_viewcontext@@Alternative-bar@@Category-Hello@World@@Extra-foo@@Product-TEST"` + ); + }); + + it('should return the viewContextId if empty callParameters are provided', () => { + const viewContextId = 'the_viewcontext'; + const callParameters = {}; + + expect(serializeContextSpecificViewContextId(viewContextId, callParameters)).toMatchInlineSnapshot( + `"the_viewcontext"` + ); + }); + + it('should return the viewContextId if if no callParameters are provided', () => { + const viewContextId = 'the_viewcontext'; + const callParameters = undefined; + + expect(serializeContextSpecificViewContextId(viewContextId, callParameters)).toMatchInlineSnapshot( + `"the_viewcontext"` + ); + }); +}); diff --git a/src/app/core/store/content/viewcontexts/viewcontexts.reducer.ts b/src/app/core/store/content/viewcontexts/viewcontexts.reducer.ts new file mode 100644 index 0000000000..392d834cd0 --- /dev/null +++ b/src/app/core/store/content/viewcontexts/viewcontexts.reducer.ts @@ -0,0 +1,42 @@ +import { EntityState, createEntityAdapter } from '@ngrx/entity'; +import { createReducer, on } from '@ngrx/store'; + +import { CallParameters } from 'ish-core/models/call-parameters/call-parameters.model'; +import { ContentPageletEntryPoint } from 'ish-core/models/content-pagelet-entry-point/content-pagelet-entry-point.model'; + +import { loadViewContextEntrypointSuccess } from './viewcontexts.actions'; + +declare type ContentPageletEntryPointWithContext = ContentPageletEntryPoint & { + viewContextId: string; + callParameters: CallParameters; +}; + +export function serializeContextSpecificViewContextId(viewContextId: string, callParameters: CallParameters) { + const serializedParams = callParameters + ? Object.entries(callParameters) + .sort() + .map(([key, value]) => `@@${key}-${value}`) + .join('') + : ''; + return viewContextId + serializedParams; +} + +export const viewcontextsAdapter = createEntityAdapter({ + selectId: viewcontext => serializeContextSpecificViewContextId(viewcontext.viewContextId, viewcontext.callParameters), +}); + +export interface ViewcontextsState extends EntityState {} + +const initialState: ViewcontextsState = viewcontextsAdapter.getInitialState({}); + +export const viewcontextsReducer = createReducer( + initialState, + + on(loadViewContextEntrypointSuccess, (state: ViewcontextsState, action) => { + const { entrypoint, viewContextId, callParameters } = action.payload; + + return { + ...viewcontextsAdapter.upsertOne({ ...entrypoint, viewContextId, callParameters }, state), + }; + }) +); diff --git a/src/app/core/store/content/viewcontexts/viewcontexts.selectors.spec.ts b/src/app/core/store/content/viewcontexts/viewcontexts.selectors.spec.ts new file mode 100644 index 0000000000..b82192ee41 --- /dev/null +++ b/src/app/core/store/content/viewcontexts/viewcontexts.selectors.spec.ts @@ -0,0 +1,74 @@ +import { TestBed } from '@angular/core/testing'; + +import { ContentPageletEntryPoint } from 'ish-core/models/content-pagelet-entry-point/content-pagelet-entry-point.model'; +import { ContentStoreModule } from 'ish-core/store/content/content-store.module'; +import { CoreStoreModule } from 'ish-core/store/core/core-store.module'; +import { makeHttpError } from 'ish-core/utils/dev/api-service-utils'; +import { StoreWithSnapshots, provideStoreSnapshots } from 'ish-core/utils/dev/ngrx-testing'; + +import { + loadViewContextEntrypoint, + loadViewContextEntrypointFail, + loadViewContextEntrypointSuccess, +} from './viewcontexts.actions'; +import { getViewContextEntities } from './viewcontexts.selectors'; + +describe('Viewcontexts Selectors', () => { + let store$: StoreWithSnapshots; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ContentStoreModule.forTesting('viewcontexts'), CoreStoreModule.forTesting()], + providers: [provideStoreSnapshots()], + }); + + store$ = TestBed.inject(StoreWithSnapshots); + }); + + describe('initial state', () => { + it('should not have entities when in initial state', () => { + expect(getViewContextEntities(store$.state)).toBeEmpty(); + }); + }); + + describe('loadViewContextEntrypoint', () => { + const action = loadViewContextEntrypoint({ + viewContextId: 'test', + callParameters: {}, + }); + + beforeEach(() => { + store$.dispatch(action); + }); + + describe('loadViewContextEntrypointSuccess', () => { + const successAction = loadViewContextEntrypointSuccess({ + entrypoint: { id: 'test' } as ContentPageletEntryPoint, + pagelets: [], + viewContextId: 'test', + callParameters: {}, + }); + + beforeEach(() => { + store$.dispatch(successAction); + }); + + it('should have entities when successfully loading', () => { + expect(getViewContextEntities(store$.state)).not.toBeEmpty(); + }); + }); + + describe('loadViewcontFail', () => { + const error = makeHttpError({ message: 'ERROR' }); + const failAction = loadViewContextEntrypointFail({ error }); + + beforeEach(() => { + store$.dispatch(failAction); + }); + + it('should not have entities when reducing error', () => { + expect(getViewContextEntities(store$.state)).toBeEmpty(); + }); + }); + }); +}); diff --git a/src/app/core/store/content/viewcontexts/viewcontexts.selectors.ts b/src/app/core/store/content/viewcontexts/viewcontexts.selectors.ts new file mode 100644 index 0000000000..b7fd7d894d --- /dev/null +++ b/src/app/core/store/content/viewcontexts/viewcontexts.selectors.ts @@ -0,0 +1,20 @@ +import { createSelector } from '@ngrx/store'; + +import { CallParameters } from 'ish-core/models/call-parameters/call-parameters.model'; +import { createContentPageletEntryPointView } from 'ish-core/models/content-view/content-view.model'; +import { getContentState } from 'ish-core/store/content/content-store'; + +import { serializeContextSpecificViewContextId, viewcontextsAdapter } from './viewcontexts.reducer'; + +const getViewcontextsState = createSelector(getContentState, state => state.viewcontexts); + +export const { selectEntities: getViewContextEntities } = viewcontextsAdapter.getSelectors(getViewcontextsState); + +const getViewContextMemoized = (viewContextId: string, callParameters: CallParameters) => + createSelector( + getViewContextEntities, + entities => entities[serializeContextSpecificViewContextId(viewContextId, callParameters)] + ); + +export const getViewContext = (viewContextId: string, callParameters: CallParameters) => + createSelector(getViewContextMemoized(viewContextId, callParameters), createContentPageletEntryPointView); diff --git a/src/app/shared/cms/components/content-viewcontext/content-viewcontext.component.html b/src/app/shared/cms/components/content-viewcontext/content-viewcontext.component.html new file mode 100644 index 0000000000..ba3544a9a8 --- /dev/null +++ b/src/app/shared/cms/components/content-viewcontext/content-viewcontext.component.html @@ -0,0 +1,3 @@ + + + diff --git a/src/app/shared/cms/components/content-viewcontext/content-viewcontext.component.spec.ts b/src/app/shared/cms/components/content-viewcontext/content-viewcontext.component.spec.ts new file mode 100644 index 0000000000..d616d08bdf --- /dev/null +++ b/src/app/shared/cms/components/content-viewcontext/content-viewcontext.component.spec.ts @@ -0,0 +1,57 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; +import { anything, instance, mock, when } from 'ts-mockito'; + +import { CMSFacade } from 'ish-core/facades/cms.facade'; +import { + ContentPageletEntryPointView, + createContentPageletEntryPointView, +} from 'ish-core/models/content-view/content-view.model'; +import { ContentPageletComponent } from 'ish-shared/cms/components/content-pagelet/content-pagelet.component'; + +import { ContentViewcontextComponent } from './content-viewcontext.component'; + +describe('Content Viewcontext Component', () => { + let component: ContentViewcontextComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + let entrypoint: ContentPageletEntryPointView; + let cmsFacade: CMSFacade; + + beforeEach(async () => { + entrypoint = createContentPageletEntryPointView({ + id: 'test.entrypoint', + definitionQualifiedName: 'test.entrypoint-Include', + domain: 'domain', + displayName: 'displayName', + resourceSetId: 'resId', + configurationParameters: { + key: '1', + }, + }); + + cmsFacade = mock(CMSFacade); + when(cmsFacade.viewContext$(anything(), anything())).thenReturn(of(entrypoint)); + + await TestBed.configureTestingModule({ + declarations: [ContentViewcontextComponent, MockComponent(ContentPageletComponent)], + providers: [{ provide: CMSFacade, useValue: instance(cmsFacade) }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ContentViewcontextComponent); + component = fixture.componentInstance; + component.viewContextId = 'vc_foo_bar'; + component.callParameters = { Product: 'product_sku' }; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => component.ngOnChanges()).not.toThrow(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/shared/cms/components/content-viewcontext/content-viewcontext.component.ts b/src/app/shared/cms/components/content-viewcontext/content-viewcontext.component.ts new file mode 100644 index 0000000000..3dd93d0a64 --- /dev/null +++ b/src/app/shared/cms/components/content-viewcontext/content-viewcontext.component.ts @@ -0,0 +1,42 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { CMSFacade } from 'ish-core/facades/cms.facade'; +import { CallParameters } from 'ish-core/models/call-parameters/call-parameters.model'; +import { ContentPageletEntryPointView } from 'ish-core/models/content-view/content-view.model'; + +/** + * The Content ViewContext Component renders the content of the ViewContext + * (identified by the 'viewContextId') for the current context + * (given by the 'callParameters') if any content is available. + * + * @example + * + */ +@Component({ + selector: 'ish-content-viewcontext', + templateUrl: './content-viewcontext.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ContentViewcontextComponent implements OnChanges { + /** + * The ID of the View Context whose content is to be rendered. + */ + @Input() viewContextId: string; + + /** + * The call parameter object to provide the context, e.g. { Product: product.sku, Category: category.categoryRef }. + */ + @Input() callParameters: CallParameters; + + viewContextEntrypoint$: Observable; + + constructor(private cmsFacade: CMSFacade) {} + + ngOnChanges() { + this.viewContextEntrypoint$ = this.cmsFacade.viewContext$(this.viewContextId, this.callParameters); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index f9576e841f..4b9d005f79 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -38,6 +38,7 @@ import { CMSVideoComponent } from './cms/components/cms-video/cms-video.componen import { ContentIncludeComponent } from './cms/components/content-include/content-include.component'; import { ContentPageletComponent } from './cms/components/content-pagelet/content-pagelet.component'; import { ContentSlotComponent } from './cms/components/content-slot/content-slot.component'; +import { ContentViewcontextComponent } from './cms/components/content-viewcontext/content-viewcontext.component'; import { AddressComponent } from './components/address/address/address.component'; import { BasketAddressSummaryComponent } from './components/basket/basket-address-summary/basket-address-summary.component'; import { BasketCostSummaryComponent } from './components/basket/basket-cost-summary/basket-cost-summary.component'; @@ -183,6 +184,7 @@ const exportedComponents = [ BreadcrumbComponent, ContentIncludeComponent, ContentPageletComponent, + ContentViewcontextComponent, ErrorMessageComponent, FilterNavigationComponent, InfoBoxComponent,