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) + }; +}