From 27977be9a21edd3168dc9a2719c03fb388259042 Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Fri, 3 Aug 2018 14:14:19 +0300 Subject: [PATCH] [ACA-1614] DocumentList - context menu actions (#544) * context menu * make same structure check * align naming * lazy loading support * update module import implementation * close context menu on Escape * focus and navigate context menu items * update with material cdk 'keycodes' name * changed module folder name --- cspell.json | 3 +- src/app/app.module.ts | 2 + .../favorites/favorites.component.html | 5 +- src/app/components/files/files.component.html | 5 +- .../libraries/libraries.component.html | 5 +- .../recent-files/recent-files.component.html | 5 +- .../shared-files/shared-files.component.html | 5 +- .../trashcan/trashcan.component.html | 5 +- src/app/context-menu/animations.ts | 52 ++++++ .../context-menu-item.directive.ts | 51 ++++++ src/app/context-menu/context-menu-overlay.ts | 35 ++++ .../context-menu/context-menu.component.html | 12 ++ .../context-menu/context-menu.component.ts | 117 ++++++++++++ .../context-menu/context-menu.directive.ts | 66 +++++++ src/app/context-menu/context-menu.module.ts | 73 ++++++++ src/app/context-menu/context-menu.service.ts | 100 +++++++++++ src/app/context-menu/interfaces.ts | 32 ++++ src/app/extensions/extension.config.ts | 1 + src/app/extensions/extension.service.ts | 16 ++ src/assets/app.extensions.json | 169 ++++++++++++++++++ 20 files changed, 752 insertions(+), 7 deletions(-) create mode 100644 src/app/context-menu/animations.ts create mode 100644 src/app/context-menu/context-menu-item.directive.ts create mode 100644 src/app/context-menu/context-menu-overlay.ts create mode 100644 src/app/context-menu/context-menu.component.html create mode 100644 src/app/context-menu/context-menu.component.ts create mode 100644 src/app/context-menu/context-menu.directive.ts create mode 100644 src/app/context-menu/context-menu.module.ts create mode 100644 src/app/context-menu/context-menu.service.ts create mode 100644 src/app/context-menu/interfaces.ts diff --git a/cspell.json b/cspell.json index 715d8afdb..34641d842 100644 --- a/cspell.json +++ b/cspell.json @@ -39,7 +39,8 @@ "unindent", "exif", "cardview", - "webm" + "webm", + "keycodes" ], "dictionaries": [ "html", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 5c89f9ac5..c3b546800 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -76,6 +76,7 @@ import { DirectivesModule } from './directives/directives.module'; import { ToggleInfoDrawerComponent } from './components/toolbar/toggle-info-drawer/toggle-info-drawer.component'; import { DocumentDisplayModeComponent } from './components/toolbar/document-display-mode/document-display-mode.component'; import { ToggleFavoriteComponent } from './components/toolbar/toggle-favorite/toggle-favorite.component'; +import { ContextMenuModule } from './context-menu/context-menu.module'; export function setupExtensionServiceFactory(service: ExtensionService): Function { return () => service.load(); @@ -98,6 +99,7 @@ export function setupExtensionServiceFactory(service: ExtensionService): Functio ExtensionsModule, DirectivesModule, + ContextMenuModule.forRoot(), AppInfoDrawerModule ], declarations: [ diff --git a/src/app/components/favorites/favorites.component.html b/src/app/components/favorites/favorites.component.html index 451045507..7b029ca71 100644 --- a/src/app/components/favorites/favorites.component.html +++ b/src/app/components/favorites/favorites.component.html @@ -13,7 +13,10 @@
- -
-
-
-
- . + */ + +import { + state, + style, + animate, + transition, + query, + group, + sequence +} from '@angular/animations'; + +export const contextMenuAnimation = [ + state('void', style({ + opacity: 0, + transform: 'scale(0.01, 0.01)' + })), + transition('void => *', sequence([ + query('.mat-menu-content', style({ opacity: 0 })), + animate('100ms linear', style({ opacity: 1, transform: 'scale(1, 0.5)' })), + group([ + query('.mat-menu-content', animate('400ms cubic-bezier(0.55, 0, 0.55, 0.2)', + style({ opacity: 1 }) + )), + animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({ transform: 'scale(1, 1)' })), + ]) + ])), + transition('* => void', animate('150ms 50ms linear', style({ opacity: 0 }))) +]; diff --git a/src/app/context-menu/context-menu-item.directive.ts b/src/app/context-menu/context-menu-item.directive.ts new file mode 100644 index 000000000..308a8423a --- /dev/null +++ b/src/app/context-menu/context-menu-item.directive.ts @@ -0,0 +1,51 @@ +/*! + * @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 { Directive, ElementRef, OnDestroy } from '@angular/core'; +import { FocusableOption, FocusMonitor, FocusOrigin } from '@angular/cdk/a11y'; + +@Directive({ + selector: '[acaContextMenuItem]', +}) +export class ContextMenuItemDirective implements OnDestroy, FocusableOption { + constructor( + private elementRef: ElementRef, + private focusMonitor: FocusMonitor) { + + focusMonitor.monitor(this.getHostElement(), false); + } + + ngOnDestroy() { + this.focusMonitor.stopMonitoring(this.getHostElement()); + } + + focus(origin: FocusOrigin = 'keyboard'): void { + this.focusMonitor.focusVia(this.getHostElement(), origin); + } + + private getHostElement(): HTMLElement { + return this.elementRef.nativeElement; + } +} diff --git a/src/app/context-menu/context-menu-overlay.ts b/src/app/context-menu/context-menu-overlay.ts new file mode 100644 index 000000000..368138067 --- /dev/null +++ b/src/app/context-menu/context-menu-overlay.ts @@ -0,0 +1,35 @@ +/*! + * @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 { OverlayRef } from '@angular/cdk/overlay'; + +export class ContextMenuOverlayRef { + + constructor(private overlayRef: OverlayRef) { } + + close(): void { + this.overlayRef.dispose(); + } +} diff --git a/src/app/context-menu/context-menu.component.html b/src/app/context-menu/context-menu.component.html new file mode 100644 index 000000000..dc2d449d9 --- /dev/null +++ b/src/app/context-menu/context-menu.component.html @@ -0,0 +1,12 @@ +
+
+ + + +
+
\ No newline at end of file diff --git a/src/app/context-menu/context-menu.component.ts b/src/app/context-menu/context-menu.component.ts new file mode 100644 index 000000000..ba1324995 --- /dev/null +++ b/src/app/context-menu/context-menu.component.ts @@ -0,0 +1,117 @@ +/*! + * @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, OnInit, OnDestroy, HostListener, + ViewChildren, QueryList, AfterViewInit +} from '@angular/core'; +import { trigger } from '@angular/animations'; +import { FocusKeyManager } from '@angular/cdk/a11y'; +import { DOWN_ARROW, UP_ARROW } from '@angular/cdk/keycodes'; + +import { ExtensionService } from '../extensions/extension.service'; +import { AppStore, SelectionState } from '../store/states'; +import { appSelection } from '../store/selectors/app.selectors'; +import { Store } from '@ngrx/store'; +import { Subject } from 'rxjs/Rx'; +import { takeUntil } from 'rxjs/operators'; + +import { ContextMenuOverlayRef } from './context-menu-overlay'; +import { ContentActionRef } from '../extensions/action.extensions'; +import { contextMenuAnimation } from './animations'; +import { ContextMenuItemDirective } from './context-menu-item.directive'; + +@Component({ + selector: 'aca-context-menu', + templateUrl: './context-menu.component.html', + host: { 'role': 'menu' }, + encapsulation: ViewEncapsulation.None, + animations: [ + trigger('panelAnimation', contextMenuAnimation) + ] +}) +export class ContextMenuComponent implements OnInit, OnDestroy, AfterViewInit { + private onDestroy$: Subject = new Subject(); + private selection: SelectionState; + private _keyManager: FocusKeyManager; + actions: Array = []; + + @ViewChildren(ContextMenuItemDirective) + private contextMenuItems: QueryList; + + @HostListener('document:keydown.Escape', ['$event']) + handleKeydownEscape(event: KeyboardEvent) { + if (event) { + this.contextMenuOverlayRef.close(); + } + } + + @HostListener('document:keydown', ['$event']) + handleKeydownEvent(event: KeyboardEvent) { + if (event) { + const keyCode = event.keyCode; + if (keyCode === UP_ARROW || keyCode === DOWN_ARROW) { + this._keyManager.onKeydown(event); + } + } + } + + constructor( + private contextMenuOverlayRef: ContextMenuOverlayRef, + private extensions: ExtensionService, + private store: Store, + ) { } + + runAction(actionId: string) { + const context = { + selection: this.selection + }; + + this.extensions.runActionById(actionId, context); + this.contextMenuOverlayRef.close(); + } + + ngOnDestroy() { + this.onDestroy$.next(true); + this.onDestroy$.complete(); + } + + ngOnInit() { + this.store + .select(appSelection) + .pipe(takeUntil(this.onDestroy$)) + .subscribe(selection => { + if (selection.count) { + this.selection = selection; + this.actions = this.extensions.getAllowedContentContextActions(); + } + }); + } + + ngAfterViewInit() { + this._keyManager = new FocusKeyManager(this.contextMenuItems); + this._keyManager.setFirstItemActive(); + } +} diff --git a/src/app/context-menu/context-menu.directive.ts b/src/app/context-menu/context-menu.directive.ts new file mode 100644 index 000000000..0e6642860 --- /dev/null +++ b/src/app/context-menu/context-menu.directive.ts @@ -0,0 +1,66 @@ +/*! + * @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 { Directive, HostListener, Input } from '@angular/core'; +import { ContextMenuOverlayRef } from './context-menu-overlay'; +import { ContextMenuService } from './context-menu.service'; + +@Directive({ + selector: '[acaContextActions]' +}) +export class ContextActionsDirective { + private overlayRef: ContextMenuOverlayRef = null; + + // tslint:disable-next-line:no-input-rename + @Input('acaContextEnable') enabled: boolean; + + @HostListener('window:resize', ['$event']) + onResize(event) { + if (event && this.overlayRef) { + this.overlayRef.close(); + } + } + + @HostListener('contextmenu', ['$event']) + onContextmenu(event: MouseEvent) { + if (event) { + event.preventDefault(); + + if (this.enabled) { + this.render(event); + } + } + } + + constructor(private contextMenuService: ContextMenuService) { } + + private render(event: MouseEvent) { + this.overlayRef = this.contextMenuService.open({ + source: event, + hasBackdrop: true, + panelClass: 'cdk-overlay-pane', + }); + } +} diff --git a/src/app/context-menu/context-menu.module.ts b/src/app/context-menu/context-menu.module.ts new file mode 100644 index 000000000..c670aef71 --- /dev/null +++ b/src/app/context-menu/context-menu.module.ts @@ -0,0 +1,73 @@ +/*! + * @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 { NgModule, ModuleWithProviders } from '@angular/core'; +import { MatMenuModule, MatListModule, MatIconModule, MatButtonModule } from '@angular/material'; +import { BrowserModule } from '@angular/platform-browser'; + +import { ContextActionsDirective } from './context-menu.directive'; +import { ContextMenuService } from './context-menu.service'; +import { ContextMenuComponent } from './context-menu.component'; +import { ContextMenuItemDirective } from './context-menu-item.directive'; +import { CoreModule } from '@alfresco/adf-core'; + +@NgModule({ + imports: [ + MatMenuModule, + MatListModule, + MatIconModule, + MatButtonModule, + BrowserModule, + CoreModule.forChild() + ], + declarations: [ + ContextActionsDirective, + ContextMenuComponent, + ContextMenuItemDirective + ], + exports: [ + ContextActionsDirective, + ContextMenuComponent + ], + entryComponents: [ + ContextMenuComponent + ] +}) +export class ContextMenuModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: ContextMenuModule, + providers: [ + ContextMenuService + ] + }; + } + + static forChild(): ModuleWithProviders { + return { + ngModule: ContextMenuModule + }; + } +} diff --git a/src/app/context-menu/context-menu.service.ts b/src/app/context-menu/context-menu.service.ts new file mode 100644 index 000000000..f7bac83ce --- /dev/null +++ b/src/app/context-menu/context-menu.service.ts @@ -0,0 +1,100 @@ +import { Injectable, Injector, ComponentRef, ElementRef } from '@angular/core'; +import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { ContextMenuOverlayRef } from './context-menu-overlay'; +import { ContextMenuComponent } from './context-menu.component'; +import { ContextmenuOverlayConfig } from './interfaces'; + +@Injectable() +export class ContextMenuService { + constructor( + private injector: Injector, + private overlay: Overlay) { } + + open(config: ContextmenuOverlayConfig) { + + const overlay = this.createOverlay(config); + + const overlayRef = new ContextMenuOverlayRef(overlay); + + this.attachDialogContainer(overlay, config, overlayRef); + + overlay.backdropClick().subscribe(() => overlayRef.close()); + + // prevent native contextmenu on overlay element if config.hasBackdrop is true + (overlay)._backdropElement + .addEventListener('contextmenu', () => { + event.preventDefault(); + (overlay)._backdropClick.next(null); + }, true); + + return overlayRef; + } + + private createOverlay(config: ContextmenuOverlayConfig) { + const overlayConfig = this.getOverlayConfig(config); + return this.overlay.create(overlayConfig); + } + + private attachDialogContainer(overlay: OverlayRef, config: ContextmenuOverlayConfig, contextmenuOverlayRef: ContextMenuOverlayRef) { + const injector = this.createInjector(config, contextmenuOverlayRef); + + const containerPortal = new ComponentPortal(ContextMenuComponent, null, injector); + const containerRef: ComponentRef = overlay.attach(containerPortal); + + return containerRef.instance; + } + + private createInjector(config: ContextmenuOverlayConfig, contextmenuOverlayRef: ContextMenuOverlayRef): PortalInjector { + const injectionTokens = new WeakMap(); + + injectionTokens.set(ContextMenuOverlayRef, contextmenuOverlayRef); + + return new PortalInjector(this.injector, injectionTokens); + } + + private getOverlayConfig(config: ContextmenuOverlayConfig): OverlayConfig { + const fakeElement: any = { + getBoundingClientRect: (): ClientRect => ({ + bottom: config.source.clientY, + height: 0, + left: config.source.clientX, + right: config.source.clientX, + top: config.source.clientY, + width: 0 + }) + }; + + const positionStrategy = this.overlay.position() + .connectedTo( + new ElementRef(fakeElement), + { originX: 'start', originY: 'bottom' }, + { overlayX: 'start', overlayY: 'top' }) + .withFallbackPosition( + { originX: 'start', originY: 'top' }, + { overlayX: 'start', overlayY: 'bottom' }) + .withFallbackPosition( + { originX: 'end', originY: 'top' }, + { overlayX: 'start', overlayY: 'top' }) + .withFallbackPosition( + { originX: 'start', originY: 'top' }, + { overlayX: 'end', overlayY: 'top' }) + .withFallbackPosition( + { originX: 'end', originY: 'center' }, + { overlayX: 'start', overlayY: 'center' }) + .withFallbackPosition( + { originX: 'start', originY: 'center' }, + { overlayX: 'end', overlayY: 'center' } + ); + + const overlayConfig = new OverlayConfig({ + hasBackdrop: config.hasBackdrop, + backdropClass: config.backdropClass, + panelClass: config.panelClass, + scrollStrategy: this.overlay.scrollStrategies.close(), + positionStrategy + }); + + return overlayConfig; + } +} diff --git a/src/app/context-menu/interfaces.ts b/src/app/context-menu/interfaces.ts new file mode 100644 index 000000000..7fb06f250 --- /dev/null +++ b/src/app/context-menu/interfaces.ts @@ -0,0 +1,32 @@ +/*! + * @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 . + */ + +export interface ContextmenuOverlayConfig { + panelClass?: string; + hasBackdrop?: boolean; + backdropClass?: string; + source?: MouseEvent; + data?: any; +} diff --git a/src/app/extensions/extension.config.ts b/src/app/extensions/extension.config.ts index 2ba72f9e3..54b7b0399 100644 --- a/src/app/extensions/extension.config.ts +++ b/src/app/extensions/extension.config.ts @@ -46,6 +46,7 @@ export interface ExtensionConfig { navbar?: Array; content?: { actions?: Array; + contextActions?: Array }; }; } diff --git a/src/app/extensions/extension.service.ts b/src/app/extensions/extension.service.ts index 3b00abf3e..99f799e50 100644 --- a/src/app/extensions/extension.service.ts +++ b/src/app/extensions/extension.service.ts @@ -54,6 +54,7 @@ export class ExtensionService implements RuleContext { contentActions: Array = []; viewerActions: Array = []; + contentContextmenuActions: Array = []; openWithActions: Array = []; createActions: Array = []; navbar: Array = []; @@ -124,6 +125,7 @@ export class ExtensionService implements RuleContext { this.routes = this.loadRoutes(config); this.contentActions = this.loadContentActions(config); this.viewerActions = this.loadViewerActions(config); + this.contentContextmenuActions = this.loadContentContextmenuActions(config); this.openWithActions = this.loadViewerOpenWith(config); this.createActions = this.loadCreateActions(config); this.navbar = this.loadNavBar(config); @@ -173,6 +175,14 @@ export class ExtensionService implements RuleContext { return []; } + protected loadContentContextmenuActions(config: ExtensionConfig): Array { + if (config && config.features && config.features.content) { + return (config.features.content.contextActions || []) + .sort(this.sortByOrder); + } + return []; + } + protected loadNavBar(config: ExtensionConfig): any { if (config && config.features) { return (config.features.navbar || []) @@ -335,6 +345,12 @@ export class ExtensionService implements RuleContext { .filter(action => this.filterByRules(action)); } + getAllowedContentContextActions(): Array { + return this.contentContextmenuActions + .filter(this.filterEnabled) + .filter(action => this.filterByRules(action)); + } + reduceSeparators( acc: ContentActionRef[], el: ContentActionRef, diff --git a/src/assets/app.extensions.json b/src/assets/app.extensions.json index 7e408273a..320176104 100644 --- a/src/assets/app.extensions.json +++ b/src/assets/app.extensions.json @@ -452,6 +452,175 @@ } ] } + ], + "contextActions": [ + { + "id": "app.contextmenu.download", + "type": "button", + "order": 10, + "title": "APP.ACTIONS.DOWNLOAD", + "icon": "get_app", + "actions": { + "click": "DOWNLOAD_NODES" + }, + "rules": { + "visible": "app.toolbar.canDownload" + } + }, + { + "id": "app.contextmenu.preview", + "type": "button", + "order": 15, + "title": "APP.ACTIONS.VIEW", + "icon": "open_in_browser", + "actions": { + "click": "VIEW_FILE" + }, + "rules": { + "visible": "app.toolbar.canViewFile" + } + }, + { + "id": "app.contextmenu.editFolder", + "type": "button", + "order": 20, + "title": "APP.ACTIONS.EDIT", + "icon": "create", + "actions": { + "click": "EDIT_FOLDER" + }, + "rules": { + "visible": "app.toolbar.canEditFolder" + } + }, + { + "id": "app.contextmenu.share", + "type": "button", + "title": "APP.ACTIONS.SHARE", + "order": 25, + "icon": "share", + "actions": { + "click": "SHARE_NODE" + }, + "rules": { + "visible": "app.selection.file.canShare" + } + }, + { + "id": "app.contextmenu.favorite.add", + "type": "button", + "title": "APP.ACTIONS.FAVORITE", + "order": 30, + "icon": "star_border", + "actions": { + "click": "ADD_FAVORITE" + }, + "rules": { + "visible": "app.toolbar.favorite.canAdd" + } + }, + { + "id": "app.contextmenu.favorite.remove", + "type": "button", + "title": "APP.ACTIONS.FAVORITE", + "order": 30, + "icon": "star", + "actions": { + "click": "REMOVE_FAVORITE" + }, + "rules": { + "visible": "app.toolbar.favorite.canRemove" + } + }, + { + "id": "app.contextmenu.copy", + "type": "button", + "title": "APP.ACTIONS.COPY", + "order": 35, + "icon": "content_copy", + "actions": { + "click": "COPY_NODES" + }, + "rules": { + "visible": "app.toolbar.canCopyNode" + } + }, + { + "id": "app.contextmenu.move", + "type": "button", + "title": "APP.ACTIONS.MOVE", + "order": 40, + "icon": "library_books", + "actions": { + "click": "MOVE_NODES" + }, + "rules": { + "visible": "app.selection.canDelete" + } + }, + { + "id": "app.contextmenu.delete", + "type": "button", + "title": "APP.ACTIONS.DELETE", + "order": 45, + "icon": "delete", + "actions": { + "click": "DELETE_NODES" + }, + "rules": { + "visible": "app.selection.canDelete" + } + }, + { + "id": "app.contextmenu.versions", + "type": "button", + "title": "APP.ACTIONS.VERSIONS", + "order": 50, + "icon": "history", + "actions": { + "click": "MANAGE_VERSIONS" + }, + "rules": { + "visible": "app.toolbar.versions" + } + }, + { + "id": "app.contextmenu.permissions", + "type": "button", + "title": "APP.ACTIONS.PERMISSIONS", + "icon": "settings_input_component", + "order": 55, + "actions": { + "click": "MANAGE_PERMISSIONS" + }, + "rules": { + "visible": "app.toolbar.permissions" + } + }, + { + "id": "app.contextmenu.purgeDeletedNodes", + "type": "button", + "title": "APP.ACTIONS.DELETE_PERMANENT", + "icon": "delete_forever", + "actions": { + "click": "PURGE_DELETED_NODES" + }, + "rules": { + "visible": "app.trashcan.hasSelection" + } + }, + { + "id": "app.contextmenu.restoreDeletedNodes", + "type": "button", + "title": "APP.ACTIONS.RESTORE", + "icon": "restore", + "actions": { + "click": "RESTORE_DELETED_NODES" + }, + "rules": { + "visible": "app.trashcan.hasSelection" + } + } ] }, "viewer": {