mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2025-02-20 17:52:46 +00:00
Context menu for list of items
This commit is contained in:
parent
4fa82bc81d
commit
ed59d2ba02
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { SelectionManager } from '../../managers/selection-manager';
|
||||
import { MapChangeDetectorRef } from '../../services/map-change-detector-ref';
|
||||
@ -12,7 +12,10 @@ export class SelectionSelectComponent implements OnInit, OnDestroy {
|
||||
private onSelected: Subscription;
|
||||
private onUnselected: Subscription;
|
||||
|
||||
constructor(private selectionManager: SelectionManager, private mapChangeDetectorRef: MapChangeDetectorRef) {}
|
||||
constructor(
|
||||
private selectionManager: SelectionManager,
|
||||
private mapChangeDetectorRef: MapChangeDetectorRef
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.onSelected = this.selectionManager.selected.subscribe(() => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { mouse, select } from 'd3-selection';
|
||||
import { Injectable, EventEmitter } from '@angular/core';
|
||||
import { mouse, select, event } from 'd3-selection';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { SVGSelection } from '../models/types';
|
||||
@ -12,20 +12,39 @@ export class SelectionTool {
|
||||
static readonly SELECTABLE_CLASS = '.selectable';
|
||||
|
||||
public rectangleSelected = new Subject<Rectangle>();
|
||||
public contextMenuOpened = new EventEmitter<any>();
|
||||
|
||||
private path;
|
||||
private enabled = false;
|
||||
|
||||
public constructor(private context: Context, private selectionEventSource: SelectionEventSource) {}
|
||||
public constructor(
|
||||
private context: Context,
|
||||
private selectionEventSource: SelectionEventSource
|
||||
) {}
|
||||
|
||||
public disableContextMenu(){
|
||||
|
||||
}
|
||||
|
||||
public setEnabled(enabled) {
|
||||
this.enabled = enabled;
|
||||
this.contextMenuOpened.emit(true);
|
||||
}
|
||||
|
||||
private activate(selection) {
|
||||
const self = this;
|
||||
|
||||
selection.on('mousedown', function() {
|
||||
// prevent deselection on right click
|
||||
if (event.button == 2) {
|
||||
selection.on('contextmenu', () => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
self.contextMenuOpened.emit(event);
|
||||
return;
|
||||
}
|
||||
|
||||
const subject = select(window);
|
||||
const parent = this.parentElement;
|
||||
|
||||
|
@ -13,8 +13,8 @@ import { DrawingService } from '../../../../../services/drawing.service';
|
||||
})
|
||||
export class MoveLayerDownActionComponent implements OnInit {
|
||||
@Input() server: Server;
|
||||
@Input() node: Node;
|
||||
@Input() drawing: Drawing;
|
||||
@Input() nodes: Node[];
|
||||
@Input() drawings: Drawing[];
|
||||
|
||||
constructor(
|
||||
private nodesDataSource: NodesDataSource,
|
||||
@ -26,16 +26,18 @@ export class MoveLayerDownActionComponent implements OnInit {
|
||||
ngOnInit() {}
|
||||
|
||||
moveLayerDown() {
|
||||
if (this.node) {
|
||||
this.node.z--;
|
||||
this.nodesDataSource.update(this.node);
|
||||
this.nodes.forEach((node) => {
|
||||
node.z--;
|
||||
this.nodesDataSource.update(node);
|
||||
|
||||
this.nodeService.update(this.server, this.node).subscribe((node: Node) => {});
|
||||
} else if (this.drawing) {
|
||||
this.drawing.z--;
|
||||
this.drawingsDataSource.update(this.drawing);
|
||||
this.nodeService.update(this.server, node).subscribe((node: Node) => {});
|
||||
});
|
||||
|
||||
this.drawingService.update(this.server, this.drawing).subscribe((drawing: Drawing) => {});
|
||||
}
|
||||
this.drawings.forEach((drawing) => {
|
||||
drawing.z--;
|
||||
this.drawingsDataSource.update(drawing);
|
||||
|
||||
this.drawingService.update(this.server, drawing).subscribe((drawing: Drawing) => {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -13,8 +13,8 @@ import { DrawingService } from '../../../../../services/drawing.service';
|
||||
})
|
||||
export class MoveLayerUpActionComponent implements OnInit {
|
||||
@Input() server: Server;
|
||||
@Input() node: Node;
|
||||
@Input() drawing: Drawing;
|
||||
@Input() nodes: Node[];
|
||||
@Input() drawings: Drawing[];
|
||||
|
||||
constructor(
|
||||
private nodesDataSource: NodesDataSource,
|
||||
@ -26,16 +26,18 @@ export class MoveLayerUpActionComponent implements OnInit {
|
||||
ngOnInit() {}
|
||||
|
||||
moveLayerUp() {
|
||||
if (this.node) {
|
||||
this.node.z++;
|
||||
this.nodesDataSource.update(this.node);
|
||||
this.nodes.forEach((node) => {
|
||||
node.z++;
|
||||
this.nodesDataSource.update(node);
|
||||
|
||||
this.nodeService.update(this.server, this.node).subscribe((node: Node) => {});
|
||||
} else if (this.drawing) {
|
||||
this.drawing.z++;
|
||||
this.drawingsDataSource.update(this.drawing);
|
||||
this.nodeService.update(this.server, node).subscribe((node: Node) => {});
|
||||
});
|
||||
|
||||
this.drawingService.update(this.server, this.drawing).subscribe((drawing: Drawing) => {});
|
||||
}
|
||||
this.drawings.forEach((drawing) => {
|
||||
drawing.z++;
|
||||
this.drawingsDataSource.update(drawing);
|
||||
|
||||
this.drawingService.update(this.server, drawing).subscribe((drawing: Drawing) => {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<button mat-menu-item *ngIf="node.status == 'stopped'" (click)="startNode()">
|
||||
<button mat-menu-item *ngIf="isNodeWithStoppedStatus" (click)="startNodes()">
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
<span>Start</span>
|
||||
</button>
|
||||
|
@ -9,13 +9,22 @@ import { Node } from '../../../../../cartography/models/node';
|
||||
})
|
||||
export class StartNodeActionComponent implements OnInit {
|
||||
@Input() server: Server;
|
||||
@Input() node: Node;
|
||||
@Input() nodes: Node[];
|
||||
private isNodeWithStoppedStatus: boolean;
|
||||
|
||||
constructor(private nodeService: NodeService) {}
|
||||
|
||||
ngOnInit() {}
|
||||
ngOnInit() {
|
||||
this.nodes.forEach((node) => {
|
||||
if (node.status === 'stopped') {
|
||||
this.isNodeWithStoppedStatus = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startNode() {
|
||||
this.nodeService.start(this.server, this.node).subscribe((n: Node) => {});
|
||||
startNodes() {
|
||||
this.nodes.forEach((node) => {
|
||||
this.nodeService.start(this.server, node).subscribe((n: Node) => {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<button mat-menu-item *ngIf="node.status == 'started'" (click)="stopNode()">
|
||||
<button mat-menu-item *ngIf="isNodeWithStartedStatus" (click)="stopNodes()">
|
||||
<mat-icon>stop</mat-icon>
|
||||
<span>Stop</span>
|
||||
</button>
|
||||
|
@ -9,13 +9,22 @@ import { Node } from '../../../../../cartography/models/node';
|
||||
})
|
||||
export class StopNodeActionComponent implements OnInit {
|
||||
@Input() server: Server;
|
||||
@Input() node: Node;
|
||||
@Input() nodes: Node[];
|
||||
private isNodeWithStartedStatus: boolean;
|
||||
|
||||
constructor(private nodeService: NodeService) {}
|
||||
|
||||
ngOnInit() {}
|
||||
ngOnInit() {
|
||||
this.nodes.forEach((node) => {
|
||||
if (node.status === 'started') {
|
||||
this.isNodeWithStartedStatus = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stopNode() {
|
||||
this.nodeService.stop(this.server, this.node).subscribe((n: Node) => {});
|
||||
stopNodes() {
|
||||
this.nodes.forEach((node) => {
|
||||
this.nodeService.stop(this.server, node).subscribe((n: Node) => {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +1,30 @@
|
||||
<div class="context-menu" [style.left]="leftPosition" [style.top]="topPosition" *ngIf="node || drawing">
|
||||
<div class="context-menu" [style.left]="leftPosition" [style.top]="topPosition">
|
||||
<span [matMenuTriggerFor]="contextMenu"></span>
|
||||
<mat-menu #contextMenu="matMenu" class="context-menu-items">
|
||||
<app-start-node-action *ngIf="hasNodeCapabilities" [server]="server" [node]="node"></app-start-node-action>
|
||||
<app-stop-node-action *ngIf="hasNodeCapabilities" [server]="server" [node]="node"></app-stop-node-action>
|
||||
<app-edit-style-action
|
||||
*ngIf="hasDrawingCapabilities && !isTextElement"
|
||||
<app-start-node-action *ngIf="nodes.length" [server]="server" [nodes]="nodes"></app-start-node-action>
|
||||
<app-stop-node-action *ngIf="nodes.length" [server]="server" [nodes]="nodes"></app-stop-node-action>
|
||||
<app-edit-style-action *ngIf="drawings.length===1 && !hasTextCapabilities"
|
||||
[server]="server"
|
||||
[project]="project"
|
||||
[drawing]="drawing"
|
||||
[drawing]="drawings[0]"
|
||||
></app-edit-style-action>
|
||||
<app-edit-text-action
|
||||
*ngIf="hasDrawingCapabilities && isTextElement"
|
||||
*ngIf="drawings.length===1 && hasTextCapabilities"
|
||||
[server]="server"
|
||||
[project]="project"
|
||||
[drawing]="drawing"
|
||||
[drawing]="drawings[0]"
|
||||
></app-edit-text-action>
|
||||
<app-move-layer-up-action
|
||||
*ngIf="!projectService.isReadOnly(project)"
|
||||
[server]="server"
|
||||
[node]="node"
|
||||
[drawing]="drawing"
|
||||
[nodes]="nodes"
|
||||
[drawings]="drawings"
|
||||
></app-move-layer-up-action>
|
||||
<app-move-layer-down-action
|
||||
*ngIf="!projectService.isReadOnly(project)"
|
||||
[server]="server"
|
||||
[node]="node"
|
||||
[drawing]="drawing"
|
||||
[nodes]="nodes"
|
||||
[drawings]="drawings"
|
||||
></app-move-layer-down-action>
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
@ -16,7 +16,10 @@ describe('ContextMenuComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatMenuModule, BrowserModule],
|
||||
providers: [{ provide: ChangeDetectorRef }, { provide: ProjectService, useClass: MockedProjectService }],
|
||||
providers: [
|
||||
{ provide: ChangeDetectorRef },
|
||||
{ provide: ProjectService, useClass: MockedProjectService }
|
||||
],
|
||||
declarations: [ContextMenuComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
@ -62,4 +65,13 @@ describe('ContextMenuComponent', () => {
|
||||
|
||||
expect(spy.calls.any()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should reset capabilities while opening menu for list of elements', () => {
|
||||
component.contextMenu = { openMenu() {} } as MatMenuTrigger;
|
||||
var spy = spyOn<any>(component, 'resetCapabilities');
|
||||
spyOn(component, 'setPosition').and.callFake(() => {});
|
||||
component.openMenuForListOfElements([], [], [], 0, 0);
|
||||
|
||||
expect(spy.calls.any()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -7,6 +7,8 @@ import { Project } from '../../../models/project';
|
||||
import { ProjectService } from '../../../services/project.service';
|
||||
import { Drawing } from '../../../cartography/models/drawing';
|
||||
import { TextElement } from '../../../cartography/models/drawings/text-element';
|
||||
import { Label } from '../../../cartography/models/label';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-context-menu',
|
||||
@ -21,11 +23,12 @@ export class ContextMenuComponent implements OnInit {
|
||||
|
||||
topPosition;
|
||||
leftPosition;
|
||||
node: Node;
|
||||
drawing: Drawing;
|
||||
private hasNodeCapabilities: boolean = false;
|
||||
private hasDrawingCapabilities: boolean = false;
|
||||
private isTextElement: boolean = false;
|
||||
|
||||
drawings: Drawing[] = [];
|
||||
nodes: Node[] = [];
|
||||
labels: Label[] = [];
|
||||
|
||||
private hasTextCapabilities: boolean = false;
|
||||
|
||||
constructor(
|
||||
private sanitizer: DomSanitizer,
|
||||
@ -43,32 +46,49 @@ export class ContextMenuComponent implements OnInit {
|
||||
this.changeDetector.detectChanges();
|
||||
}
|
||||
|
||||
public openMenuForNode(node: Node, top: number, left: number) {
|
||||
public openMenuForDrawing(drawing: Drawing, top: number, left: number) {
|
||||
this.resetCapabilities();
|
||||
this.hasNodeCapabilities = true;
|
||||
this.hasTextCapabilities = drawing.element instanceof TextElement;
|
||||
|
||||
this.node = node;
|
||||
this.drawings = [drawing];
|
||||
this.setPosition(top, left);
|
||||
|
||||
this.contextMenu.openMenu();
|
||||
}
|
||||
|
||||
public openMenuForDrawing(drawing: Drawing, top: number, left: number) {
|
||||
public openMenuForNode(node: Node, top: number, left: number) {
|
||||
this.resetCapabilities();
|
||||
this.hasDrawingCapabilities = true;
|
||||
this.isTextElement = drawing.element instanceof TextElement;
|
||||
|
||||
this.drawing = drawing;
|
||||
this.nodes = [node];
|
||||
this.setPosition(top, left);
|
||||
|
||||
this.contextMenu.openMenu();
|
||||
}
|
||||
|
||||
public openMenuForLabel(label: Label, top: number, left: number) {
|
||||
this.resetCapabilities();
|
||||
|
||||
this.labels = [label];
|
||||
this.setPosition(top, left);
|
||||
|
||||
this.contextMenu.openMenu();
|
||||
}
|
||||
|
||||
public openMenuForListOfElements(drawings: Drawing[], nodes: Node[], labels: Label[], top: number, left: number) {
|
||||
this.resetCapabilities();
|
||||
|
||||
this.drawings = drawings;
|
||||
this.nodes = nodes;
|
||||
this.labels = labels;
|
||||
this.setPosition(top, left);
|
||||
|
||||
this.contextMenu.openMenu();
|
||||
}
|
||||
|
||||
private resetCapabilities() {
|
||||
this.node = null;
|
||||
this.drawing = null;
|
||||
this.hasDrawingCapabilities = false;
|
||||
this.hasNodeCapabilities = false;
|
||||
this.isTextElement = false;
|
||||
this.drawings = [];
|
||||
this.nodes = [];
|
||||
this.labels = [];
|
||||
this.hasTextCapabilities = false;
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,9 @@ import { Node } from '../../cartography/models/node';
|
||||
import { ToolsService } from '../../services/tools.service';
|
||||
import { DrawingsWidget } from '../../cartography/widgets/drawings';
|
||||
import { MapDrawingToDrawingConverter } from '../../cartography/converters/map/map-drawing-to-drawing-converter';
|
||||
import { MapLabelToLabelConverter } from '../../cartography/converters/map/map-label-to-label-converter';
|
||||
import { SelectionManager } from '../../cartography/managers/selection-manager';
|
||||
import { SelectionTool } from '../../cartography/tools/selection-tool';
|
||||
|
||||
export class MockedProgressService {
|
||||
public activate() {}
|
||||
@ -139,11 +142,14 @@ describe('ProjectMapComponent', () => {
|
||||
{ provide: DrawingsWidget },
|
||||
{ provide: MapNodeToNodeConverter },
|
||||
{ provide: MapDrawingToDrawingConverter },
|
||||
{ provide: MapLabelToLabelConverter },
|
||||
{ provide: NodesDataSource },
|
||||
{ provide: LinksDataSource },
|
||||
{ provide: DrawingsDataSource, useValue: drawingsDataSource },
|
||||
{ provide: SettingsService, useClass: MockedSettingsService },
|
||||
{ provide: ToolsService }
|
||||
{ provide: ToolsService },
|
||||
{ provide: SelectionManager },
|
||||
{ provide: SelectionTool }
|
||||
],
|
||||
declarations: [ProjectMapComponent, D3MapComponent, ...ANGULAR_MAP_DECLARATIONS],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@ -32,6 +32,14 @@ import { D3MapComponent } from '../../cartography/components/d3-map/d3-map.compo
|
||||
import { ToolsService } from '../../services/tools.service';
|
||||
import { DrawingContextMenu } from '../../cartography/events/event-source';
|
||||
import { MapDrawingToDrawingConverter } from '../../cartography/converters/map/map-drawing-to-drawing-converter';
|
||||
import { SelectionManager } from '../../cartography/managers/selection-manager';
|
||||
import { SelectionTool } from '../../cartography/tools/selection-tool';
|
||||
import { MapDrawing } from '../../cartography/models/map/map-drawing';
|
||||
import { MapLabel } from '../../cartography/models/map/map-label';
|
||||
import { Label } from '../../cartography/models/label';
|
||||
import { MapNode } from '../../cartography/models/map/map-node';
|
||||
import { MapLabelToLabelConverter } from '../../cartography/converters/map/map-label-to-label-converter';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-map',
|
||||
@ -86,11 +94,14 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
private drawingsWidget: DrawingsWidget,
|
||||
private mapNodeToNode: MapNodeToNodeConverter,
|
||||
private mapDrawingToDrawing: MapDrawingToDrawingConverter,
|
||||
private mapLabelToLabel: MapLabelToLabelConverter,
|
||||
private nodesDataSource: NodesDataSource,
|
||||
private linksDataSource: LinksDataSource,
|
||||
private drawingsDataSource: DrawingsDataSource,
|
||||
private settingsService: SettingsService,
|
||||
private toolsService: ToolsService
|
||||
private toolsService: ToolsService,
|
||||
private selectionManager: SelectionManager,
|
||||
private selectionTool: SelectionTool
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@ -206,8 +217,30 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
||||
this.contextMenu.openMenuForDrawing(drawing, eventDrawing.event.clientY, eventDrawing.event.clientX);
|
||||
});
|
||||
|
||||
const onContextMenu = this.selectionTool.contextMenuOpened.subscribe((event) => {
|
||||
const selectedItems = this.selectionManager.getSelected();
|
||||
if (selectedItems.length === 0) return;
|
||||
|
||||
let drawings: Drawing[] = [];
|
||||
let nodes: Node[] = [];
|
||||
let labels: Label[] = [];
|
||||
|
||||
selectedItems.forEach((elem) => {
|
||||
if (elem instanceof MapDrawing) {
|
||||
drawings.push(this.mapDrawingToDrawing.convert(elem));
|
||||
} else if (elem instanceof MapNode) {
|
||||
nodes.push(this.mapNodeToNode.convert(elem));
|
||||
} else if (elem instanceof MapLabel) {
|
||||
labels.push(this.mapLabelToLabel.convert(elem));
|
||||
}
|
||||
});
|
||||
|
||||
this.contextMenu.openMenuForListOfElements(drawings, nodes, labels, event.clientY, event.clientX);
|
||||
});
|
||||
|
||||
this.subscriptions.push(onNodeContextMenu);
|
||||
this.subscriptions.push(onDrawingContextMenu);
|
||||
this.subscriptions.push(onContextMenu);
|
||||
this.mapChangeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
|
@ -45,6 +45,10 @@ export class MockedProjectService {
|
||||
add() {
|
||||
return of(this.projects.pop);
|
||||
}
|
||||
|
||||
isReadOnly(project: Project){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
describe('AddBlankProjectDialogComponent', () => {
|
||||
|
@ -34,7 +34,7 @@
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body class="mat-app-background">
|
||||
<body class="mat-app-background" oncontextmenu="return false;">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
x
Reference in New Issue
Block a user