Skip to content

Commit

Permalink
feat: render view context content (#4)
Browse files Browse the repository at this point in the history
- introduce View Context service, state management, facade and rendering component
- add 'categoryRef' to category data
  • Loading branch information
shauke authored Nov 3, 2020
1 parent f97b114 commit dc36c87
Show file tree
Hide file tree
Showing 21 changed files with 444 additions and 1 deletion.
7 changes: 7 additions & 0 deletions src/app/core/facades/cms.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)));
}
}
3 changes: 3 additions & 0 deletions src/app/core/models/call-parameters/call-parameters.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface CallParameters {
[key: string]: string;
}
1 change: 1 addition & 0 deletions src/app/core/models/category/category.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface CategoryPathElement {
}

export interface CategoryData {
categoryRef: string;
name: string;
hasOnlineProducts: boolean;
hasOnlineSubCategories: boolean;
Expand Down
1 change: 1 addition & 0 deletions src/app/core/models/category/category.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export class CategoryMapper {

return {
uniqueId,
categoryRef: categoryData.categoryRef,
categoryPath,
name: categoryData.name,
hasOnlineProducts: categoryData.hasOnlineProducts,
Expand Down
1 change: 1 addition & 0 deletions src/app/core/models/category/category.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
32 changes: 32 additions & 0 deletions src/app/core/services/cms/cms.service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<ContentPageletEntryPointData>(`cms/viewcontexts/${viewContextId}/entrypoint`, {
params,
skipApiErrorHandling: true,
})
.pipe(
map(entrypoint => this.contentPageletEntryPointMapper.fromData(entrypoint)),
map(({ pageletEntryPoint, pagelets }) => ({ entrypoint: pageletEntryPoint, pagelets }))
);
}
}
5 changes: 4 additions & 1 deletion src/app/core/store/content/content-store.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContentState> = {
includes: includesReducer,
pagelets: pageletsReducer,
pages: pagesReducer,
viewcontexts: viewcontextsReducer,
};

const contentEffects = [IncludesEffects, PagesEffects];
const contentEffects = [IncludesEffects, PagesEffects, ViewcontextsEffects];

const metaReducers = [resetOnLogoutMeta];

Expand Down
2 changes: 2 additions & 0 deletions src/app/core/store/content/content-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContentState>('content');
4 changes: 4 additions & 0 deletions src/app/core/store/content/pagelets/pagelets.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContentPagelet> {}

Expand All @@ -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)
)
);
4 changes: 4 additions & 0 deletions src/app/core/store/content/viewcontexts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// tslint:disable no-barrel-files
// API to access ngrx viewcontexts state
export * from './viewcontexts.actions';
export * from './viewcontexts.selectors';
26 changes: 26 additions & 0 deletions src/app/core/store/content/viewcontexts/viewcontexts.actions.ts
Original file line number Diff line number Diff line change
@@ -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;
}>()
);
Original file line number Diff line number Diff line change
@@ -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<Action>;
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$);
});
});
});
32 changes: 32 additions & 0 deletions src/app/core/store/content/viewcontexts/viewcontexts.effects.ts
Original file line number Diff line number Diff line change
@@ -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)
)
)
)
);
}
Original file line number Diff line number Diff line change
@@ -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"`
);
});
});
42 changes: 42 additions & 0 deletions src/app/core/store/content/viewcontexts/viewcontexts.reducer.ts
Original file line number Diff line number Diff line change
@@ -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<ContentPageletEntryPointWithContext>({
selectId: viewcontext => serializeContextSpecificViewContextId(viewcontext.viewContextId, viewcontext.callParameters),
});

export interface ViewcontextsState extends EntityState<ContentPageletEntryPointWithContext> {}

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),
};
})
);
Loading

0 comments on commit dc36c87

Please sign in to comment.