diff --git a/src/app.config.json b/src/app.config.json
index e8f87d16f..39041e5ec 100644
--- a/src/app.config.json
+++ b/src/app.config.json
@@ -200,8 +200,13 @@
"content": {
"actions": [
{
- "disabled": false,
+ "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",
@@ -212,8 +217,8 @@
}
},
{
- "disabled": false,
"id": "aca:toolbar/preview",
+ "type": "button",
"order": 15,
"title": "APP.ACTIONS.VIEW",
"icon": "open_in_browser",
@@ -224,8 +229,8 @@
}
},
{
- "disabled": false,
"id": "aca:toolbar/download",
+ "type": "button",
"order": 20,
"title": "APP.ACTIONS.DOWNLOAD",
"icon": "get_app",
@@ -237,8 +242,8 @@
}
},
{
- "disabled": false,
"id": "aca:toolbar/edit-folder",
+ "type": "button",
"order": 30,
"title": "APP.ACTIONS.EDIT",
"icon": "create",
@@ -249,30 +254,44 @@
}
},
-
{
- "disabled": false,
- "id": "aca:action3",
- "order": 101,
- "title": "Settings",
- "icon": "settings_applications",
- "target": {
- "types": [],
- "permissions": [],
- "action": "aca:actions/settings"
- }
+ "id": "aca:toolbar/separator-2",
+ "order": 200,
+ "type": "separator"
},
{
- "disabled": false,
- "id": "aca:action4",
- "order": 101,
- "title": "Error",
- "icon": "report_problem",
- "target": {
- "types": ["file"],
- "permissions": ["update", "delete"],
- "action": "aca:actions/error"
- }
+ "id": "aca:toolbar/menu-1",
+ "type": "menu",
+ "icon": "storage",
+ "order": 300,
+ "children": [
+ {
+ "id": "aca:action3",
+ "type": "button",
+ "title": "Settings",
+ "icon": "settings_applications",
+ "target": {
+ "types": [],
+ "permissions": [],
+ "action": "aca:actions/settings"
+ }
+ },
+ {
+ "id": "aca:action4",
+ "type": "button",
+ "title": "Error",
+ "icon": "report_problem",
+ "target": {
+ "types": ["file"],
+ "permissions": ["update", "delete"],
+ "action": "aca:actions/error"
+ }
+ }
+ ]
+ },
+ {
+ "id": "aca:toolbar/separator-3",
+ "type": "separator"
}
]
}
diff --git a/src/app/components/favorites/favorites.component.html b/src/app/components/favorites/favorites.component.html
index e64fd52a9..4105319d7 100644
--- a/src/app/components/favorites/favorites.component.html
+++ b/src/app/components/favorites/favorites.component.html
@@ -13,15 +13,9 @@
-
-
-
+
+
+
diff --git a/src/app/components/files/files.component.html b/src/app/components/files/files.component.html
index d9ebe1826..00a54cfa4 100644
--- a/src/app/components/files/files.component.html
+++ b/src/app/components/files/files.component.html
@@ -16,15 +16,9 @@
-
-
-
+
+
+
diff --git a/src/app/components/page.component.ts b/src/app/components/page.component.ts
index 02f282ce6..e84c547a0 100644
--- a/src/app/components/page.component.ts
+++ b/src/app/components/page.component.ts
@@ -83,7 +83,8 @@ export abstract class PageComponent implements OnInit, OnDestroy {
if (selection.isEmpty) {
this.infoDrawerOpened = false;
}
- this.actions = this.extensions.getSelectedContentActions(selection, this.node);
+ const selectedNodes = selection ? selection.nodes : null;
+ this.actions = this.extensions.getAllowedContentActions(selectedNodes, this.node);
this.canUpdateFile = this.selection.file && this.content.canUpdateNode(selection.file);
this.canUpdateNode = this.selection.count === 1 && this.content.canUpdateNode(selection.first);
this.canDelete = !this.selection.isEmpty && this.content.canDeleteNodes(selection.nodes);
diff --git a/src/app/components/recent-files/recent-files.component.html b/src/app/components/recent-files/recent-files.component.html
index 0eec19a18..1f6c334de 100644
--- a/src/app/components/recent-files/recent-files.component.html
+++ b/src/app/components/recent-files/recent-files.component.html
@@ -13,15 +13,9 @@
-
-
-
+
+
+
diff --git a/src/app/components/search/search-results/search-results.component.html b/src/app/components/search/search-results/search-results.component.html
index fcfb76bd5..68976ed8e 100644
--- a/src/app/components/search/search-results/search-results.component.html
+++ b/src/app/components/search/search-results/search-results.component.html
@@ -4,15 +4,9 @@
-
-
-
+
+
+
diff --git a/src/app/components/shared-files/shared-files.component.html b/src/app/components/shared-files/shared-files.component.html
index 65d67faf1..786766de1 100644
--- a/src/app/components/shared-files/shared-files.component.html
+++ b/src/app/components/shared-files/shared-files.component.html
@@ -13,15 +13,9 @@
-
-
-
+
+
+
diff --git a/src/app/components/trashcan/trashcan.component.html b/src/app/components/trashcan/trashcan.component.html
index 6c9570de8..0515ed64d 100644
--- a/src/app/components/trashcan/trashcan.component.html
+++ b/src/app/components/trashcan/trashcan.component.html
@@ -13,15 +13,9 @@
-
-
-
+
+
+
diff --git a/src/app/extensions/components/toolbar-action/toolbar-action.component.html b/src/app/extensions/components/toolbar-action/toolbar-action.component.html
new file mode 100644
index 000000000..c4e03c64d
--- /dev/null
+++ b/src/app/extensions/components/toolbar-action/toolbar-action.component.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/extensions/components/toolbar-action/toolbar-action.component.ts b/src/app/extensions/components/toolbar-action/toolbar-action.component.ts
new file mode 100644
index 000000000..5225fa07f
--- /dev/null
+++ b/src/app/extensions/components/toolbar-action/toolbar-action.component.ts
@@ -0,0 +1,81 @@
+/*!
+ * @license
+ * Alfresco Example Content Application
+ *
+ * Copyright (C) 2005 - 2018 Alfresco Software Limited
+ *
+ * This file is part of the Alfresco Example Content Application.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Alfresco Example Content Application is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+
+import {
+ Component,
+ ViewEncapsulation,
+ ChangeDetectionStrategy,
+ Input,
+ 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';
+
+@Component({
+ selector: 'aca-toolbar-action',
+ templateUrl: './toolbar-action.component.html',
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: { class: 'aca-toolbar-action' }
+})
+export class ToolbarActionComponent implements OnInit, OnDestroy {
+ @Input() entry: ContentActionExtension;
+
+ selection: SelectionState;
+ onDestroy$: Subject = new Subject();
+
+ constructor(
+ protected store: Store,
+ protected extensions: ExtensionService
+ ) {}
+
+ ngOnInit() {
+ this.store
+ .select(appSelection)
+ .pipe(takeUntil(this.onDestroy$))
+ .subscribe(selection => {
+ this.selection = selection;
+ });
+ }
+
+ ngOnDestroy() {
+ this.onDestroy$.next(true);
+ this.onDestroy$.complete();
+ }
+
+ runAction(actionId: string) {
+ const context = {
+ selection: this.selection
+ };
+
+ this.extensions.runActionById(actionId, context);
+ }
+}
diff --git a/src/app/extensions/content-action.extension.ts b/src/app/extensions/content-action.extension.ts
index 5126b09f2..0f4478c2d 100644
--- a/src/app/extensions/content-action.extension.ts
+++ b/src/app/extensions/content-action.extension.ts
@@ -23,12 +23,21 @@
* along with Alfresco. If not, see .
*/
+export enum ContentActionType {
+ default = 'button',
+ button = 'button',
+ separator = 'separator',
+ menu = 'menu'
+}
+
export interface ContentActionExtension {
id: string;
+ type: ContentActionType;
order?: number;
title: string;
icon?: string;
disabled?: boolean;
+ children?: Array;
target: {
types: Array;
permissions: Array,
diff --git a/src/app/extensions/core.extensions.ts b/src/app/extensions/core.extensions.ts
index f32e91d94..e58bfc6cb 100644
--- a/src/app/extensions/core.extensions.ts
+++ b/src/app/extensions/core.extensions.ts
@@ -24,14 +24,20 @@
*/
import { NgModule } from '@angular/core';
-import { AuthGuardEcm } from '@alfresco/adf-core';
+import { AuthGuardEcm, CoreModule } from '@alfresco/adf-core';
import { ExtensionService } from './extension.service';
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';
@NgModule({
- imports: [],
- declarations: [],
+ imports: [
+ CommonModule,
+ CoreModule.forChild()
+ ],
+ declarations: [ToolbarActionComponent],
+ exports: [ToolbarActionComponent],
entryComponents: [AboutComponent]
})
export class CoreExtensionsModule {
diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts
index 7aa9afce2..d17ad4c75 100644
--- a/src/app/extensions/extension.service.ts
+++ b/src/app/extensions/extension.service.ts
@@ -27,13 +27,14 @@ import { Injectable, Type } from '@angular/core';
import { RouteExtension } from './route.extension';
import { ActionExtension } from './action.extension';
import { AppConfigService } from '@alfresco/adf-core';
-import { ContentActionExtension } from './content-action.extension';
+import { ContentActionExtension, ContentActionType } from './content-action.extension';
import { OpenWithExtension } from './open-with.extension';
-import { AppStore, SelectionState } from '../store/states';
+import { AppStore } from '../store/states';
import { Store } from '@ngrx/store';
import { NavigationExtension } from './navigation.extension';
import { Route } from '@angular/router';
-import { Node } from 'alfresco-js-api';
+import { Node, MinimalNodeEntity } from 'alfresco-js-api';
+import { reduceSeparators, sortByOrder, filterEnabled, copyAction, reduceEmptyMenus } from './utils';
@Injectable()
export class ExtensionService {
@@ -70,7 +71,7 @@ export class ExtensionService {
'extensions.core.features.content.actions',
[]
)
- .sort(this.sortByOrder);
+ .sort(sortByOrder);
this.openWithActions = this.config
.get>(
@@ -78,14 +79,14 @@ export class ExtensionService {
[]
)
.filter(entry => !entry.disabled)
- .sort(this.sortByOrder);
+ .sort(sortByOrder);
this.createActions = this.config
.get>(
'extensions.core.features.create',
[]
)
- .sort(this.sortByOrder);
+ .sort(sortByOrder);
}
getRouteById(id: string): RouteExtension {
@@ -178,7 +179,7 @@ export class ExtensionService {
// evaluates create actions for the folder node
getFolderCreateActions(folder: Node): Array {
- return this.createActions.filter(this.filterOutDisabled).map(action => {
+ return this.createActions.filter(filterEnabled).map(action => {
if (
action.target &&
action.target.permissions &&
@@ -200,64 +201,72 @@ export class ExtensionService {
}
// evaluates content actions for the selection and parent folder node
- getSelectedContentActions(
- selection: SelectionState,
+ getAllowedContentActions(
+ nodes: MinimalNodeEntity[],
parentNode: Node
): Array {
return this.contentActions
- .filter(this.filterOutDisabled)
- .filter(action => action.target)
- .filter(action => this.filterByTarget(selection, action))
- .filter(action =>
- this.filterByPermission(selection, action, parentNode)
- );
+ .filter(filterEnabled)
+ .filter(action => this.filterByTarget(nodes, action))
+ .filter(action => this.filterByPermission(nodes, action, parentNode))
+ .reduce(reduceSeparators, [])
+ .map(action => {
+ if (action.type === ContentActionType.menu) {
+ const copy = copyAction(action);
+ if (copy.children && copy.children.length > 0) {
+ copy.children = copy.children
+ .filter(childAction => this.filterByTarget(nodes, childAction))
+ .filter(childAction => this.filterByPermission(nodes, childAction, parentNode))
+ .reduce(reduceSeparators, []);
+ }
+ return copy;
+ }
+ return action;
+ })
+ .reduce(reduceEmptyMenus, []);
}
- private sortByOrder(
- a: { order?: number | undefined },
- b: { order?: number | undefined }
- ) {
- const left = a.order === undefined ? Number.MAX_SAFE_INTEGER : a.order;
- const right = b.order === undefined ? Number.MAX_SAFE_INTEGER : b.order;
- return left - right;
- }
-
- private filterOutDisabled(entry: { disabled?: boolean }): boolean {
- return !entry.disabled;
- }
-
- // todo: support multiple selected nodes
private filterByTarget(
- selection: SelectionState,
+ nodes: MinimalNodeEntity[],
action: ContentActionExtension
): boolean {
+
+ if (!action) {
+ return false;
+ }
+
+ if (!action.target) {
+ return action.type === ContentActionType.separator
+ || action.type === ContentActionType.menu;
+ }
+
const types = action.target.types;
if (!types || types.length === 0) {
return true;
}
- if (selection && !selection.isEmpty) {
+ if (nodes && nodes.length > 0) {
- if (selection.nodes.length === 1) {
- if (selection.folder && types.includes('folder')) {
- return true;
+ if (nodes.length === 1) {
+ if (types.includes('folder')) {
+ return nodes.every(node => node.entry.isFolder);
}
- if (selection.file && types.includes('file')) {
- return true;
+ if (types.includes('file')) {
+ return nodes.every(node => node.entry.isFile);
}
return false;
} else {
if (types.length === 1) {
if (types.includes('folder')) {
if (action.target.multiple) {
- return selection.nodes.every(node => node.entry.isFolder);
+ return nodes.every(node => node.entry.isFolder);
}
return false;
}
if (types.includes('file')) {
if (action.target.multiple) {
- return selection.nodes.every(node => node.entry.isFile);
+ return nodes.every(node => node.entry.isFile);
}
return false;
}
@@ -265,13 +274,13 @@ export class ExtensionService {
return types.some(type => {
if (type === 'folder') {
return action.target.multiple
- ? selection.nodes.some(node => node.entry.isFolder)
- : selection.nodes.every(node => node.entry.isFolder);
+ ? nodes.some(node => node.entry.isFolder)
+ : nodes.every(node => node.entry.isFolder);
}
if (type === 'file') {
return action.target.multiple
- ? selection.nodes.some(node => node.entry.isFile)
- : selection.nodes.every(node => node.entry.isFile);
+ ? nodes.some(node => node.entry.isFile)
+ : nodes.every(node => node.entry.isFile);
}
return false;
});
@@ -284,10 +293,19 @@ export class ExtensionService {
// todo: support multiple selected nodes
private filterByPermission(
- selection: SelectionState,
+ nodes: MinimalNodeEntity[],
action: ContentActionExtension,
parentNode: Node
): boolean {
+ if (!action) {
+ return false;
+ }
+
+ if (!action.target) {
+ return action.type === ContentActionType.separator
+ || action.type === ContentActionType.menu;
+ }
+
const permissions = action.target.permissions;
if (!permissions || permissions.length === 0) {
@@ -298,15 +316,14 @@ export class ExtensionService {
if (permission.startsWith('parent.')) {
if (parentNode) {
const parentQuery = permission.split('.')[1];
- // console.log(parentNode.allowableOperations, parentQuery);
return this.nodeHasPermissions(parentNode, [parentQuery]);
}
return false;
}
- if (selection && selection.first) {
+ if (nodes && nodes.length > 0) {
return this.nodeHasPermissions(
- selection.first.entry,
+ nodes[0].entry,
permissions
);
}
diff --git a/src/app/extensions/utils.ts b/src/app/extensions/utils.ts
new file mode 100644
index 000000000..5bc44cb71
--- /dev/null
+++ b/src/app/extensions/utils.ts
@@ -0,0 +1,87 @@
+/*!
+ * @license
+ * Alfresco Example Content Application
+ *
+ * Copyright (C) 2005 - 2018 Alfresco Software Limited
+ *
+ * This file is part of the Alfresco Example Content Application.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Alfresco Example Content Application is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+
+import {
+ ContentActionExtension,
+ ContentActionType
+} from './content-action.extension';
+
+export function reduceSeparators(
+ acc: ContentActionExtension[],
+ el: ContentActionExtension,
+ i: number,
+ arr: ContentActionExtension[]
+): ContentActionExtension[] {
+ // remove duplicate separators
+ if (i > 0) {
+ const prev = arr[i - 1];
+ if (
+ prev.type === ContentActionType.separator &&
+ el.type === ContentActionType.separator
+ ) {
+ return acc;
+ }
+ }
+ // remove trailing separator
+ if (i === arr.length - 1) {
+ if (el.type === ContentActionType.separator) {
+ return acc;
+ }
+ }
+ return acc.concat(el);
+}
+
+
+export function reduceEmptyMenus(
+ acc: ContentActionExtension[],
+ el: ContentActionExtension
+): ContentActionExtension[] {
+ if (el.type === ContentActionType.menu) {
+ if ((el.children || []).length === 0) {
+ return acc;
+ }
+ }
+ return acc.concat(el);
+}
+
+export function sortByOrder(
+ a: { order?: number | undefined },
+ b: { order?: number | undefined }
+) {
+ const left = a.order === undefined ? Number.MAX_SAFE_INTEGER : a.order;
+ const right = b.order === undefined ? Number.MAX_SAFE_INTEGER : b.order;
+ return left - right;
+}
+
+export function filterEnabled(entry: { disabled?: boolean }): boolean {
+ return !entry.disabled;
+}
+
+export function copyAction(action: ContentActionExtension): ContentActionExtension {
+ return {
+ ...action,
+ children: (action.children || []).map(copyAction)
+ };
+}