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"
]
},