-
.
+ */
+
+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": {