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 +};