26 KiB
title
title |
---|
Extending |
Work is still in progress, the documentation and examples may change.
Extending
Application extensibility is performed via the root /src/assets/app.extensions.json
,
and any number of external plugins that are references of the main entry point.
The application also comes with the /src/assets/plugins/
folder
already preconfigured to store external files.
You can create plugins that change, toggle or extend the following areas:
- Navigation sidebar links and groups
- Context Menu
- Toolbar entries
- buttons
- menu buttons
- separators
- Viewer actions
- "Open With" entries
- "More actions" toolbar entries
Extensions can also:
- Overwrite or disable extension points of the main application or other plugins
- Change rules, actions or any visual element
- Register new application routes based on empty pages or layouts
- Register new rule evaluators, components, guards, etc.
Format
The format is represented by a JSON file with the structure similar to the following one:
{
"$name": "app",
"$version": "1.0.0",
"routes": [],
"actions": [],
"rules": [],
"features": {}
}
Schema
You can find the JSON schema at the project root folder: extension.schema.json.
Schema allows validating extension files, provides code completion and documentation hints.
{
"$schema": "../../extension.schema.json",
"$name": "app",
"$version": "1.0.0",
}
Multiple files
You can have multiple extension files distributed separately.
All additional files are linked via the $references
property,
the order of declaration defines also the order of loading.
{
"$schema": "../../extension.schema.json",
"$name": "app",
"$version": "1.0.0",
"$references": [
"plugin1.json",
"plugin2.json"
]
}
Always keep in mind that all extension files are merged together at runtime. That allows plugins overwriting the code from the main application or altering other plugins.
Routes
To create a new route, populate the routes
section with the corresponding entries.
{
"$schema": "../../../extension.schema.json",
"$version": "1.0.0",
"$name": "plugin1",
"routes": [
{
"id": "plugin1.routes.bin",
"path": "ext/bin",
"layout": "app.layout.main",
"component": "app.components.trashcan"
}
]
}
Route properties
Name | Description |
---|---|
id | Unique identifier. |
path | Runtime path of the route. |
component | The main component to use for the route. |
layout | The layout component to use for the route. |
auth | List of authentication guards. Defaults to [ "app.auth" ] . |
data | Custom property bag to carry with the route. |
Use the `app.layout.main` value for the `layout` property to get the default application layout,
with header, navigation sidebar and main content area.
Leave the `layout` property empty if you want your route component take the whole page.
You can define the full route schema like in the next example:
{
"$schema": "../../../extension.schema.json",
"$version": "1.0.0",
"$name": "plugin1",
"routes": [
{
"id": "plugin1.routes.bin",
"path": "ext/bin",
"component": "app.components.trashcan",
"layout": "app.layout.main",
"auth": [ "app.auth" ],
"data": {
"title": "Custom Trashcan"
}
}
]
}
Authentication Guards
Below is the list of the authentication guards main application registers on startup.
Key | Type | Description |
---|---|---|
app.auth | AuthGuardEcm | ADF guard, validates ACS authentication and redirects to Login if needed. |
You can refer those guards from within your custom extensions, or register your custom implementations.
Components
You can register any Angular component to participate in extensibility.
The components are used to create custom:
- routes and pages
- toolbar buttons
- menu items
Key | Type | Description |
---|---|---|
app.layout.main | LayoutComponent | Main application layout with the menu bar, navigation sidebar and main content area to project your components. |
app.components.trashcan | TrashcanComponent | Trashcan component, used for demo purposes. |
app.toolbar.toggleInfoDrawer | ToggleInfoDrawerComponent | The toolbar button component that toggles Info Drawer for the selection. |
app.toolbar.toggleFavorite | ToggleFavoriteComponent | The toolbar button component that toggles Favorite state for the selection. |
See [Registration](#registration) section for more details on how to register your own entries to be re-used at runtime.
Note that custom extensions can also replace any existing component at runtime by a known identifier, besides registering a new one.
Actions
Name | Description |
---|---|
id | Unique identifier. |
type | Action type, see Application Actions for more details. |
payload | Action payload, a string containing value or expression. |
{
"$schema": "../../../extension.schema.json",
"$version": "1.0.0",
"$name": "plugin1",
"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)"
}
]
}
Value expressions
You can use light-weight expression syntax to provide custom parameters for the action payloads.
$(<expression>)
Expressions are valid JavaScript blocks that evaluate to values.
Examples:
$('hello world') // 'hello world'
$('hello' + ', ' + 'world') // 'hello, world'
$(1 + 1) // 2
$([1, 2, 1 + 2]) // [1, 2, 3]
Application Actions
Application is using NgRx (Reactive libraries for Angular, inspired by Redux). To get more information on NxRx please refer to the following resources:
Most of the application features are already exposed in the form of NgRx Actions and corresponding Effects.
You can invoke any action via a single Store
dispatcher, similar to the following:
export class MyComponent {
constructor(private store: Store<AppStore>) {}
onClick() {
this.store.dispatch(new SearchByTermAction('*'));
}
}
The code above demonstrates a simple 'click' handler that invokes Search by Term
feature
and automatically redirects user to the Search Results page.
Another example demonstrates viewing a node from a custom application service API:
export class MyService {
constructor(private store: Store<AppStore>) {}
viewFile(node: MinimalNodeEntity) {
this.store.dispatch(new ViewFileAction(node));
}
}
Using with Extensions
You can invoke every application action from the extensions, i.e. buttons, menus, etc.
Many of the actions take currently selected nodes if no payload provided. That simplifies declaring and invoking actions from the extension files.
In the example below, we create a new entry to the "NEW" menu dropdown
and provide a new Create Folder
command that invokes the CREATE_FOLDER
application action.
{
"$schema": "../../../extension.schema.json",
"$version": "1.0.0",
"$name": "plugin1",
"features": {
"create": [
{
"id": "app.create.folder",
"type": "default",
"icon": "create_new_folder",
"title": "Create Folder",
"actions": {
"click": "CREATE_FOLDER"
}
}
]
}
}
The CREATE_FOLDER
action will trigger corresponding NgRx Effects to show the dialog
and perform document list reload if needed.
Below is the list of public actions types you can use in the plugin definitions as a reference to the action:
Name | Payload | Description |
---|---|---|
SET_CURRENT_FOLDER | Node | Notify components about currently opened folder. |
SET_CURRENT_URL | string | Notify components about current browser URL. |
SET_USER_PROFILE | Person | Assign current user profile. |
TOGGLE_INFO_DRAWER | n/a | Toggle info drawer for the selected node. |
ADD_FAVORITE | MinimalNodeEntity[] | Add nodes (or selection) to favorites. |
REMOVE_FAVORITE | MinimalNodeEntity[] | Removes nodes (or selection) from favorites. |
DELETE_LIBRARY | string | Delete a Library by id. Takes selected node if payload not provided. |
CREATE_LIBRARY | n/a | Invoke a "Create Library" dialog. |
SET_SELECTED_NODES | MinimalNodeEntity[] | Notify components about selected nodes. |
DELETE_NODES | MinimalNodeEntity[] | Delete the nodes (or selection). Supports undo actions. |
UNDO_DELETE_NODES | any[] | Reverts deletion of nodes (or selection). |
RESTORE_DELETED_NODES | MinimalNodeEntity[] | Restores deleted nodes (or selection). Typically used with Trashcan. |
PURGE_DELETED_NODES | MinimalNodeEntity[] | Permanently delete nodes (or selection). Typically used with Trashcan. |
DOWNLOAD_NODES | MinimalNodeEntity[] | Download nodes (or selections). Creates a ZIP archive for folders or multiple items. |
CREATE_FOLDER | string | Invoke a "Create Folder" dialog for the opened folder (or the parent folder id in the payload). |
EDIT_FOLDER | MinimalNodeEntity | Invoke an "Edit Folder" dialog for the node (or selection). |
SHARE_NODE | MinimalNodeEntity | Invoke a "Share" dialog for the node (or selection). |
UNSHARE_NODES | MinimalNodeEntity[] | Remove nodes (or selection) from the shared nodes (does not remove content). |
COPY_NODES | MinimalNodeEntity[] | Invoke a "Copy" dialog for the nodes (or selection). Supports undo actions. |
MOVE_NODES | MinimalNodeEntity[] | Invoke a "Move" dialog for the nodes (or selection). Supports undo actions. |
MANAGE_PERMISSIONS | MinimalNodeEntity | Invoke a "Manage Permissions" dialog for the node (or selection). |
MANAGE_VERSIONS | MinimalNodeEntity | Invoke a "Manage Versions" dialog for the node (or selection). |
NAVIGATE_URL | string | Navigate to a given route URL within the application. |
NAVIGATE_ROUTE | any[] | Navigate to a particular Route (supports parameters) |
NAVIGATE_FOLDER | MinimalNodeEntity | Navigate to a folder based on the Node properties. |
NAVIGATE_PARENT_FOLDER | MinimalNodeEntity | Navigate to a containing folder based on the Node properties. |
SEARCH_BY_TERM | string | Perform a simple search by the term and navigate to Search results. |
SNACKBAR_INFO | string | Show information snackbar with the message provided. |
SNACKBAR_WARNING | string | Show warning snackbar with the message provided. |
SNACKBAR_ERROR | string | Show error snackbar with the message provided. |
UPLOAD_FILES | n/a | Invoke "Upload Files" dialog and upload files to the currently opened folder. |
UPLOAD_FOLDER | n/a | Invoke "Upload Folder" dialog and upload selected folder to the currently opened one. |
VIEW_FILE | MinimalNodeEntity | Preview the file (or selection) in the Viewer. |
Rules
Rules allow evaluating conditions for extension components. For example, you can disable or hide elements based on certain rules.
Every rule is backed by a condition evaluator.
{
"$schema": "../../../extension.schema.json",
"$version": "1.0.0",
"$name": "plugin1",
"rules": [
{
"id": "app.trashcan",
"type": "app.navigation.isTrashcan"
}
]
}
Rules can accept other rules as parameters:
{
"$schema": "../../../extension.schema.json",
"$version": "1.0.0",
"$name": "plugin1",
"rules": [
{
"id": "app.toolbar.favorite.canAdd",
"type": "core.every",
"parameters": [
{ "type": "rule", "value": "app.selection.canAddFavorite" },
{ "type": "rule", "value": "app.navigation.isNotRecentFiles" },
{ "type": "rule", "value": "app.navigation.isNotSharedFiles" },
{ "type": "rule", "value": "app.navigation.isNotSearchResults" }
]
}
]
}
You can also negate any rule by utilizing a `!` prefix: `!app.navigation.isTrashcan` is the opposite of the `app.navigation.isTrashcan`.
It is also possible to use inline references to registered evaluators without declaring rules, in case you do not need providing extra parameters, or chaining multiple rules together.
Core Evaluators
You can create new rules by chaining other rules and evaluators.
Key | Description |
---|---|
core.every | Evaluates to true if all chained rules evaluate to true . |
core.some | Evaluates to true if at least one of the chained rules evaluates to true . |
core.not | Evaluates to true if all chained rules evaluate to false . |
Below is an example of the composite rule definition that combines the following conditions:
- user has selected a single file
- user is not using Trashcan page
{
"$schema": "../../../extension.schema.json",
"$version": "1.0.0",
"$name": "plugin1",
"rules": [
{
"id": "app.toolbar.canViewFile",
"type": "core.every",
"parameters": [
{
"type": "rule",
"value": "app.selection.file"
},
{
"type": "rule",
"value": "core.not",
"parameters": [
{
"type": "rule",
"value": "app.navigation.isTrashcan"
}
]
}
]
}
]
}
You can now declare a toolbar button action that is based on the rule above.
{
"$schema": "../../../extension.schema.json",
"$version": "1.0.0",
"$name": "plugin1",
"features": {
"content": {
"actions": [
{
"id": "app.toolbar.preview",
"type": "button",
"title": "View File",
"icon": "open_in_browser",
"actions": {
"click": "VIEW_FILE"
},
"rules": {
"visible": "app.toolbar.canViewFile"
}
},
]
}
}
}
The button will be visible only when the linked rule evaluates to true
.
Application Evaluators
Key | Description |
---|---|
app.selection.canDelete | User has permission to delete selected node(s). |
app.selection.canDownload | User can download selected node(s). |
app.selection.notEmpty | At least one node is selected. |
app.selection.canUnshare | User is able to remove selected node(s) from public sharing. |
app.selection.canAddFavorite | User can add selected node(s) to favorites. |
app.selection.canRemoveFavorite | User can remove selected node(s) from favorites. |
app.selection.first.canUpdate | User has permission to update selected node(s). |
app.selection.file | A single File node is selected. |
app.selection.file.canShare | User is able to remove selected file from public sharing. |
app.selection.library | A single Library node is selected. |
app.selection.folder | A single Folder node is selected. |
app.selection.folder.canUpdate | User has permissions to update selected folder. |
Navigation Evaluators
The application exposes a set of navigation-related evaluators to help developers restrict or enable certain actions based on the route or page displayed.
The negated evaluators are provided just to simplify development,
and to avoid having a complex rule trees just to negate the rules,
for example mixing core.every
and core.not
.
You can also negate any rule by utilizing a `!` prefix: `!app.navigation.isTrashcan` is the opposite of the `app.navigation.isTrashcan`.
Key | Description |
---|---|
app.navigation.folder.canCreate | User can create content in the currently opened folder. |
app.navigation.folder.canUpload | User can upload content to the currently opened folder. |
app.navigation.isTrashcan | User is using Trashcan page. |
app.navigation.isNotTrashcan | Current page is not a Trashcan. |
app.navigation.isLibraries | User is using Libraries page. |
app.navigation.isNotLibraries | Current page is not Libraries. |
app.navigation.isSharedFiles | User is using Shared Files page. |
app.navigation.isNotSharedFiles | Current page is not Shared Files. |
app.navigation.isFavorites | User is using Favorites page. |
app.navigation.isNotFavorites | Current page is not Favorites |
app.navigation.isRecentFiles | User is using Recent Files page. |
app.navigation.isNotRecentFiles | Current page is not Recent Files. |
app.navigation.isSearchResults | User is using Search Results page. |
app.navigation.isNotSearchResults | Current page is not Search Results. |
See [Registration](#registration) section for more details on how to register your own entries to be re-used at runtime.
Example
The rule in the example below evaluates to true
if all the conditions are met:
- user has selected node(s)
- user is not using Trashcan page
- user is not using Libraries page
{
"$schema": "../../../extension.schema.json",
"$version": "1.0.0",
"$name": "plugin1",
"rules": [
{
"id": "app.toolbar.canCopyNode",
"type": "core.every",
"parameters": [
{ "type": "rule", "value": "app.selection.notEmpty" },
{ "type": "rule", "value": "app.navigation.isNotTrashcan" },
{ "type": "rule", "value": "app.navigation.isNotLibraries" }
]
}
]
}
Application Features
Extending Create Menu
Extending Navigation Sidebar
Extending Toolbar
Extending Context Menu
Extending Viewer
Open With actions
Toolbar actions
Registration
You can use ExtensionService
to register custom components, authentication guards,
rule evaluators, etc.
It is recommended to register custom content during application startup
by utilising the APP_INITIALIZER
injection token that comes with Angular.
In that case all plugins will be available right after main application component is ready.
Update the main application module app.module.ts
, or create your own module,
and use the following snippet to register custom content:
export function setupExtensions(extensions: ExtensionService): Function {
return () =>
new Promise(resolve => {
extensions.setComponents({
'plugin1.components.my': MyComponent1,
'plugin1.layouts.my': MyLayout
});
extensions.setAuthGuards({
'plugin.auth': MyAuthGuard
});
extensions.setEvaluators({
'plugin1.rules.custom1': MyCustom1Evaluator,
'plugin1.rules.custom2': MyCustom2Evaluator
});
resolve(true);
});
}
@NgModule({
declarations: [ MyComponent1, MyLayout ],
entryComponents: [ MyComponent1, MyLayout ],
providers: [
{
provide: APP_INITIALIZER,
useFactory: setupExtensions,
deps: [ ExtensionService ],
multi: true
}
]
})
export class MyExtensionModule {}
According to Angular rules, all components that are created dynamically at runtime need to be registered within the `entryComponents` section of the NgModule.
The registration API is not limited to the custom content only. You can replace any existing entries by replacing the values from your module.
Creating custom evaluator
Rule evaluators are plain JavaScript (or TypeScript) functions
that take RuleContext
reference and an optional list of RuleParameter
instances.
Application provides a special RuleEvaluator type alias for evaluator functions:
export type RuleEvaluator = (context: RuleContext, ...args: any[]) => boolean;
Create a function that is going to check if user has selected one or multiple nodes.
export function hasSelection(
context: RuleContext,
...args: RuleParameter[]
): boolean {
return !context.selection.isEmpty;
}
The context
is a reference to a special instance of the RuleContext type,
that provides each evaluator access to runtime entities.
export interface RuleContext {
selection: SelectionState;
navigation: NavigationState;
permissions: NodePermissions;
getEvaluator(key: string): RuleEvaluator;
}
The SelectionState
interface exposes information about global selection state:
export interface SelectionState {
count: number;
nodes: MinimalNodeEntity[];
libraries: SiteEntry[];
isEmpty: boolean;
first?: MinimalNodeEntity;
last?: MinimalNodeEntity;
folder?: MinimalNodeEntity;
file?: MinimalNodeEntity;
library?: SiteEntry;
}
Next, register the function you have created earlier with the ExtensionService
and give it a unique identifier:
extensions.setEvaluators({
'plugin1.rules.hasSelection': hasSelection
});
Now, the plugin1.rules.hasSelection
evaluator can be used as an inline rule reference,
or part of the composite rule like core.every
.
See [Registration](#registration) section for more details on how to register your own entries to be re-used at runtime.
Tutorials
Custom route with parameters
In this tutorial, we are going to implement the following features:
- update the Trashcan component to receive and log route parameters
- create a new route that points to the Trashcan component and uses main layout
- create an action reference that allows redirecting to the new route
- create a button in the New menu that invokes an action
Update src/app/components/trashcan/trashcan.component.ts
and append the following code to the ngOnInit
body:
import { ActivatedRoute, Params } from '@angular/router';
@Component({...})
export class TrashcanComponent {
constructor(
// ...
private route: ActivatedRoute
) {}
ngOnInit() {
// ...
this.route.params.subscribe(({ nodeId }: Params) => {
console.log('node: ', nodeId);
});
}
}
The code above logs current route parameters to the browser console and is a simple proof the integration works as expected.
Next, add a new route definition as in the example below:
{
"$schema": "../../../extension.schema.json",
"$version": "1.0.0",
"$name": "plugin1",
"routes": [
{
"id": "custom.routes.trashcan",
"path": "ext/trashcan/:nodeId",
"component": "app.components.trashcan",
"layout": "app.layout.main",
"auth": [ "app.auth" ]
}
]
}
The template above creates a new route reference with the id custom.routes.trashcan
that points to the ext/trashcan/
route and accepts the nodeId
parameter.
Also, we are going to use default application layout (app.layout.main
)
and authentication guards (app.auth
).
Next, create an action reference for the NAVIGATE_ROUTE
application action
and pass route parameters: /ext/trashcan
for the path, and 10
for the nodeId
value.
{
"$schema": "../../../extension.schema.json",
"$version": "1.0.0",
"$name": "plugin1",
"routes": [...],
"actions": [
{
"id": "custom.actions.trashcan",
"type": "NAVIGATE_ROUTE",
"payload": "$(['/ext/trashcan', '10'])"
}
]
}
Finally, declare a new menu item for the NEW
button and use the custom.actions.trashcan
action created above.
{
"$schema": "../../../extension.schema.json",
"$version": "1.0.0",
"$name": "plugin1",
"routes": [...],
"actions": [...],
"features": {
"create": [
{
"id": "custom.create.trashcan",
"type": "default",
"icon": "build",
"title": "Custom trashcan route",
"actions": {
"click": "custom.actions.trashcan"
}
}
]
}
}
Now, if you run the application, you should see a new menu item called "Custom Trashcan Route" in the "NEW" dropdown.
Upon clicking this item you should navigate to the /ext/trashcan/10
route containing a Trashcan component.
Check the browser console output and ensure you have the following output:
node: 10
You have successfully created a new menu button that invokes your custom action and redirects you to the extra application route.