mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-05-19 17:14:45 +00:00
[ACA-1591] Load extensions from multiple files (#521)
* rework extension service, separate file with config * improve loading, optional entries * simplify config and unify content actions * load and merge multiple files * improve plugin loading, introduce second demo * move demo stuff to a plugin * rework navbar to make it pluggable * code and naming convention cleanup * extension schema * switch off custom navbar group by default * hotfix for facetQueries issue * consolidate files, final renames
This commit is contained in:
parent
43a71aa1c8
commit
8c9ffc1160
300
extension.schema.json
Normal file
300
extension.schema.json
Normal file
@ -0,0 +1,300 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://github.com/Alfresco/alfresco-content-app/blob/development/extension.schema.json",
|
||||
"title": "ACA Extension Schema",
|
||||
"description": "Provides a validation schema for ACA extensions",
|
||||
|
||||
"definitions": {
|
||||
"ruleRef": {
|
||||
"type": "object",
|
||||
"required": ["id", "type"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Unique rule definition id",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "Rule evaluator type",
|
||||
"type": "string"
|
||||
},
|
||||
"parameters": {
|
||||
"description": "Rule evaluator parameters",
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/ruleParameter" },
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"ruleParameter": {
|
||||
"type": "object",
|
||||
"required": ["type", "value"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"description": "Rule parameter type",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"description": "Rule parameter value",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"routeRef": {
|
||||
"type": "object",
|
||||
"required": ["id", "path", "component"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Unique route reference identifier.",
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"description": "Route path to register.",
|
||||
"type": "string"
|
||||
},
|
||||
"component": {
|
||||
"description": "Unique identifier for the Component to use with the route.",
|
||||
"type": "string"
|
||||
},
|
||||
"layout": {
|
||||
"description": "Unique identifier for the custom layout component to use.",
|
||||
"type": "string"
|
||||
},
|
||||
"auth": {
|
||||
"description": "List of the authentication guards to use with the route.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minLength": 1,
|
||||
"uniqueItems": true
|
||||
},
|
||||
"data": {
|
||||
"description": "Custom data to pass to the activated route so that your components can access it",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"actionRef": {
|
||||
"type": "object",
|
||||
"required": ["id", "type"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Unique action identifier",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "Action type",
|
||||
"type": "string"
|
||||
},
|
||||
"payload": {
|
||||
"description": "Action payload value (string or expression)",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contentActionRef": {
|
||||
"type": "object",
|
||||
"required": ["id", "type"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Unique action identifier.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "Element type",
|
||||
"type": "string",
|
||||
"enum": ["default", "button", "separator", "menu"]
|
||||
},
|
||||
"title": {
|
||||
"description": "Element title",
|
||||
"type": "string"
|
||||
},
|
||||
"order": {
|
||||
"description": "Element order",
|
||||
"type": "number"
|
||||
},
|
||||
"icon": {
|
||||
"description": "Element icon",
|
||||
"type": "string"
|
||||
},
|
||||
"disabled": {
|
||||
"description": "Toggles disabled state",
|
||||
"type": "boolean"
|
||||
},
|
||||
"children": {
|
||||
"description": "Child entries for the container types.",
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/contentActionRef" },
|
||||
"minItems": 1
|
||||
},
|
||||
"actions": {
|
||||
"description": "Element actions",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"click": {
|
||||
"description": "Action reference for the click handler",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"description": "Element rules",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Rule to evaluate the enabled state",
|
||||
"type": "string"
|
||||
},
|
||||
"visible": {
|
||||
"description": "Rule to evaluate the visibility state",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"navBarLinkRef": {
|
||||
"type": "object",
|
||||
"required": ["id", "icon", "title", "route"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Unique identifier",
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"description": "Element icon",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "Element title",
|
||||
"type": "string"
|
||||
},
|
||||
"route": {
|
||||
"description": "Route reference identifier",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Element description or tooltip",
|
||||
"type": "string"
|
||||
},
|
||||
"order": {
|
||||
"description": "Element order",
|
||||
"type": "number"
|
||||
},
|
||||
"disabled": {
|
||||
"description": "Toggles the disabled state",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"navBarGroupRef": {
|
||||
"type": "object",
|
||||
"required": ["id", "items"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Unique identifier for the navigation group",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Navigation group items",
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/navBarLinkRef" },
|
||||
"minItems": 1
|
||||
},
|
||||
"order": {
|
||||
"description": "Group order",
|
||||
"type": "number"
|
||||
},
|
||||
"disabled": {
|
||||
"description": "Toggles the disabled state",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"type": "object",
|
||||
"required": ["name", "version"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Extension name",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "Extension version",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Brief description on what the extension does"
|
||||
},
|
||||
"references": {
|
||||
"description": "References to external files",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
},
|
||||
"rules": {
|
||||
"description": "List of rule definitions",
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/ruleRef" },
|
||||
"minItems": 1
|
||||
},
|
||||
"routes": {
|
||||
"description": "List of custom application routes",
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/routeRef" },
|
||||
"minItems": 1
|
||||
},
|
||||
"actions": {
|
||||
"description": "List of action definitions",
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/actionRef" },
|
||||
"minItems": 1
|
||||
},
|
||||
"features": {
|
||||
"description": "Application-specific features and extensions",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"create": {
|
||||
"description": "The [New] menu component extensions",
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/contentActionRef" },
|
||||
"minItems": 1
|
||||
},
|
||||
"viewer": {
|
||||
"description": "Viewer component extensions",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"openWith": {
|
||||
"description": "The [Open With] menu extensions",
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/contentActionRef" },
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"navbar": {
|
||||
"description": "Navigation bar extensions",
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/navBarGroupRef" },
|
||||
"minItems": 1
|
||||
},
|
||||
"content": {
|
||||
"description": "Main application content extensions",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"actions": {
|
||||
"description": "Content actions (toolbar, context menus, etc.)",
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/contentActionRef" },
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -36,275 +36,6 @@
|
||||
"preserveState": true,
|
||||
"expandedSidenav": true
|
||||
},
|
||||
"extensions": {
|
||||
"external": [
|
||||
"plugin1.json",
|
||||
"plugin2.json"
|
||||
],
|
||||
"core": {
|
||||
"rules": [
|
||||
{
|
||||
"id": "app.create.canCreateFolder",
|
||||
"type": "app.navigation.folder.canCreate"
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.canEditFolder",
|
||||
"type": "core.every",
|
||||
"parameters": [
|
||||
{ "type": "rule", "value": "app.selection.folder" },
|
||||
{ "type": "rule", "value": "app.selection.folder.canUpdate" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.canViewFile",
|
||||
"type": "app.selection.file"
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.canDownload",
|
||||
"type": "app.selection.canDownload"
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"id": "aca:routes/about",
|
||||
"path": "ext/about",
|
||||
"component": "aca:components/about",
|
||||
"layout": "aca:layouts/main",
|
||||
"auth":[ "aca:auth" ],
|
||||
"data": {
|
||||
"title": "Custom About"
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "aca:actions/create-folder",
|
||||
"type": "CREATE_FOLDER",
|
||||
"payload": null
|
||||
},
|
||||
{
|
||||
"id": "aca:actions/edit-folder",
|
||||
"type": "EDIT_FOLDER",
|
||||
"payload": null
|
||||
},
|
||||
{
|
||||
"id": "aca:actions/download",
|
||||
"type": "DOWNLOAD_NODES",
|
||||
"payload": null
|
||||
},
|
||||
{
|
||||
"id": "aca:actions/preview",
|
||||
"type": "VIEW_FILE",
|
||||
"payload": null
|
||||
},
|
||||
|
||||
{
|
||||
"id": "aca:actions/info",
|
||||
"type": "SNACKBAR_INFO",
|
||||
"payload": "I'm a nice little popup raised by extension."
|
||||
},
|
||||
{
|
||||
"id": "aca:actions/node-name",
|
||||
"type": "SNACKBAR_INFO",
|
||||
"payload": "$('Action for ' + context.selection.first.entry.name)"
|
||||
},
|
||||
{
|
||||
"id": "aca:actions/settings",
|
||||
"type": "NAVIGATE_URL",
|
||||
"payload": "/settings"
|
||||
}
|
||||
],
|
||||
"features": {
|
||||
"create": [
|
||||
{
|
||||
"id": "app.create.folder",
|
||||
"icon": "create_new_folder",
|
||||
"title": "ext: Create Folder",
|
||||
"actions": {
|
||||
"click": "aca:actions/create-folder"
|
||||
},
|
||||
"rules": {
|
||||
"enabled": "app.create.canCreateFolder"
|
||||
}
|
||||
}
|
||||
],
|
||||
"navigation": {
|
||||
"aca:main": [
|
||||
{
|
||||
"id": "aca/personal-files",
|
||||
"order": 100,
|
||||
"icon": "folder",
|
||||
"title": "APP.BROWSE.PERSONAL.SIDENAV_LINK.LABEL",
|
||||
"description": "APP.BROWSE.PERSONAL.SIDENAV_LINK.TOOLTIP",
|
||||
"route": "personal-files"
|
||||
},
|
||||
{
|
||||
"id": "aca/libraries",
|
||||
"order": 101,
|
||||
"icon": "group_work",
|
||||
"title": "APP.BROWSE.LIBRARIES.SIDENAV_LINK.LABEL",
|
||||
"description": "APP.BROWSE.LIBRARIES.SIDENAV_LINK.TOOLTIP",
|
||||
"route": "libraries"
|
||||
}
|
||||
],
|
||||
"aca:secondary": [
|
||||
{
|
||||
"id": "aca/shared",
|
||||
"order": 100,
|
||||
"icon": "people",
|
||||
"title": "APP.BROWSE.SHARED.SIDENAV_LINK.LABEL",
|
||||
"description": "APP.BROWSE.SHARED.SIDENAV_LINK.TOOLTIP",
|
||||
"route": "shared"
|
||||
},
|
||||
{
|
||||
"id": "aca/recent-files",
|
||||
"order": 101,
|
||||
"icon": "schedule",
|
||||
"title": "APP.BROWSE.RECENT.SIDENAV_LINK.LABEL",
|
||||
"description": "APP.BROWSE.RECENT.SIDENAV_LINK.TOOLTIP",
|
||||
"route": "recent-files"
|
||||
},
|
||||
{
|
||||
"id": "aca/favorites",
|
||||
"order": 102,
|
||||
"icon": "star",
|
||||
"title": "APP.BROWSE.FAVORITES.SIDENAV_LINK.LABEL",
|
||||
"description": "APP.BROWSE.FAVORITES.SIDENAV_LINK.TOOLTIP",
|
||||
"route": "favorites"
|
||||
},
|
||||
{
|
||||
"id": "aca/trashcan",
|
||||
"order": 103,
|
||||
"icon": "delete",
|
||||
"title": "APP.BROWSE.TRASHCAN.SIDENAV_LINK.LABEL",
|
||||
"description": "APP.BROWSE.TRASHCAN.SIDENAV_LINK.TOOLTIP",
|
||||
"route": "trashcan"
|
||||
}
|
||||
],
|
||||
"aca:demo": [
|
||||
{
|
||||
"disabled": true,
|
||||
"id": "aca:demo/link1",
|
||||
"order": 100,
|
||||
"icon": "build",
|
||||
"title": "About (native)",
|
||||
"description": "Uses native application route",
|
||||
"route": "about"
|
||||
},
|
||||
{
|
||||
"disabled": true,
|
||||
"id": "aca:demo/link2",
|
||||
"order": 100,
|
||||
"icon": "build",
|
||||
"title": "About (custom)",
|
||||
"description": "Uses custom defined route",
|
||||
"route": "aca:routes/about"
|
||||
}
|
||||
]
|
||||
},
|
||||
"viewer": {
|
||||
"open-with": [
|
||||
{
|
||||
"disabled": false,
|
||||
"id": "aca:viewer/action1",
|
||||
"order": 100,
|
||||
"icon": "build",
|
||||
"title": "Snackbar",
|
||||
"action": "aca:actions/info"
|
||||
}
|
||||
]
|
||||
},
|
||||
"content": {
|
||||
"actions": [
|
||||
{
|
||||
"id": "aca:toolbar/separator-1",
|
||||
"order": 5,
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"id": "aca:toolbar/create-folder",
|
||||
"type": "button",
|
||||
"order": 10,
|
||||
"title": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER",
|
||||
"icon": "create_new_folder",
|
||||
"actions": {
|
||||
"click": "aca:actions/create-folder"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.create.canCreateFolder"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "aca:toolbar/preview",
|
||||
"type": "button",
|
||||
"order": 15,
|
||||
"title": "APP.ACTIONS.VIEW",
|
||||
"icon": "open_in_browser",
|
||||
"actions": {
|
||||
"click": "aca:actions/preview"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.toolbar.canViewFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "aca:toolbar/download",
|
||||
"type": "button",
|
||||
"order": 20,
|
||||
"title": "APP.ACTIONS.DOWNLOAD",
|
||||
"icon": "get_app",
|
||||
"actions": {
|
||||
"click": "aca:actions/download"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.toolbar.canDownload"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "aca:toolbar/edit-folder",
|
||||
"type": "button",
|
||||
"order": 30,
|
||||
"title": "APP.ACTIONS.EDIT",
|
||||
"icon": "create",
|
||||
"actions": {
|
||||
"click": "aca:actions/edit-folder"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.toolbar.canEditFolder"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"id": "aca:toolbar/separator-2",
|
||||
"order": 200,
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"id": "aca:toolbar/menu-1",
|
||||
"type": "menu",
|
||||
"icon": "storage",
|
||||
"order": 300,
|
||||
"children": [
|
||||
{
|
||||
"id": "aca:action3",
|
||||
"type": "button",
|
||||
"title": "Settings",
|
||||
"icon": "settings_applications",
|
||||
"actions": {
|
||||
"click": "aca:actions/settings"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "aca:toolbar/separator-3",
|
||||
"type": "separator"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"languages": [
|
||||
{
|
||||
"key": "de",
|
||||
@ -460,6 +191,7 @@
|
||||
{ "field": "modifier", "mincount": 1, "label": "SEARCH.FACET_FIELDS.MODIFIER" },
|
||||
{ "field": "SITE", "mincount": 1, "label": "SEARCH.FACET_FIELDS.FILE_LIBRARY" }
|
||||
],
|
||||
"facetQueries": {},
|
||||
"categories": [
|
||||
{
|
||||
"id": "modifiedDate",
|
||||
|
@ -91,8 +91,6 @@ export class AppComponent implements OnInit {
|
||||
pageTitle.setTitle(data.title || '');
|
||||
});
|
||||
|
||||
this.extensions.init();
|
||||
|
||||
this.router.config.unshift(...this.extensions.getApplicationRoutes());
|
||||
|
||||
this.uploadService.fileUploadError.subscribe(error =>
|
||||
|
@ -24,7 +24,7 @@
|
||||
*/
|
||||
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { NgModule, APP_INITIALIZER } from '@angular/core';
|
||||
import { RouterModule, RouteReuseStrategy } from '@angular/router';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
@ -84,7 +84,11 @@ import { NodePermissionsDialogComponent } from './dialogs/node-permissions/node-
|
||||
import { NodePermissionsDirective } from './directives/node-permissions.directive';
|
||||
import { PermissionsManagerComponent } from './components/permission-manager/permissions-manager.component';
|
||||
import { AppRouteReuseStrategy } from './app.routes.strategy';
|
||||
import { ExtensionService } from './extensions/extension.service';
|
||||
|
||||
export function setupExtensionServiceFactory(service: ExtensionService): Function {
|
||||
return () => service.load();
|
||||
}
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@ -96,7 +100,7 @@ import { AppRouteReuseStrategy } from './app.routes.strategy';
|
||||
enableTracing: false // enable for debug only
|
||||
}),
|
||||
MaterialModule,
|
||||
CoreModule,
|
||||
CoreModule.forRoot(),
|
||||
ContentModule,
|
||||
AppStoreModule,
|
||||
CoreExtensionsModule,
|
||||
@ -159,7 +163,13 @@ import { AppRouteReuseStrategy } from './app.routes.strategy';
|
||||
NodePermissionService,
|
||||
ProfileResolver,
|
||||
ExperimentalGuard,
|
||||
ContentApiService
|
||||
ContentApiService,
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: setupExtensionServiceFactory,
|
||||
deps: [ExtensionService],
|
||||
multi: true
|
||||
}
|
||||
],
|
||||
entryComponents: [
|
||||
LibraryDialogComponent,
|
||||
|
@ -36,8 +36,8 @@ import { AppStore } from '../store/states/app.state';
|
||||
import { SelectionState } from '../store/states/selection.state';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { ExtensionService } from '../extensions/extension.service';
|
||||
import { ContentActionExtension } from '../extensions/content-action.extension';
|
||||
import { ContentManagementService } from '../services/content-management.service';
|
||||
import { ContentActionRef } from '../extensions/action.extensions';
|
||||
|
||||
export abstract class PageComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -52,7 +52,7 @@ export abstract class PageComponent implements OnInit, OnDestroy {
|
||||
selection: SelectionState;
|
||||
displayMode = DisplayMode.List;
|
||||
sharedPreviewUrl$: Observable<string>;
|
||||
actions: Array<ContentActionExtension> = [];
|
||||
actions: Array<ContentActionRef> = [];
|
||||
canUpdateFile = false;
|
||||
canUpdateNode = false;
|
||||
canDelete = false;
|
||||
|
@ -18,7 +18,7 @@
|
||||
<adf-viewer-open-with *ifExperimental="'extensions'">
|
||||
<button *ngFor="let entry of openWith"
|
||||
mat-menu-item
|
||||
(click)="runAction(entry.action)">
|
||||
(click)="runAction(entry.actions.click)">
|
||||
<mat-icon>{{ entry.icon }}</mat-icon>
|
||||
<span>{{ entry.title }}</span>
|
||||
</button>
|
||||
|
@ -32,8 +32,8 @@ import { DeleteNodesAction, SetSelectedNodesAction } from '../../store/actions';
|
||||
import { PageComponent } from '../page.component';
|
||||
import { ContentApiService } from '../../services/content-api.service';
|
||||
import { ExtensionService } from '../../extensions/extension.service';
|
||||
import { OpenWithExtension } from '../../extensions/open-with.extension';
|
||||
import { ContentManagementService } from '../../services/content-management.service';
|
||||
import { ContentActionRef } from '../../extensions/action.extensions';
|
||||
@Component({
|
||||
selector: 'app-preview',
|
||||
templateUrl: 'preview.component.html',
|
||||
@ -52,7 +52,7 @@ export class PreviewComponent extends PageComponent implements OnInit {
|
||||
previousNodeId: string;
|
||||
nextNodeId: string;
|
||||
navigateMultiple = false;
|
||||
openWith: Array<OpenWithExtension> = [];
|
||||
openWith: Array<ContentActionRef> = [];
|
||||
|
||||
constructor(
|
||||
private contentApi: ContentApiService,
|
||||
|
@ -59,13 +59,13 @@
|
||||
|
||||
<div class="sidenav__section sidenav__section--menu" *ngFor="let group of groups">
|
||||
<ul class="sidenav-menu">
|
||||
<li *ngFor="let item of group" class="sidenav-menu__item"
|
||||
<li *ngFor="let item of group.items" class="sidenav-menu__item"
|
||||
routerLinkActive
|
||||
#rla="routerLinkActive"
|
||||
title="{{ item.description | translate }}">
|
||||
|
||||
<button
|
||||
[routerLink]="item.route"
|
||||
[routerLink]="item.url"
|
||||
[color]="rla.isActive ? 'accent': 'primary'"
|
||||
[attr.aria-label]="item.title | translate"
|
||||
mat-icon-button
|
||||
@ -78,7 +78,7 @@
|
||||
</button>
|
||||
|
||||
<span #rippleTrigger
|
||||
[routerLink]="item.route"
|
||||
[routerLink]="item.url"
|
||||
class="menu__item--label"
|
||||
[hidden]="!showLabel"
|
||||
[ngClass]="{
|
||||
|
@ -28,13 +28,13 @@ import { Component, Input, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Node } from 'alfresco-js-api';
|
||||
import { NodePermissionService } from '../../services/node-permission.service';
|
||||
import { ExtensionService } from '../../extensions/extension.service';
|
||||
import { NavigationExtension } from '../../extensions/navigation.extension';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../../store/states';
|
||||
import { CreateFolderAction } from '../../store/actions';
|
||||
import { currentFolder } from '../../store/selectors/app.selectors';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { ContentActionExtension } from '../../extensions/content-action.extension';
|
||||
import { NavBarGroupRef } from '../../extensions/navbar.extensions';
|
||||
import { ContentActionRef } from '../../extensions/action.extensions';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sidenav',
|
||||
@ -45,8 +45,8 @@ export class SidenavComponent implements OnInit, OnDestroy {
|
||||
@Input() showLabel: boolean;
|
||||
|
||||
node: Node = null;
|
||||
groups: Array<NavigationExtension[]> = [];
|
||||
createActions: Array<ContentActionExtension> = [];
|
||||
groups: Array<NavBarGroupRef> = [];
|
||||
createActions: Array<ContentActionRef> = [];
|
||||
canCreateContent = false;
|
||||
onDestroy$: Subject<boolean> = new Subject<boolean>();
|
||||
|
||||
@ -64,7 +64,7 @@ export class SidenavComponent implements OnInit, OnDestroy {
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe(node => {
|
||||
this.node = node;
|
||||
this.createActions = this.extensions.getFolderCreateActions(node);
|
||||
this.createActions = this.extensions.getCreateActions();
|
||||
this.canCreateContent = node && this.permission.check(node, ['create']);
|
||||
});
|
||||
}
|
||||
|
@ -30,21 +30,28 @@ export enum ContentActionType {
|
||||
menu = 'menu'
|
||||
}
|
||||
|
||||
export interface ContentActionExtension {
|
||||
export interface ContentActionRef {
|
||||
id: string;
|
||||
type: ContentActionType;
|
||||
|
||||
title?: string;
|
||||
order?: number;
|
||||
title: string;
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
children?: Array<ContentActionExtension>;
|
||||
children?: Array<ContentActionRef>;
|
||||
actions?: {
|
||||
click?: string;
|
||||
[key: string]: string;
|
||||
};
|
||||
rules: {
|
||||
rules?: {
|
||||
enabled?: string;
|
||||
visible?: string;
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ActionRef {
|
||||
id: string;
|
||||
type: string;
|
||||
payload?: string;
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
/*!
|
||||
* @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 ActionRef {
|
||||
id: string;
|
||||
type: string;
|
||||
payload?: string;
|
||||
}
|
@ -1,141 +0,0 @@
|
||||
/*!
|
||||
* @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 { AppConfigService } from '@alfresco/adf-core';
|
||||
import { ActionService } from './action.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../../store/states';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AppTestingModule } from '../../testing/app-testing.module';
|
||||
|
||||
describe('ActionService', () => {
|
||||
let config: AppConfigService;
|
||||
let actions: ActionService;
|
||||
let store: Store<AppStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [AppTestingModule]
|
||||
});
|
||||
|
||||
actions = TestBed.get(ActionService);
|
||||
store = TestBed.get(Store);
|
||||
|
||||
config = TestBed.get(AppConfigService);
|
||||
config.config['extensions'] = {};
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
beforeEach(() => {
|
||||
config.config.extensions = {
|
||||
core: {
|
||||
actions: [
|
||||
{
|
||||
id: 'aca:actions/create-folder',
|
||||
type: 'CREATE_FOLDER',
|
||||
payload: 'folder-name'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should load actions from the config', () => {
|
||||
actions.init();
|
||||
expect(actions.actions.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should have an empty action list if config provides nothing', () => {
|
||||
config.config.extensions = {};
|
||||
actions.init();
|
||||
|
||||
expect(actions.actions).toEqual([]);
|
||||
});
|
||||
|
||||
it('should find action by id', () => {
|
||||
actions.init();
|
||||
|
||||
const action = actions.getActionById(
|
||||
'aca:actions/create-folder'
|
||||
);
|
||||
expect(action).toBeTruthy();
|
||||
expect(action.type).toBe('CREATE_FOLDER');
|
||||
expect(action.payload).toBe('folder-name');
|
||||
});
|
||||
|
||||
it('should not find action by id', () => {
|
||||
actions.init();
|
||||
|
||||
const action = actions.getActionById('missing');
|
||||
expect(action).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should run the action via store', () => {
|
||||
actions.init();
|
||||
spyOn(store, 'dispatch').and.stub();
|
||||
|
||||
actions.runActionById('aca:actions/create-folder');
|
||||
expect(store.dispatch).toHaveBeenCalledWith({
|
||||
type: 'CREATE_FOLDER',
|
||||
payload: 'folder-name'
|
||||
});
|
||||
});
|
||||
|
||||
it('should not use store if action is missing', () => {
|
||||
actions.init();
|
||||
spyOn(store, 'dispatch').and.stub();
|
||||
|
||||
actions.runActionById('missing');
|
||||
expect(store.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('expressions', () => {
|
||||
it('should eval static value', () => {
|
||||
const value = actions.runExpression('hello world');
|
||||
expect(value).toBe('hello world');
|
||||
});
|
||||
|
||||
it('should eval string as an expression', () => {
|
||||
const value = actions.runExpression('$( "hello world" )');
|
||||
expect(value).toBe('hello world');
|
||||
});
|
||||
|
||||
it('should eval expression with no context', () => {
|
||||
const value = actions.runExpression('$( 1 + 1 )');
|
||||
expect(value).toBe(2);
|
||||
});
|
||||
|
||||
it('should eval expression with context', () => {
|
||||
const context = {
|
||||
a: 'hey',
|
||||
b: 'there'
|
||||
};
|
||||
const expression = '$( context.a + " " + context.b + "!" )';
|
||||
const value = actions.runExpression(expression, context);
|
||||
expect(value).toBe('hey there!');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,76 +0,0 @@
|
||||
/*!
|
||||
* @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 { AppConfigService } from '@alfresco/adf-core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../../store/states';
|
||||
import { ActionRef } from './action-ref';
|
||||
|
||||
@Injectable()
|
||||
export class ActionService {
|
||||
actions: Array<ActionRef> = [];
|
||||
|
||||
constructor(
|
||||
private config: AppConfigService,
|
||||
private store: Store<AppStore>
|
||||
) {}
|
||||
|
||||
init() {
|
||||
this.actions = this.config.get<Array<ActionRef>>(
|
||||
'extensions.core.actions',
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
getActionById(id: string): ActionRef {
|
||||
return this.actions.find(action => action.id === id);
|
||||
}
|
||||
|
||||
runActionById(id: string, context?: any) {
|
||||
const action = this.getActionById(id);
|
||||
if (action) {
|
||||
const { type, payload } = action;
|
||||
const expression = this.runExpression(payload, context);
|
||||
|
||||
this.store.dispatch({ type, payload: expression });
|
||||
}
|
||||
}
|
||||
|
||||
runExpression(value: string, context?: any) {
|
||||
const pattern = new RegExp(/\$\((.*\)?)\)/g);
|
||||
const matches = pattern.exec(value);
|
||||
|
||||
if (matches && matches.length > 1) {
|
||||
const expression = matches[1];
|
||||
const fn = new Function('context', `return ${expression}`);
|
||||
const result = fn(context);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
@ -31,13 +31,13 @@ import {
|
||||
OnInit,
|
||||
OnDestroy
|
||||
} from '@angular/core';
|
||||
import { ContentActionExtension } from '../../content-action.extension';
|
||||
import { AppStore, SelectionState } from '../../../store/states';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { ExtensionService } from '../../extension.service';
|
||||
import { appSelection } from '../../../store/selectors/app.selectors';
|
||||
import { Subject } from 'rxjs/Rx';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { ContentActionRef } from '../../action.extensions';
|
||||
|
||||
@Component({
|
||||
selector: 'aca-toolbar-action',
|
||||
@ -47,7 +47,7 @@ import { takeUntil } from 'rxjs/operators';
|
||||
host: { class: 'aca-toolbar-action' }
|
||||
})
|
||||
export class ToolbarActionComponent implements OnInit, OnDestroy {
|
||||
@Input() entry: ContentActionExtension;
|
||||
@Input() entry: ContentActionRef;
|
||||
|
||||
selection: SelectionState;
|
||||
onDestroy$: Subject<boolean> = new Subject<boolean>();
|
||||
|
@ -30,21 +30,38 @@ import { AboutComponent } from '../components/about/about.component';
|
||||
import { LayoutComponent } from '../components/layout/layout.component';
|
||||
import { ToolbarActionComponent } from './components/toolbar-action/toolbar-action.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RuleService } from './rules/rule.service';
|
||||
import { ActionService } from './actions/action.service';
|
||||
import { every, some } from './evaluators/core.evaluators';
|
||||
import {
|
||||
canCreateFolder,
|
||||
hasFolderSelected,
|
||||
canUpdateSelectedFolder,
|
||||
hasFileSelected,
|
||||
canDownloadSelection
|
||||
} from './evaluators/app.evaluators';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, CoreModule.forChild()],
|
||||
declarations: [ToolbarActionComponent],
|
||||
exports: [ToolbarActionComponent],
|
||||
entryComponents: [AboutComponent],
|
||||
providers: [ExtensionService, RuleService, ActionService]
|
||||
providers: [ExtensionService]
|
||||
})
|
||||
export class CoreExtensionsModule {
|
||||
constructor(extensions: ExtensionService) {
|
||||
extensions
|
||||
.setComponent('aca:layouts/main', LayoutComponent)
|
||||
.setComponent('aca:components/about', AboutComponent)
|
||||
.setAuthGuard('aca:auth', AuthGuardEcm);
|
||||
.setComponent('app.layout.main', LayoutComponent)
|
||||
.setComponent('app.components.about', AboutComponent)
|
||||
.setAuthGuard('app.auth', AuthGuardEcm)
|
||||
|
||||
.setEvaluator('core.every', every)
|
||||
.setEvaluator('core.some', some)
|
||||
.setEvaluator('app.selection.canDownload', canDownloadSelection)
|
||||
.setEvaluator('app.selection.file', hasFileSelected)
|
||||
.setEvaluator('app.selection.folder', hasFolderSelected)
|
||||
.setEvaluator(
|
||||
'app.selection.folder.canUpdate',
|
||||
canUpdateSelectedFolder
|
||||
)
|
||||
.setEvaluator('app.navigation.folder.canCreate', canCreateFolder);
|
||||
}
|
||||
}
|
||||
|
@ -23,9 +23,8 @@
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { RuleContext } from './rule-context';
|
||||
import { RuleParameter } from './rule-parameter';
|
||||
import { Node } from 'alfresco-js-api';
|
||||
import { RuleContext, RuleParameter } from '../rule.extensions';
|
||||
|
||||
export function canCreateFolder(context: RuleContext, ...args: RuleParameter[]): boolean {
|
||||
const folder = context.navigation.currentFolder;
|
@ -23,8 +23,7 @@
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { RuleContext } from './rule-context';
|
||||
import { RuleParameter } from './rule-parameter';
|
||||
import { RuleContext, RuleParameter } from '../rule.extensions';
|
||||
|
||||
export function every(context: RuleContext, ...args: RuleParameter[]): boolean {
|
||||
if (!args || args.length === 0) {
|
@ -23,13 +23,26 @@
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { RuleRef } from './rules/rule-ref';
|
||||
import { ActionRef } from './actions/action-ref';
|
||||
import { RouteRef } from './route-ref';
|
||||
import { NavBarGroupRef } from './navbar.extensions';
|
||||
import { RouteRef } from './routing.extensions';
|
||||
import { RuleRef } from './rule.extensions';
|
||||
import { ActionRef, ContentActionRef } from './action.extensions';
|
||||
|
||||
export interface ExtensionConfig {
|
||||
version: string;
|
||||
references?: Array<string>;
|
||||
rules?: Array<RuleRef>;
|
||||
routes?: Array<RouteRef>;
|
||||
actions?: Array<ActionRef>;
|
||||
features?: { [key: string]: any };
|
||||
features?: {
|
||||
[key: string]: any;
|
||||
create?: Array<ContentActionRef>;
|
||||
viewer?: {
|
||||
openWith?: Array<ContentActionRef>;
|
||||
};
|
||||
navbar?: Array<NavBarGroupRef>;
|
||||
content?: {
|
||||
actions?: Array<ContentActionRef>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -26,22 +26,98 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AppTestingModule } from '../testing/app-testing.module';
|
||||
import { ExtensionService } from './extension.service';
|
||||
import { AppConfigService } from '@alfresco/adf-core';
|
||||
import { ContentActionType } from './content-action.extension';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppStore } from '../store/states';
|
||||
import { ContentActionType } from './action.extensions';
|
||||
|
||||
describe('ExtensionService', () => {
|
||||
let config: AppConfigService;
|
||||
let extensions: ExtensionService;
|
||||
let store: Store<AppStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [AppTestingModule]
|
||||
});
|
||||
|
||||
store = TestBed.get(Store);
|
||||
extensions = TestBed.get(ExtensionService);
|
||||
});
|
||||
|
||||
config = TestBed.get(AppConfigService);
|
||||
config.config['extensions'] = {};
|
||||
describe('actions', () => {
|
||||
beforeEach(() => {
|
||||
extensions.setup({
|
||||
version: '1.0.0',
|
||||
actions: [
|
||||
{
|
||||
id: 'aca:actions/create-folder',
|
||||
type: 'CREATE_FOLDER',
|
||||
payload: 'folder-name'
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should load actions from the config', () => {
|
||||
expect(extensions.actions.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should find action by id', () => {
|
||||
const action = extensions.getActionById(
|
||||
'aca:actions/create-folder'
|
||||
);
|
||||
expect(action).toBeTruthy();
|
||||
expect(action.type).toBe('CREATE_FOLDER');
|
||||
expect(action.payload).toBe('folder-name');
|
||||
});
|
||||
|
||||
it('should not find action by id', () => {
|
||||
const action = extensions.getActionById('missing');
|
||||
expect(action).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should run the action via store', () => {
|
||||
spyOn(store, 'dispatch').and.stub();
|
||||
|
||||
extensions.runActionById('aca:actions/create-folder');
|
||||
expect(store.dispatch).toHaveBeenCalledWith({
|
||||
type: 'CREATE_FOLDER',
|
||||
payload: 'folder-name'
|
||||
});
|
||||
});
|
||||
|
||||
it('should not use store if action is missing', () => {
|
||||
spyOn(store, 'dispatch').and.stub();
|
||||
|
||||
extensions.runActionById('missing');
|
||||
expect(store.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('expressions', () => {
|
||||
it('should eval static value', () => {
|
||||
const value = extensions.runExpression('hello world');
|
||||
expect(value).toBe('hello world');
|
||||
});
|
||||
|
||||
it('should eval string as an expression', () => {
|
||||
const value = extensions.runExpression('$( "hello world" )');
|
||||
expect(value).toBe('hello world');
|
||||
});
|
||||
|
||||
it('should eval expression with no context', () => {
|
||||
const value = extensions.runExpression('$( 1 + 1 )');
|
||||
expect(value).toBe(2);
|
||||
});
|
||||
|
||||
it('should eval expression with context', () => {
|
||||
const context = {
|
||||
a: 'hey',
|
||||
b: 'there'
|
||||
};
|
||||
const expression = '$( context.a + " " + context.b + "!" )';
|
||||
const value = extensions.runExpression(expression, context);
|
||||
expect(value).toBe('hey there!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('auth guards', () => {
|
||||
@ -54,8 +130,6 @@ describe('ExtensionService', () => {
|
||||
|
||||
extensions.authGuards['guard1'] = guard1;
|
||||
extensions.authGuards['guard2'] = guard2;
|
||||
|
||||
extensions.init();
|
||||
});
|
||||
|
||||
it('should fetch auth guards by ids', () => {
|
||||
@ -86,7 +160,6 @@ describe('ExtensionService', () => {
|
||||
component1 = {};
|
||||
|
||||
extensions.components['component-1'] = component1;
|
||||
extensions.init();
|
||||
});
|
||||
|
||||
it('should fetch registered component', () => {
|
||||
@ -105,22 +178,21 @@ describe('ExtensionService', () => {
|
||||
let guard1;
|
||||
|
||||
beforeEach(() => {
|
||||
config.config.extensions = {
|
||||
core: {
|
||||
routes: [
|
||||
{
|
||||
id: 'aca:routes/about',
|
||||
path: 'ext/about',
|
||||
component: 'aca:components/about',
|
||||
layout: 'aca:layouts/main',
|
||||
auth: ['aca:auth'],
|
||||
data: {
|
||||
title: 'Custom About'
|
||||
}
|
||||
extensions.setup({
|
||||
version: '1.0.0',
|
||||
routes: [
|
||||
{
|
||||
id: 'aca:routes/about',
|
||||
path: 'ext/about',
|
||||
component: 'aca:components/about',
|
||||
layout: 'aca:layouts/main',
|
||||
auth: ['aca:auth'],
|
||||
data: {
|
||||
title: 'Custom About'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
component1 = {};
|
||||
component2 = {};
|
||||
@ -129,8 +201,6 @@ describe('ExtensionService', () => {
|
||||
|
||||
guard1 = {};
|
||||
extensions.authGuards['aca:auth'] = guard1;
|
||||
|
||||
extensions.init();
|
||||
});
|
||||
|
||||
it('should load routes from the config', () => {
|
||||
@ -166,60 +236,54 @@ describe('ExtensionService', () => {
|
||||
|
||||
describe('content actions', () => {
|
||||
it('should load content actions from the config', () => {
|
||||
config.config.extensions = {
|
||||
core: {
|
||||
features: {
|
||||
content: {
|
||||
actions: [
|
||||
{
|
||||
id: 'aca:toolbar/separator-1',
|
||||
order: 1,
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
id: 'aca:toolbar/separator-2',
|
||||
order: 2,
|
||||
type: 'separator'
|
||||
}
|
||||
]
|
||||
}
|
||||
extensions.setup({
|
||||
version: '1.0.0',
|
||||
features: {
|
||||
content: {
|
||||
actions: [
|
||||
{
|
||||
id: 'aca:toolbar/separator-1',
|
||||
order: 1,
|
||||
type: ContentActionType.separator,
|
||||
title: 'action1',
|
||||
},
|
||||
{
|
||||
id: 'aca:toolbar/separator-2',
|
||||
order: 2,
|
||||
type: ContentActionType.separator,
|
||||
title: 'action2'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
extensions.init();
|
||||
expect(extensions.contentActions.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should have an empty content action list if config is empty', () => {
|
||||
config.config.extensions = {};
|
||||
extensions.init();
|
||||
expect(extensions.contentActions).toEqual([]);
|
||||
});
|
||||
|
||||
it('should sort content actions by order', () => {
|
||||
config.config.extensions = {
|
||||
core: {
|
||||
features: {
|
||||
content: {
|
||||
actions: [
|
||||
{
|
||||
id: 'aca:toolbar/separator-2',
|
||||
order: 2,
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
id: 'aca:toolbar/separator-1',
|
||||
order: 1,
|
||||
type: 'separator'
|
||||
}
|
||||
]
|
||||
}
|
||||
extensions.setup({
|
||||
version: '1.0.0',
|
||||
features: {
|
||||
content: {
|
||||
actions: [
|
||||
{
|
||||
id: 'aca:toolbar/separator-2',
|
||||
order: 2,
|
||||
type: ContentActionType.separator,
|
||||
title: 'action2'
|
||||
},
|
||||
{
|
||||
id: 'aca:toolbar/separator-1',
|
||||
order: 1,
|
||||
type: ContentActionType.separator,
|
||||
title: 'action1'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
extensions.init();
|
||||
expect(extensions.contentActions.length).toBe(2);
|
||||
expect(extensions.contentActions[0].id).toBe(
|
||||
'aca:toolbar/separator-1'
|
||||
@ -232,94 +296,97 @@ describe('ExtensionService', () => {
|
||||
|
||||
describe('open with', () => {
|
||||
it('should load [open with] actions for the viewer', () => {
|
||||
config.config.extensions = {
|
||||
core: {
|
||||
features: {
|
||||
viewer: {
|
||||
'open-with': [
|
||||
{
|
||||
disabled: false,
|
||||
id: 'aca:viewer/action1',
|
||||
order: 100,
|
||||
icon: 'build',
|
||||
title: 'Snackbar',
|
||||
action: 'aca:actions/info'
|
||||
extensions.setup({
|
||||
version: '1.0.0',
|
||||
features: {
|
||||
viewer: {
|
||||
openWith: [
|
||||
{
|
||||
disabled: false,
|
||||
id: 'aca:viewer/action1',
|
||||
order: 100,
|
||||
icon: 'build',
|
||||
title: 'Snackbar',
|
||||
type: ContentActionType.default,
|
||||
actions: {
|
||||
click: 'aca:actions/info'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
extensions.init();
|
||||
expect(extensions.openWithActions.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should have an empty [open with] list if config is empty', () => {
|
||||
config.config.extensions = {};
|
||||
extensions.init();
|
||||
expect(extensions.openWithActions).toEqual([]);
|
||||
});
|
||||
|
||||
it('should load only enabled [open with] actions for the viewer', () => {
|
||||
config.config.extensions = {
|
||||
core: {
|
||||
features: {
|
||||
viewer: {
|
||||
'open-with': [
|
||||
{
|
||||
id: 'aca:viewer/action2',
|
||||
order: 200,
|
||||
icon: 'build',
|
||||
title: 'Snackbar',
|
||||
action: 'aca:actions/info'
|
||||
},
|
||||
{
|
||||
disabled: true,
|
||||
id: 'aca:viewer/action1',
|
||||
order: 100,
|
||||
icon: 'build',
|
||||
title: 'Snackbar',
|
||||
action: 'aca:actions/info'
|
||||
extensions.setup({
|
||||
version: '1.0.0',
|
||||
features: {
|
||||
viewer: {
|
||||
openWith: [
|
||||
{
|
||||
id: 'aca:viewer/action2',
|
||||
order: 200,
|
||||
icon: 'build',
|
||||
title: 'Snackbar',
|
||||
type: ContentActionType.default,
|
||||
actions: {
|
||||
click: 'aca:actions/info'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
disabled: true,
|
||||
id: 'aca:viewer/action1',
|
||||
order: 100,
|
||||
icon: 'build',
|
||||
title: 'Snackbar',
|
||||
type: ContentActionType.default,
|
||||
actions: {
|
||||
click: 'aca:actions/info'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
extensions.init();
|
||||
expect(extensions.openWithActions.length).toBe(1);
|
||||
expect(extensions.openWithActions[0].id).toBe('aca:viewer/action2');
|
||||
});
|
||||
|
||||
it('should sort [open with] actions by order', () => {
|
||||
config.config.extensions = {
|
||||
core: {
|
||||
features: {
|
||||
viewer: {
|
||||
'open-with': [
|
||||
{
|
||||
id: 'aca:viewer/action2',
|
||||
order: 200,
|
||||
icon: 'build',
|
||||
title: 'Snackbar',
|
||||
action: 'aca:actions/info'
|
||||
},
|
||||
{
|
||||
id: 'aca:viewer/action1',
|
||||
order: 100,
|
||||
icon: 'build',
|
||||
title: 'Snackbar',
|
||||
action: 'aca:actions/info'
|
||||
extensions.setup({
|
||||
version: '1.0.0',
|
||||
features: {
|
||||
viewer: {
|
||||
openWith: [
|
||||
{
|
||||
id: 'aca:viewer/action2',
|
||||
order: 200,
|
||||
icon: 'build',
|
||||
title: 'Snackbar',
|
||||
type: ContentActionType.default,
|
||||
actions: {
|
||||
click: 'aca:actions/info'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'aca:viewer/action1',
|
||||
order: 100,
|
||||
icon: 'build',
|
||||
title: 'Snackbar',
|
||||
type: ContentActionType.default,
|
||||
actions: {
|
||||
click: 'aca:actions/info'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
extensions.init();
|
||||
expect(extensions.openWithActions.length).toBe(2);
|
||||
expect(extensions.openWithActions[0].id).toBe('aca:viewer/action1');
|
||||
expect(extensions.openWithActions[1].id).toBe('aca:viewer/action2');
|
||||
@ -328,67 +395,47 @@ describe('ExtensionService', () => {
|
||||
|
||||
describe('create', () => {
|
||||
it('should load [create] actions from config', () => {
|
||||
config.config.extensions = {
|
||||
core: {
|
||||
features: {
|
||||
create: [
|
||||
{
|
||||
disabled: false,
|
||||
id: 'aca:create/folder',
|
||||
order: 100,
|
||||
icon: 'create_new_folder',
|
||||
title: 'ext: Create Folder',
|
||||
target: {
|
||||
permissions: ['create'],
|
||||
action: 'aca:actions/create-folder'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
extensions.setup({
|
||||
version: '1.0.0',
|
||||
features: {
|
||||
create: [
|
||||
{
|
||||
id: 'aca:create/folder',
|
||||
order: 100,
|
||||
icon: 'create_new_folder',
|
||||
title: 'ext: Create Folder',
|
||||
type: ContentActionType.default
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
extensions.init();
|
||||
expect(extensions.createActions.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should have an empty [create] actions if config is empty', () => {
|
||||
config.config.extensions = {};
|
||||
extensions.init();
|
||||
expect(extensions.createActions).toEqual([]);
|
||||
});
|
||||
|
||||
it('should sort [create] actions by order', () => {
|
||||
config.config.extensions = {
|
||||
core: {
|
||||
features: {
|
||||
create: [
|
||||
{
|
||||
id: 'aca:create/folder',
|
||||
order: 100,
|
||||
icon: 'create_new_folder',
|
||||
title: 'ext: Create Folder',
|
||||
target: {
|
||||
permissions: ['create'],
|
||||
action: 'aca:actions/create-folder'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'aca:create/folder-2',
|
||||
order: 10,
|
||||
icon: 'create_new_folder',
|
||||
title: 'ext: Create Folder',
|
||||
target: {
|
||||
permissions: ['create'],
|
||||
action: 'aca:actions/create-folder'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
extensions.setup({
|
||||
version: '1.0.0',
|
||||
features: {
|
||||
create: [
|
||||
{
|
||||
id: 'aca:create/folder',
|
||||
order: 100,
|
||||
icon: 'create_new_folder',
|
||||
title: 'ext: Create Folder',
|
||||
type: ContentActionType.default
|
||||
},
|
||||
{
|
||||
id: 'aca:create/folder-2',
|
||||
order: 10,
|
||||
icon: 'create_new_folder',
|
||||
title: 'ext: Create Folder',
|
||||
type: ContentActionType.default
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
extensions.init();
|
||||
expect(extensions.createActions.length).toBe(2);
|
||||
expect(extensions.createActions[0].id).toBe('aca:create/folder-2');
|
||||
expect(extensions.createActions[1].id).toBe('aca:create/folder');
|
||||
|
@ -24,69 +24,190 @@
|
||||
*/
|
||||
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
import { AppConfigService } from '@alfresco/adf-core';
|
||||
import {
|
||||
ContentActionExtension,
|
||||
ContentActionType
|
||||
} from './content-action.extension';
|
||||
import { OpenWithExtension } from './open-with.extension';
|
||||
import { NavigationExtension } from './navigation.extension';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Route } from '@angular/router';
|
||||
import { Node } from 'alfresco-js-api';
|
||||
import { RuleService } from './rules/rule.service';
|
||||
import { ActionService } from './actions/action.service';
|
||||
import { ActionRef } from './actions/action-ref';
|
||||
import { RouteRef } from './route-ref';
|
||||
import { ExtensionConfig } from './extension.config';
|
||||
import { AppStore, SelectionState } from '../store/states';
|
||||
import { NavigationState } from '../store/states/navigation.state';
|
||||
import { selectionWithFolder } from '../store/selectors/app.selectors';
|
||||
import { NavBarGroupRef } from './navbar.extensions';
|
||||
import { RouteRef } from './routing.extensions';
|
||||
import { RuleContext, RuleRef, RuleEvaluator } from './rule.extensions';
|
||||
import { ActionRef, ContentActionRef, ContentActionType } from './action.extensions';
|
||||
|
||||
@Injectable()
|
||||
export class ExtensionService {
|
||||
export class ExtensionService implements RuleContext {
|
||||
configPath = 'assets/app.extensions.json';
|
||||
pluginsPath = 'assets/plugins';
|
||||
|
||||
contentActions: Array<ContentActionExtension> = [];
|
||||
openWithActions: Array<OpenWithExtension> = [];
|
||||
createActions: Array<ContentActionExtension> = [];
|
||||
defaults = {
|
||||
layout: 'app.layout.main',
|
||||
auth: ['app.auth']
|
||||
};
|
||||
|
||||
rules: Array<RuleRef> = [];
|
||||
routes: Array<RouteRef> = [];
|
||||
actions: Array<ActionRef> = [];
|
||||
|
||||
contentActions: Array<ContentActionRef> = [];
|
||||
openWithActions: Array<ContentActionRef> = [];
|
||||
createActions: Array<ContentActionRef> = [];
|
||||
navbar: Array<NavBarGroupRef> = [];
|
||||
|
||||
authGuards: { [key: string]: Type<{}> } = {};
|
||||
components: { [key: string]: Type<{}> } = {};
|
||||
|
||||
constructor(
|
||||
private config: AppConfigService,
|
||||
private ruleService: RuleService,
|
||||
private actionService: ActionService
|
||||
) {}
|
||||
evaluators: { [key: string]: RuleEvaluator } = {};
|
||||
selection: SelectionState;
|
||||
navigation: NavigationState;
|
||||
|
||||
// initialise extension service
|
||||
// in future will also load and merge data from the external plugins
|
||||
init() {
|
||||
this.routes = this.config.get<Array<RouteRef>>(
|
||||
'extensions.core.routes',
|
||||
[]
|
||||
);
|
||||
constructor(private http: HttpClient, private store: Store<AppStore>) {
|
||||
this.store.select(selectionWithFolder).subscribe(result => {
|
||||
this.selection = result.selection;
|
||||
this.navigation = result.navigation;
|
||||
});
|
||||
}
|
||||
|
||||
this.contentActions = this.config
|
||||
.get<Array<ContentActionExtension>>(
|
||||
'extensions.core.features.content.actions',
|
||||
[]
|
||||
)
|
||||
.sort(this.sortByOrder);
|
||||
load(): Promise<boolean> {
|
||||
return new Promise<any>(resolve => {
|
||||
this.loadConfig(this.configPath, 0).then(result => {
|
||||
let config = result.config;
|
||||
|
||||
this.openWithActions = this.config
|
||||
.get<Array<OpenWithExtension>>(
|
||||
'extensions.core.features.viewer.open-with',
|
||||
[]
|
||||
)
|
||||
.filter(entry => !entry.disabled)
|
||||
.sort(this.sortByOrder);
|
||||
if (config.references && config.references.length > 0) {
|
||||
const plugins = config.references.map(
|
||||
(name, idx) => this.loadConfig(`${this.pluginsPath}/${name}`, idx)
|
||||
);
|
||||
|
||||
this.createActions = this.config
|
||||
.get<Array<ContentActionExtension>>(
|
||||
'extensions.core.features.create',
|
||||
[]
|
||||
)
|
||||
.sort(this.sortByOrder);
|
||||
Promise.all(plugins).then((results => {
|
||||
const configs = results
|
||||
.filter(entry => entry)
|
||||
.sort(this.sortByOrder)
|
||||
.map(entry => entry.config);
|
||||
|
||||
this.ruleService.init();
|
||||
this.actionService.init();
|
||||
if (configs.length > 0) {
|
||||
config = this.mergeConfigs(config, ...configs);
|
||||
}
|
||||
|
||||
this.setup(config);
|
||||
resolve(true);
|
||||
}));
|
||||
} else {
|
||||
this.setup(config);
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setup(config: ExtensionConfig) {
|
||||
if (!config) {
|
||||
console.error('Extension configuration not found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.rules = this.loadRules(config);
|
||||
this.actions = this.loadActions(config);
|
||||
this.routes = this.loadRoutes(config);
|
||||
this.contentActions = this.loadContentActions(config);
|
||||
this.openWithActions = this.loadViewerOpenWith(config);
|
||||
this.createActions = this.loadCreateActions(config);
|
||||
this.navbar = this.loadNavBar(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);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
protected loadCreateActions(config: ExtensionConfig): Array<ContentActionRef> {
|
||||
if (config && config.features) {
|
||||
return (config.features.create || []).sort(
|
||||
this.sortByOrder
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
protected loadContentActions(config: ExtensionConfig) {
|
||||
if (config && config.features && config.features.content) {
|
||||
return (config.features.content.actions || []).sort(
|
||||
this.sortByOrder
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
protected loadNavBar(config: ExtensionConfig): any {
|
||||
if (config && config.features) {
|
||||
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 loadViewerOpenWith(config: ExtensionConfig): Array<ContentActionRef> {
|
||||
if (config && config.features && config.features.viewer) {
|
||||
return (config.features.viewer.openWith || [])
|
||||
.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 [];
|
||||
}
|
||||
|
||||
setEvaluator(key: string, value: RuleEvaluator): ExtensionService {
|
||||
this.evaluators[key] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
setAuthGuard(key: string, value: Type<{}>): ExtensionService {
|
||||
@ -98,45 +219,14 @@ export class ExtensionService {
|
||||
return this.routes.find(route => route.id === id);
|
||||
}
|
||||
|
||||
getActionById(id: string): ActionRef {
|
||||
return this.actionService.getActionById(id);
|
||||
}
|
||||
|
||||
runActionById(id: string, context?: any) {
|
||||
this.actionService.runActionById(id, context);
|
||||
}
|
||||
|
||||
getAuthGuards(ids: string[]): Array<Type<{}>> {
|
||||
return (ids || [])
|
||||
.map(id => this.authGuards[id])
|
||||
.filter(guard => guard);
|
||||
}
|
||||
|
||||
getNavigationGroups(): Array<NavigationExtension[]> {
|
||||
const settings = this.config.get<any>(
|
||||
'extensions.core.features.navigation'
|
||||
);
|
||||
if (settings) {
|
||||
const groups = Object.keys(settings).map(key => {
|
||||
return settings[key]
|
||||
.map(group => {
|
||||
const customRoute = this.getRouteById(group.route);
|
||||
const route = `/${
|
||||
customRoute ? customRoute.path : group.route
|
||||
}`;
|
||||
|
||||
return {
|
||||
...group,
|
||||
route
|
||||
};
|
||||
})
|
||||
.filter(entry => !entry.disabled);
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
return [];
|
||||
getNavigationGroups(): Array<NavBarGroupRef> {
|
||||
return this.navbar;
|
||||
}
|
||||
|
||||
setComponent(id: string, value: Type<{}>): ExtensionService {
|
||||
@ -150,11 +240,15 @@ export class ExtensionService {
|
||||
|
||||
getApplicationRoutes(): Array<Route> {
|
||||
return this.routes.map(route => {
|
||||
const guards = this.getAuthGuards(route.auth);
|
||||
const guards = this.getAuthGuards(
|
||||
route.auth && route.auth.length > 0
|
||||
? route.auth
|
||||
: this.defaults.auth
|
||||
);
|
||||
|
||||
return {
|
||||
path: route.path,
|
||||
component: this.getComponentById(route.layout),
|
||||
component: this.getComponentById(route.layout || this.defaults.layout),
|
||||
canActivateChild: guards,
|
||||
canActivate: guards,
|
||||
children: [
|
||||
@ -168,8 +262,7 @@ export class ExtensionService {
|
||||
});
|
||||
}
|
||||
|
||||
// evaluates create actions for the folder node
|
||||
getFolderCreateActions(folder: Node): Array<ContentActionExtension> {
|
||||
getCreateActions(): Array<ContentActionRef> {
|
||||
return this.createActions
|
||||
.filter(this.filterEnabled)
|
||||
.filter(action => this.filterByRules(action))
|
||||
@ -177,18 +270,18 @@ export class ExtensionService {
|
||||
let disabled = false;
|
||||
|
||||
if (action.rules && action.rules.enabled) {
|
||||
disabled = !this.ruleService.evaluateRule(action.rules.enabled);
|
||||
disabled = !this.evaluateRule(action.rules.enabled);
|
||||
}
|
||||
|
||||
return {
|
||||
...action,
|
||||
disabled
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// evaluates content actions for the selection and parent folder node
|
||||
getAllowedContentActions(): Array<ContentActionExtension> {
|
||||
getAllowedContentActions(): Array<ContentActionRef> {
|
||||
return this.contentActions
|
||||
.filter(this.filterEnabled)
|
||||
.filter(action => this.filterByRules(action))
|
||||
@ -211,11 +304,11 @@ export class ExtensionService {
|
||||
}
|
||||
|
||||
reduceSeparators(
|
||||
acc: ContentActionExtension[],
|
||||
el: ContentActionExtension,
|
||||
acc: ContentActionRef[],
|
||||
el: ContentActionRef,
|
||||
i: number,
|
||||
arr: ContentActionExtension[]
|
||||
): ContentActionExtension[] {
|
||||
arr: ContentActionRef[]
|
||||
): ContentActionRef[] {
|
||||
// remove duplicate separators
|
||||
if (i > 0) {
|
||||
const prev = arr[i - 1];
|
||||
@ -238,9 +331,9 @@ export class ExtensionService {
|
||||
}
|
||||
|
||||
reduceEmptyMenus(
|
||||
acc: ContentActionExtension[],
|
||||
el: ContentActionExtension
|
||||
): ContentActionExtension[] {
|
||||
acc: ContentActionRef[],
|
||||
el: ContentActionRef
|
||||
): ContentActionRef[] {
|
||||
if (el.type === ContentActionType.menu) {
|
||||
if ((el.children || []).length === 0) {
|
||||
return acc;
|
||||
@ -262,7 +355,7 @@ export class ExtensionService {
|
||||
return !entry.disabled;
|
||||
}
|
||||
|
||||
copyAction(action: ContentActionExtension): ContentActionExtension {
|
||||
copyAction(action: ContentActionRef): ContentActionRef {
|
||||
return {
|
||||
...action,
|
||||
children: (action.children || []).map(child =>
|
||||
@ -271,10 +364,70 @@ export class ExtensionService {
|
||||
};
|
||||
}
|
||||
|
||||
filterByRules(action: ContentActionExtension): boolean {
|
||||
filterByRules(action: ContentActionRef): boolean {
|
||||
if (action && action.rules && action.rules.visible) {
|
||||
return this.ruleService.evaluateRule(action.rules.visible);
|
||||
return this.evaluateRule(action.rules.visible);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getActionById(id: string): ActionRef {
|
||||
return this.actions.find(action => action.id === id);
|
||||
}
|
||||
|
||||
runActionById(id: string, context?: any) {
|
||||
const action = this.getActionById(id);
|
||||
if (action) {
|
||||
const { type, payload } = action;
|
||||
const expression = this.runExpression(payload, context);
|
||||
|
||||
this.store.dispatch({ type, payload: expression });
|
||||
}
|
||||
}
|
||||
|
||||
runExpression(value: string, context?: any) {
|
||||
const pattern = new RegExp(/\$\((.*\)?)\)/g);
|
||||
const matches = pattern.exec(value);
|
||||
|
||||
if (matches && matches.length > 1) {
|
||||
const expression = matches[1];
|
||||
const fn = new Function('context', `return ${expression}`);
|
||||
const result = fn(context);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
evaluateRule(ruleId: string): boolean {
|
||||
const ruleRef = this.rules.find(ref => ref.id === ruleId);
|
||||
if (ruleRef) {
|
||||
const evaluator = this.evaluators[ruleRef.type];
|
||||
if (evaluator) {
|
||||
return evaluator(this, ...ruleRef.parameters);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// todo: requires overwrite support for array entries
|
||||
// todo: overwrite only particular areas, don't touch version or other top-level props
|
||||
protected mergeConfigs(...objects): any {
|
||||
const result = {};
|
||||
|
||||
objects.forEach(source => {
|
||||
Object.keys(source).forEach(prop => {
|
||||
if (prop in result && Array.isArray(result[prop])) {
|
||||
result[prop] = result[prop].concat(source[prop]);
|
||||
} else if (prop in result && typeof result[prop] === 'object') {
|
||||
result[prop] = this.mergeConfigs(result[prop], source[prop]);
|
||||
} else {
|
||||
result[prop] = source[prop];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -23,12 +23,22 @@
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export interface NavigationExtension {
|
||||
export interface NavBarGroupRef {
|
||||
id: string;
|
||||
items: Array<NavBarLinkRef>;
|
||||
|
||||
order?: number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface NavBarLinkRef {
|
||||
id: string;
|
||||
order: number;
|
||||
icon: string;
|
||||
title: string;
|
||||
route: string;
|
||||
|
||||
url?: string; // evaluated at runtime based on route ref
|
||||
description?: string;
|
||||
order?: number;
|
||||
disabled?: boolean;
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
/*!
|
||||
* @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 OpenWithExtension {
|
||||
id: string;
|
||||
order?: number;
|
||||
icon: string;
|
||||
title: string;
|
||||
action: string;
|
||||
disabled?: boolean;
|
||||
}
|
@ -27,7 +27,8 @@ export interface RouteRef {
|
||||
id: string;
|
||||
path: string;
|
||||
component: string;
|
||||
layout: string;
|
||||
auth: string[];
|
||||
|
||||
layout?: string;
|
||||
auth?: string[];
|
||||
data?: { [key: string]: string };
|
||||
}
|
@ -23,12 +23,24 @@
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { SelectionState } from '../../store/states';
|
||||
import { RuleEvaluator } from './rule.service';
|
||||
import { NavigationState } from '../../store/states/navigation.state';
|
||||
import { SelectionState } from '../store/states';
|
||||
import { NavigationState } from '../store/states/navigation.state';
|
||||
|
||||
export type RuleEvaluator = (context: RuleContext, ...args: any[]) => boolean;
|
||||
|
||||
export interface RuleContext {
|
||||
selection: SelectionState;
|
||||
navigation: NavigationState;
|
||||
evaluators: { [key: string]: RuleEvaluator };
|
||||
}
|
||||
|
||||
export class RuleRef {
|
||||
type: string;
|
||||
id?: string;
|
||||
parameters?: Array<RuleParameter>;
|
||||
}
|
||||
|
||||
export interface RuleParameter {
|
||||
type: string;
|
||||
value: any;
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
/*!
|
||||
* @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 RuleParameter {
|
||||
type: string;
|
||||
value: any;
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/*!
|
||||
* @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 { RuleParameter } from './rule-parameter';
|
||||
import { RuleEvaluator } from './rule.service';
|
||||
|
||||
export class RuleRef {
|
||||
type: string;
|
||||
id?: string;
|
||||
parameters?: Array<RuleParameter>;
|
||||
evaluator?: RuleEvaluator;
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
/*!
|
||||
* @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 { AppConfigService } from '@alfresco/adf-core';
|
||||
import { every, some } from './core.evaluators';
|
||||
import { RuleContext } from './rule-context';
|
||||
import { RuleRef } from './rule-ref';
|
||||
import { createSelector, Store } from '@ngrx/store';
|
||||
import {
|
||||
appSelection,
|
||||
appNavigation
|
||||
} from '../../store/selectors/app.selectors';
|
||||
import { AppStore, SelectionState } from '../../store/states';
|
||||
import { NavigationState } from '../../store/states/navigation.state';
|
||||
import { canCreateFolder, hasFolderSelected, canUpdateSelectedFolder, hasFileSelected, canDownloadSelection } from './app.evaluators';
|
||||
|
||||
export type RuleEvaluator = (context: RuleContext, ...args: any[]) => boolean;
|
||||
|
||||
export const selectionWithFolder = createSelector(
|
||||
appSelection,
|
||||
appNavigation,
|
||||
(selection, navigation) => {
|
||||
return {
|
||||
selection,
|
||||
navigation
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@Injectable()
|
||||
export class RuleService implements RuleContext {
|
||||
rules: Array<RuleRef> = [];
|
||||
evaluators: { [key: string]: RuleEvaluator } = {};
|
||||
selection: SelectionState;
|
||||
navigation: NavigationState;
|
||||
|
||||
constructor(
|
||||
private config: AppConfigService,
|
||||
private store: Store<AppStore>
|
||||
) {
|
||||
this.evaluators['core.every'] = every;
|
||||
this.evaluators['core.some'] = some;
|
||||
this.evaluators['app.selection.canDownload'] = canDownloadSelection;
|
||||
this.evaluators['app.selection.file'] = hasFileSelected;
|
||||
this.evaluators['app.selection.folder'] = hasFolderSelected;
|
||||
this.evaluators['app.selection.folder.canUpdate'] = canUpdateSelectedFolder;
|
||||
this.evaluators['app.navigation.folder.canCreate'] = canCreateFolder;
|
||||
|
||||
this.store
|
||||
.select(selectionWithFolder)
|
||||
.subscribe(result => {
|
||||
this.selection = result.selection;
|
||||
this.navigation = result.navigation;
|
||||
});
|
||||
}
|
||||
|
||||
init() {
|
||||
this.rules = this.config
|
||||
.get<Array<RuleRef>>('extensions.core.rules', [])
|
||||
.map(rule => {
|
||||
rule.evaluator = this.evaluators[rule.type];
|
||||
return rule;
|
||||
});
|
||||
}
|
||||
|
||||
evaluateRule(ruleId: string): boolean {
|
||||
const ruleRef = this.rules.find(ref => ref.id === ruleId);
|
||||
|
||||
if (ruleRef.evaluator) {
|
||||
return ruleRef.evaluator(this, ...ruleRef.parameters);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -36,3 +36,14 @@ export const selectUser = createSelector(selectApp, state => state.user);
|
||||
export const sharedUrl = createSelector(selectApp, state => state.sharedUrl);
|
||||
export const appNavigation = createSelector(selectApp, state => state.navigation);
|
||||
export const currentFolder = createSelector(selectApp, state => state.navigation.currentFolder);
|
||||
|
||||
export const selectionWithFolder = createSelector(
|
||||
appSelection,
|
||||
appNavigation,
|
||||
(selection, navigation) => {
|
||||
return {
|
||||
selection,
|
||||
navigation
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -60,8 +60,6 @@ import { NodeActionsService } from '../services/node-actions.service';
|
||||
import { NodePermissionService } from '../services/node-permission.service';
|
||||
import { ContentApiService } from '../services/content-api.service';
|
||||
import { ExtensionService } from '../extensions/extension.service';
|
||||
import { RuleService } from '../extensions/rules/rule.service';
|
||||
import { ActionService } from '../extensions/actions/action.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@ -114,9 +112,7 @@ import { ActionService } from '../extensions/actions/action.service';
|
||||
NodeActionsService,
|
||||
NodePermissionService,
|
||||
ContentApiService,
|
||||
ExtensionService,
|
||||
RuleService,
|
||||
ActionService
|
||||
ExtensionService
|
||||
]
|
||||
})
|
||||
export class AppTestingModule {}
|
||||
|
189
src/assets/app.extensions.json
Normal file
189
src/assets/app.extensions.json
Normal file
@ -0,0 +1,189 @@
|
||||
{
|
||||
"$schema": "../../extension.schema.json",
|
||||
"name": "app",
|
||||
"version": "1.0.0",
|
||||
|
||||
"references": [
|
||||
"plugin1.json",
|
||||
"plugin2.json"
|
||||
],
|
||||
|
||||
"rules": [
|
||||
{
|
||||
"id": "app.create.canCreateFolder",
|
||||
"type": "app.navigation.folder.canCreate"
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.canEditFolder",
|
||||
"type": "core.every",
|
||||
"parameters": [
|
||||
{ "type": "rule", "value": "app.selection.folder" },
|
||||
{ "type": "rule", "value": "app.selection.folder.canUpdate" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.canViewFile",
|
||||
"type": "app.selection.file"
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.canDownload",
|
||||
"type": "app.selection.canDownload"
|
||||
}
|
||||
],
|
||||
|
||||
"actions": [
|
||||
{
|
||||
"id": "app.actions.createFolder",
|
||||
"type": "CREATE_FOLDER"
|
||||
},
|
||||
{
|
||||
"id": "app.actions.editFolder",
|
||||
"type": "EDIT_FOLDER"
|
||||
},
|
||||
{
|
||||
"id": "app.actions.download",
|
||||
"type": "DOWNLOAD_NODES"
|
||||
},
|
||||
{
|
||||
"id": "app.actions.preview",
|
||||
"type": "VIEW_FILE"
|
||||
}
|
||||
],
|
||||
|
||||
"features": {
|
||||
"create": [
|
||||
{
|
||||
"id": "app.create.folder",
|
||||
"type": "default",
|
||||
"icon": "create_new_folder",
|
||||
"title": "ext: Create Folder",
|
||||
"actions": {
|
||||
"click": "app.actions.createFolder"
|
||||
},
|
||||
"rules": {
|
||||
"enabled": "app.create.canCreateFolder"
|
||||
}
|
||||
}
|
||||
],
|
||||
"navbar": [
|
||||
{
|
||||
"id": "app.navbar.primary",
|
||||
"items": [
|
||||
{
|
||||
"id": "app.navbar.personalFiles",
|
||||
"icon": "folder",
|
||||
"title": "APP.BROWSE.PERSONAL.SIDENAV_LINK.LABEL",
|
||||
"description": "APP.BROWSE.PERSONAL.SIDENAV_LINK.TOOLTIP",
|
||||
"route": "personal-files"
|
||||
},
|
||||
{
|
||||
"id": "app.navbar.libraries",
|
||||
"icon": "group_work",
|
||||
"title": "APP.BROWSE.LIBRARIES.SIDENAV_LINK.LABEL",
|
||||
"description": "APP.BROWSE.LIBRARIES.SIDENAV_LINK.TOOLTIP",
|
||||
"route": "libraries"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "app.navbar.secondary",
|
||||
"items": [
|
||||
{
|
||||
"id": "app.navbar.shared",
|
||||
"icon": "people",
|
||||
"title": "APP.BROWSE.SHARED.SIDENAV_LINK.LABEL",
|
||||
"description": "APP.BROWSE.SHARED.SIDENAV_LINK.TOOLTIP",
|
||||
"route": "shared"
|
||||
},
|
||||
{
|
||||
"id": "app.navbar.recentFiles",
|
||||
"icon": "schedule",
|
||||
"title": "APP.BROWSE.RECENT.SIDENAV_LINK.LABEL",
|
||||
"description": "APP.BROWSE.RECENT.SIDENAV_LINK.TOOLTIP",
|
||||
"route": "recent-files"
|
||||
},
|
||||
{
|
||||
"id": "app.navbar.favorites",
|
||||
"icon": "star",
|
||||
"title": "APP.BROWSE.FAVORITES.SIDENAV_LINK.LABEL",
|
||||
"description": "APP.BROWSE.FAVORITES.SIDENAV_LINK.TOOLTIP",
|
||||
"route": "favorites"
|
||||
},
|
||||
{
|
||||
"id": "app.navbar.trashcan",
|
||||
"icon": "delete",
|
||||
"title": "APP.BROWSE.TRASHCAN.SIDENAV_LINK.LABEL",
|
||||
"description": "APP.BROWSE.TRASHCAN.SIDENAV_LINK.TOOLTIP",
|
||||
"route": "trashcan"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"content": {
|
||||
"actions": [
|
||||
{
|
||||
"id": "app.toolbar.separator.1",
|
||||
"order": 5,
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.createFolder",
|
||||
"type": "button",
|
||||
"order": 10,
|
||||
"title": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER",
|
||||
"icon": "create_new_folder",
|
||||
"actions": {
|
||||
"click": "app.actions.createFolder"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.create.canCreateFolder"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.preview",
|
||||
"type": "button",
|
||||
"order": 15,
|
||||
"title": "APP.ACTIONS.VIEW",
|
||||
"icon": "open_in_browser",
|
||||
"actions": {
|
||||
"click": "app.actions.preview"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.toolbar.canViewFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.download",
|
||||
"type": "button",
|
||||
"order": 20,
|
||||
"title": "APP.ACTIONS.DOWNLOAD",
|
||||
"icon": "get_app",
|
||||
"actions": {
|
||||
"click": "app.actions.download"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.toolbar.canDownload"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.editFolder",
|
||||
"type": "button",
|
||||
"order": 30,
|
||||
"title": "APP.ACTIONS.EDIT",
|
||||
"icon": "create",
|
||||
"actions": {
|
||||
"click": "app.actions.editFolder"
|
||||
},
|
||||
"rules": {
|
||||
"visible": "app.toolbar.canEditFolder"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app.toolbar.separator.2",
|
||||
"order": 200,
|
||||
"type": "separator"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
0
src/assets/plugins/.gitkeep
Normal file
0
src/assets/plugins/.gitkeep
Normal file
66
src/assets/plugins/plugin1.json
Normal file
66
src/assets/plugins/plugin1.json
Normal file
@ -0,0 +1,66 @@
|
||||
{
|
||||
"$schema": "../../../extension.schema.json",
|
||||
"version": "1.0.0",
|
||||
"name": "plugin1",
|
||||
"description": "demo plugin",
|
||||
|
||||
"actions": [
|
||||
{
|
||||
"id": "plugin1.actions.settings",
|
||||
"type": "NAVIGATE_URL",
|
||||
"payload": "/settings"
|
||||
},
|
||||
{
|
||||
"id": "plugin1.actions.info",
|
||||
"type": "SNACKBAR_INFO",
|
||||
"payload": "I'm a nice little popup raised by extension."
|
||||
},
|
||||
{
|
||||
"id": "plugin1.actions.node-name",
|
||||
"type": "SNACKBAR_INFO",
|
||||
"payload": "$('Action for ' + context.selection.first.entry.name)"
|
||||
}
|
||||
],
|
||||
|
||||
"features": {
|
||||
"viewer": {
|
||||
"openWith": [
|
||||
{
|
||||
"id": "plugin1.viewer.openWith.action1",
|
||||
"type": "default",
|
||||
"icon": "build",
|
||||
"title": "Snackbar",
|
||||
"actions": {
|
||||
"click": "plugin1.actions.info"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"content": {
|
||||
"actions": [
|
||||
{
|
||||
"id": "plugin1.toolbar.menu1",
|
||||
"type": "menu",
|
||||
"icon": "storage",
|
||||
"order": 300,
|
||||
"children": [
|
||||
{
|
||||
"id": "plugin1.toolbar.menu1.settings",
|
||||
"type": "button",
|
||||
"title": "Settings",
|
||||
"icon": "settings_applications",
|
||||
"actions": {
|
||||
"click": "plugin1.actions.settings"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "plugin1.toolbar.separator3",
|
||||
"order": 301,
|
||||
"type": "separator"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
42
src/assets/plugins/plugin2.json
Normal file
42
src/assets/plugins/plugin2.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"$schema": "../../../extension.schema.json",
|
||||
"version": "1.0.0",
|
||||
"name": "plugin2",
|
||||
"description": "demo plugin",
|
||||
|
||||
"routes": [
|
||||
{
|
||||
"id": "plugin2.routes.about",
|
||||
"path": "ext/about",
|
||||
"component": "app.components.about",
|
||||
"data": {
|
||||
"title": "Custom About"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"features": {
|
||||
"navbar": [
|
||||
{
|
||||
"id": "plugin2.navbar.group1",
|
||||
"disabled": true,
|
||||
"items": [
|
||||
{
|
||||
"id": "plugin2.navbar.group1.link1",
|
||||
"icon": "build",
|
||||
"title": "About (native)",
|
||||
"description": "Uses native application route",
|
||||
"route": "about"
|
||||
},
|
||||
{
|
||||
"id": "plugin2.navbar.group1.link2",
|
||||
"icon": "build",
|
||||
"title": "About (custom)",
|
||||
"description": "Uses custom defined route",
|
||||
"route": "plugin2.routes.about"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -69,7 +69,6 @@
|
||||
"no-trailing-whitespace": true,
|
||||
"no-unnecessary-initializer": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-var-keyword": true,
|
||||
"object-literal-sort-keys": false,
|
||||
"one-line": [
|
||||
|
Loading…
x
Reference in New Issue
Block a user