From a67dd43ad6c7b26f043b7cb15d07170a05489977 Mon Sep 17 00:00:00 2001
From: Denys Vuika <denys.vuika@gmail.com>
Date: Mon, 4 Jun 2018 08:57:50 +0100
Subject: [PATCH] [ACA-1430] Initial NgRx setup (#384)

* initial ngrx integration, migrate header

* update header tests

* update spellcheck rules

* fix locked image path for virtual paths
---
 cspell.json                                   |  1 +
 package-lock.json                             | 20 +++++++++
 package.json                                  |  4 ++
 src/app/app.component.ts                      | 26 ++++++++++-
 src/app/app.module.ts                         | 14 +++++-
 .../components/header/header.component.html   |  6 +--
 .../header/header.component.spec.ts           | 31 ++++++++++---
 src/app/components/header/header.component.ts | 35 ++++++---------
 src/app/components/page.component.ts          |  2 +-
 src/app/store/actions/app-name.action.ts      |  8 ++++
 src/app/store/actions/header-color.action.ts  |  8 ++++
 src/app/store/actions/logo-path.action.ts     |  8 ++++
 src/app/store/reducers/app.reducer.ts         | 44 +++++++++++++++++++
 src/app/store/selectors/app.selectors.ts      |  7 +++
 src/app/store/states/app.state.ts             | 19 ++++++++
 15 files changed, 198 insertions(+), 35 deletions(-)
 create mode 100644 src/app/store/actions/app-name.action.ts
 create mode 100644 src/app/store/actions/header-color.action.ts
 create mode 100644 src/app/store/actions/logo-path.action.ts
 create mode 100644 src/app/store/reducers/app.reducer.ts
 create mode 100644 src/app/store/selectors/app.selectors.ts
 create mode 100644 src/app/store/states/app.state.ts

diff --git a/cspell.json b/cspell.json
index 9e9b86be0..9789a3c84 100644
--- a/cspell.json
+++ b/cspell.json
@@ -4,6 +4,7 @@
     "words": [
         "succes",
 
+        "ngrx",
         "ngstack",
         "sidenav",
         "injectable",
diff --git a/package-lock.json b/package-lock.json
index 4d46e20e3..944545eca 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -445,6 +445,26 @@
       "resolved": "https://registry.npmjs.org/@mat-datetimepicker/moment/-/moment-1.0.1.tgz",
       "integrity": "sha1-YYUwbd/QeTBlq9XbBjKpQZgjdPQ="
     },
+    "@ngrx/effects": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-5.2.0.tgz",
+      "integrity": "sha1-qnYractv1GRNckoc7NJlyqQrrwk="
+    },
+    "@ngrx/router-store": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@ngrx/router-store/-/router-store-5.2.0.tgz",
+      "integrity": "sha1-v0sXTOGaNuuoIR/B3erx41rnQ2g="
+    },
+    "@ngrx/store": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-5.2.0.tgz",
+      "integrity": "sha1-Yn7XTJzZVGKTBIXZEqVXEXsjkD4="
+    },
+    "@ngrx/store-devtools": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-5.2.0.tgz",
+      "integrity": "sha1-L/+RapqjSTdYJncrNZ27ZLnl1iI="
+    },
     "@ngstack/electron": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/@ngstack/electron/-/electron-0.1.0.tgz",
diff --git a/package.json b/package.json
index bba3d1050..2799ebc8f 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,10 @@
     "@angular/router": "5.1.1",
     "@mat-datetimepicker/core": "1.0.1",
     "@mat-datetimepicker/moment": "1.0.1",
+    "@ngrx/effects": "^5.2.0",
+    "@ngrx/router-store": "^5.2.0",
+    "@ngrx/store": "^5.2.0",
+    "@ngrx/store-devtools": "^5.2.0",
     "@ngstack/electron": "0.1.0",
     "@ngx-translate/core": "9.1.1",
     "alfresco-js-api": "2.4.0-beta9",
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index e1f84dca9..f2846ea89 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -30,6 +30,11 @@ import {
     FileModel, UploadService
 } from '@alfresco/adf-core';
 import { ElectronService } from '@ngstack/electron';
+import { Store } from '@ngrx/store';
+import { AcaState } from './store/states/app.state';
+import { SetHeaderColorAction } from './store/actions/header-color.action';
+import { SetAppNameAction } from './store/actions/app-name.action';
+import { SetLogoPathAction } from './store/actions/logo-path.action';
 
 @Component({
     selector: 'app-root',
@@ -42,7 +47,8 @@ export class AppComponent implements OnInit {
         private router: Router,
         private pageTitle: PageTitleService,
         preferences: UserPreferencesService,
-        config: AppConfigService,
+        private store: Store<AcaState>,
+        private config: AppConfigService,
         private electronService: ElectronService,
         private uploadService: UploadService) {
         // TODO: remove once ADF 2.3.0 is out (needs bug fixes)
@@ -51,6 +57,9 @@ export class AppComponent implements OnInit {
     }
 
     ngOnInit() {
+
+        this.loadAppSettings();
+
         const { router, pageTitle, route } = this;
 
         router
@@ -89,4 +98,19 @@ export class AppComponent implements OnInit {
             }
         });
     }
+
+    private loadAppSettings() {
+        const headerColor = this.config.get<string>('headerColor');
+        if (headerColor) {
+            this.store.dispatch(new SetHeaderColorAction(headerColor));
+        }
+        const appName = this.config.get<string>('application.name');
+        if (appName) {
+            this.store.dispatch(new SetAppNameAction(appName));
+        }
+        const logoPath = this.config.get<string>('application.logo');
+        if (logoPath) {
+            this.store.dispatch(new SetLogoPathAction(logoPath));
+        }
+    }
 }
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index cef4d50fc..8776393e8 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -32,6 +32,10 @@ import { TRANSLATION_PROVIDER, CoreModule, AppConfigService } from '@alfresco/ad
 import { ContentModule } from '@alfresco/adf-content-services';
 import { ElectronModule } from '@ngstack/electron';
 
+import { StoreModule } from '@ngrx/store';
+import { EffectsModule } from '@ngrx/effects';
+import { StoreRouterConnectingModule } from '@ngrx/router-store';
+
 import { AppComponent } from './app.component';
 import { APP_ROUTES } from './app.routes';
 
@@ -71,6 +75,10 @@ import { SettingsComponent } from './components/settings/settings.component';
 import { HybridAppConfigService } from './common/services/hybrid-app-config.service';
 import { SortingPreferenceKeyDirective } from './directives/sorting-preference-key.directive';
 
+import { INITIAL_STATE } from './store/states/app.state';
+import { appReducer } from './store/reducers/app.reducer';
+
+
 @NgModule({
     imports: [
         BrowserModule,
@@ -88,7 +96,11 @@ import { SortingPreferenceKeyDirective } from './directives/sorting-preference-k
         MatInputModule,
         CoreModule,
         ContentModule,
-        ElectronModule
+        ElectronModule,
+
+        StoreModule.forRoot({ app: appReducer }, { initialState: INITIAL_STATE }),
+        StoreRouterConnectingModule.forRoot({ stateKey: 'router' }),
+        EffectsModule.forRoot([])
     ],
     declarations: [
         AppComponent,
diff --git a/src/app/components/header/header.component.html b/src/app/components/header/header.component.html
index d17090b6c..c389a6072 100644
--- a/src/app/components/header/header.component.html
+++ b/src/app/components/header/header.component.html
@@ -1,4 +1,4 @@
-<adf-toolbar class="app-menu" [style.background-color]="backgroundColor" role="heading" aria-level="1">
+<adf-toolbar class="app-menu" [style.background-color]="headerColor$ | async" role="heading" aria-level="1">
     <adf-toolbar-title>
         <button
             title="{{ 'APP.ACTIONS.TOGGLE-SIDENAV' | translate }}"
@@ -8,9 +8,9 @@
         </button>
 
         <a class="app-menu__title"
-            title="{{ appName }}"
+            title="{{ appName$ | async }}"
             [routerLink]="[ '/' ]">
-            <img [src]="logo" alt="{{ appName }}" />
+            <img [src]="logo$ | async" alt="{{ appName$ | async }}" />
         </a>
     </adf-toolbar-title>
 
diff --git a/src/app/components/header/header.component.spec.ts b/src/app/components/header/header.component.spec.ts
index 468d3f63d..139ecda59 100644
--- a/src/app/components/header/header.component.spec.ts
+++ b/src/app/components/header/header.component.spec.ts
@@ -24,7 +24,7 @@
  */
 
 import { NO_ERRORS_SCHEMA } from '@angular/core';
-import { TestBed } from '@angular/core/testing';
+import { TestBed, ComponentFixture } from '@angular/core/testing';
 import { RouterTestingModule } from '@angular/router/testing';
 import { AppConfigService, PeopleContentService, TranslationService, TranslationMock } from '@alfresco/adf-core';
 import { HttpClientModule } from '@angular/common/http';
@@ -32,11 +32,17 @@ import { Observable } from 'rxjs/Rx';
 
 import { HeaderComponent } from './header.component';
 import { TranslateModule } from '@ngx-translate/core';
+import { StoreModule, Store } from '@ngrx/store';
+import { appReducer } from '../../store/reducers/app.reducer';
+import { INITIAL_STATE, AcaState } from '../../store/states/app.state';
+import { SetAppNameAction } from '../../store/actions/app-name.action';
+import { SetHeaderColorAction } from '../../store/actions/header-color.action';
 
 describe('HeaderComponent', () => {
-    let fixture;
-    let component;
+    let fixture: ComponentFixture<HeaderComponent>;
+    let component: HeaderComponent;
     let appConfigService: AppConfigService;
+    let store: Store<AcaState>;
 
     beforeEach(() => {
         TestBed.configureTestingModule({
@@ -44,6 +50,7 @@ describe('HeaderComponent', () => {
                 HttpClientModule,
                 RouterTestingModule,
                 TranslateModule.forRoot(),
+                StoreModule.forRoot({ app: appReducer }, { initialState: INITIAL_STATE }),
             ],
             declarations: [
                 HeaderComponent
@@ -61,6 +68,10 @@ describe('HeaderComponent', () => {
             }
         });
 
+        store = TestBed.get(Store);
+        store.dispatch(new SetAppNameAction('app-name'));
+        store.dispatch(new SetHeaderColorAction('some-color'));
+
         fixture = TestBed.createComponent(HeaderComponent);
         component = fixture.componentInstance;
         appConfigService = TestBed.get(AppConfigService);
@@ -82,11 +93,17 @@ describe('HeaderComponent', () => {
         fixture.detectChanges();
     });
 
-    it('it should set application name', () => {
-        expect(component.appName).toBe('app-name');
+    it('it should set application name', done => {
+        component.appName$.subscribe(val => {
+            expect(val).toBe('app-name');
+            done();
+        });
     });
 
-    it('it should set header background color', () => {
-        expect(component.backgroundColor).toBe('some-color');
+    it('it should set header background color', done => {
+        component.headerColor$.subscribe(val => {
+            expect(val).toBe('some-color');
+            done();
+        });
     });
 });
diff --git a/src/app/components/header/header.component.ts b/src/app/components/header/header.component.ts
index 235610db8..7ef397765 100644
--- a/src/app/components/header/header.component.ts
+++ b/src/app/components/header/header.component.ts
@@ -23,9 +23,11 @@
  * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
  */
 
-import { DomSanitizer } from '@angular/platform-browser';
-import { Component, Output, EventEmitter, ViewEncapsulation, SecurityContext } from '@angular/core';
-import { AppConfigService } from '@alfresco/adf-core';
+import { Component, Output, EventEmitter, ViewEncapsulation } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { Observable } from 'rxjs/Rx';
+import { AcaState } from '../../store/states/app.state';
+import { selectHeaderColor, selectAppName, selectLogoPath } from '../../store/selectors/app.selectors';
 
 @Component({
     selector: 'app-header',
@@ -36,28 +38,17 @@ import { AppConfigService } from '@alfresco/adf-core';
 export class HeaderComponent {
     @Output() menu: EventEmitter<any> = new EventEmitter<any>();
 
-    private defaultPath = '/assets/images/alfresco-logo-white.svg';
-    private defaultBackgroundColor = '#2196F3';
+    appName$: Observable<string>;
+    headerColor$: Observable<string>;
+    logo$: Observable<string>;
 
-    constructor(
-        private appConfig: AppConfigService,
-        private sanitizer: DomSanitizer
-    ) {}
+    constructor(store: Store<AcaState>) {
+        this.headerColor$ = store.select(selectHeaderColor);
+        this.appName$ = store.select(selectAppName);
+        this.logo$ = store.select(selectLogoPath);
+    }
 
     toggleMenu() {
         this.menu.emit();
     }
-
-    get appName(): string {
-        return <string>this.appConfig.get('application.name');
-    }
-
-    get logo() {
-        return this.appConfig.get('application.logo', this.defaultPath);
-    }
-
-    get backgroundColor() {
-        const color = this.appConfig.get('headerColor', this.defaultBackgroundColor);
-        return this.sanitizer.sanitize(SecurityContext.STYLE, color);
-    }
 }
diff --git a/src/app/components/page.component.ts b/src/app/components/page.component.ts
index 44dd89583..edcd1b126 100644
--- a/src/app/components/page.component.ts
+++ b/src/app/components/page.component.ts
@@ -127,7 +127,7 @@ export abstract class PageComponent implements OnDestroy {
         const entry: MinimalNodeEntryEntity = row.node.entry;
 
         if (PageComponent.isLockedNode(entry)) {
-            return '/assets/images/ic_lock_black_24dp_1x.png';
+            return 'assets/images/ic_lock_black_24dp_1x.png';
         }
         return null;
     }
diff --git a/src/app/store/actions/app-name.action.ts b/src/app/store/actions/app-name.action.ts
new file mode 100644
index 000000000..2cc00a1df
--- /dev/null
+++ b/src/app/store/actions/app-name.action.ts
@@ -0,0 +1,8 @@
+import { Action } from '@ngrx/store';
+
+export const SET_APP_NAME = 'SET_APP_NAME';
+
+export class SetAppNameAction implements Action {
+    readonly type = SET_APP_NAME;
+    constructor(public payload: string) {}
+}
diff --git a/src/app/store/actions/header-color.action.ts b/src/app/store/actions/header-color.action.ts
new file mode 100644
index 000000000..b265d19de
--- /dev/null
+++ b/src/app/store/actions/header-color.action.ts
@@ -0,0 +1,8 @@
+import { Action } from '@ngrx/store';
+
+export const SET_HEADER_COLOR = 'SET_HEADER_COLOR';
+
+export class SetHeaderColorAction implements Action {
+    readonly type = SET_HEADER_COLOR;
+    constructor(public payload: string) {}
+}
diff --git a/src/app/store/actions/logo-path.action.ts b/src/app/store/actions/logo-path.action.ts
new file mode 100644
index 000000000..f01321cba
--- /dev/null
+++ b/src/app/store/actions/logo-path.action.ts
@@ -0,0 +1,8 @@
+import { Action } from '@ngrx/store';
+
+export const SET_LOGO_PATH = 'SET_LOGO_PATH';
+
+export class SetLogoPathAction implements Action {
+    readonly type = SET_LOGO_PATH;
+    constructor(public payload: string) {}
+}
diff --git a/src/app/store/reducers/app.reducer.ts b/src/app/store/reducers/app.reducer.ts
new file mode 100644
index 000000000..48fa50df3
--- /dev/null
+++ b/src/app/store/reducers/app.reducer.ts
@@ -0,0 +1,44 @@
+import { Action } from '@ngrx/store';
+import { AppState, INITIAL_APP_STATE } from '../states/app.state';
+import { SET_HEADER_COLOR, SetHeaderColorAction } from '../actions/header-color.action';
+import { SET_APP_NAME, SetAppNameAction } from '../actions/app-name.action';
+import { SET_LOGO_PATH, SetLogoPathAction } from '../actions/logo-path.action';
+
+
+export function appReducer(state: AppState = INITIAL_APP_STATE, action: Action): AppState {
+    let newState: AppState;
+
+    switch (action.type) {
+        case SET_APP_NAME:
+            newState = updateAppName(state, <SetAppNameAction> action);
+            break;
+        case SET_HEADER_COLOR:
+            newState = updateHeaderColor(state, <SetHeaderColorAction> action);
+            break;
+        case SET_LOGO_PATH:
+            newState = updateLogoPath(state, <SetLogoPathAction> action);
+            break;
+        default:
+            newState = Object.assign({}, state);
+    }
+
+    return newState;
+}
+
+function updateHeaderColor(state: AppState, action: SetHeaderColorAction): AppState {
+    const newState = Object.assign({}, state);
+    newState.headerColor = action.payload;
+    return newState;
+}
+
+function updateAppName(state: AppState, action: SetAppNameAction): AppState {
+    const newState = Object.assign({}, state);
+    newState.appName = action.payload;
+    return newState;
+}
+
+function updateLogoPath(state: AppState, action: SetLogoPathAction): AppState {
+    const newState = Object.assign({}, state);
+    newState.logoPath = action.payload;
+    return newState;
+}
diff --git a/src/app/store/selectors/app.selectors.ts b/src/app/store/selectors/app.selectors.ts
new file mode 100644
index 000000000..52e6a81f3
--- /dev/null
+++ b/src/app/store/selectors/app.selectors.ts
@@ -0,0 +1,7 @@
+import { createSelector } from '@ngrx/store';
+import { AcaState, AppState } from '../states/app.state';
+
+export const selectApp = (state: AcaState) => state.app;
+export const selectHeaderColor = createSelector(selectApp, (state: AppState) => state.headerColor);
+export const selectAppName = createSelector(selectApp, (state: AppState) => state.appName);
+export const selectLogoPath = createSelector(selectApp, (state: AppState) => state.logoPath);
diff --git a/src/app/store/states/app.state.ts b/src/app/store/states/app.state.ts
new file mode 100644
index 000000000..75fcd165d
--- /dev/null
+++ b/src/app/store/states/app.state.ts
@@ -0,0 +1,19 @@
+export interface AppState {
+    appName: string;
+    headerColor: string;
+    logoPath: string;
+}
+
+export const INITIAL_APP_STATE: AppState = {
+    appName: 'Alfresco Example Content Application',
+    headerColor: '#2196F3',
+    logoPath: 'assets/images/alfresco-logo-white.svg'
+};
+
+export interface AcaState {
+    app: AppState;
+}
+
+export const INITIAL_STATE: AcaState = {
+    app: INITIAL_APP_STATE
+};