basic docs on extensibility apis (#543)

basic docs on extensibility apis
This commit is contained in:
Denys Vuika 2018-08-04 08:26:33 +01:00 committed by GitHub
parent 744b03d22e
commit 22eac50d27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 934 additions and 150 deletions

829
docs/extending.md Normal file
View File

@ -0,0 +1,829 @@
---
title: Extending
---
<p class="danger">
Work is still in progress, the documentation and examples may change.
</p>
# 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:
```json
{
"$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](https://github.com/Alfresco/alfresco-content-app/blob/master/extension.schema.json).
<p class="tip">
Schema allows validating extension files, provides code completion and documentation hints.
</p>
```json
{
"$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.
```json
{
"$schema": "../../extension.schema.json",
"$name": "app",
"$version": "1.0.0",
"$references": [
"plugin1.json",
"plugin2.json"
]
}
```
<p class="warning">
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.
</p>
## Routes
To create a new route, populate the `routes` section with the corresponding entries.
```json
{
"$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](#components) to use for the route. |
| *layout* | The layout [component](#components) to use for the route. |
| *auth* | List of [authentication guards](#authentication-guards). Defaults to `[ "app.auth" ]`. |
| *data* | Custom property bag to carry with the route. |
<p class="tip">
Use the `app.layout.main` value for the `layout` property to get the default application layout,
with header, navigation sidebar and main content area.
<br/><br/>
Leave the `layout` property empty if you want your route component take the whole page.
</p>
You can define the full route schema like in the next example:
```json
{
"$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](#registration) 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. |
<p class="tip">
See [Registration](#registration) section for more details
on how to register your own entries to be re-used at runtime.
</p>
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](#application-actions) for more details. |
| *payload* | Action payload, a string containing value or expression. |
```json
{
"$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.
```text
$(<expression>)
```
Expressions are valid JavaScript blocks that evaluate to values.
Examples:
```text
$('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:
* [Comprehensive Introduction to @ngrx/store](https://gist.github.com/btroncone/a6e4347326749f938510)
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:
```typescript
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:
```typescript
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.
<p class="tip">
Many of the actions take currently selected nodes if no payload provided.
That simplifies declaring and invoking actions from the extension files.
</p>
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.
```json
{
"$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.
```json
{
"$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:
```json
{
"$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" }
]
}
]
}
```
<p class="tip">
You can also negate any rule by utilizing a `!` prefix:
`!app.navigation.isTrashcan` is the opposite of the `app.navigation.isTrashcan`.
</p>
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
```json
{
"$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.
```json
{
"$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`.
<p class="tip">
You can also negate any rule by utilizing a `!` prefix:
`!app.navigation.isTrashcan` is the opposite of the `app.navigation.isTrashcan`.
</p>
| 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**. |
<p class="tip">
See [Registration](#registration) section for more details
on how to register your own entries to be re-used at runtime.
</p>
#### 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
```json
{
"$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:
```typescript
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 {}
```
<p class="warning">
According to Angular rules, all components that are created dynamically at runtime
need to be registered within the `entryComponents` section of the NgModule.
</p>
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](https://github.com/Alfresco/alfresco-content-app/blob/master/src/app/extensions/rule.extensions.ts#L30) type alias for evaluator functions:
```typescript
export type RuleEvaluator = (context: RuleContext, ...args: any[]) => boolean;
```
Create a function that is going to check if user has selected one or multiple nodes.
```typescript
export function hasSelection(
context: RuleContext,
...args: RuleParameter[]
): boolean {
return !context.selection.isEmpty;
}
```
The `context` is a reference to a special instance of the [RuleContext](https://github.com/Alfresco/alfresco-content-app/blob/master/src/app/extensions/rule.extensions.ts#L32) type,
that provides each evaluator access to runtime entities.
```typescript
export interface RuleContext {
selection: SelectionState;
navigation: NavigationState;
permissions: NodePermissions;
getEvaluator(key: string): RuleEvaluator;
}
```
The `SelectionState` interface exposes information about global selection state:
```typescript
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:
```typescript
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`.
<p class="tip">
See [Registration](#registration) section for more details
on how to register your own entries to be re-used at runtime.
</p>
## 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:
```typescript
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:
```json
{
"$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.
```json
{
"$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.
```json
{
"$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:
```text
node: 10
```
You have successfully created a new menu button that invokes your custom action
and redirects you to the extra application route.

View File

@ -1,3 +1,7 @@
---
title: Getting Started
---
# Getting Started
## Prerequisites

View File

@ -1,3 +1,7 @@
---
title: Get Help
---
# Where to get help
There are several ways to get help with building applications using the Alfresco Application Development Framework:

View File

@ -11,6 +11,7 @@
<body>
<div id="app"></div>
<script src="https://unpkg.com/docute@3/dist/docute.js"></script>
<script src="https://unpkg.com/prismjs/components/prism-typescript.js"></script>
<script>
docute.init({
nav: [
@ -22,6 +23,10 @@
title: 'Getting Started',
path: 'getting-started'
},
{
title: 'Extending',
path: 'extending'
},
{
title: 'Get Help',
path: 'help'

View File

@ -315,6 +315,12 @@
"type": "array",
"items": { "$ref": "#/definitions/contentActionRef" },
"minItems": 1
},
"contextActions": {
"description": "Content actions (toolbar, context menus, etc.)",
"type": "array",
"items": { "$ref": "#/definitions/contentActionRef" },
"minItems": 1
}
}
}

View File

@ -36,15 +36,12 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { ExtensionService } from './extensions/extension.service';
import {
SetAppNameAction,
SetHeaderColorAction,
SetLanguagePickerAction,
SetLogoPathAction,
SetSharedUrlAction,
SnackbarErrorAction,
SetCurrentUrlAction
SetCurrentUrlAction,
SetInitialStateAction
} from './store/actions';
import { AppStore } from './store/states/app.state';
import { AppStore, AppState, INITIAL_APP_STATE } from './store/states/app.state';
@Component({
selector: 'app-root',
@ -103,24 +100,18 @@ export class AppComponent implements OnInit {
}
private loadAppSettings() {
const headerColor = this.config.get<string>('headerColor');
if (headerColor) {
this.store.dispatch(new SetHeaderColorAction(headerColor));
}
const appName = this.config.get<string>('application.name');
if (appName) {
this.store.dispatch(new SetAppNameAction(appName));
}
const logoPath = this.config.get<string>('application.logo');
if (logoPath) {
this.store.dispatch(new SetLogoPathAction(logoPath));
}
const languagePicker = this.config.get<boolean>('languagePicker');
this.store.dispatch(new SetLanguagePickerAction(languagePicker));
const sharedPreviewUrl =
this.config.get<string>('ecmHost') + '/#/preview/s/';
this.store.dispatch(new SetSharedUrlAction(sharedPreviewUrl));
const state: AppState = {
... INITIAL_APP_STATE,
appName: this.config.get<string>('application.name'),
headerColor: this.config.get<string>('headerColor'),
logoPath: this.config.get<string>('application.logo'),
sharedUrl: this.config.get<string>('ecmHost') + '/#/preview/s/'
};
this.store.dispatch(new SetInitialStateAction(state));
}
onFileUploadedError(error: FileUploadErrorEvent) {

View File

@ -18,6 +18,7 @@
<adf-layout-header
[logo]="logo$ | async"
[color]="'accent'"
[title]="appName$ | async"
(clicked)="toggleMenu($event)">
<div class="adf-toolbar--spacer"></div>

View File

@ -29,7 +29,7 @@ import { Resolve, Router } from '@angular/router';
import { Person } from 'alfresco-js-api';
import { Observable } from 'rxjs/Observable';
import { AppStore } from '../store/states/app.state';
import { SetUserAction } from '../store/actions';
import { SetUserProfileAction } from '../store/actions';
import { ContentApiService } from './content-api.service';
@Injectable()
@ -44,7 +44,7 @@ export class ProfileResolver implements Resolve<Person> {
return new Observable(observer => {
this.contentApi.getPerson('-me-').subscribe(
person => {
this.store.dispatch(new SetUserAction(person.entry));
this.store.dispatch(new SetUserProfileAction(person.entry));
observer.next(person.entry);
observer.complete();
},

View File

@ -30,6 +30,5 @@ export * from './actions/snackbar.actions';
export * from './actions/router.actions';
export * from './actions/viewer.actions';
export * from './actions/search.actions';
export * from './actions/user.actions';
export * from './actions/library.actions';
export * from './actions/upload.actions';

View File

@ -24,31 +24,20 @@
*/
import { Action } from '@ngrx/store';
import { Node } from 'alfresco-js-api';
import { Node, Person } from 'alfresco-js-api';
import { AppState } from '../states';
export const SET_APP_NAME = 'SET_APP_NAME';
export const SET_HEADER_COLOR = 'SET_HEADER_COLOR';
export const SET_LOGO_PATH = 'SET_LOGO_PATH';
export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
export const SET_LANGUAGE_PICKER = 'SET_LANGUAGE_PICKER';
export const SET_SHARED_URL = 'SET_SHARED_URL';
export const SET_CURRENT_FOLDER = 'SET_CURRENT_FOLDER';
export const SET_CURRENT_URL = 'SET_CURRENT_URL';
export const SET_USER_PROFILE = 'SET_USER_PROFILE';
export const TOGGLE_INFO_DRAWER = 'TOGGLE_INFO_DRAWER';
export const TOGGLE_DOCUMENT_DISPLAY_MODE = 'TOGGLE_DOCUMENT_DISPLAY_MODE';
export class SetAppNameAction implements Action {
readonly type = SET_APP_NAME;
constructor(public payload: string) {}
}
export class SetHeaderColorAction implements Action {
readonly type = SET_HEADER_COLOR;
constructor(public payload: string) {}
}
export class SetLogoPathAction implements Action {
readonly type = SET_LOGO_PATH;
constructor(public payload: string) {}
export class SetInitialStateAction implements Action {
readonly type = SET_INITIAL_STATE;
constructor(public payload: AppState) {}
}
export class SetLanguagePickerAction implements Action {
@ -56,11 +45,6 @@ export class SetLanguagePickerAction implements Action {
constructor(public payload: boolean) {}
}
export class SetSharedUrlAction implements Action {
readonly type = SET_SHARED_URL;
constructor(public payload: string) {}
}
export class SetCurrentFolderAction implements Action {
readonly type = SET_CURRENT_FOLDER;
constructor(public payload: Node) {}
@ -71,6 +55,11 @@ export class SetCurrentUrlAction implements Action {
constructor(public payload: string) {}
}
export class SetUserProfileAction implements Action {
readonly type = SET_USER_PROFILE;
constructor(public payload: Person) { }
}
export class ToggleInfoDrawerAction implements Action {
readonly type = TOGGLE_INFO_DRAWER;
constructor(public payload?: any) {}

View File

@ -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 { Action } from '@ngrx/store';
import { Person } from 'alfresco-js-api';
export const SET_USER = 'SET_USER';
export class SetUserAction implements Action {
readonly type = SET_USER;
constructor(public payload: Person) { }
}

View File

@ -26,20 +26,12 @@
import { Action } from '@ngrx/store';
import { AppState, INITIAL_APP_STATE } from '../states/app.state';
import {
SET_HEADER_COLOR,
SetHeaderColorAction,
SET_APP_NAME,
SetAppNameAction,
SET_LOGO_PATH,
SetLogoPathAction,
SET_SELECTED_NODES,
SetSelectedNodesAction,
SET_USER,
SetUserAction,
SET_USER_PROFILE,
SetUserProfileAction,
SET_LANGUAGE_PICKER,
SetLanguagePickerAction,
SET_SHARED_URL,
SetSharedUrlAction,
SET_CURRENT_FOLDER,
SetCurrentFolderAction,
SET_CURRENT_URL,
@ -49,7 +41,9 @@ import {
TOGGLE_INFO_DRAWER,
ToggleInfoDrawerAction,
TOGGLE_DOCUMENT_DISPLAY_MODE,
ToggleDocumentDisplayMode
ToggleDocumentDisplayMode,
SET_INITIAL_STATE,
SetInitialStateAction
} from '../actions/app.actions';
export function appReducer(
@ -59,31 +53,22 @@ export function appReducer(
let newState: AppState;
switch (action.type) {
case SET_APP_NAME:
newState = updateAppName(state, <SetAppNameAction>action);
break;
case SET_HEADER_COLOR:
newState = updateHeaderColor(state, <SetHeaderColorAction>action);
break;
case SET_LOGO_PATH:
newState = updateLogoPath(state, <SetLogoPathAction>action);
case SET_INITIAL_STATE:
newState = Object.assign({}, (<SetInitialStateAction>action).payload);
break;
case SET_SELECTED_NODES:
newState = updateSelectedNodes(state, <SetSelectedNodesAction>(
action
));
break;
case SET_USER:
newState = updateUser(state, <SetUserAction>action);
case SET_USER_PROFILE:
newState = updateUser(state, <SetUserProfileAction>action);
break;
case SET_LANGUAGE_PICKER:
newState = updateLanguagePicker(state, <SetLanguagePickerAction>(
action
));
break;
case SET_SHARED_URL:
newState = updateSharedUrl(state, <SetSharedUrlAction>action);
break;
case SET_CURRENT_FOLDER:
newState = updateCurrentFolder(state, <SetCurrentFolderAction>(
action
@ -107,15 +92,6 @@ export function appReducer(
return newState;
}
function updateHeaderColor(
state: AppState,
action: SetHeaderColorAction
): AppState {
const newState = Object.assign({}, state);
newState.headerColor = action.payload;
return newState;
}
function updateLanguagePicker(
state: AppState,
action: SetLanguagePickerAction
@ -125,28 +101,7 @@ function updateLanguagePicker(
return newState;
}
function updateAppName(state: AppState, action: SetAppNameAction): AppState {
const newState = Object.assign({}, state);
newState.appName = action.payload;
return newState;
}
function updateLogoPath(state: AppState, action: SetLogoPathAction): AppState {
const newState = Object.assign({}, state);
newState.logoPath = action.payload;
return newState;
}
function updateSharedUrl(
state: AppState,
action: SetSharedUrlAction
): AppState {
const newState = Object.assign({}, state);
newState.sharedUrl = action.payload;
return newState;
}
function updateUser(state: AppState, action: SetUserAction): AppState {
function updateUser(state: AppState, action: SetUserProfileAction): AppState {
const newState = Object.assign({}, state);
const user = action.payload;

View File

@ -136,6 +136,7 @@
{
"id": "app.create.folder",
"type": "default",
"order": 100,
"icon": "create_new_folder",
"title": "APP.NEW_MENU.MENU_ITEMS.CREATE_FOLDER",
"description": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER",
@ -150,6 +151,7 @@
{
"id": "app.create.uploadFile",
"type": "default",
"order": 200,
"icon": "file_upload",
"title": "APP.NEW_MENU.MENU_ITEMS.UPLOAD_FILE",
"description": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES",
@ -164,6 +166,7 @@
{
"id": "app.create.uploadFolder",
"type": "default",
"order": 300,
"icon": "file_upload",
"title": "APP.NEW_MENU.MENU_ITEMS.UPLOAD_FOLDER",
"description": "APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS",
@ -182,6 +185,7 @@
"items": [
{
"id": "app.navbar.personalFiles",
"order": 100,
"icon": "folder",
"title": "APP.BROWSE.PERSONAL.SIDENAV_LINK.LABEL",
"description": "APP.BROWSE.PERSONAL.SIDENAV_LINK.TOOLTIP",
@ -189,6 +193,7 @@
},
{
"id": "app.navbar.libraries",
"order": 200,
"icon": "group_work",
"title": "APP.BROWSE.LIBRARIES.SIDENAV_LINK.LABEL",
"description": "APP.BROWSE.LIBRARIES.SIDENAV_LINK.TOOLTIP",
@ -201,6 +206,7 @@
"items": [
{
"id": "app.navbar.shared",
"order": 100,
"icon": "people",
"title": "APP.BROWSE.SHARED.SIDENAV_LINK.LABEL",
"description": "APP.BROWSE.SHARED.SIDENAV_LINK.TOOLTIP",
@ -208,6 +214,7 @@
},
{
"id": "app.navbar.recentFiles",
"order": 200,
"icon": "schedule",
"title": "APP.BROWSE.RECENT.SIDENAV_LINK.LABEL",
"description": "APP.BROWSE.RECENT.SIDENAV_LINK.TOOLTIP",
@ -215,6 +222,7 @@
},
{
"id": "app.navbar.favorites",
"order": 300,
"icon": "star",
"title": "APP.BROWSE.FAVORITES.SIDENAV_LINK.LABEL",
"description": "APP.BROWSE.FAVORITES.SIDENAV_LINK.TOOLTIP",
@ -222,6 +230,7 @@
},
{
"id": "app.navbar.trashcan",
"order": 400,
"icon": "delete",
"title": "APP.BROWSE.TRASHCAN.SIDENAV_LINK.LABEL",
"description": "APP.BROWSE.TRASHCAN.SIDENAV_LINK.TOOLTIP",
@ -235,7 +244,7 @@
{
"id": "app.toolbar.preview",
"type": "button",
"order": 15,
"order": 100,
"title": "APP.ACTIONS.VIEW",
"icon": "open_in_browser",
"actions": {
@ -248,7 +257,7 @@
{
"id": "app.toolbar.download",
"type": "button",
"order": 20,
"order": 200,
"title": "APP.ACTIONS.DOWNLOAD",
"icon": "get_app",
"actions": {
@ -261,7 +270,7 @@
{
"id": "app.toolbar.editFolder",
"type": "button",
"order": 30,
"order": 300,
"title": "APP.ACTIONS.EDIT",
"icon": "create",
"actions": {
@ -274,6 +283,7 @@
{
"id": "app.toolbar.purgeDeletedNodes",
"type": "button",
"order": 400,
"title": "APP.ACTIONS.DELETE_PERMANENT",
"icon": "delete_forever",
"actions": {
@ -286,6 +296,7 @@
{
"id": "app.toolbar.restoreDeletedNodes",
"type": "button",
"order": 500,
"title": "APP.ACTIONS.RESTORE",
"icon": "restore",
"actions": {
@ -298,6 +309,7 @@
{
"id": "app.toolbar.createLibrary",
"type": "button",
"order": 600,
"title": "Create Library",
"icon": "create_new_folder",
"actions": {
@ -310,6 +322,7 @@
{
"id": "app.toolbar.info",
"type": "custom",
"order": 700,
"component": "app.toolbar.toggleInfoDrawer",
"rules": {
"visible": "app.toolbar.info"
@ -318,6 +331,7 @@
{
"id": "app.toolbar.more",
"type": "menu",
"order": 10000,
"icon": "more_vert",
"title": "APP.ACTIONS.MORE",
"children": [
@ -325,6 +339,7 @@
"id": "app.toolbar.favorite",
"comment": "workaround for Recent Files and Search API issue",
"type": "custom",
"order": 100,
"component": "app.toolbar.toggleFavorite",
"rules": {
"visible": "app.toolbar.favorite.canToggle"
@ -333,6 +348,7 @@
{
"id": "app.toolbar.favorite.add",
"type": "button",
"order": 200,
"title": "APP.ACTIONS.FAVORITE",
"icon": "star_border",
"actions": {
@ -345,6 +361,7 @@
{
"id": "app.toolbar.favorite.remove",
"type": "button",
"order": 300,
"title": "APP.ACTIONS.FAVORITE",
"icon": "star",
"actions": {
@ -357,6 +374,7 @@
{
"id": "app.toolbar.copy",
"type": "button",
"order": 400,
"title": "APP.ACTIONS.COPY",
"icon": "content_copy",
"actions": {
@ -369,6 +387,7 @@
{
"id": "app.toolbar.move",
"type": "button",
"order": 500,
"title": "APP.ACTIONS.MOVE",
"icon": "library_books",
"actions": {
@ -381,6 +400,7 @@
{
"id": "app.toolbar.share",
"type": "button",
"order": 600,
"title": "APP.ACTIONS.SHARE",
"icon": "share",
"actions": {
@ -393,6 +413,7 @@
{
"id": "app.toolbar.unshare",
"type": "button",
"order": 700,
"title": "APP.ACTIONS.UNSHARE",
"icon": "stop_screen_share",
"actions": {
@ -405,6 +426,7 @@
{
"id": "app.toolbar.delete",
"type": "button",
"order": 800,
"title": "APP.ACTIONS.DELETE",
"icon": "delete",
"actions": {
@ -417,6 +439,7 @@
{
"id": "app.toolbar.deleteLibrary",
"type": "button",
"order": 900,
"title": "APP.ACTIONS.DELETE",
"icon": "delete",
"actions": {
@ -429,6 +452,7 @@
{
"id": "app.toolbar.versions",
"type": "button",
"order": 1000,
"title": "APP.ACTIONS.VERSIONS",
"icon": "history",
"actions": {
@ -441,6 +465,7 @@
{
"id": "app.toolbar.permissions",
"type": "button",
"order": 1100,
"title": "APP.ACTIONS.PERMISSIONS",
"icon": "settings_input_component",
"actions": {
@ -457,7 +482,7 @@
{
"id": "app.contextmenu.download",
"type": "button",
"order": 10,
"order": 100,
"title": "APP.ACTIONS.DOWNLOAD",
"icon": "get_app",
"actions": {
@ -470,7 +495,7 @@
{
"id": "app.contextmenu.preview",
"type": "button",
"order": 15,
"order": 200,
"title": "APP.ACTIONS.VIEW",
"icon": "open_in_browser",
"actions": {
@ -483,7 +508,7 @@
{
"id": "app.contextmenu.editFolder",
"type": "button",
"order": 20,
"order": 300,
"title": "APP.ACTIONS.EDIT",
"icon": "create",
"actions": {
@ -497,7 +522,7 @@
"id": "app.contextmenu.share",
"type": "button",
"title": "APP.ACTIONS.SHARE",
"order": 25,
"order": 400,
"icon": "share",
"actions": {
"click": "SHARE_NODE"
@ -510,7 +535,7 @@
"id": "app.contextmenu.favorite.add",
"type": "button",
"title": "APP.ACTIONS.FAVORITE",
"order": 30,
"order": 500,
"icon": "star_border",
"actions": {
"click": "ADD_FAVORITE"
@ -523,7 +548,7 @@
"id": "app.contextmenu.favorite.remove",
"type": "button",
"title": "APP.ACTIONS.FAVORITE",
"order": 30,
"order": 600,
"icon": "star",
"actions": {
"click": "REMOVE_FAVORITE"
@ -536,7 +561,7 @@
"id": "app.contextmenu.copy",
"type": "button",
"title": "APP.ACTIONS.COPY",
"order": 35,
"order": 700,
"icon": "content_copy",
"actions": {
"click": "COPY_NODES"
@ -549,7 +574,7 @@
"id": "app.contextmenu.move",
"type": "button",
"title": "APP.ACTIONS.MOVE",
"order": 40,
"order": 800,
"icon": "library_books",
"actions": {
"click": "MOVE_NODES"
@ -562,7 +587,7 @@
"id": "app.contextmenu.delete",
"type": "button",
"title": "APP.ACTIONS.DELETE",
"order": 45,
"order": 900,
"icon": "delete",
"actions": {
"click": "DELETE_NODES"
@ -575,7 +600,7 @@
"id": "app.contextmenu.versions",
"type": "button",
"title": "APP.ACTIONS.VERSIONS",
"order": 50,
"order": 1000,
"icon": "history",
"actions": {
"click": "MANAGE_VERSIONS"
@ -589,7 +614,7 @@
"type": "button",
"title": "APP.ACTIONS.PERMISSIONS",
"icon": "settings_input_component",
"order": 55,
"order": 1100,
"actions": {
"click": "MANAGE_PERMISSIONS"
},
@ -600,6 +625,7 @@
{
"id": "app.contextmenu.purgeDeletedNodes",
"type": "button",
"order": 1200,
"title": "APP.ACTIONS.DELETE_PERMANENT",
"icon": "delete_forever",
"actions": {
@ -612,6 +638,7 @@
{
"id": "app.contextmenu.restoreDeletedNodes",
"type": "button",
"order": 1300,
"title": "APP.ACTIONS.RESTORE",
"icon": "restore",
"actions": {
@ -628,6 +655,7 @@
{
"id": "app.viewer.favorite.add",
"type": "button",
"order": 100,
"title": "APP.ACTIONS.FAVORITE",
"icon": "star_border",
"actions": {
@ -640,6 +668,7 @@
{
"id": "app.viewer.favorite.remove",
"type": "button",
"order": 200,
"title": "APP.ACTIONS.FAVORITE",
"icon": "star",
"actions": {
@ -652,6 +681,7 @@
{
"id": "app.viewer.share",
"type": "button",
"order": 300,
"title": "APP.ACTIONS.SHARE",
"icon": "share",
"actions": {
@ -664,6 +694,7 @@
{
"id": "app.viewer.copy",
"type": "button",
"order": 400,
"title": "APP.ACTIONS.COPY",
"icon": "content_copy",
"actions": {
@ -676,6 +707,7 @@
{
"id": "app.viewer.move",
"type": "button",
"order": 500,
"title": "APP.ACTIONS.MOVE",
"icon": "library_books",
"actions": {
@ -688,6 +720,7 @@
{
"id": "app.viewer.delete",
"type": "button",
"order": 600,
"title": "APP.ACTIONS.DELETE",
"icon": "delete",
"actions": {
@ -700,6 +733,7 @@
{
"id": "app.viewer.versions",
"type": "button",
"order": 700,
"title": "APP.ACTIONS.VERSIONS",
"icon": "history",
"actions": {
@ -712,6 +746,7 @@
{
"id": "app.viewer.permissions",
"type": "button",
"order": 800,
"title": "APP.ACTIONS.PERMISSIONS",
"icon": "settings_input_component",
"actions": {