diff --git a/.travis.yml b/.travis.yml index a3813e8375..ff7b309c07 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,6 +30,9 @@ jobs: - stage: Unit test env: STAGE=core script: (./scripts/npm-build-all.sh -si -sb -t "core" --skip-lint || exit 1;); + - stage: Unit test + env: STAGE=extensions + script: (./scripts/npm-build-all.sh -si -sb -t "extensions" --skip-lint || exit 1;); - stage: Unit test env: STAGE=process-services script: (./scripts/npm-build-all.sh -si -sb -t "process-services" --skip-lint|| exit 1;); diff --git a/angular.json b/angular.json index 528f305468..b3c14c86bb 100644 --- a/angular.json +++ b/angular.json @@ -660,6 +660,42 @@ } } } + }, + "extensions": { + "root": "lib/extensions", + "sourceRoot": "lib/extensions/src/lib", + "projectType": "library", + "prefix": "adf", + "architect": { + "build": { + "builder": "@angular-devkit/build-ng-packagr:build", + "options": { + "tsConfig": "lib/extensions/tsconfig.lib.json", + "project": "lib/extensions/ng-package.json" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "lib/extensions/src/test.ts", + "tsConfig": "lib/extensions/tsconfig.spec.json", + "karmaConfig": "lib/extensions/karma.conf.js", + "sourceMap": true + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "lib/extensions/tsconfig.lib.json", + "lib/extensions/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } } }, "defaultProject": "dist", diff --git a/lib/extensions/karma.conf.js b/lib/extensions/karma.conf.js new file mode 100644 index 0000000000..f43d65fffd --- /dev/null +++ b/lib/extensions/karma.conf.js @@ -0,0 +1,42 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../../coverage'), + reports: ['html', 'lcovonly'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: [/*'Chrome',*/ 'ChromeHeadless'], + customLaunchers: { + ChromeHeadless: { + base: 'Chrome', + flags: [ + '--no-sandbox', + '--headless', + '--disable-gpu', + '--remote-debugging-port=9222' + ] + } + }, + singleRun: false + }); +}; diff --git a/lib/extensions/ng-package.json b/lib/extensions/ng-package.json new file mode 100644 index 0000000000..decf1da480 --- /dev/null +++ b/lib/extensions/ng-package.json @@ -0,0 +1,13 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../dist/extensions/", + "deleteDestPath": false, + "lib": { + "languageLevel": ["dom", "es2017"], + "entryFile": "src/public_api.ts", + "flatModuleFile": "adf-extensions", + "umdModuleIds": { + "alfresco-js-api": "alfresco-js-api" + } + } +} diff --git a/lib/extensions/package.json b/lib/extensions/package.json new file mode 100644 index 0000000000..7010a53672 --- /dev/null +++ b/lib/extensions/package.json @@ -0,0 +1,10 @@ +{ + "name": "@alfresco/adf-extensions", + "version": "0.1.0", + "peerDependencies": { + "@angular/common": "^6.0.0", + "@angular/core": "^6.0.0", + "@angular/http": "^6.1.4", + "alfresco-js-api": "^2.5.0" + } +} diff --git a/lib/extensions/src/lib/components/dynamic-component/dynamic.component.ts b/lib/extensions/src/lib/components/dynamic-component/dynamic.component.ts new file mode 100644 index 0000000000..8314d30968 --- /dev/null +++ b/lib/extensions/src/lib/components/dynamic-component/dynamic.component.ts @@ -0,0 +1,67 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Component, + Input, + ComponentRef, + OnInit, + ComponentFactoryResolver, + ViewChild, + ViewContainerRef, + OnDestroy +} from '@angular/core'; +import { ExtensionService } from '../../services/extension.service'; + +@Component({ + selector: 'adf-dynamic-component', + template: `
` +}) +export class DynamicExtensionComponent implements OnInit, OnDestroy { + @ViewChild('content', { read: ViewContainerRef }) + content: ViewContainerRef; + + @Input() id: string; + + private componentRef: ComponentRef; + + constructor( + private extensions: ExtensionService, + private componentFactoryResolver: ComponentFactoryResolver + ) {} + + ngOnInit() { + const componentType = this.extensions.getComponentById(this.id); + if (componentType) { + const factory = this.componentFactoryResolver.resolveComponentFactory( + componentType + ); + if (factory) { + this.content.clear(); + this.componentRef = this.content.createComponent(factory, 0); + // this.setupWidget(this.componentRef); + } + } + } + + ngOnDestroy() { + if (this.componentRef) { + this.componentRef.destroy(); + this.componentRef = null; + } + } +} diff --git a/lib/extensions/src/lib/components/dynamic-tab/dynamic-tab.component.ts b/lib/extensions/src/lib/components/dynamic-tab/dynamic-tab.component.ts new file mode 100644 index 0000000000..bbd3e0c9f1 --- /dev/null +++ b/lib/extensions/src/lib/components/dynamic-tab/dynamic-tab.component.ts @@ -0,0 +1,94 @@ +/*! + * @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 { + Component, + Input, + OnInit, + OnDestroy, + ViewChild, + ViewContainerRef, + ComponentRef, + ComponentFactoryResolver, + OnChanges, + SimpleChanges +} from '@angular/core'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { ExtensionService } from '../../services/extension.service'; + +@Component({ + selector: 'adf-dynamic-tab', + template: `
` +}) +export class DynamicTabComponent implements OnInit, OnChanges, OnDestroy { + @ViewChild('content', { read: ViewContainerRef }) + content: ViewContainerRef; + + @Input() + id: string; + + @Input() + node: MinimalNodeEntryEntity; + + private componentRef: ComponentRef; + + constructor( + private extensions: ExtensionService, + private componentFactoryResolver: ComponentFactoryResolver + ) {} + + ngOnInit() { + const componentType = this.extensions.getComponentById(this.id); + if (componentType) { + const factory = this.componentFactoryResolver.resolveComponentFactory( + componentType + ); + if (factory) { + this.content.clear(); + this.componentRef = this.content.createComponent(factory, 0); + this.updateInstance(); + } + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.node) { + this.updateInstance(); + } + } + + ngOnDestroy() { + if (this.componentRef) { + this.componentRef.destroy(); + this.componentRef = null; + } + } + + private updateInstance() { + if (this.componentRef && this.componentRef.instance) { + this.componentRef.instance.node = this.node; + } + } +} diff --git a/lib/extensions/src/lib/config/action.extensions.ts b/lib/extensions/src/lib/config/action.extensions.ts new file mode 100644 index 0000000000..0906c1c240 --- /dev/null +++ b/lib/extensions/src/lib/config/action.extensions.ts @@ -0,0 +1,51 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ExtensionElement } from './extension-element'; + +export enum ContentActionType { + default = 'default', + button = 'button', + separator = 'separator', + menu = 'menu', + custom = 'custom' +} + +export interface ContentActionRef extends ExtensionElement { + type: ContentActionType; + + title?: string; + description?: string; + icon?: string; + children?: Array; + component?: string; + actions?: { + click?: string; + [key: string]: string; + }; + rules?: { + enabled?: string; + visible?: string; + [key: string]: string; + }; +} + +export interface ActionRef { + id: string; + type: string; + payload?: string; +} diff --git a/lib/extensions/src/lib/config/extension-element.ts b/lib/extensions/src/lib/config/extension-element.ts new file mode 100644 index 0000000000..be0c02a63e --- /dev/null +++ b/lib/extensions/src/lib/config/extension-element.ts @@ -0,0 +1,23 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface ExtensionElement { + id: string; + + order?: number; + disabled?: boolean; +} diff --git a/lib/extensions/src/lib/config/extension-utils.ts b/lib/extensions/src/lib/config/extension-utils.ts new file mode 100644 index 0000000000..511e61e7b1 --- /dev/null +++ b/lib/extensions/src/lib/config/extension-utils.ts @@ -0,0 +1,150 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ContentActionRef, ContentActionType } from './action.extensions'; + +export function getValue(target: any, key: string): any { + if (!target) { + return undefined; + } + + const keys = key.split('.'); + key = ''; + + do { + key += keys.shift(); + const value = target[key]; + if ( + value !== undefined && + (typeof value === 'object' || !keys.length) + ) { + target = value; + key = ''; + } else if (!keys.length) { + target = undefined; + } else { + key += '.'; + } + } while (keys.length); + + return target; +} + +export function filterEnabled(entry: { disabled?: boolean }): boolean { + return !entry.disabled; +} + +export function sortByOrder( + a: { order?: number | undefined }, + b: { order?: number | undefined } +) { + const left = a.order === undefined ? Number.MAX_SAFE_INTEGER : a.order; + const right = b.order === undefined ? Number.MAX_SAFE_INTEGER : b.order; + return left - right; +} + +export function reduceSeparators( + acc: ContentActionRef[], + el: ContentActionRef, + i: number, + arr: ContentActionRef[] +): ContentActionRef[] { + // remove leading separator + if (i === 0) { + if (arr[i].type === ContentActionType.separator) { + return acc; + } + } + // remove duplicate separators + if (i > 0) { + const prev = arr[i - 1]; + if ( + prev.type === ContentActionType.separator && + el.type === ContentActionType.separator + ) { + return acc; + } + + // remove trailing separator + if (i === arr.length - 1) { + if (el.type === ContentActionType.separator) { + return acc; + } + } + } + + return acc.concat(el); +} + +export function reduceEmptyMenus( + acc: ContentActionRef[], + el: ContentActionRef +): ContentActionRef[] { + if (el.type === ContentActionType.menu) { + if ((el.children || []).length === 0) { + return acc; + } + } + return acc.concat(el); +} + +export function mergeObjects(...objects): any { + const result = {}; + + objects.forEach(source => { + Object.keys(source).forEach(prop => { + if (!prop.startsWith('$')) { + if (prop in result && Array.isArray(result[prop])) { + // result[prop] = result[prop].concat(source[prop]); + result[prop] = mergeArrays(result[prop], source[prop]); + } else if (prop in result && typeof result[prop] === 'object') { + result[prop] = mergeObjects(result[prop], source[prop]); + } else { + result[prop] = source[prop]; + } + } + }); + }); + + return result; +} + +export function mergeArrays(left: any[], right: any[]): any[] { + const result = []; + const map = {}; + + (left || []).forEach(entry => { + const element = entry; + if (element && element.hasOwnProperty('id')) { + map[element.id] = element; + } else { + result.push(element); + } + }); + + (right || []).forEach(entry => { + const element = entry; + if (element && element.hasOwnProperty('id') && map[element.id]) { + const merged = mergeObjects(map[element.id], element); + map[element.id] = merged; + } else { + result.push(element); + } + }); + + return Object.values(map).concat(result); +} diff --git a/lib/extensions/src/lib/config/extension.config.ts b/lib/extensions/src/lib/config/extension.config.ts new file mode 100644 index 0000000000..201c360006 --- /dev/null +++ b/lib/extensions/src/lib/config/extension.config.ts @@ -0,0 +1,35 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RouteRef } from './routing.extensions'; +import { RuleRef } from './rule.extensions'; +import { ActionRef } from './action.extensions'; + +export interface ExtensionConfig { + $name: string; + $version: string; + $description?: string; + $references?: Array; + + rules?: Array; + routes?: Array; + actions?: Array; + + features?: { + [key: string]: any; + }; +} diff --git a/lib/extensions/src/lib/config/navbar.extensions.ts b/lib/extensions/src/lib/config/navbar.extensions.ts new file mode 100644 index 0000000000..ed797da687 --- /dev/null +++ b/lib/extensions/src/lib/config/navbar.extensions.ts @@ -0,0 +1,31 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ExtensionElement } from './extension-element'; + +export interface NavBarGroupRef extends ExtensionElement { + items: Array; +} + +export interface NavBarLinkRef extends ExtensionElement { + icon: string; + title: string; + route: string; + + url?: string; // evaluated at runtime based on route ref + description?: string; +} diff --git a/lib/extensions/src/lib/config/permission.extensions.ts b/lib/extensions/src/lib/config/permission.extensions.ts new file mode 100644 index 0000000000..09e6ab4e8b --- /dev/null +++ b/lib/extensions/src/lib/config/permission.extensions.ts @@ -0,0 +1,20 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface NodePermissions { + check(source: any, permissions: string[], options?: any): boolean; +} diff --git a/lib/extensions/src/lib/config/routing.extensions.ts b/lib/extensions/src/lib/config/routing.extensions.ts new file mode 100644 index 0000000000..e9cff64cd5 --- /dev/null +++ b/lib/extensions/src/lib/config/routing.extensions.ts @@ -0,0 +1,26 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface RouteRef { + id: string; + path: string; + component: string; + + layout?: string; + auth?: string[]; + data?: { [key: string]: string }; +} diff --git a/lib/extensions/src/lib/config/rule.extensions.ts b/lib/extensions/src/lib/config/rule.extensions.ts new file mode 100644 index 0000000000..44c8714900 --- /dev/null +++ b/lib/extensions/src/lib/config/rule.extensions.ts @@ -0,0 +1,44 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SelectionState } from '../store/states/selection.state'; +import { NavigationState } from '../store/states/navigation.state'; +import { NodePermissions } from './permission.extensions'; +import { ProfileState } from '../store/states/profile.state'; + +export type RuleEvaluator = (context: RuleContext, ...args: any[]) => boolean; + +export interface RuleContext { + selection: SelectionState; + navigation: NavigationState; + profile: ProfileState; + permissions: NodePermissions; + + getEvaluator(key: string): RuleEvaluator; +} + +export class RuleRef { + type: string; + id?: string; + parameters?: Array; +} + +export interface RuleParameter { + type: string; + value: any; + parameters?: Array; +} diff --git a/lib/extensions/src/lib/config/sidebar.extensions.ts b/lib/extensions/src/lib/config/sidebar.extensions.ts new file mode 100644 index 0000000000..a654c3a084 --- /dev/null +++ b/lib/extensions/src/lib/config/sidebar.extensions.ts @@ -0,0 +1,29 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ExtensionElement } from './extension-element'; + +export interface SidebarTabRef extends ExtensionElement { + title: string; + component: string; + + icon?: string; + rules?: { + visible?: string; + [key: string]: string; + }; +} diff --git a/lib/extensions/src/lib/config/viewer.extensions.ts b/lib/extensions/src/lib/config/viewer.extensions.ts new file mode 100644 index 0000000000..22c6d95b3c --- /dev/null +++ b/lib/extensions/src/lib/config/viewer.extensions.ts @@ -0,0 +1,23 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ExtensionElement } from './extension-element'; + +export interface ViewerExtensionRef extends ExtensionElement { + fileExtension: string; + component: string; +} diff --git a/lib/extensions/src/lib/evaluators/core.evaluators.spec.ts b/lib/extensions/src/lib/evaluators/core.evaluators.spec.ts new file mode 100644 index 0000000000..304be9919e --- /dev/null +++ b/lib/extensions/src/lib/evaluators/core.evaluators.spec.ts @@ -0,0 +1,237 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { every, not, some } from './core.evaluators'; +import { RuleParameter } from '../config/rule.extensions'; + +describe('Core Evaluators', () => { + + const context: any = { + getEvaluator(key: string) { + switch (key) { + case 'positive': + return () => true; + case 'negative': + return () => false; + default: + return null; + } + } + }; + + describe('not', () => { + it('should evaluate a single rule to [true]', () => { + const parameter: RuleParameter = { + type: 'primitive', + value: 'negative' + }; + + const result = not(context, parameter); + expect(result).toBeTruthy(); + }); + + it('should evaluate to [false] when no parameters provided', () => { + const result = not(context); + expect(result).toBeFalsy(); + }); + + it('should evaluate to [false] when evaluator not available', () => { + const parameter: RuleParameter = { + type: 'primitive', + value: 'missing' + }; + + const result = not(context, parameter); + expect(result).toBeFalsy(); + }); + + it('should evaluate a single rule to [false]', () => { + const parameter: RuleParameter = { + type: 'primitive', + value: 'positive' + }; + + const result = not(context, parameter); + expect(result).toBeFalsy(); + }); + + it('should evaluate multiple rules to [true]', () => { + const parameter1: RuleParameter = { + type: 'primitive', + value: 'negative' + }; + + const parameter2: RuleParameter = { + type: 'primitive', + value: 'negative' + }; + + const parameter3: RuleParameter = { + type: 'primitive', + value: 'negative' + }; + + const result = not(context, parameter1, parameter2, parameter3); + expect(result).toBeTruthy(); + }); + + it('should evaluate to [false] when one of the rules fails', () => { + const parameter1: RuleParameter = { + type: 'primitive', + value: 'negative' + }; + + const parameter2: RuleParameter = { + type: 'primitive', + value: 'negative' + }; + + const parameter3: RuleParameter = { + type: 'primitive', + value: 'positive' + }; + + const result = not(context, parameter1, parameter2, parameter3); + expect(result).toBeFalsy(); + }); + }); + + describe('every', () => { + it('should evaluate a single rule to [true]', () => { + const parameter: RuleParameter = { + type: 'primitive', + value: 'positive' + }; + + const result = every(context, parameter); + expect(result).toBeTruthy(); + }); + + it('should evaluate to [false] when no parameters provided', () => { + const result = every(context); + expect(result).toBeFalsy(); + }); + + it('should evaluate to [false] when evaluator not available', () => { + const parameter: RuleParameter = { + type: 'primitive', + value: 'missing' + }; + + const result = every(context, parameter); + expect(result).toBeFalsy(); + }); + + it('should evaluate a single rule to [false]', () => { + const parameter: RuleParameter = { + type: 'primitive', + value: 'negative' + }; + + const result = every(context, parameter); + expect(result).toBeFalsy(); + }); + + it('should evaluate multiple rules to [true]', () => { + const parameter1: RuleParameter = { + type: 'primitive', + value: 'positive' + }; + + const parameter2: RuleParameter = { + type: 'primitive', + value: 'positive' + }; + + const parameter3: RuleParameter = { + type: 'primitive', + value: 'positive' + }; + + const result = every(context, parameter1, parameter2, parameter3); + expect(result).toBeTruthy(); + }); + + it('should evaluate to [false] when one of the rules fails', () => { + const parameter1: RuleParameter = { + type: 'primitive', + value: 'positive' + }; + + const parameter2: RuleParameter = { + type: 'primitive', + value: 'positive' + }; + + const parameter3: RuleParameter = { + type: 'primitive', + value: 'negative' + }; + + const result = every(context, parameter1, parameter2, parameter3); + expect(result).toBeFalsy(); + }); + }); + + describe('some', () => { + it('should evaluate a single rule to [true]', () => { + const parameter: RuleParameter = { + type: 'primitive', + value: 'positive' + }; + + const result = some(context, parameter); + expect(result).toBeTruthy(); + }); + + it('should evaluate to [false] when no parameters provided', () => { + const result = some(context); + expect(result).toBeFalsy(); + }); + + it('should evaluate to [false] when evaluator not available', () => { + const parameter: RuleParameter = { + type: 'primitive', + value: 'missing' + }; + + const result = some(context, parameter); + expect(result).toBeFalsy(); + }); + + it('should evaluate to [true] if any rule succeeds', () => { + const parameter1: RuleParameter = { + type: 'primitive', + value: 'negative' + }; + + const parameter2: RuleParameter = { + type: 'primitive', + value: 'positive' + }; + + const parameter3: RuleParameter = { + type: 'primitive', + value: 'negative' + }; + + const result = some(context, parameter1, parameter2, parameter3); + expect(result).toBeTruthy(); + }); + }); + +}); diff --git a/lib/extensions/src/lib/evaluators/core.evaluators.ts b/lib/extensions/src/lib/evaluators/core.evaluators.ts new file mode 100644 index 0000000000..b6ce7748aa --- /dev/null +++ b/lib/extensions/src/lib/evaluators/core.evaluators.ts @@ -0,0 +1,66 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RuleContext, RuleParameter } from '../config/rule.extensions'; + +export function not(context: RuleContext, ...args: RuleParameter[]): boolean { + if (!args || args.length === 0) { + return false; + } + + return args + .every(arg => { + const evaluator = context.getEvaluator(arg.value); + if (!evaluator) { + console.warn('evaluator not found: ' + arg.value); + return false; + } + return !evaluator(context, ...(arg.parameters || [])); + }); +} + +export function every(context: RuleContext, ...args: RuleParameter[]): boolean { + if (!args || args.length === 0) { + return false; + } + + return args + .every(arg => { + const evaluator = context.getEvaluator(arg.value); + if (!evaluator) { + console.warn('evaluator not found: ' + arg.value); + return false; + } + return evaluator(context, ...(arg.parameters || [])); + }); +} + +export function some(context: RuleContext, ...args: RuleParameter[]): boolean { + if (!args || args.length === 0) { + return false; + } + + return args + .some(arg => { + const evaluator = context.getEvaluator(arg.value); + if (!evaluator) { + console.warn('evaluator not found: ' + arg.value); + return false; + } + return evaluator(context, ...(arg.parameters || [])); + }); +} diff --git a/lib/extensions/src/lib/extensions.module.ts b/lib/extensions/src/lib/extensions.module.ts new file mode 100644 index 0000000000..e4950e5c01 --- /dev/null +++ b/lib/extensions/src/lib/extensions.module.ts @@ -0,0 +1,42 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NgModule, ModuleWithProviders } from '@angular/core'; +import { ExtensionLoaderService } from './services/extension-loader.service'; +import { ExtensionService } from './services/extension.service'; +import { DynamicExtensionComponent } from './components/dynamic-component/dynamic.component'; +import { DynamicTabComponent } from './components/dynamic-tab/dynamic-tab.component'; + +@NgModule({ + imports: [], + declarations: [DynamicExtensionComponent, DynamicTabComponent], + exports: [DynamicExtensionComponent, DynamicTabComponent] +}) +export class ExtensionsModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: ExtensionsModule, + providers: [ExtensionLoaderService, ExtensionService] + }; + } + + static forChild(): ModuleWithProviders { + return { + ngModule: ExtensionsModule + }; + } +} diff --git a/lib/extensions/src/lib/services/extension-loader.service.ts b/lib/extensions/src/lib/services/extension-loader.service.ts new file mode 100644 index 0000000000..a74c1a832e --- /dev/null +++ b/lib/extensions/src/lib/services/extension-loader.service.ts @@ -0,0 +1,138 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { ActionRef, ContentActionRef, ContentActionType } from '../config/action.extensions'; +import { ExtensionElement } from '../config/extension-element'; +import { filterEnabled, getValue, mergeObjects, sortByOrder } from '../config/extension-utils'; +import { ExtensionConfig } from '../config/extension.config'; +import { RouteRef } from '../config/routing.extensions'; +import { RuleRef } from '../config/rule.extensions'; + +@Injectable({ + providedIn: 'root' +}) +export class ExtensionLoaderService { + constructor(private http: HttpClient) {} + + load(configPath: string, pluginsPath: string): Promise { + return new Promise(resolve => { + this.loadConfig(configPath, 0).then(result => { + let config = result.config; + + const override = sessionStorage.getItem('aca.extension.config'); + if (override) { + config = JSON.parse(override); + } + + const externalPlugins = + localStorage.getItem('experimental.external-plugins') === + 'true'; + + if ( + externalPlugins && + config.$references && + config.$references.length > 0 + ) { + const plugins = config.$references.map((name, idx) => + this.loadConfig(`${pluginsPath}/${name}`, idx) + ); + + Promise.all(plugins).then(results => { + const configs = results + .filter(entry => entry) + .sort(sortByOrder) + .map(entry => entry.config); + + if (configs.length > 0) { + config = mergeObjects(config, ...configs); + } + + resolve(config); + }); + } else { + resolve(config); + } + }); + }); + } + + protected loadConfig( + url: string, + order: number + ): Promise<{ order: number; config: ExtensionConfig }> { + return new Promise(resolve => { + this.http.get(url).subscribe( + config => { + resolve({ + order, + config + }); + }, + error => { + resolve(null); + } + ); + }); + } + + getElements( + config: ExtensionConfig, + key: string, + fallback: Array = [] + ): Array { + const values = getValue(config, key) || fallback || []; + return values.filter(filterEnabled).sort(sortByOrder); + } + + getContentActions( + config: ExtensionConfig, + key: string + ): Array { + return this.getElements(config, key).map(this.setActionDefaults); + } + + getRules(config: ExtensionConfig): Array { + if (config && config.rules) { + return config.rules; + } + return []; + } + + getRoutes(config: ExtensionConfig): Array { + if (config) { + return config.routes || []; + } + return []; + } + + getActions(config: ExtensionConfig): Array { + if (config) { + return config.actions || []; + } + return []; + } + + protected setActionDefaults(action: ContentActionRef): ContentActionRef { + if (action) { + action.type = action.type || ContentActionType.default; + action.icon = action.icon || 'extension'; + } + return action; + } +} diff --git a/lib/extensions/src/lib/services/extension.service.spec.ts b/lib/extensions/src/lib/services/extension.service.spec.ts new file mode 100644 index 0000000000..701c68f95b --- /dev/null +++ b/lib/extensions/src/lib/services/extension.service.spec.ts @@ -0,0 +1,379 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ExtensionService } from './extension.service'; +import { ExtensionLoaderService } from './extension-loader.service'; +import { ExtensionConfig } from '../config/extension.config'; +import { RuleRef } from '../config/rule.extensions'; +import { RouteRef } from '../config/routing.extensions'; +import { ActionRef } from '../config/action.extensions'; + +describe('ExtensionService', () => { + const blankConfig: ExtensionConfig = { + $name: 'test.config', + $version: '1.0.0' + }; + + let loader: ExtensionLoaderService; + let service: ExtensionService; + + beforeEach(() => { + loader = new ExtensionLoaderService(null); + service = new ExtensionService(loader); + }); + + it('should load and setup a config', async () => { + spyOn(loader, 'load').and.callFake(() => { + return Promise.resolve(blankConfig); + }); + + spyOn(service, 'setup').and.stub(); + + await service.load(); + + expect(loader.load).toHaveBeenCalled(); + expect(service.setup).toHaveBeenCalledWith(blankConfig); + }); + + it('should raise warning if setting up with missing config', () => { + spyOn(console, 'warn').and.stub(); + + service.setup(null); + + expect(console.warn).toHaveBeenCalledWith('Extension configuration not found'); + }); + + it('should setup default evaluators', () => { + service.setup(blankConfig); + + const evaluators = ['core.every', 'core.some', 'core.not']; + evaluators.forEach(key => { + expect(service.getEvaluator(key)).toBeDefined(`Evaluator ${key} is missing`); + }); + }); + + it('should set custom evaluators', () => { + const evaluator1 = () => true; + const evaluator2 = () => false; + + service.setEvaluators({ + 'eval1': evaluator1, + 'eval2': evaluator2 + }); + + expect(service.getEvaluator('eval1')).toBe(evaluator1); + expect(service.getEvaluator('eval2')).toBe(evaluator2); + }); + + it('should override existing evaluators', () => { + const evaluator1 = () => true; + const evaluator2 = () => false; + + service.setup(blankConfig); + expect(service.getEvaluator('core.every')).toBeDefined(); + expect(service.getEvaluator('core.every')).not.toBe(evaluator1); + + service.setEvaluators({ + 'core.every': evaluator1, + 'eval2': evaluator2 + }); + + expect(service.getEvaluator('core.every')).toBe(evaluator1); + expect(service.getEvaluator('eval2')).toBe(evaluator2); + }); + + it('should negate existing evaluator', () => { + const positive = () => true; + + service.setEvaluators({ + 'positive': positive + }); + + let evaluator = service.getEvaluator('positive'); + expect(evaluator(null)).toBe(true); + + evaluator = service.getEvaluator('!positive'); + expect(evaluator(null, 'param1', 'param2')).toBe(false); + }); + + it('should not update evaluators with null value', () => { + service.setup(blankConfig); + service.setEvaluators(null); + + expect(service.getEvaluator('core.every')).toBeDefined(); + }); + + it('should set authentication guards', () => { + let registered = service.getAuthGuards(['guard1']); + expect(registered.length).toBe(0); + + const guard1: any = {}; + const guard2: any = {}; + + service.setAuthGuards({ + 'auth1': guard1, + 'auth2': guard2 + }); + + registered = service.getAuthGuards(['auth1', 'auth2']); + expect(registered.length).toBe(2); + expect(registered[0]).toBe(guard1); + expect(registered[1]).toBe(guard2); + }); + + it('should overwrite authentication guards', () => { + const guard1: any = {}; + const guard2: any = {}; + + service.setAuthGuards({ + 'auth': guard1 + }); + + expect(service.getAuthGuards(['auth'])).toEqual([guard1]); + + service.setAuthGuards({ + 'auth': guard2 + }); + + expect(service.getAuthGuards(['auth'])).toEqual([guard2]); + }); + + it('should not set authentication guards with null value', () => { + const guard1: any = {}; + + service.setAuthGuards({ + 'auth': guard1 + }); + + service.setAuthGuards(null); + + expect(service.getAuthGuards(['auth'])).toEqual([guard1]); + }); + + it('should not fetch auth guards for missing ids', () => { + const guards = service.getAuthGuards(null); + expect(guards).toEqual([]); + }); + + it('should set components', () => { + const component: any = {}; + + service.setComponents({ + 'component1': component + }); + + expect(service.getComponentById('component1')).toBe(component); + }); + + it('should overwrite components', () => { + const component1: any = {}; + const component2: any = {}; + + service.setComponents({ + 'component': component1 + }); + + expect(service.getComponentById('component')).toBe(component1); + + service.setComponents({ + 'component': component2 + }); + + expect(service.getComponentById('component')).toBe(component2); + }); + + it('should not set components with null value', () => { + const component: any = {}; + + service.setComponents({ + 'component1': component + }); + + expect(service.getComponentById('component1')).toBe(component); + + service.setComponents(null); + + expect(service.getComponentById('component1')).toBe(component); + }); + + it('should fetch route by id', () => { + const route: RouteRef = { + id: 'test.route', + component: 'component', + path: '/ext/route1' + }; + + spyOn(loader, 'getRoutes').and.returnValue([route]); + service.setup(blankConfig); + + expect(service.getRouteById('test.route')).toBe(route); + }); + + it('should fetch action by id', () => { + const action: ActionRef = { + id: 'test.action', + type: 'action' + }; + + spyOn(loader, 'getActions').and.returnValue([action]); + service.setup(blankConfig); + + expect(service.getActionById('test.action')).toBe(action); + }); + + it('should fetch rule by id', () => { + const rule: RuleRef = { + id: 'test.rule', + type: 'core.every' + }; + + spyOn(loader, 'getRules').and.returnValue([rule]); + service.setup(blankConfig); + + expect(service.getRuleById('test.rule')).toBe(rule); + }); + + it('should evaluate condition', () => { + const condition = () => true; + + service.setEvaluators({ + 'test.condition': condition + }); + + const context: any = { + getEvaluator(key: string) { + return service.getEvaluator(key); + } + }; + + const result = service.evaluateRule('test.condition', context); + expect(result).toBe(true); + }); + + it('should evaluate missing condition as [false]', () => { + const context: any = { + getEvaluator(key: string) { + return service.getEvaluator(key); + } + }; + + const result = service.evaluateRule('missing.condition', context); + expect(result).toBe(false); + }); + + it('should evaluate rule by reference', () => { + const ruleRef: RuleRef = { + id: 'test.rule', + type: 'core.every', + parameters: [ + { + type: 'rule', + value: 'test.condition' + } + ] + }; + + spyOn(loader, 'getRules').and.returnValue([ruleRef]); + service.setup(blankConfig); + + const condition = () => true; + + service.setEvaluators({ + 'test.condition': condition + }); + + const context: any = { + getEvaluator(key: string) { + return service.getEvaluator(key); + } + }; + + const result = service.evaluateRule('test.rule', context); + expect(result).toBe(true); + }); + + it('should evaluate rule ref with missing condition as [false]', () => { + const ruleRef: RuleRef = { + id: 'test.rule', + type: 'missing.evaluator' + }; + + spyOn(loader, 'getRules').and.returnValue([ruleRef]); + service.setup(blankConfig); + + const context: any = { + getEvaluator(key: string) { + return service.getEvaluator(key); + } + }; + + const result = service.evaluateRule('test.rule', context); + expect(result).toBe(false); + }); + + it('should evaluate rule ref with missing evaluator as [false]', () => { + const ruleRef: RuleRef = { + id: 'test.rule', + type: 'core.every', + parameters: [ + { + type: 'rule', + value: 'missing.condition' + } + ] + }; + + spyOn(loader, 'getRules').and.returnValue([ruleRef]); + service.setup(blankConfig); + + const context: any = { + getEvaluator(key: string) { + return service.getEvaluator(key); + } + }; + + const result = service.evaluateRule('test.rule', context); + expect(result).toBe(false); + }); + + describe('expressions', () => { + it('should eval static value', () => { + const value = service.runExpression('hello world'); + expect(value).toBe('hello world'); + }); + + it('should eval string as an expression', () => { + const value = service.runExpression('$( "hello world" )'); + expect(value).toBe('hello world'); + }); + + it('should eval expression with no context', () => { + const value = service.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 = service.runExpression(expression, context); + expect(value).toBe('hey there!'); + }); + }); +}); diff --git a/lib/extensions/src/lib/services/extension.service.ts b/lib/extensions/src/lib/services/extension.service.ts new file mode 100644 index 0000000000..2f78199a3c --- /dev/null +++ b/lib/extensions/src/lib/services/extension.service.ts @@ -0,0 +1,148 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable, Type } from '@angular/core'; +import { RuleEvaluator, RuleRef, RuleContext, RuleParameter } from '../config/rule.extensions'; +import { ExtensionConfig } from '../config/extension.config'; +import { ExtensionLoaderService } from './extension-loader.service'; +import { RouteRef } from '../config/routing.extensions'; +import { ActionRef } from '../config/action.extensions'; +import * as core from '../evaluators/core.evaluators'; + +@Injectable() +export class ExtensionService { + configPath = 'assets/app.extensions.json'; + pluginsPath = 'assets/plugins'; + + rules: Array = []; + routes: Array = []; + actions: Array = []; + + authGuards: { [key: string]: Type<{}> } = {}; + components: { [key: string]: Type<{}> } = {}; + evaluators: { [key: string]: RuleEvaluator } = {}; + + constructor(private loader: ExtensionLoaderService) {} + + async load(): Promise { + const config = await this.loader.load( + this.configPath, + this.pluginsPath + ); + this.setup(config); + return config; + } + + setup(config: ExtensionConfig) { + if (!config) { + console.warn('Extension configuration not found'); + return; + } + + this.setEvaluators({ + 'core.every': core.every, + 'core.some': core.some, + 'core.not': core.not + }); + + this.rules = this.loader.getRules(config); + this.actions = this.loader.getActions(config); + this.routes = this.loader.getRoutes(config); + } + + setEvaluators(values: { [key: string]: RuleEvaluator }) { + if (values) { + this.evaluators = Object.assign({}, this.evaluators, values); + } + } + + setAuthGuards(values: { [key: string]: Type<{}> }) { + if (values) { + this.authGuards = Object.assign({}, this.authGuards, values); + } + } + + setComponents(values: { [key: string]: Type<{}> }) { + if (values) { + this.components = Object.assign({}, this.components, values); + } + } + + getRouteById(id: string): RouteRef { + return this.routes.find(route => route.id === id); + } + + getAuthGuards(ids: string[]): Array> { + return (ids || []) + .map(id => this.authGuards[id]) + .filter(guard => guard); + } + + getActionById(id: string): ActionRef { + return this.actions.find(action => action.id === id); + } + + getEvaluator(key: string): RuleEvaluator { + if (key && key.startsWith('!')) { + const fn = this.evaluators[key.substring(1)]; + return (context: RuleContext, ...args: RuleParameter[]): boolean => { + return !fn(context, ...args); + }; + } + return this.evaluators[key]; + } + + evaluateRule(ruleId: string, context: RuleContext): boolean { + const ruleRef = this.getRuleById(ruleId); + + if (ruleRef) { + const evaluator = this.getEvaluator(ruleRef.type); + if (evaluator) { + return evaluator(context, ...ruleRef.parameters); + } + } else { + const evaluator = this.getEvaluator(ruleId); + if (evaluator) { + return evaluator(context); + } + } + return false; + } + + getComponentById(id: string): Type<{}> { + return this.components[id]; + } + + getRuleById(id: string): RuleRef { + return this.rules.find(ref => ref.id === 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; + } +} diff --git a/lib/extensions/src/lib/store/states/navigation.state.ts b/lib/extensions/src/lib/store/states/navigation.state.ts new file mode 100644 index 0000000000..f3897e3f87 --- /dev/null +++ b/lib/extensions/src/lib/store/states/navigation.state.ts @@ -0,0 +1,23 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Node } from 'alfresco-js-api'; + +export interface NavigationState { + currentFolder?: Node; + url?: string; +} diff --git a/lib/extensions/src/lib/store/states/profile.state.ts b/lib/extensions/src/lib/store/states/profile.state.ts new file mode 100644 index 0000000000..3cd4e67a83 --- /dev/null +++ b/lib/extensions/src/lib/store/states/profile.state.ts @@ -0,0 +1,25 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface ProfileState { + id: string; + isAdmin: boolean; + firstName: string; + lastName: string; + userName?: string; + initials?: string; +} diff --git a/lib/extensions/src/lib/store/states/selection.state.ts b/lib/extensions/src/lib/store/states/selection.state.ts new file mode 100644 index 0000000000..8baa38d4b9 --- /dev/null +++ b/lib/extensions/src/lib/store/states/selection.state.ts @@ -0,0 +1,30 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MinimalNodeEntity, SiteEntry } from 'alfresco-js-api'; + +export interface SelectionState { + count: number; + nodes: MinimalNodeEntity[]; + libraries: SiteEntry[]; + isEmpty: boolean; + first?: MinimalNodeEntity; + last?: MinimalNodeEntity; + folder?: MinimalNodeEntity; + file?: MinimalNodeEntity; + library?: SiteEntry; +} diff --git a/lib/extensions/src/public_api.ts b/lib/extensions/src/public_api.ts new file mode 100644 index 0000000000..4001b6ccd1 --- /dev/null +++ b/lib/extensions/src/public_api.ts @@ -0,0 +1,36 @@ +/*! + * @license + * Copyright 2016 - 2018 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './lib/config/action.extensions'; +export * from './lib/config/extension-element'; +export * from './lib/config/extension-utils'; +export * from './lib/config/extension.config'; +export * from './lib/config/navbar.extensions'; +export * from './lib/config/permission.extensions'; +export * from './lib/config/routing.extensions'; +export * from './lib/config/rule.extensions'; +export * from './lib/config/sidebar.extensions'; +export * from './lib/config/viewer.extensions'; + +export * from './lib/services/extension-loader.service'; +export * from './lib/services/extension.service'; + +export * from './lib/store/states/navigation.state'; +export * from './lib/store/states/profile.state'; +export * from './lib/store/states/selection.state'; + +export * from './lib/extensions.module'; diff --git a/lib/extensions/src/test.ts b/lib/extensions/src/test.ts new file mode 100644 index 0000000000..e11ff1c97b --- /dev/null +++ b/lib/extensions/src/test.ts @@ -0,0 +1,22 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'core-js/es7/reflect'; +import 'zone.js/dist/zone'; +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/lib/extensions/tsconfig.lib.json b/lib/extensions/tsconfig.lib.json new file mode 100644 index 0000000000..bd8c4dab32 --- /dev/null +++ b/lib/extensions/tsconfig.lib.json @@ -0,0 +1,34 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "target": "es2015", + "module": "es2015", + "moduleResolution": "node", + "declaration": true, + "sourceMap": true, + "inlineSources": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "types": [], + "lib": [ + "dom", + "es2015" + ] + }, + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "flatModuleId": "AUTOGENERATED", + "flatModuleOutFile": "AUTOGENERATED", + "enableResourceInlining": true + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/lib/extensions/tsconfig.spec.json b/lib/extensions/tsconfig.spec.json new file mode 100644 index 0000000000..16da33db07 --- /dev/null +++ b/lib/extensions/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/lib/extensions/tslint.json b/lib/extensions/tslint.json new file mode 100644 index 0000000000..65bb2a595e --- /dev/null +++ b/lib/extensions/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "adf", + "camelCase" + ], + "component-selector": [ + true, + "element", + "adf", + "kebab-case" + ] + } +} diff --git a/scripts/ng-packagr.sh b/scripts/ng-packagr.sh index f8b508eae2..a281a62871 100755 --- a/scripts/ng-packagr.sh +++ b/scripts/ng-packagr.sh @@ -9,22 +9,32 @@ rm -rf node_modules/@alfresco echo "====== Build lib =====" +echo "------ Build core -----" npm run ng-packagr -- -p ./lib/core/ && \ mkdir -p ./node_modules/@alfresco/adf-core/ && \ cp -R ./lib/dist/core/* ./node_modules/@alfresco/adf-core/ +echo "------ Build content-services -----" npm run ng-packagr -- -p ./lib/content-services/ && \ mkdir -p ./node_modules/@alfresco/adf-content-services/ && \ cp -R ./lib/dist/content-services/* ./node_modules/@alfresco/adf-content-services/ +echo "------ Build process-services -----" npm run ng-packagr -- -p ./lib/process-services/ && \ mkdir -p ./node_modules/@alfresco/adf-process-services/ && \ cp -R ./lib/dist/process-services/* ./node_modules/@alfresco/adf-process-services/ +echo "------ Build insights -----" npm run ng-packagr -- -p ./lib/insights/ && \ mkdir -p ./node_modules/@alfresco/adf-insights/ && \ cp -R ./lib/dist/insights/* ./node_modules/@alfresco/adf-insights/ +echo "------ Build extensions -----" +npm run ng-packagr -- -p ./lib/extensions/ && \ +mkdir -p ./node_modules/@alfresco/adf-extensions/ && \ +cp -R ./lib/dist/extensions/* ./node_modules/@alfresco/adf-extensions/ + + echo "====== Build style =====" node ./lib/config/bundle-scss.js @@ -45,6 +55,7 @@ cp -R ./lib/process-services/i18n/* ./lib/dist/process-services/bundles/assets/a mkdir -p ./lib/dist/insights/bundles/assets/adf-insights/i18n cp -R ./lib/insights/i18n/* ./lib/dist/insights/bundles/assets/adf-insights/i18n + echo "====== Copy assets =====" cp -R ./lib/core/assets/* ./lib/dist/core/bundles/assets diff --git a/scripts/npm-build-all.sh b/scripts/npm-build-all.sh index 6eda5775b9..fd418d658c 100755 --- a/scripts/npm-build-all.sh +++ b/scripts/npm-build-all.sh @@ -18,7 +18,8 @@ eval EXECLINT=true eval projects=( "core" "content-services" "insights" - "process-services" ) + "process-services" + "extensions" ) show_help() { echo "Usage: npm-build-all.sh" diff --git a/scripts/npm-check-bundles.sh b/scripts/npm-check-bundles.sh index bae489e478..2e285613cb 100755 --- a/scripts/npm-check-bundles.sh +++ b/scripts/npm-check-bundles.sh @@ -7,6 +7,7 @@ eval VERSION="" eval projects=( "adf-core" "adf-insights" "adf-content-services" + "extensions" "adf-process-services" ) show_help() { diff --git a/scripts/npm-move-tag.sh b/scripts/npm-move-tag.sh index c4900352ba..62c45c53ae 100644 --- a/scripts/npm-move-tag.sh +++ b/scripts/npm-move-tag.sh @@ -5,7 +5,8 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" eval projects=( "@alfresco/adf-core" "@alfresco/adf-content-services" "@alfresco/adf-insights" - "@alfresco/adf-process-services" ) + "@alfresco/adf-process-services" + "@alfresco/adf-extensions" ) show_help() { echo "Usage: npm-clean.sh" diff --git a/scripts/npm-publish.sh b/scripts/npm-publish.sh index 64d4981d9b..a1a96584dd 100755 --- a/scripts/npm-publish.sh +++ b/scripts/npm-publish.sh @@ -20,7 +20,8 @@ eval projects=( "core" "insights" "content-services" - "process-services" ) + "process-services" + "extensions" ) cd "$DIR/../" diff --git a/scripts/start.sh b/scripts/start.sh index a951de60cc..4976fbf6dc 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -19,7 +19,8 @@ eval GIT_ISH="" eval projects=( "@alfresco/core" "@alfresco/content-service" - "@alfresco/process-service" ) + "@alfresco/process-service" + "@alfresco/extensions" ) show_help() { echo "Usage: start.sh" diff --git a/scripts/test-dist.sh b/scripts/test-dist.sh index cecf9368e6..de826109bd 100755 --- a/scripts/test-dist.sh +++ b/scripts/test-dist.sh @@ -43,11 +43,13 @@ mkdir -p $DIR/../node_modules/@alfresco/adf-core mkdir -p $DIR/../node_modules/@alfresco/adf-content-services mkdir -p $DIR/../node_modules/@alfresco/adf-process-services mkdir -p $DIR/../node_modules/@alfresco/adf-insights +mkdir -p $DIR/../node_modules/@alfresco/adf-extensions cp -R $DIR/../lib/dist/core/* $DIR/../node_modules/@alfresco/adf-core cp -R $DIR/../lib/dist/content-services/* $DIR/../node_modules/@alfresco/adf-content-services cp -R $DIR/../lib/dist/process-services/* $DIR/../node_modules/@alfresco/adf-process-services cp -R $DIR/../lib/dist/insights/* $DIR/../node_modules/@alfresco/adf-insights +cp -R $DIR/../lib/dist/extensions/* $DIR/../node_modules/@alfresco/adf-extensions echo "====== Build dist demo shell ===== " diff --git a/scripts/update-version.sh b/scripts/update-version.sh index 25ed44e1a1..c0ab9047ec 100755 --- a/scripts/update-version.sh +++ b/scripts/update-version.sh @@ -10,7 +10,8 @@ eval TOTAL_BUILD=true; eval projects=( "core" "content-services" "process-services" - "insights" ) + "insights" + "extensions" ) cd `dirname $0` diff --git a/tsconfig.json b/tsconfig.json index daf9f9eb64..748a3768a4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ "node_modules/@types" ], "lib": [ - "es2016", + "es2017", "dom" ] },