From 79a20c65fb88d9bf646fbe231bc1fc2d5f8adbee Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Wed, 18 Jul 2018 05:16:10 +0100 Subject: [PATCH] split extension actions into separate service (#512) * move action management to a separate ActionService * code improvements and registration chaining * code fixes --- src/app/app.module.ts | 8 +- .../action-ref.ts} | 2 +- .../extensions/actions/action.service.spec.ts | 141 ++++++++++++++++++ src/app/extensions/actions/action.service.ts | 76 ++++++++++ ...xtensions.ts => core.extensions.module.ts} | 17 ++- src/app/extensions/extension.config.ts | 8 +- src/app/extensions/extension.service.spec.ts | 96 ------------ src/app/extensions/extension.service.ts | 67 ++++----- .../{route.extension.ts => route-ref.ts} | 2 +- src/app/testing/app-testing.module.ts | 4 +- 10 files changed, 262 insertions(+), 159 deletions(-) rename src/app/extensions/{action.extension.ts => actions/action-ref.ts} (97%) create mode 100644 src/app/extensions/actions/action.service.spec.ts create mode 100644 src/app/extensions/actions/action.service.ts rename src/app/extensions/{core.extensions.ts => core.extensions.module.ts} (79%) rename src/app/extensions/{route.extension.ts => route-ref.ts} (97%) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 88140f203..79285a631 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -78,13 +78,11 @@ import { MaterialModule } from './material.module'; import { ExperimentalDirective } from './directives/experimental.directive'; import { ContentApiService } from './services/content-api.service'; import { ExtensionsModule } from './extensions.module'; -import { ExtensionService } from './extensions/extension.service'; -import { CoreExtensionsModule } from './extensions/core.extensions'; +import { CoreExtensionsModule } from './extensions/core.extensions.module'; import { SearchResultsRowComponent } from './components/search/search-results-row/search-results-row.component'; import { NodePermissionsDialogComponent } from './dialogs/node-permissions/node-permissions.dialog'; import { NodePermissionsDirective } from './common/directives/node-permissions.directive'; import { PermissionsManagerComponent } from './components/permission-manager/permissions-manager.component'; -import { RuleService } from './extensions/rules/rule.service'; @NgModule({ imports: [ @@ -159,9 +157,7 @@ import { RuleService } from './extensions/rules/rule.service'; NodePermissionService, ProfileResolver, ExperimentalGuard, - ContentApiService, - ExtensionService, - RuleService + ContentApiService ], entryComponents: [ LibraryDialogComponent, diff --git a/src/app/extensions/action.extension.ts b/src/app/extensions/actions/action-ref.ts similarity index 97% rename from src/app/extensions/action.extension.ts rename to src/app/extensions/actions/action-ref.ts index b7fac6928..f77d166d5 100644 --- a/src/app/extensions/action.extension.ts +++ b/src/app/extensions/actions/action-ref.ts @@ -23,7 +23,7 @@ * along with Alfresco. If not, see . */ -export interface ActionExtension { +export interface ActionRef { id: string; type: string; payload?: string; diff --git a/src/app/extensions/actions/action.service.spec.ts b/src/app/extensions/actions/action.service.spec.ts new file mode 100644 index 000000000..75658758c --- /dev/null +++ b/src/app/extensions/actions/action.service.spec.ts @@ -0,0 +1,141 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +import { AppConfigService } from '@alfresco/adf-core'; +import { ActionService } from './action.service'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../store/states'; +import { TestBed } from '@angular/core/testing'; +import { AppTestingModule } from '../../testing/app-testing.module'; + +describe('ActionService', () => { + let config: AppConfigService; + let actions: ActionService; + let store: Store; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [AppTestingModule] + }); + + actions = TestBed.get(ActionService); + store = TestBed.get(Store); + + config = TestBed.get(AppConfigService); + config.config['extensions'] = {}; + }); + + describe('actions', () => { + beforeEach(() => { + config.config.extensions = { + core: { + actions: [ + { + id: 'aca:actions/create-folder', + type: 'CREATE_FOLDER', + payload: 'folder-name' + } + ] + } + }; + }); + + it('should load actions from the config', () => { + actions.init(); + expect(actions.actions.length).toBe(1); + }); + + it('should have an empty action list if config provides nothing', () => { + config.config.extensions = {}; + actions.init(); + + expect(actions.actions).toEqual([]); + }); + + it('should find action by id', () => { + actions.init(); + + const action = actions.getActionById( + 'aca:actions/create-folder' + ); + expect(action).toBeTruthy(); + expect(action.type).toBe('CREATE_FOLDER'); + expect(action.payload).toBe('folder-name'); + }); + + it('should not find action by id', () => { + actions.init(); + + const action = actions.getActionById('missing'); + expect(action).toBeFalsy(); + }); + + it('should run the action via store', () => { + actions.init(); + spyOn(store, 'dispatch').and.stub(); + + actions.runActionById('aca:actions/create-folder'); + expect(store.dispatch).toHaveBeenCalledWith({ + type: 'CREATE_FOLDER', + payload: 'folder-name' + }); + }); + + it('should not use store if action is missing', () => { + actions.init(); + spyOn(store, 'dispatch').and.stub(); + + actions.runActionById('missing'); + expect(store.dispatch).not.toHaveBeenCalled(); + }); + }); + + describe('expressions', () => { + it('should eval static value', () => { + const value = actions.runExpression('hello world'); + expect(value).toBe('hello world'); + }); + + it('should eval string as an expression', () => { + const value = actions.runExpression('$( "hello world" )'); + expect(value).toBe('hello world'); + }); + + it('should eval expression with no context', () => { + const value = actions.runExpression('$( 1 + 1 )'); + expect(value).toBe(2); + }); + + it('should eval expression with context', () => { + const context = { + a: 'hey', + b: 'there' + }; + const expression = '$( context.a + " " + context.b + "!" )'; + const value = actions.runExpression(expression, context); + expect(value).toBe('hey there!'); + }); + }); +}); diff --git a/src/app/extensions/actions/action.service.ts b/src/app/extensions/actions/action.service.ts new file mode 100644 index 000000000..c0245049f --- /dev/null +++ b/src/app/extensions/actions/action.service.ts @@ -0,0 +1,76 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +import { Injectable } from '@angular/core'; +import { AppConfigService } from '@alfresco/adf-core'; +import { Store } from '@ngrx/store'; +import { AppStore } from '../../store/states'; +import { ActionRef } from './action-ref'; + +@Injectable() +export class ActionService { + actions: Array = []; + + constructor( + private config: AppConfigService, + private store: Store + ) {} + + init() { + this.actions = this.config.get>( + 'extensions.core.actions', + [] + ); + } + + getActionById(id: string): ActionRef { + return this.actions.find(action => action.id === id); + } + + runActionById(id: string, context?: any) { + const action = this.getActionById(id); + if (action) { + const { type, payload } = action; + const expression = this.runExpression(payload, context); + + this.store.dispatch({ type, payload: expression }); + } + } + + runExpression(value: string, context?: any) { + const pattern = new RegExp(/\$\((.*\)?)\)/g); + const matches = pattern.exec(value); + + if (matches && matches.length > 1) { + const expression = matches[1]; + const fn = new Function('context', `return ${expression}`); + const result = fn(context); + + return result; + } + + return value; + } +} diff --git a/src/app/extensions/core.extensions.ts b/src/app/extensions/core.extensions.module.ts similarity index 79% rename from src/app/extensions/core.extensions.ts rename to src/app/extensions/core.extensions.module.ts index e58bfc6cb..488175068 100644 --- a/src/app/extensions/core.extensions.ts +++ b/src/app/extensions/core.extensions.module.ts @@ -30,20 +30,21 @@ import { AboutComponent } from '../components/about/about.component'; import { LayoutComponent } from '../components/layout/layout.component'; import { ToolbarActionComponent } from './components/toolbar-action/toolbar-action.component'; import { CommonModule } from '@angular/common'; +import { RuleService } from './rules/rule.service'; +import { ActionService } from './actions/action.service'; @NgModule({ - imports: [ - CommonModule, - CoreModule.forChild() - ], + imports: [CommonModule, CoreModule.forChild()], declarations: [ToolbarActionComponent], exports: [ToolbarActionComponent], - entryComponents: [AboutComponent] + entryComponents: [AboutComponent], + providers: [ExtensionService, RuleService, ActionService] }) export class CoreExtensionsModule { constructor(extensions: ExtensionService) { - extensions.components['aca:layouts/main'] = LayoutComponent; - extensions.components['aca:components/about'] = AboutComponent; - extensions.authGuards['aca:auth'] = AuthGuardEcm; + extensions + .setComponent('aca:layouts/main', LayoutComponent) + .setComponent('aca:components/about', AboutComponent) + .setAuthGuard('aca:auth', AuthGuardEcm); } } diff --git a/src/app/extensions/extension.config.ts b/src/app/extensions/extension.config.ts index 0a5e9af20..d239c47af 100644 --- a/src/app/extensions/extension.config.ts +++ b/src/app/extensions/extension.config.ts @@ -23,13 +23,13 @@ * along with Alfresco. If not, see . */ -import { RouteExtension } from './route.extension'; -import { ActionExtension } from './action.extension'; import { RuleRef } from './rules/rule-ref'; +import { ActionRef } from './actions/action-ref'; +import { RouteRef } from './route-ref'; export interface ExtensionConfig { rules?: Array; - routes?: Array; - actions?: Array; + routes?: Array; + actions?: Array; features?: { [key: string]: any }; } diff --git a/src/app/extensions/extension.service.spec.ts b/src/app/extensions/extension.service.spec.ts index fa1e27278..ac0af9e5c 100644 --- a/src/app/extensions/extension.service.spec.ts +++ b/src/app/extensions/extension.service.spec.ts @@ -27,14 +27,11 @@ import { TestBed } from '@angular/core/testing'; import { AppTestingModule } from '../testing/app-testing.module'; import { ExtensionService } from './extension.service'; import { AppConfigService } from '@alfresco/adf-core'; -import { Store } from '@ngrx/store'; -import { AppStore } from '../store/states'; import { ContentActionType } from './content-action.extension'; describe('ExtensionService', () => { let config: AppConfigService; let extensions: ExtensionService; - let store: Store; beforeEach(() => { TestBed.configureTestingModule({ @@ -42,7 +39,6 @@ describe('ExtensionService', () => { }); extensions = TestBed.get(ExtensionService); - store = TestBed.get(Store); config = TestBed.get(AppConfigService); config.config['extensions'] = {}; @@ -168,71 +164,6 @@ describe('ExtensionService', () => { }); }); - describe('actions', () => { - beforeEach(() => { - config.config.extensions = { - core: { - actions: [ - { - id: 'aca:actions/create-folder', - type: 'CREATE_FOLDER', - payload: 'folder-name' - } - ] - } - }; - }); - - it('should load actions from the config', () => { - extensions.init(); - expect(extensions.actions.length).toBe(1); - }); - - it('should have an empty action list if config provides nothing', () => { - config.config.extensions = {}; - extensions.init(); - - expect(extensions.actions).toEqual([]); - }); - - it('should find action by id', () => { - extensions.init(); - - const action = extensions.getActionById( - 'aca:actions/create-folder' - ); - expect(action).toBeTruthy(); - expect(action.type).toBe('CREATE_FOLDER'); - expect(action.payload).toBe('folder-name'); - }); - - it('should not find action by id', () => { - extensions.init(); - - const action = extensions.getActionById('missing'); - expect(action).toBeFalsy(); - }); - - it('should run the action via store', () => { - extensions.init(); - spyOn(store, 'dispatch').and.stub(); - - extensions.runActionById('aca:actions/create-folder'); - expect(store.dispatch).toHaveBeenCalledWith({ - type: 'CREATE_FOLDER', - payload: 'folder-name' - }); - }); - - it('should not use store if action is missing', () => { - extensions.init(); - spyOn(store, 'dispatch').and.stub(); - - extensions.runActionById('missing'); - expect(store.dispatch).not.toHaveBeenCalled(); - }); - }); - describe('content actions', () => { it('should load content actions from the config', () => { config.config.extensions = { @@ -464,33 +395,6 @@ describe('ExtensionService', () => { }); }); - describe('expressions', () => { - it('should eval static value', () => { - const value = extensions.runExpression('hello world'); - expect(value).toBe('hello world'); - }); - - it('should eval string as an expression', () => { - const value = extensions.runExpression('$( "hello world" )'); - expect(value).toBe('hello world'); - }); - - it('should eval expression with no context', () => { - const value = extensions.runExpression('$( 1 + 1 )'); - expect(value).toBe(2); - }); - - it('should eval expression with context', () => { - const context = { - a: 'hey', - b: 'there' - }; - const expression = '$( context.a + " " + context.b + "!" )'; - const value = extensions.runExpression(expression, context); - expect(value).toBe('hey there!'); - }); - }); - describe('sorting', () => { it('should sort by provided order', () => { const sorted = [ diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index 6991c81fa..3d514ee15 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -24,52 +24,45 @@ */ import { Injectable, Type } from '@angular/core'; -import { RouteExtension } from './route.extension'; -import { ActionExtension } from './action.extension'; import { AppConfigService } from '@alfresco/adf-core'; import { ContentActionExtension, ContentActionType } from './content-action.extension'; import { OpenWithExtension } from './open-with.extension'; -import { AppStore } from '../store/states'; -import { Store } from '@ngrx/store'; import { NavigationExtension } from './navigation.extension'; import { Route } from '@angular/router'; import { Node } from 'alfresco-js-api'; import { RuleService } from './rules/rule.service'; +import { ActionService } from './actions/action.service'; +import { ActionRef } from './actions/action-ref'; +import { RouteRef } from './route-ref'; @Injectable() export class ExtensionService { - routes: Array = []; - actions: Array = []; contentActions: Array = []; openWithActions: Array = []; createActions: Array = []; + routes: Array = []; authGuards: { [key: string]: Type<{}> } = {}; components: { [key: string]: Type<{}> } = {}; constructor( private config: AppConfigService, - private store: Store, - private ruleService: RuleService + private ruleService: RuleService, + private actionService: ActionService ) {} // initialise extension service // in future will also load and merge data from the external plugins init() { - this.routes = this.config.get>( + this.routes = this.config.get>( 'extensions.core.routes', [] ); - this.actions = this.config.get>( - 'extensions.core.actions', - [] - ); - this.contentActions = this.config .get>( 'extensions.core.features.content.actions', @@ -93,24 +86,30 @@ export class ExtensionService { .sort(this.sortByOrder); this.ruleService.init(); + this.actionService.init(); } - getRouteById(id: string): RouteExtension { + setAuthGuard(key: string, value: Type<{}>): ExtensionService { + this.authGuards[key] = value; + return this; + } + + getRouteById(id: string): RouteRef { return this.routes.find(route => route.id === id); } - getActionById(id: string): ActionExtension { - return this.actions.find(action => action.id === id); + getActionById(id: string): ActionRef { + return this.actionService.getActionById(id); } runActionById(id: string, context?: any) { - const action = this.getActionById(id); - if (action) { - const { type, payload } = action; - const expression = this.runExpression(payload, context); + this.actionService.runActionById(id, context); + } - this.store.dispatch({ type, payload: expression }); - } + getAuthGuards(ids: string[]): Array> { + return (ids || []) + .map(id => this.authGuards[id]) + .filter(guard => guard); } getNavigationGroups(): Array { @@ -140,31 +139,15 @@ export class ExtensionService { return []; } - getAuthGuards(ids: string[]): Array> { - return (ids || []) - .map(id => this.authGuards[id]) - .filter(guard => guard); + setComponent(id: string, value: Type<{}>): ExtensionService { + this.components[id] = value; + return this; } getComponentById(id: string): Type<{}> { return this.components[id]; } - runExpression(value: string, context?: any) { - const pattern = new RegExp(/\$\((.*\)?)\)/g); - const matches = pattern.exec(value); - - if (matches && matches.length > 1) { - const expression = matches[1]; - const fn = new Function('context', `return ${expression}`); - const result = fn(context); - - return result; - } - - return value; - } - getApplicationRoutes(): Array { return this.routes.map(route => { const guards = this.getAuthGuards(route.auth); diff --git a/src/app/extensions/route.extension.ts b/src/app/extensions/route-ref.ts similarity index 97% rename from src/app/extensions/route.extension.ts rename to src/app/extensions/route-ref.ts index 8f2cb9959..2a0d5992c 100644 --- a/src/app/extensions/route.extension.ts +++ b/src/app/extensions/route-ref.ts @@ -23,7 +23,7 @@ * along with Alfresco. If not, see . */ -export interface RouteExtension { +export interface RouteRef { id: string; path: string; component: string; diff --git a/src/app/testing/app-testing.module.ts b/src/app/testing/app-testing.module.ts index 1376ba23b..43d40f3ad 100644 --- a/src/app/testing/app-testing.module.ts +++ b/src/app/testing/app-testing.module.ts @@ -61,6 +61,7 @@ import { NodePermissionService } from '../common/services/node-permission.servic import { ContentApiService } from '../services/content-api.service'; import { ExtensionService } from '../extensions/extension.service'; import { RuleService } from '../extensions/rules/rule.service'; +import { ActionService } from '../extensions/actions/action.service'; @NgModule({ imports: [ @@ -114,7 +115,8 @@ import { RuleService } from '../extensions/rules/rule.service'; NodePermissionService, ContentApiService, ExtensionService, - RuleService + RuleService, + ActionService ] }) export class AppTestingModule {}