[ACA-1430] Initial NgRx setup (#384)

* initial ngrx integration, migrate header

* update header tests

* update spellcheck rules

* fix locked image path for virtual paths
This commit is contained in:
Denys Vuika 2018-06-04 08:57:50 +01:00 committed by Cilibiu Bogdan
parent c9cd7ae5c4
commit a67dd43ad6
15 changed files with 198 additions and 35 deletions

View File

@ -4,6 +4,7 @@
"words": [ "words": [
"succes", "succes",
"ngrx",
"ngstack", "ngstack",
"sidenav", "sidenav",
"injectable", "injectable",

20
package-lock.json generated
View File

@ -445,6 +445,26 @@
"resolved": "https://registry.npmjs.org/@mat-datetimepicker/moment/-/moment-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@mat-datetimepicker/moment/-/moment-1.0.1.tgz",
"integrity": "sha1-YYUwbd/QeTBlq9XbBjKpQZgjdPQ=" "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": { "@ngstack/electron": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/@ngstack/electron/-/electron-0.1.0.tgz", "resolved": "https://registry.npmjs.org/@ngstack/electron/-/electron-0.1.0.tgz",

View File

@ -42,6 +42,10 @@
"@angular/router": "5.1.1", "@angular/router": "5.1.1",
"@mat-datetimepicker/core": "1.0.1", "@mat-datetimepicker/core": "1.0.1",
"@mat-datetimepicker/moment": "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", "@ngstack/electron": "0.1.0",
"@ngx-translate/core": "9.1.1", "@ngx-translate/core": "9.1.1",
"alfresco-js-api": "2.4.0-beta9", "alfresco-js-api": "2.4.0-beta9",

View File

@ -30,6 +30,11 @@ import {
FileModel, UploadService FileModel, UploadService
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { ElectronService } from '@ngstack/electron'; 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({ @Component({
selector: 'app-root', selector: 'app-root',
@ -42,7 +47,8 @@ export class AppComponent implements OnInit {
private router: Router, private router: Router,
private pageTitle: PageTitleService, private pageTitle: PageTitleService,
preferences: UserPreferencesService, preferences: UserPreferencesService,
config: AppConfigService, private store: Store<AcaState>,
private config: AppConfigService,
private electronService: ElectronService, private electronService: ElectronService,
private uploadService: UploadService) { private uploadService: UploadService) {
// TODO: remove once ADF 2.3.0 is out (needs bug fixes) // TODO: remove once ADF 2.3.0 is out (needs bug fixes)
@ -51,6 +57,9 @@ export class AppComponent implements OnInit {
} }
ngOnInit() { ngOnInit() {
this.loadAppSettings();
const { router, pageTitle, route } = this; const { router, pageTitle, route } = this;
router 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));
}
}
} }

View File

@ -32,6 +32,10 @@ import { TRANSLATION_PROVIDER, CoreModule, AppConfigService } from '@alfresco/ad
import { ContentModule } from '@alfresco/adf-content-services'; import { ContentModule } from '@alfresco/adf-content-services';
import { ElectronModule } from '@ngstack/electron'; 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 { AppComponent } from './app.component';
import { APP_ROUTES } from './app.routes'; 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 { HybridAppConfigService } from './common/services/hybrid-app-config.service';
import { SortingPreferenceKeyDirective } from './directives/sorting-preference-key.directive'; import { SortingPreferenceKeyDirective } from './directives/sorting-preference-key.directive';
import { INITIAL_STATE } from './store/states/app.state';
import { appReducer } from './store/reducers/app.reducer';
@NgModule({ @NgModule({
imports: [ imports: [
BrowserModule, BrowserModule,
@ -88,7 +96,11 @@ import { SortingPreferenceKeyDirective } from './directives/sorting-preference-k
MatInputModule, MatInputModule,
CoreModule, CoreModule,
ContentModule, ContentModule,
ElectronModule ElectronModule,
StoreModule.forRoot({ app: appReducer }, { initialState: INITIAL_STATE }),
StoreRouterConnectingModule.forRoot({ stateKey: 'router' }),
EffectsModule.forRoot([])
], ],
declarations: [ declarations: [
AppComponent, AppComponent,

View File

@ -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> <adf-toolbar-title>
<button <button
title="{{ 'APP.ACTIONS.TOGGLE-SIDENAV' | translate }}" title="{{ 'APP.ACTIONS.TOGGLE-SIDENAV' | translate }}"
@ -8,9 +8,9 @@
</button> </button>
<a class="app-menu__title" <a class="app-menu__title"
title="{{ appName }}" title="{{ appName$ | async }}"
[routerLink]="[ '/' ]"> [routerLink]="[ '/' ]">
<img [src]="logo" alt="{{ appName }}" /> <img [src]="logo$ | async" alt="{{ appName$ | async }}" />
</a> </a>
</adf-toolbar-title> </adf-toolbar-title>

View File

@ -24,7 +24,7 @@
*/ */
import { NO_ERRORS_SCHEMA } from '@angular/core'; 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 { RouterTestingModule } from '@angular/router/testing';
import { AppConfigService, PeopleContentService, TranslationService, TranslationMock } from '@alfresco/adf-core'; import { AppConfigService, PeopleContentService, TranslationService, TranslationMock } from '@alfresco/adf-core';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
@ -32,11 +32,17 @@ import { Observable } from 'rxjs/Rx';
import { HeaderComponent } from './header.component'; import { HeaderComponent } from './header.component';
import { TranslateModule } from '@ngx-translate/core'; 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', () => { describe('HeaderComponent', () => {
let fixture; let fixture: ComponentFixture<HeaderComponent>;
let component; let component: HeaderComponent;
let appConfigService: AppConfigService; let appConfigService: AppConfigService;
let store: Store<AcaState>;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -44,6 +50,7 @@ describe('HeaderComponent', () => {
HttpClientModule, HttpClientModule,
RouterTestingModule, RouterTestingModule,
TranslateModule.forRoot(), TranslateModule.forRoot(),
StoreModule.forRoot({ app: appReducer }, { initialState: INITIAL_STATE }),
], ],
declarations: [ declarations: [
HeaderComponent 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); fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
appConfigService = TestBed.get(AppConfigService); appConfigService = TestBed.get(AppConfigService);
@ -82,11 +93,17 @@ describe('HeaderComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('it should set application name', () => { it('it should set application name', done => {
expect(component.appName).toBe('app-name'); component.appName$.subscribe(val => {
expect(val).toBe('app-name');
done();
});
}); });
it('it should set header background color', () => { it('it should set header background color', done => {
expect(component.backgroundColor).toBe('some-color'); component.headerColor$.subscribe(val => {
expect(val).toBe('some-color');
done();
});
}); });
}); });

View File

@ -23,9 +23,11 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { DomSanitizer } from '@angular/platform-browser'; import { Component, Output, EventEmitter, ViewEncapsulation } from '@angular/core';
import { Component, Output, EventEmitter, ViewEncapsulation, SecurityContext } from '@angular/core'; import { Store } from '@ngrx/store';
import { AppConfigService } from '@alfresco/adf-core'; import { Observable } from 'rxjs/Rx';
import { AcaState } from '../../store/states/app.state';
import { selectHeaderColor, selectAppName, selectLogoPath } from '../../store/selectors/app.selectors';
@Component({ @Component({
selector: 'app-header', selector: 'app-header',
@ -36,28 +38,17 @@ import { AppConfigService } from '@alfresco/adf-core';
export class HeaderComponent { export class HeaderComponent {
@Output() menu: EventEmitter<any> = new EventEmitter<any>(); @Output() menu: EventEmitter<any> = new EventEmitter<any>();
private defaultPath = '/assets/images/alfresco-logo-white.svg'; appName$: Observable<string>;
private defaultBackgroundColor = '#2196F3'; headerColor$: Observable<string>;
logo$: Observable<string>;
constructor( constructor(store: Store<AcaState>) {
private appConfig: AppConfigService, this.headerColor$ = store.select(selectHeaderColor);
private sanitizer: DomSanitizer this.appName$ = store.select(selectAppName);
) {} this.logo$ = store.select(selectLogoPath);
}
toggleMenu() { toggleMenu() {
this.menu.emit(); 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);
}
} }

View File

@ -127,7 +127,7 @@ export abstract class PageComponent implements OnDestroy {
const entry: MinimalNodeEntryEntity = row.node.entry; const entry: MinimalNodeEntryEntity = row.node.entry;
if (PageComponent.isLockedNode(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; return null;
} }

View File

@ -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) {}
}

View File

@ -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) {}
}

View File

@ -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) {}
}

View File

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

View File

@ -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);

View File

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