mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-07-24 17:31:52 +00:00
refactor extenstion layer to simplify testing and maintenance (#577)
* refactor extenstion layer to simplify testing and maintenance * generic extension config * move loading code to loader
This commit is contained in:
@@ -39,6 +39,7 @@ import { ToolbarButtonComponent } from './components/toolbar/toolbar-button.comp
|
|||||||
import { MetadataTabComponent } from '../components/info-drawer/metadata-tab/metadata-tab.component';
|
import { MetadataTabComponent } from '../components/info-drawer/metadata-tab/metadata-tab.component';
|
||||||
import { CommentsTabComponent } from '../components/info-drawer/comments-tab/comments-tab.component';
|
import { CommentsTabComponent } from '../components/info-drawer/comments-tab/comments-tab.component';
|
||||||
import { VersionsTabComponent } from '../components/info-drawer/versions-tab/versions-tab.component';
|
import { VersionsTabComponent } from '../components/info-drawer/versions-tab/versions-tab.component';
|
||||||
|
import { ExtensionLoaderService } from './extension-loader.service';
|
||||||
|
|
||||||
export function setupExtensions(extensions: ExtensionService): Function {
|
export function setupExtensions(extensions: ExtensionService): Function {
|
||||||
extensions.setComponents({
|
extensions.setComponents({
|
||||||
@@ -106,6 +107,7 @@ export class CoreExtensionsModule {
|
|||||||
return {
|
return {
|
||||||
ngModule: CoreExtensionsModule,
|
ngModule: CoreExtensionsModule,
|
||||||
providers: [
|
providers: [
|
||||||
|
ExtensionLoaderService,
|
||||||
ExtensionService,
|
ExtensionService,
|
||||||
{
|
{
|
||||||
provide: APP_INITIALIZER,
|
provide: APP_INITIALIZER,
|
||||||
|
31
src/app/extensions/extension-element.ts
Normal file
31
src/app/extensions/extension-element.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*!
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ExtensionElement {
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
order?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
140
src/app/extensions/extension-loader.service.ts
Normal file
140
src/app/extensions/extension-loader.service.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
/*!
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ExtensionConfig } from './extension.config';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { ExtensionElement } from './extension-element';
|
||||||
|
import { ContentActionRef, ContentActionType, ActionRef } from './action.extensions';
|
||||||
|
import { RuleRef } from './rule.extensions';
|
||||||
|
import { RouteRef } from './routing.extensions';
|
||||||
|
import { sortByOrder, filterEnabled, getValue, mergeObjects } from './extension-utils';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ExtensionLoaderService {
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
load(configPath: string, pluginsPath: string): Promise<ExtensionConfig> {
|
||||||
|
return new Promise<any>(resolve => {
|
||||||
|
this.loadConfig(configPath, 0).then(result => {
|
||||||
|
let config = result.config;
|
||||||
|
|
||||||
|
const override = sessionStorage.getItem('aca.extension.config');
|
||||||
|
if (override) {
|
||||||
|
console.log('overriding extension config');
|
||||||
|
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<ExtensionConfig>(url).subscribe(
|
||||||
|
config => {
|
||||||
|
resolve({
|
||||||
|
order,
|
||||||
|
config
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
console.log(error);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getElements<T extends ExtensionElement>(
|
||||||
|
config: ExtensionConfig,
|
||||||
|
key: string,
|
||||||
|
fallback: Array<T> = []
|
||||||
|
): Array<T> {
|
||||||
|
const values = getValue(config, key) || fallback || [];
|
||||||
|
return values.filter(filterEnabled).sort(sortByOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
getContentActions(
|
||||||
|
config: ExtensionConfig,
|
||||||
|
key: string
|
||||||
|
): Array<ContentActionRef> {
|
||||||
|
return this.getElements(config, key).map(this.setActionDefaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRules(config: ExtensionConfig): Array<RuleRef> {
|
||||||
|
if (config && config.rules) {
|
||||||
|
return config.rules;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoutes(config: ExtensionConfig): Array<RouteRef> {
|
||||||
|
if (config) {
|
||||||
|
return config.routes || [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getActions(config: ExtensionConfig): Array<ActionRef> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
158
src/app/extensions/extension-utils.ts
Normal file
158
src/app/extensions/extension-utils.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
/*!
|
||||||
|
* @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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
@@ -23,32 +23,21 @@
|
|||||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NavBarGroupRef } from './navbar.extensions';
|
|
||||||
import { RouteRef } from './routing.extensions';
|
import { RouteRef } from './routing.extensions';
|
||||||
import { RuleRef } from './rule.extensions';
|
import { RuleRef } from './rule.extensions';
|
||||||
import { ActionRef, ContentActionRef } from './action.extensions';
|
import { ActionRef } from './action.extensions';
|
||||||
import { SidebarTabRef } from './sidebar.extensions';
|
|
||||||
import { ViewerExtensionRef } from './viewer.extensions';
|
|
||||||
|
|
||||||
export interface ExtensionConfig {
|
export interface ExtensionConfig {
|
||||||
$name: string;
|
$name: string;
|
||||||
$version: string;
|
$version: string;
|
||||||
$description?: string;
|
$description?: string;
|
||||||
$references?: Array<string>;
|
$references?: Array<string>;
|
||||||
|
|
||||||
rules?: Array<RuleRef>;
|
rules?: Array<RuleRef>;
|
||||||
routes?: Array<RouteRef>;
|
routes?: Array<RouteRef>;
|
||||||
actions?: Array<ActionRef>;
|
actions?: Array<ActionRef>;
|
||||||
|
|
||||||
features?: {
|
features?: {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
create?: Array<ContentActionRef>;
|
|
||||||
viewer?: {
|
|
||||||
openWith?: Array<ContentActionRef>;
|
|
||||||
toolbar?: Array<ContentActionRef>;
|
|
||||||
content?: Array<ViewerExtensionRef>;
|
|
||||||
};
|
|
||||||
navbar?: Array<NavBarGroupRef>;
|
|
||||||
sidebar?: Array<SidebarTabRef>;
|
|
||||||
toolbar?: Array<ContentActionRef>;
|
|
||||||
contextMenu?: Array<ContentActionRef>;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -29,6 +29,7 @@ import { ExtensionService } from './extension.service';
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppStore } from '../store/states';
|
import { AppStore } from '../store/states';
|
||||||
import { ContentActionType } from './action.extensions';
|
import { ContentActionType } from './action.extensions';
|
||||||
|
import { mergeArrays, sortByOrder, filterEnabled, reduceSeparators, reduceEmptyMenus } from './extension-utils';
|
||||||
|
|
||||||
describe('ExtensionService', () => {
|
describe('ExtensionService', () => {
|
||||||
let extensions: ExtensionService;
|
let extensions: ExtensionService;
|
||||||
@@ -70,7 +71,7 @@ describe('ExtensionService', () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = extensions.mergeArrays(left, right);
|
const result = mergeArrays(left, right);
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
id: '#1',
|
id: '#1',
|
||||||
@@ -501,7 +502,7 @@ describe('ExtensionService', () => {
|
|||||||
{ id: '1', order: 10 },
|
{ id: '1', order: 10 },
|
||||||
{ id: '2', order: 1 },
|
{ id: '2', order: 1 },
|
||||||
{ id: '3', order: 5 }
|
{ id: '3', order: 5 }
|
||||||
].sort(extensions.sortByOrder);
|
].sort(sortByOrder);
|
||||||
|
|
||||||
expect(sorted[0].id).toBe('2');
|
expect(sorted[0].id).toBe('2');
|
||||||
expect(sorted[1].id).toBe('3');
|
expect(sorted[1].id).toBe('3');
|
||||||
@@ -513,7 +514,7 @@ describe('ExtensionService', () => {
|
|||||||
{ id: '3'},
|
{ id: '3'},
|
||||||
{ id: '2' },
|
{ id: '2' },
|
||||||
{ id: '1', order: 1 }
|
{ id: '1', order: 1 }
|
||||||
].sort(extensions.sortByOrder);
|
].sort(sortByOrder);
|
||||||
|
|
||||||
expect(sorted[0].id).toBe('1');
|
expect(sorted[0].id).toBe('1');
|
||||||
expect(sorted[1].id).toBe('3');
|
expect(sorted[1].id).toBe('3');
|
||||||
@@ -527,7 +528,7 @@ describe('ExtensionService', () => {
|
|||||||
{ id: 1, disabled: true },
|
{ id: 1, disabled: true },
|
||||||
{ id: 2 },
|
{ id: 2 },
|
||||||
{ id: 3, disabled: true }
|
{ id: 3, disabled: true }
|
||||||
].filter(extensions.filterEnabled);
|
].filter(filterEnabled);
|
||||||
|
|
||||||
expect(items.length).toBe(1);
|
expect(items.length).toBe(1);
|
||||||
expect(items[0].id).toBe(2);
|
expect(items[0].id).toBe(2);
|
||||||
@@ -543,7 +544,7 @@ describe('ExtensionService', () => {
|
|||||||
{ id: '5', type: ContentActionType.button }
|
{ id: '5', type: ContentActionType.button }
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = actions.reduce(extensions.reduceSeparators, []);
|
const result = actions.reduce(reduceSeparators, []);
|
||||||
expect(result.length).toBe(3);
|
expect(result.length).toBe(3);
|
||||||
expect(result[0].id).toBe('1');
|
expect(result[0].id).toBe('1');
|
||||||
expect(result[1].id).toBe('2');
|
expect(result[1].id).toBe('2');
|
||||||
@@ -556,7 +557,7 @@ describe('ExtensionService', () => {
|
|||||||
{ id: '2', type: ContentActionType.separator }
|
{ id: '2', type: ContentActionType.separator }
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = actions.reduce(extensions.reduceSeparators, []);
|
const result = actions.reduce(reduceSeparators, []);
|
||||||
expect(result.length).toBe(1);
|
expect(result.length).toBe(1);
|
||||||
expect(result[0].id).toBe('1');
|
expect(result[0].id).toBe('1');
|
||||||
});
|
});
|
||||||
@@ -575,7 +576,7 @@ describe('ExtensionService', () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = actions.reduce(extensions.reduceEmptyMenus, []);
|
const result = actions.reduce(reduceEmptyMenus, []);
|
||||||
|
|
||||||
expect(result.length).toBe(2);
|
expect(result.length).toBe(2);
|
||||||
expect(result[0].id).toBe('1');
|
expect(result[0].id).toBe('1');
|
||||||
|
@@ -24,7 +24,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, Type } from '@angular/core';
|
import { Injectable, Type } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { Route } from '@angular/router';
|
import { Route } from '@angular/router';
|
||||||
import { ExtensionConfig } from './extension.config';
|
import { ExtensionConfig } from './extension.config';
|
||||||
@@ -40,6 +39,8 @@ import { NodePermissionService } from '../services/node-permission.service';
|
|||||||
import { SidebarTabRef } from './sidebar.extensions';
|
import { SidebarTabRef } from './sidebar.extensions';
|
||||||
import { ProfileResolver } from '../services/profile.resolver';
|
import { ProfileResolver } from '../services/profile.resolver';
|
||||||
import { ViewerExtensionRef } from './viewer.extensions';
|
import { ViewerExtensionRef } from './viewer.extensions';
|
||||||
|
import { ExtensionLoaderService } from './extension-loader.service';
|
||||||
|
import { sortByOrder, filterEnabled, reduceSeparators, reduceEmptyMenus } from './extension-utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExtensionService implements RuleContext {
|
export class ExtensionService implements RuleContext {
|
||||||
@@ -72,8 +73,8 @@ export class ExtensionService implements RuleContext {
|
|||||||
navigation: NavigationState;
|
navigation: NavigationState;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient,
|
|
||||||
private store: Store<AppStore>,
|
private store: Store<AppStore>,
|
||||||
|
private loader: ExtensionLoaderService,
|
||||||
public permissions: NodePermissionService) {
|
public permissions: NodePermissionService) {
|
||||||
|
|
||||||
this.evaluators = {
|
this.evaluators = {
|
||||||
@@ -88,43 +89,9 @@ export class ExtensionService implements RuleContext {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
load(): Promise<boolean> {
|
async load() {
|
||||||
return new Promise<any>(resolve => {
|
const config = await this.loader.load(this.configPath, this.pluginsPath);
|
||||||
this.loadConfig(this.configPath, 0).then(result => {
|
this.setup(config);
|
||||||
let config = result.config;
|
|
||||||
|
|
||||||
const override = sessionStorage.getItem('aca.extension.config');
|
|
||||||
if (override) {
|
|
||||||
console.log('overriding extension config');
|
|
||||||
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(`${this.pluginsPath}/${name}`, idx)
|
|
||||||
);
|
|
||||||
|
|
||||||
Promise.all(plugins).then((results => {
|
|
||||||
const configs = results
|
|
||||||
.filter(entry => entry)
|
|
||||||
.sort(this.sortByOrder)
|
|
||||||
.map(entry => entry.config);
|
|
||||||
|
|
||||||
if (configs.length > 0) {
|
|
||||||
config = this.mergeObjects(config, ...configs);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setup(config);
|
|
||||||
resolve(true);
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
this.setup(config);
|
|
||||||
resolve(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setup(config: ExtensionConfig) {
|
setup(config: ExtensionConfig) {
|
||||||
@@ -133,143 +100,38 @@ export class ExtensionService implements RuleContext {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.rules = this.loadRules(config);
|
this.rules = this.loader.getRules(config);
|
||||||
this.actions = this.loadActions(config);
|
this.actions = this.loader.getActions(config);
|
||||||
this.routes = this.loadRoutes(config);
|
this.routes = this.loader.getRoutes(config);
|
||||||
this.toolbarActions = this.loadToolbarActions(config);
|
this.toolbarActions = this.loader.getContentActions(config, 'features.toolbar');
|
||||||
this.viewerToolbarActions = this.loadViewerToolbarActions(config);
|
this.viewerToolbarActions = this.loader.getContentActions(config, 'features.viewer.toolbar');
|
||||||
this.viewerContentExtensions = this.loadViewerContentExtensions(config);
|
this.viewerContentExtensions = this.loader.getElements<ViewerExtensionRef>(config, 'features.viewer.content');
|
||||||
this.contextMenuActions = this.loadContextMenuActions(config);
|
this.contextMenuActions = this.loader.getContentActions(config, 'features.contextMenu');
|
||||||
this.openWithActions = this.loadViewerOpenWith(config);
|
this.openWithActions = this.loader.getContentActions(config, 'features.viewer.openWith');
|
||||||
this.createActions = this.loadCreateActions(config);
|
this.createActions = this.loader.getElements<ContentActionRef>(config, 'features.create');
|
||||||
this.navbar = this.loadNavBar(config);
|
this.navbar = this.loadNavBar(config);
|
||||||
this.sidebar = this.loadSidebar(config);
|
this.sidebar = this.loader.getElements<SidebarTabRef>(config, 'features.sidebar');
|
||||||
}
|
|
||||||
|
|
||||||
protected loadConfig(url: string, order: number): Promise<{ order: number, config: ExtensionConfig }> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
this.http.get<ExtensionConfig>(url).subscribe(
|
|
||||||
config => {
|
|
||||||
resolve({
|
|
||||||
order,
|
|
||||||
config
|
|
||||||
});
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
console.log(error);
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadCreateActions(config: ExtensionConfig): Array<ContentActionRef> {
|
|
||||||
if (config && config.features) {
|
|
||||||
return (config.features.create || []).sort(
|
|
||||||
this.sortByOrder
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadToolbarActions(config: ExtensionConfig) {
|
|
||||||
if (config && config.features && config.features.toolbar) {
|
|
||||||
return (config.features.toolbar || [])
|
|
||||||
.sort(this.sortByOrder)
|
|
||||||
.map(this.setActionDefaults);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadViewerToolbarActions(config: ExtensionConfig): Array<ContentActionRef> {
|
|
||||||
if (config && config.features && config.features.viewer) {
|
|
||||||
return (config.features.viewer.toolbar || [])
|
|
||||||
.sort(this.sortByOrder)
|
|
||||||
.map(this.setActionDefaults);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadViewerContentExtensions(config: ExtensionConfig): Array<ViewerExtensionRef> {
|
|
||||||
if (config && config.features && config.features.viewer) {
|
|
||||||
return (config.features.viewer.content || [])
|
|
||||||
.filter(entry => !entry.disabled)
|
|
||||||
.sort(this.sortByOrder);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadContextMenuActions(config: ExtensionConfig): Array<ContentActionRef> {
|
|
||||||
if (config && config.features && config.features.contextMenu) {
|
|
||||||
return (config.features.contextMenu || [])
|
|
||||||
.sort(this.sortByOrder)
|
|
||||||
.map(this.setActionDefaults);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected loadNavBar(config: ExtensionConfig): Array<NavBarGroupRef> {
|
protected loadNavBar(config: ExtensionConfig): Array<NavBarGroupRef> {
|
||||||
if (config && config.features) {
|
const elements = this.loader.getElements<NavBarGroupRef>(config, 'features.navbar');
|
||||||
return (config.features.navbar || [])
|
|
||||||
.filter(entry => !entry.disabled)
|
|
||||||
.sort(this.sortByOrder)
|
|
||||||
.map(group => {
|
|
||||||
return {
|
|
||||||
...group,
|
|
||||||
items: (group.items || [])
|
|
||||||
.filter(item => !item.disabled)
|
|
||||||
.sort(this.sortByOrder)
|
|
||||||
.map(item => {
|
|
||||||
const routeRef = this.getRouteById(item.route);
|
|
||||||
const url = `/${routeRef ? routeRef.path : item.route}`;
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
url
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadSidebar(config: ExtensionConfig): Array<SidebarTabRef> {
|
return elements.map(group => {
|
||||||
if (config && config.features) {
|
return {
|
||||||
return (config.features.sidebar || [])
|
...group,
|
||||||
.filter(entry => !entry.disabled)
|
items: (group.items || [])
|
||||||
.sort(this.sortByOrder);
|
.filter(item => !item.disabled)
|
||||||
}
|
.sort(sortByOrder)
|
||||||
return [];
|
.map(item => {
|
||||||
}
|
const routeRef = this.getRouteById(item.route);
|
||||||
|
const url = `/${routeRef ? routeRef.path : item.route}`;
|
||||||
protected loadViewerOpenWith(config: ExtensionConfig): Array<ContentActionRef> {
|
return {
|
||||||
if (config && config.features && config.features.viewer) {
|
...item,
|
||||||
return (config.features.viewer.openWith || [])
|
url
|
||||||
.filter(entry => !entry.disabled)
|
};
|
||||||
.sort(this.sortByOrder);
|
})
|
||||||
}
|
};
|
||||||
return [];
|
});
|
||||||
}
|
|
||||||
|
|
||||||
protected loadRules(config: ExtensionConfig): Array<RuleRef> {
|
|
||||||
if (config && config.rules) {
|
|
||||||
return config.rules;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadRoutes(config: ExtensionConfig): Array<RouteRef> {
|
|
||||||
if (config) {
|
|
||||||
return config.routes || [];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadActions(config: ExtensionConfig): Array<ActionRef> {
|
|
||||||
if (config) {
|
|
||||||
return config.actions || [];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setEvaluators(values: { [key: string]: RuleEvaluator }) {
|
setEvaluators(values: { [key: string]: RuleEvaluator }) {
|
||||||
@@ -339,7 +201,7 @@ export class ExtensionService implements RuleContext {
|
|||||||
|
|
||||||
getCreateActions(): Array<ContentActionRef> {
|
getCreateActions(): Array<ContentActionRef> {
|
||||||
return this.createActions
|
return this.createActions
|
||||||
.filter(this.filterEnabled)
|
.filter(filterEnabled)
|
||||||
.filter(action => this.filterByRules(action))
|
.filter(action => this.filterByRules(action))
|
||||||
.map(action => {
|
.map(action => {
|
||||||
let disabled = false;
|
let disabled = false;
|
||||||
@@ -358,7 +220,7 @@ export class ExtensionService implements RuleContext {
|
|||||||
// evaluates content actions for the selection and parent folder node
|
// evaluates content actions for the selection and parent folder node
|
||||||
getAllowedToolbarActions(): Array<ContentActionRef> {
|
getAllowedToolbarActions(): Array<ContentActionRef> {
|
||||||
return this.toolbarActions
|
return this.toolbarActions
|
||||||
.filter(this.filterEnabled)
|
.filter(filterEnabled)
|
||||||
.filter(action => this.filterByRules(action))
|
.filter(action => this.filterByRules(action))
|
||||||
.map(action => {
|
.map(action => {
|
||||||
if (action.type === ContentActionType.menu) {
|
if (action.type === ContentActionType.menu) {
|
||||||
@@ -368,94 +230,28 @@ export class ExtensionService implements RuleContext {
|
|||||||
.filter(childAction =>
|
.filter(childAction =>
|
||||||
this.filterByRules(childAction)
|
this.filterByRules(childAction)
|
||||||
)
|
)
|
||||||
.reduce(this.reduceSeparators, []);
|
.reduce(reduceSeparators, []);
|
||||||
}
|
}
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
return action;
|
return action;
|
||||||
})
|
})
|
||||||
.reduce(this.reduceEmptyMenus, [])
|
.reduce(reduceEmptyMenus, [])
|
||||||
.reduce(this.reduceSeparators, []);
|
.reduce(reduceSeparators, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
getViewerToolbarActions(): Array<ContentActionRef> {
|
getViewerToolbarActions(): Array<ContentActionRef> {
|
||||||
return this.viewerToolbarActions
|
return this.viewerToolbarActions
|
||||||
.filter(this.filterEnabled)
|
.filter(filterEnabled)
|
||||||
.filter(action => this.filterByRules(action));
|
.filter(action => this.filterByRules(action));
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllowedContextMenuActions(): Array<ContentActionRef> {
|
getAllowedContextMenuActions(): Array<ContentActionRef> {
|
||||||
return this.contextMenuActions
|
return this.contextMenuActions
|
||||||
.filter(this.filterEnabled)
|
.filter(filterEnabled)
|
||||||
.filter(action => this.filterByRules(action));
|
.filter(action => this.filterByRules(action));
|
||||||
}
|
}
|
||||||
|
|
||||||
setActionDefaults(action: ContentActionRef): ContentActionRef {
|
|
||||||
if (action) {
|
|
||||||
action.type = action.type || ContentActionType.default;
|
|
||||||
action.icon = action.icon || 'extension';
|
|
||||||
}
|
|
||||||
return action;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
reduceEmptyMenus(
|
|
||||||
acc: ContentActionRef[],
|
|
||||||
el: ContentActionRef
|
|
||||||
): ContentActionRef[] {
|
|
||||||
if (el.type === ContentActionType.menu) {
|
|
||||||
if ((el.children || []).length === 0) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return acc.concat(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterEnabled(entry: { disabled?: boolean }): boolean {
|
|
||||||
return !entry.disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
copyAction(action: ContentActionRef): ContentActionRef {
|
copyAction(action: ContentActionRef): ContentActionRef {
|
||||||
return {
|
return {
|
||||||
...action,
|
...action,
|
||||||
@@ -529,51 +325,4 @@ export class ExtensionService implements RuleContext {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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] = this.mergeArrays(result[prop], source[prop]);
|
|
||||||
} else if (prop in result && typeof result[prop] === 'object') {
|
|
||||||
result[prop] = this.mergeObjects(result[prop], source[prop]);
|
|
||||||
} else {
|
|
||||||
result[prop] = source[prop];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = this.mergeObjects(map[element.id], element);
|
|
||||||
map[element.id] = merged;
|
|
||||||
} else {
|
|
||||||
result.push(element);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Object.values(map).concat(result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -23,22 +23,17 @@
|
|||||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface NavBarGroupRef {
|
import { ExtensionElement } from './extension-element';
|
||||||
id: string;
|
|
||||||
items: Array<NavBarLinkRef>;
|
|
||||||
|
|
||||||
order?: number;
|
export interface NavBarGroupRef extends ExtensionElement {
|
||||||
disabled?: boolean;
|
items: Array<NavBarLinkRef>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavBarLinkRef {
|
export interface NavBarLinkRef extends ExtensionElement {
|
||||||
id: string;
|
|
||||||
icon: string;
|
icon: string;
|
||||||
title: string;
|
title: string;
|
||||||
route: string;
|
route: string;
|
||||||
|
|
||||||
url?: string; // evaluated at runtime based on route ref
|
url?: string; // evaluated at runtime based on route ref
|
||||||
description?: string;
|
description?: string;
|
||||||
order?: number;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
}
|
||||||
|
@@ -23,14 +23,13 @@
|
|||||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class SidebarTabRef {
|
import { ExtensionElement } from './extension-element';
|
||||||
id: string;
|
|
||||||
|
export interface SidebarTabRef extends ExtensionElement {
|
||||||
title: string;
|
title: string;
|
||||||
component: string;
|
component: string;
|
||||||
|
|
||||||
icon?: string;
|
icon?: string;
|
||||||
disabled?: boolean;
|
|
||||||
order?: number;
|
|
||||||
rules?: {
|
rules?: {
|
||||||
visible?: string;
|
visible?: string;
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
|
@@ -23,11 +23,9 @@
|
|||||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface ViewerExtensionRef {
|
import { ExtensionElement } from './extension-element';
|
||||||
id: string;
|
|
||||||
|
export interface ViewerExtensionRef extends ExtensionElement {
|
||||||
fileExtension: string;
|
fileExtension: string;
|
||||||
component: string;
|
component: string;
|
||||||
|
|
||||||
disabled?: boolean;
|
|
||||||
order?: number;
|
|
||||||
}
|
}
|
||||||
|
@@ -61,6 +61,7 @@ import { NodePermissionService } from '../services/node-permission.service';
|
|||||||
import { ContentApiService } from '../services/content-api.service';
|
import { ContentApiService } from '../services/content-api.service';
|
||||||
import { ExtensionService } from '../extensions/extension.service';
|
import { ExtensionService } from '../extensions/extension.service';
|
||||||
import { ViewUtilService } from '../components/preview/view-util.service';
|
import { ViewUtilService } from '../components/preview/view-util.service';
|
||||||
|
import { ExtensionLoaderService } from '../extensions/extension-loader.service';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -114,6 +115,7 @@ import { ViewUtilService } from '../components/preview/view-util.service';
|
|||||||
NodePermissionService,
|
NodePermissionService,
|
||||||
ContentApiService,
|
ContentApiService,
|
||||||
ExtensionService,
|
ExtensionService,
|
||||||
|
ExtensionLoaderService,
|
||||||
ViewUtilService
|
ViewUtilService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
Reference in New Issue
Block a user