Merge pull request #192 from GNS3/drag-multiple-nodes-at-once

Drag multiple nodes/drawings. Ref: #85, #183
This commit is contained in:
ziajka 2018-11-08 10:56:14 +01:00 committed by GitHub
commit e79522d00e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 807 additions and 347 deletions

View File

@ -67,6 +67,7 @@ import { CreateSnapshotDialogComponent } from './components/snapshots/create-sna
import { SnapshotsComponent } from './components/snapshots/snapshots.component';
import { SnapshotMenuItemComponent } from './components/snapshots/snapshot-menu-item/snapshot-menu-item.component';
import { MATERIAL_IMPORTS } from './material.imports';
import { DrawingService } from './services/drawing.service';
if (environment.production) {
@ -131,6 +132,7 @@ if (environment.production) {
ApplianceService,
NodeService,
LinkService,
DrawingService,
IndexedDbService,
HttpServer,
SnapshotService,

View File

@ -16,6 +16,11 @@ import { MapChangeDetectorRef } from './services/map-change-detector-ref';
import { Context } from './models/context';
import { D3_MAP_IMPORTS } from './d3-map.imports';
import { CanvasSizeDetector } from './helpers/canvas-size-detector';
import { MapListeners } from './listeners/map-listeners';
import { DrawingsDraggableListener } from './listeners/drawings-draggable-listener';
import { NodesDraggableListener } from './listeners/nodes-draggable-listener';
import { DrawingsEventSource } from './events/drawings-event-source';
import { NodesEventSource } from './events/nodes-event-source';
@NgModule({
@ -39,6 +44,11 @@ import { CanvasSizeDetector } from './helpers/canvas-size-detector';
MapChangeDetectorRef,
CanvasSizeDetector,
Context,
MapListeners,
DrawingsDraggableListener,
NodesDraggableListener,
DrawingsEventSource,
NodesEventSource,
...D3_MAP_IMPORTS
],
exports: [ MapComponent ]

View File

@ -2,11 +2,11 @@ import { Component, OnInit, Output, EventEmitter, OnDestroy, ViewChild } from '@
import { Port } from '../../../models/port';
import { DrawingLineWidget } from '../../widgets/drawing-line';
import { Node } from '../../models/node';
import { NodesWidget } from '../../widgets/nodes';
import { Subscription } from 'rxjs';
import { NodeSelectInterfaceComponent } from '../node-select-interface/node-select-interface.component';
import { LinkCreated } from '../../events/links';
import { NodeClicked } from '../../events/nodes';
import { NodeWidget } from '../../widgets/node';
@Component({
@ -23,11 +23,11 @@ export class DrawLinkToolComponent implements OnInit, OnDestroy {
constructor(
private drawingLineTool: DrawingLineWidget,
private nodesWidget: NodesWidget
private nodeWidget: NodeWidget
) { }
ngOnInit() {
this.onNodeClicked = this.nodesWidget.onNodeClicked.subscribe((eventNode: NodeClicked) => {
this.onNodeClicked = this.nodeWidget.onNodeClicked.subscribe((eventNode: NodeClicked) => {
this.nodeSelectInterfaceMenu.open(
eventNode.node,
eventNode.event.clientY,

View File

@ -17,9 +17,15 @@ import { SelectionTool } from '../../tools/selection-tool';
import { MovingTool } from '../../tools/moving-tool';
import { LinksWidget } from '../../widgets/links';
import { MapChangeDetectorRef } from '../../services/map-change-detector-ref';
import { NodeDragging, NodeDragged } from '../../events/nodes';
import { NodeDragging, NodeDragged, NodeClicked } from '../../events/nodes';
import { LinkCreated } from '../../events/links';
import { CanvasSizeDetector } from '../../helpers/canvas-size-detector';
import { NodeWidget } from '../../widgets/node';
import { MapListeners } from '../../listeners/map-listeners';
import { DraggedDataEvent } from '../../events/event-source';
import { NodesEventSource } from '../../events/nodes-event-source';
import { DrawingsEventSource } from '../../events/drawings-event-source';
import { DrawingsWidget } from '../../widgets/drawings';
@Component({
@ -36,13 +42,13 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
@Input() width = 1500;
@Input() height = 600;
@Output() onNodeDragged: EventEmitter<NodeDragged>;
@Output() nodeDragged: EventEmitter<DraggedDataEvent<Node>>;
@Output() drawingDragged: EventEmitter<DraggedDataEvent<Drawing>>;
@Output() onLinkCreated = new EventEmitter<LinkCreated>();
private parentNativeElement: any;
private svg: Selection<SVGSVGElement, any, null, undefined>;
private onNodeDraggingSubscription: Subscription;
private onChangesDetected: Subscription;
protected settings = {
@ -53,16 +59,22 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
private context: Context,
private mapChangeDetectorRef: MapChangeDetectorRef,
private canvasSizeDetector: CanvasSizeDetector,
private mapListeners: MapListeners,
protected element: ElementRef,
protected nodesWidget: NodesWidget,
protected nodeWidget: NodeWidget,
protected linksWidget: LinksWidget,
protected drawingsWidget: DrawingsWidget,
protected interfaceLabelWidget: InterfaceLabelWidget,
protected selectionToolWidget: SelectionTool,
protected movingToolWidget: MovingTool,
public graphLayout: GraphLayout
public graphLayout: GraphLayout,
nodesEventSource: NodesEventSource,
drawingsEventSource: DrawingsEventSource,
) {
this.parentNativeElement = element.nativeElement;
this.onNodeDragged = nodesWidget.onNodeDragged;
this.nodeDragged = nodesEventSource.dragged;
this.drawingDragged = drawingsEventSource.dragged;
}
@Input('show-interface-labels')
@ -85,6 +97,11 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
}
@Input('draw-link-tool') drawLinkTool: boolean;
@Input('readonly') set readonly(value) {
this.nodesWidget.draggingEnabled = !value;
this.drawingsWidget.draggingEnabled == !value;
}
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
if (
@ -110,31 +127,25 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
}
}
ngOnDestroy() {
this.graphLayout.disconnect(this.svg);
this.onNodeDraggingSubscription.unsubscribe();
this.onChangesDetected.unsubscribe();
}
ngOnInit() {
if (this.parentNativeElement !== null) {
this.createGraph(this.parentNativeElement);
}
this.context.size = this.getSize();
this.onNodeDraggingSubscription = this.nodesWidget.onNodeDragging.subscribe((eventNode: NodeDragging) => {
const links = this.links.filter((link) => link.target.node_id === eventNode.node.node_id || link.source.node_id === eventNode.node.node_id);
links.forEach((link) => {
this.linksWidget.redrawLink(this.svg, link);
});
});
this.onChangesDetected = this.mapChangeDetectorRef.changesDetected.subscribe(() => {
if (this.mapChangeDetectorRef.hasBeenDrawn) {
this.reload();
}
});
this.mapListeners.onInit(this.svg);
}
ngOnDestroy() {
this.graphLayout.disconnect(this.svg);
this.onChangesDetected.unsubscribe();
this.mapListeners.onDestroy();
}
public createGraph(domElement: HTMLElement) {
@ -193,7 +204,7 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
}
private onSymbolsChange(change: SimpleChange) {
this.graphLayout.getNodesWidget().setSymbols(this.symbols);
this.nodeWidget.setSymbols(this.symbols);
}
public redraw() {

View File

@ -14,11 +14,14 @@ import { ImageDrawingWidget } from './widgets/drawings/image-drawing';
import { RectDrawingWidget } from './widgets/drawings/rect-drawing';
import { TextDrawingWidget } from './widgets/drawings/text-drawing';
import { LineDrawingWidget } from './widgets/drawings/line-drawing';
import { NodeWidget } from './widgets/node';
import { DrawingWidget } from './widgets/drawing';
export const D3_MAP_IMPORTS = [
GraphLayout,
LinksWidget,
NodesWidget,
NodeWidget,
DrawingsWidget,
DrawingLineWidget,
SelectionTool,
@ -32,4 +35,5 @@ export const D3_MAP_IMPORTS = [
LineDrawingWidget,
RectDrawingWidget,
TextDrawingWidget,
DrawingWidget
];

View File

@ -0,0 +1,72 @@
import { EventEmitter } from "@angular/core";
import { drag, DraggedElementBaseType } from "d3-drag";
import { event } from "d3-selection";
class DraggableEvent {
public x: number;
public y: number;
public dx: number;
public dy: number;
}
export class DraggableStart<T> extends DraggableEvent {
constructor(
public datum: T
){
super();
}
}
export class DraggableDrag<T> extends DraggableEvent {
constructor(
public datum: T
){
super();
}
}
export class DraggableEnd<T> extends DraggableEvent {
constructor(
public datum: T
){
super();
}
}
export class Draggable<GElement extends DraggedElementBaseType, Datum> {
public start = new EventEmitter<DraggableStart<Datum>>();
public drag = new EventEmitter<DraggableStart<Datum>>();
public end = new EventEmitter<DraggableStart<Datum>>();
public call(selection) {
selection.call(this.behaviour());
}
private behaviour() {
return drag<GElement, Datum>()
.on('start', (datum: Datum) => {
const evt = new DraggableStart<Datum>(datum);
evt.dx = event.dx;
evt.dy = event.dy;
evt.x = event.x;
evt.y = event.y;
this.start.emit(evt);
})
.on('drag', (datum: Datum) => {
const evt = new DraggableDrag<Datum>(datum);
evt.dx = event.dx;
evt.dy = event.dy;
evt.x = event.x;
evt.y = event.y;
this.drag.emit(evt);
})
.on('end', (datum: Datum) => {
const evt = new DraggableEnd<Datum>(datum);
evt.dx = event.dx;
evt.dy = event.dy;
evt.x = event.x;
evt.y = event.y;
this.end.emit(evt);
});
}
}

View File

@ -0,0 +1,9 @@
import { Injectable, EventEmitter } from "@angular/core";
import { Drawing } from "../models/drawing";
import { DraggedDataEvent } from "./event-source";
@Injectable()
export class DrawingsEventSource {
public dragged = new EventEmitter<DraggedDataEvent<Drawing>>();
}

View File

@ -0,0 +1,9 @@
export class DataEventSource<T> {
constructor(
public datum: T
) {}
}
// class CreatedDataEvent<T> extends DataEventSource<T> {}
export class DraggedDataEvent<T> extends DataEventSource<T> {}

View File

@ -0,0 +1,9 @@
import { Injectable, EventEmitter } from "@angular/core";
import { Node } from "../models/node";
import { DraggedDataEvent } from "./event-source";
@Injectable()
export class NodesEventSource {
public dragged = new EventEmitter<DraggedDataEvent<Node>>();
}

View File

@ -0,0 +1,55 @@
import { Injectable } from "@angular/core";
import { DrawingsWidget } from "../widgets/drawings";
import { DraggableStart } from "../events/draggable";
import { Drawing } from "../models/drawing";
import { Subscription } from "rxjs";
import { SelectionManager } from "../managers/selection-manager";
import { DrawingsEventSource } from "../events/drawings-event-source";
import { DraggedDataEvent } from "../events/event-source";
@Injectable()
export class DrawingsDraggableListener {
private start: Subscription;
private drag: Subscription;
private end: Subscription;
constructor(
private drawingsWidget: DrawingsWidget,
private selectionManager: SelectionManager,
private drawingsEventSource: DrawingsEventSource
) {
}
public onInit(svg: any) {
this.start = this.drawingsWidget.draggable.start.subscribe((evt: DraggableStart<Drawing>) => {
let drawings = this.selectionManager.getSelectedDrawings();
if (drawings.filter((n: Drawing) => n.drawing_id === evt.datum.drawing_id).length === 0) {
this.selectionManager.setSelectedDrawings([evt.datum]);
drawings = this.selectionManager.getSelectedDrawings();
}
});
this.drag = this.drawingsWidget.draggable.drag.subscribe((evt: DraggableStart<Drawing>) => {
let drawings = this.selectionManager.getSelectedDrawings();
drawings.forEach((drawing: Drawing) => {
drawing.x += evt.dx;
drawing.y += evt.dy;
this.drawingsWidget.redrawDrawing(svg, drawing);
});
});
this.end = this.drawingsWidget.draggable.end.subscribe((evt: DraggableStart<Drawing>) => {
let drawings = this.selectionManager.getSelectedDrawings();
drawings.forEach((drawing: Drawing) => {
this.drawingsEventSource.dragged.emit(new DraggedDataEvent<Drawing>(drawing));
});
});
}
public onDestroy() {
this.start.unsubscribe();
this.drag.unsubscribe();
this.end.unsubscribe();
}
}

View File

@ -0,0 +1,4 @@
export interface MapListener {
onInit(svg: any);
onDestroy();
}

View File

@ -0,0 +1,29 @@
import { Injectable } from "@angular/core";
import { MapListener } from "./map-listener";
import { DrawingsDraggableListener } from "./drawings-draggable-listener";
import { NodesDraggableListener } from "./nodes-draggable-listener";
@Injectable()
export class MapListeners {
private listeners: MapListener[] = [];
constructor(
private drawingsDraggableListener: DrawingsDraggableListener,
private nodesDraggableListener: NodesDraggableListener
) {
this.listeners.push(this.drawingsDraggableListener);
this.listeners.push(this.nodesDraggableListener);
}
public onInit(svg: any) {
this.listeners.forEach((listener) => {
listener.onInit(svg);
});
}
public onDestroy() {
this.listeners.forEach((listener) => {
listener.onDestroy();
});
}
}

View File

@ -0,0 +1,64 @@
import { Injectable } from "@angular/core";
import { NodesWidget } from "../widgets/nodes";
import { DraggableStart } from "../events/draggable";
import { Node } from "../models/node";
import { Subscription } from "rxjs";
import { SelectionManager } from "../managers/selection-manager";
import { LinksWidget } from "../widgets/links";
import { GraphLayout } from "../widgets/graph-layout";
import { NodesEventSource } from "../events/nodes-event-source";
import { DraggedDataEvent } from "../events/event-source";
@Injectable()
export class NodesDraggableListener {
private start: Subscription;
private drag: Subscription;
private end: Subscription;
constructor(
private nodesWidget: NodesWidget,
private linksWidget: LinksWidget,
private selectionManager: SelectionManager,
private graphLayout: GraphLayout,
private nodesEventSource: NodesEventSource
) {
}
public onInit(svg: any) {
this.start = this.nodesWidget.draggable.start.subscribe((evt: DraggableStart<Node>) => {
let nodes = this.selectionManager.getSelectedNodes();
if (nodes.filter((n: Node) => n.node_id === evt.datum.node_id).length === 0) {
this.selectionManager.setSelectedNodes([evt.datum]);
nodes = this.selectionManager.getSelectedNodes();
}
});
this.drag = this.nodesWidget.draggable.drag.subscribe((evt: DraggableStart<Node>) => {
let nodes = this.selectionManager.getSelectedNodes();
nodes.forEach((node: Node) => {
node.x += evt.dx;
node.y += evt.dy;
this.nodesWidget.redrawNode(svg, node);
const links = this.graphLayout.getLinks().filter((link) => link.target.node_id === node.node_id || link.source.node_id === node.node_id);
links.forEach((link) => {
this.linksWidget.redrawLink(svg, link);
});
});
});
this.end = this.nodesWidget.draggable.end.subscribe((evt: DraggableStart<Node>) => {
let nodes = this.selectionManager.getSelectedNodes();
nodes.forEach((node: Node) => {
this.nodesEventSource.dragged.emit(new DraggedDataEvent<Node>(node));
});
});
}
public onDestroy() {
this.start.unsubscribe();
this.drag.unsubscribe();
this.end.unsubscribe();
}
}

View File

@ -0,0 +1,52 @@
import { Injectable } from "@angular/core";
import { Widget } from "./widget";
import { SVGSelection } from "../models/types";
import { Drawing } from "../models/drawing";
import { DrawingShapeWidget } from "./drawings/drawing-shape-widget";
import { TextDrawingWidget } from "./drawings/text-drawing";
import { ImageDrawingWidget } from "./drawings/image-drawing";
import { RectDrawingWidget } from "./drawings/rect-drawing";
import { LineDrawingWidget } from "./drawings/line-drawing";
import { EllipseDrawingWidget } from "./drawings/ellipse-drawing";
@Injectable()
export class DrawingWidget implements Widget {
private drawingWidgets: DrawingShapeWidget[] = [];
constructor(
private textDrawingWidget: TextDrawingWidget,
private imageDrawingWidget: ImageDrawingWidget,
private rectDrawingWidget: RectDrawingWidget,
private lineDrawingWidget: LineDrawingWidget,
private ellipseDrawingWidget: EllipseDrawingWidget
) {
this.drawingWidgets = [
this.textDrawingWidget,
this.imageDrawingWidget,
this.rectDrawingWidget,
this.lineDrawingWidget,
this.ellipseDrawingWidget
];
}
public draw(view: SVGSelection) {
const drawing_body = view.selectAll<SVGGElement, Drawing>("g.drawing_body")
.data((l) => [l]);
const drawing_body_enter = drawing_body.enter()
.append<SVGGElement>('g')
.attr("class", "drawing_body");
const drawing_body_merge = drawing_body.merge(drawing_body_enter)
.attr('transform', (d: Drawing) => {
return `translate(${d.x},${d.y}) rotate(${d.rotation})`;
});
this.drawingWidgets.forEach((widget) => {
widget.draw(drawing_body_merge);
});
}
}

View File

@ -4,70 +4,67 @@ import { Widget } from "./widget";
import { Drawing } from "../models/drawing";
import { SVGSelection } from "../models/types";
import { Layer } from "../models/layer";
import { TextDrawingWidget } from "./drawings/text-drawing";
import { SvgToDrawingConverter } from "../helpers/svg-to-drawing-converter";
import { ImageDrawingWidget } from "./drawings/image-drawing";
import { RectDrawingWidget } from "./drawings/rect-drawing";
import { LineDrawingWidget } from "./drawings/line-drawing";
import { EllipseDrawingWidget } from "./drawings/ellipse-drawing";
import { DrawingWidget } from "./drawings/drawing-widget";
import { Draggable } from "../events/draggable";
import { DrawingWidget } from "./drawing";
@Injectable()
export class DrawingsWidget implements Widget {
private drawingWidgets: DrawingWidget[] = [];
public draggable = new Draggable<SVGGElement, Drawing>();
public draggingEnabled = false;
// public onContextMenu = new EventEmitter<NodeContextMenu>();
// public onDrawingClicked = new EventEmitter<NodeClicked>();
// public onDrawingDragged = new EventEmitter<NodeDragged>();
// public onDrawingDragging = new EventEmitter<NodeDragging>();
constructor(
private drawingWidget: DrawingWidget,
private svgToDrawingConverter: SvgToDrawingConverter,
private textDrawingWidget: TextDrawingWidget,
private imageDrawingWidget: ImageDrawingWidget,
private rectDrawingWidget: RectDrawingWidget,
private lineDrawingWidget: LineDrawingWidget,
private ellipseDrawingWidget: EllipseDrawingWidget
) {
this.svgToDrawingConverter = new SvgToDrawingConverter();
this.drawingWidgets = [
this.textDrawingWidget,
this.imageDrawingWidget,
this.rectDrawingWidget,
this.lineDrawingWidget,
this.ellipseDrawingWidget
];
}
public draw(view: SVGSelection, drawings?: Drawing[]) {
public redrawDrawing(view: SVGSelection, drawing: Drawing) {
this.drawingWidget.draw(this.selectDrawing(view, drawing));
}
public draw(view: SVGSelection) {
const drawing = view
.selectAll<SVGGElement, Drawing>('g.drawing')
.data((l: Layer) => {
l.drawings.forEach((d: Drawing) => {
.selectAll<SVGGElement, Drawing>("g.drawing")
.data((layer: Layer) => {
layer.drawings.forEach((d: Drawing) => {
try {
d.element = this.svgToDrawingConverter.convert(d.svg);
} catch (error) {
console.log(`Cannot convert due to Error: '${error}'`);
}
});
return l.drawings;
}, (d: Drawing) => {
return d.drawing_id;
return layer.drawings;
}, (l: Drawing) => {
return l.drawing_id;
});
const drawing_enter = drawing.enter()
.append<SVGGElement>('g')
.attr('class', 'drawing');
.attr('class', 'drawing')
.attr('drawing_id', (l: Drawing) => l.drawing_id)
const drawing_merge = drawing.merge(drawing_enter)
.attr('transform', (d: Drawing) => {
return `translate(${d.x},${d.y}) rotate(${d.rotation})`;
});
const merge = drawing.merge(drawing_enter);
this.drawingWidgets.forEach((widget) => {
widget.draw(drawing_merge);
});
this.drawingWidget.draw(merge);
drawing
.exit()
.remove();
if (this.draggingEnabled) {
this.draggable.call(merge);
}
}
private selectDrawing(view: SVGSelection, drawing: Drawing) {
return view.selectAll<SVGGElement, Drawing>(`g.drawing[drawing_id="${drawing.drawing_id}"]`);
}
}

View File

@ -1,5 +1,5 @@
import { SVGSelection } from "../../models/types";
export interface DrawingWidget {
export interface DrawingShapeWidget {
draw(view: SVGSelection);
}

View File

@ -3,12 +3,12 @@ import { Injectable } from "@angular/core";
import { SVGSelection } from "../../models/types";
import { Drawing } from "../../models/drawing";
import { EllipseElement } from "../../models/drawings/ellipse-element";
import { DrawingWidget } from "./drawing-widget";
import { DrawingShapeWidget } from "./drawing-shape-widget";
import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer";
@Injectable()
export class EllipseDrawingWidget implements DrawingWidget {
export class EllipseDrawingWidget implements DrawingShapeWidget {
constructor(
private qtDasharrayFixer: QtDasharrayFixer

View File

@ -3,11 +3,11 @@ import { Injectable } from "@angular/core";
import { SVGSelection } from "../../models/types";
import { Drawing } from "../../models/drawing";
import { ImageElement } from "../../models/drawings/image-element";
import { DrawingWidget } from "./drawing-widget";
import { DrawingShapeWidget } from "./drawing-shape-widget";
@Injectable()
export class ImageDrawingWidget implements DrawingWidget {
export class ImageDrawingWidget implements DrawingShapeWidget {
public draw(view: SVGSelection) {
const drawing = view
.selectAll<SVGImageElement, ImageElement>('image.image_element')

View File

@ -3,12 +3,12 @@ import { Injectable } from "@angular/core";
import { SVGSelection } from "../../models/types";
import { Drawing } from "../../models/drawing";
import { LineElement } from "../../models/drawings/line-element";
import { DrawingWidget } from "./drawing-widget";
import { DrawingShapeWidget } from "./drawing-shape-widget";
import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer";
@Injectable()
export class LineDrawingWidget implements DrawingWidget {
export class LineDrawingWidget implements DrawingShapeWidget {
constructor(
private qtDasharrayFixer: QtDasharrayFixer

View File

@ -3,12 +3,12 @@ import { Injectable } from "@angular/core";
import { SVGSelection } from "../../models/types";
import { Drawing } from "../../models/drawing";
import { RectElement } from "../../models/drawings/rect-element";
import { DrawingWidget } from "./drawing-widget";
import { DrawingShapeWidget } from "./drawing-shape-widget";
import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer";
@Injectable()
export class RectDrawingWidget implements DrawingWidget {
export class RectDrawingWidget implements DrawingShapeWidget {
constructor(
private qtDasharrayFixer: QtDasharrayFixer
) {}

View File

@ -3,13 +3,13 @@ import { Injectable } from "@angular/core";
import { SVGSelection } from "../../models/types";
import { TextElement } from "../../models/drawings/text-element";
import { Drawing } from "../../models/drawing";
import { DrawingWidget } from "./drawing-widget";
import { DrawingShapeWidget } from "./drawing-shape-widget";
import { FontFixer } from "../../helpers/font-fixer";
import { select } from "d3-selection";
@Injectable()
export class TextDrawingWidget implements DrawingWidget {
export class TextDrawingWidget implements DrawingShapeWidget {
static MARGIN = 4;
constructor(

View File

@ -36,6 +36,10 @@ export class GraphLayout implements Widget {
this.links = links;
}
public getLinks() {
return this.links;
}
public setDrawings(drawings: Drawing[]) {
this.drawings = drawings;
}

View File

@ -19,14 +19,6 @@ export class LinkWidget implements Widget {
private interfaceStatusWidget: InterfaceStatusWidget
) {}
public getInterfaceLabelWidget() {
return this.interfaceLabelWidget;
}
public getInterfaceStatusWidget() {
return this.interfaceStatusWidget;
}
public draw(view: SVGSelection) {
const link_body = view.selectAll<SVGGElement, Link>("g.link_body")
.data((l) => [l]);
@ -51,8 +43,8 @@ export class LinkWidget implements Widget {
.select<SVGPathElement>('path')
.classed('selected', (l: Link) => l.is_selected);
this.getInterfaceLabelWidget().draw(link_body_merge);
this.getInterfaceStatusWidget().draw(link_body_merge);
this.interfaceLabelWidget.draw(link_body_merge);
this.interfaceStatusWidget.draw(link_body_merge);
}
}

View File

@ -16,12 +16,12 @@ describe('LinksWidget', () => {
let widget: LinksWidget;
let layersEnter: Selection<SVGGElement, Layer, SVGGElement, any>;
let layer: Layer;
let mockedLinkWidget: LinkWidget;
let linkWidget: LinkWidget;
beforeEach(() => {
svg = new TestSVGCanvas();
mockedLinkWidget = instance(mock(LinkWidget));
widget = new LinksWidget(new MultiLinkCalculatorHelper(), mockedLinkWidget);
linkWidget = instance(mock(LinkWidget));
widget = new LinksWidget(new MultiLinkCalculatorHelper(), linkWidget);
const node_1 = new Node();
node_1.node_id = "1";
@ -65,10 +65,6 @@ describe('LinksWidget', () => {
});
it('should draw links', () => {
const linkWidgetMock = mock(LinkWidget);
const linkWidget = instance(linkWidgetMock);
spyOn(widget, 'getLinkWidget').and.returnValue(linkWidget);
widget.draw(layersEnter);
const drew = svg.canvas.selectAll<SVGGElement, Link>('g.link');

View File

@ -11,16 +11,12 @@ import { LinkWidget } from "./link";
export class LinksWidget implements Widget {
constructor(
private multiLinkCalculatorHelper: MultiLinkCalculatorHelper,
private linkWidget: LinkWidget,
private linkWidget: LinkWidget
) {
}
public getLinkWidget() {
return this.linkWidget;
}
public redrawLink(view: SVGSelection, link: Link) {
this.getLinkWidget().draw(this.selectLink(view, link));
this.linkWidget.draw(this.selectLink(view, link));
}
public draw(view: SVGSelection) {
@ -48,7 +44,7 @@ export class LinksWidget implements Widget {
const merge = link.merge(link_enter);
this.getLinkWidget().draw(merge);
this.linkWidget.draw(merge);
link
.exit()

View File

@ -0,0 +1,70 @@
import { TestSVGCanvas } from "../testing";
import { Node } from "../models/node";
import { Label } from "../models/label";
import { CssFixer } from "../helpers/css-fixer";
import { FontFixer } from "../helpers/font-fixer";
import { NodeWidget } from "./node";
describe('NodesWidget', () => {
let svg: TestSVGCanvas;
let widget: NodeWidget;
beforeEach(() => {
svg = new TestSVGCanvas();
widget = new NodeWidget(new CssFixer(), new FontFixer());
});
afterEach(() => {
svg.destroy();
});
describe('draggable behaviour', () => {
let node: Node;
const tryToDrag = () => {
const drew = svg.canvas.selectAll<SVGGElement, Node>('g.node');
const drewNode = drew.nodes()[0];
drewNode.dispatchEvent(
new MouseEvent('mousedown', {
clientX: 150, clientY: 250, relatedTarget: drewNode,
screenY: 1024, screenX: 1024, view: window
})
);
window.dispatchEvent(new MouseEvent('mousemove', {clientX: 300, clientY: 300}));
window.dispatchEvent(new MouseEvent('mouseup', {clientX: 300, clientY: 300, view: window}));
};
beforeEach(() => {
node = new Node();
node.x = 100;
node.y = 200;
node.width = 100;
node.height = 100;
node.label = new Label();
});
// it('should be draggable when enabled', () => {
// widget.setDraggingEnabled(true);
// widget.draw(svg.canvas);
// tryToDrag();
// expect(node.x).toEqual(250);
// expect(node.y).toEqual(250);
// });
// it('should be not draggable when disabled', () => {
// widget.setDraggingEnabled(false);
// widget.draw(svg.canvas);
// tryToDrag();
// expect(node.x).toEqual(100);
// expect(node.y).toEqual(200);
// });
});
});

View File

@ -0,0 +1,117 @@
import { Injectable, EventEmitter } from "@angular/core";
import { Widget } from "./widget";
import { SVGSelection } from "../models/types";
import { Node } from "../models/node";
import { NodeContextMenu, NodeClicked, NodeDragged, NodeDragging } from "../events/nodes";
import { CssFixer } from "../helpers/css-fixer";
import { FontFixer } from "../helpers/font-fixer";
import { select, event } from "d3-selection";
import { Symbol } from "../../models/symbol";
import { D3DragEvent, drag } from "d3-drag";
@Injectable()
export class NodeWidget implements Widget {
static NODE_LABEL_MARGIN = 3;
public onContextMenu = new EventEmitter<NodeContextMenu>();
public onNodeClicked = new EventEmitter<NodeClicked>();
public onNodeDragged = new EventEmitter<NodeDragged>();
public onNodeDragging = new EventEmitter<NodeDragging>();
private symbols: Symbol[] = [];
constructor(
private cssFixer: CssFixer,
private fontFixer: FontFixer,
) {}
public setSymbols(symbols: Symbol[]) {
this.symbols = symbols;
}
public draw(view: SVGSelection) {
const self = this;
const node_body = view.selectAll<SVGGElement, Node>("g.node_body")
.data((n) => [n]);
const node_body_enter = node_body.enter()
.append<SVGGElement>('g')
.attr("class", "node_body");
node_body_enter
.append<SVGImageElement>('image');
// add label of node
node_body_enter
.append<SVGTextElement>('text')
.attr('class', 'label');
const node_body_merge = node_body.merge(node_body_enter)
.classed('selected', (n: Node) => n.is_selected)
.on("contextmenu", function (n: Node, i: number) {
event.preventDefault();
self.onContextMenu.emit(new NodeContextMenu(event, n));
})
.on('click', (n: Node) => {
this.onNodeClicked.emit(new NodeClicked(event, n));
});
// update image of node
node_body_merge
.select<SVGImageElement>('image')
.attr('xnode:href', (n: Node) => {
const symbol = this.symbols.find((s: Symbol) => s.symbol_id === n.symbol);
if (symbol) {
return 'data:image/svg+xml;base64,' + btoa(symbol.raw);
}
// @todo; we need to have default image
return 'data:image/svg+xml;base64,none';
})
.attr('width', (n: Node) => n.width)
.attr('height', (n: Node) => n.height)
.attr('x', (n: Node) => 0)
.attr('y', (n: Node) => 0)
.on('mouseover', function (this, n: Node) {
select(this).attr("class", "over");
})
.on('mouseout', function (this, n: Node) {
select(this).attr("class", "");
});
node_body_merge
.attr('transform', (n: Node) => {
return `translate(${n.x},${n.y})`;
});
node_body_merge
.select<SVGTextElement>('text.label')
// .attr('y', (n: Node) => n.label.y - n.height / 2. + 10) // @todo: server computes y in auto way
.attr('style', (n: Node) => {
let styles = this.cssFixer.fix(n.label.style);
styles = this.fontFixer.fixStyles(styles);
return styles;
})
.text((n: Node) => n.label.text)
.attr('x', function (this: SVGTextElement, n: Node) {
if (n.label.x === null) {
// center
const bbox = this.getBBox();
return -bbox.width / 2.;
}
return n.label.x + NodeWidget.NODE_LABEL_MARGIN;
})
.attr('y', function (this: SVGTextElement, n: Node) {
let bbox = this.getBBox();
if (n.label.x === null) {
// center
bbox = this.getBBox();
return - n.height / 2. - bbox.height ;
}
return n.label.y + bbox.height - NodeWidget.NODE_LABEL_MARGIN;
});
}
}

View File

@ -1,74 +1,23 @@
import { TestSVGCanvas } from "../testing";
import { NodesWidget } from "./nodes";
import { Node } from "../models/node";
import { Label } from "../models/label";
import { CssFixer } from "../helpers/css-fixer";
import { FontFixer } from "../helpers/font-fixer";
import { NodeWidget } from "./node";
import { instance, mock } from "ts-mockito";
describe('NodesWidget', () => {
let svg: TestSVGCanvas;
let nodeWidget: NodeWidget;
let widget: NodesWidget;
beforeEach(() => {
svg = new TestSVGCanvas();
widget = new NodesWidget(
new CssFixer(),
new FontFixer()
);
nodeWidget = instance(mock(NodeWidget));
widget = new NodesWidget(nodeWidget);
});
afterEach(() => {
svg.destroy();
});
describe('draggable behaviour', () => {
let node: Node;
const tryToDrag = () => {
const drew = svg.canvas.selectAll<SVGGElement, Node>('g.node');
const drewNode = drew.nodes()[0];
drewNode.dispatchEvent(
new MouseEvent('mousedown', {
clientX: 150, clientY: 250, relatedTarget: drewNode,
screenY: 1024, screenX: 1024, view: window
})
);
window.dispatchEvent(new MouseEvent('mousemove', {clientX: 300, clientY: 300}));
window.dispatchEvent(new MouseEvent('mouseup', {clientX: 300, clientY: 300, view: window}));
};
beforeEach(() => {
node = new Node();
node.x = 100;
node.y = 200;
node.width = 100;
node.height = 100;
node.label = new Label();
});
it('should be draggable when enabled', () => {
widget.setDraggingEnabled(true);
widget.draw(svg.canvas, [node]);
tryToDrag();
expect(node.x).toEqual(250);
expect(node.y).toEqual(250);
});
it('should be not draggable when disabled', () => {
widget.setDraggingEnabled(false);
widget.draw(svg.canvas, [node]);
tryToDrag();
expect(node.x).toEqual(100);
expect(node.y).toEqual(200);
});
});
});

View File

@ -1,190 +1,61 @@
import { Injectable, EventEmitter } from "@angular/core";
import { event, select, Selection } from "d3-selection";
import { D3DragEvent, drag } from "d3-drag";
import { Injectable } from "@angular/core";
import { Widget } from "./widget";
import { Node } from "../models/node";
import { SVGSelection } from "../models/types";
import { Symbol } from "../../models/symbol";
import { Layer } from "../models/layer";
import { CssFixer } from "../helpers/css-fixer";
import { FontFixer } from "../helpers/font-fixer";
import { NodeDragging, NodeDragged, NodeContextMenu, NodeClicked } from "../events/nodes";
import { NodeWidget } from "./node";
import { Draggable } from "../events/draggable";
@Injectable()
export class NodesWidget implements Widget {
static NODE_LABEL_MARGIN = 3;
private debug = false;
private draggingEnabled = false;
private symbols: Symbol[] = [];
public onContextMenu = new EventEmitter<NodeContextMenu>();
public onNodeClicked = new EventEmitter<NodeClicked>();
public onNodeDragged = new EventEmitter<NodeDragged>();
public onNodeDragging = new EventEmitter<NodeDragging>();
public draggable = new Draggable<SVGGElement, Node>();
public draggingEnabled = false;
constructor(
private cssFixer: CssFixer,
private fontFixer: FontFixer
private nodeWidget: NodeWidget
) {
this.symbols = [];
}
public setSymbols(symbols: Symbol[]) {
this.symbols = symbols;
public redrawNode(view: SVGSelection, node: Node) {
this.nodeWidget.draw(this.selectNode(view, node));
}
public setDraggingEnabled(enabled: boolean) {
this.draggingEnabled = enabled;
}
public revise(selection: SVGSelection) {
selection
.attr('transform', (n: Node) => {
return `translate(${n.x},${n.y})`;
});
selection
.select<SVGTextElement>('text.label')
// .attr('y', (n: Node) => n.label.y - n.height / 2. + 10) // @todo: server computes y in auto way
.attr('style', (n: Node) => {
let styles = this.cssFixer.fix(n.label.style);
styles = this.fontFixer.fixStyles(styles);
return styles;
})
.text((n: Node) => n.label.text)
.attr('x', function (this: SVGTextElement, n: Node) {
if (n.label.x === null) {
// center
const bbox = this.getBBox();
return -bbox.width / 2.;
}
return n.label.x + NodesWidget.NODE_LABEL_MARGIN;
})
.attr('y', function (this: SVGTextElement, n: Node) {
let bbox = this.getBBox();
if (n.label.x === null) {
// center
bbox = this.getBBox();
return - n.height / 2. - bbox.height ;
}
return n.label.y + bbox.height - NodesWidget.NODE_LABEL_MARGIN;
});
selection
.select<SVGTextElement>('text.node_point_label')
.text((n: Node) => `(${n.x}, ${n.y})`);
}
public draw(view: SVGSelection, nodes?: Node[]) {
const self = this;
let nodes_selection: Selection<SVGGElement, Node, any, any> = view
.selectAll<SVGGElement, Node>('g.node');
if (nodes) {
nodes_selection = nodes_selection.data(nodes);
} else {
nodes_selection = nodes_selection.data((l: Layer) => {
return l.nodes;
public draw(view: SVGSelection) {
const node = view
.selectAll<SVGGElement, Node>("g.node")
.data((layer: Layer) => {
if (layer.nodes) {
return layer.nodes;
}
return [];
}, (n: Node) => {
return n.node_id;
});
}
const node_enter = nodes_selection
.enter()
.append<SVGGElement>('g')
.attr('class', 'node');
const node_enter = node.enter()
.append<SVGGElement>('g')
.attr('class', 'node')
.attr('node_id', (n: Node) => n.node_id)
// add image to node
node_enter
.append<SVGImageElement>('image');
const merge = node.merge(node_enter);
// add label of node
node_enter
.append<SVGTextElement>('text')
.attr('class', 'label');
this.nodeWidget.draw(merge);
if (this.debug) {
node_enter
.append<SVGCircleElement>('circle')
.attr('class', 'node_point')
.attr('r', 2);
node_enter
.append<SVGTextElement>('text')
.attr('class', 'node_point_label')
.attr('x', '-100')
.attr('y', '0');
}
const node_merge = nodes_selection
.merge(node_enter)
.classed('selected', (n: Node) => n.is_selected)
.on("contextmenu", function (n: Node, i: number) {
event.preventDefault();
self.onContextMenu.emit(new NodeContextMenu(event, n));
})
.on('click', (n: Node) => {
this.onNodeClicked.emit(new NodeClicked(event, n));
});
// update image of node
node_merge
.select<SVGImageElement>('image')
.attr('xlink:href', (n: Node) => {
const symbol = this.symbols.find((s: Symbol) => s.symbol_id === n.symbol);
if (symbol) {
return 'data:image/svg+xml;base64,' + btoa(symbol.raw);
}
// @todo; we need to have default image
return 'data:image/svg+xml;base64,none';
})
.attr('width', (n: Node) => n.width)
.attr('height', (n: Node) => n.height)
.attr('x', (n: Node) => 0)
.attr('y', (n: Node) => 0)
.on('mouseover', function (this, n: Node) {
select(this).attr("class", "over");
})
.on('mouseout', function (this, n: Node) {
select(this).attr("class", "");
});
this.revise(node_merge);
const callback = function (this: SVGGElement, n: Node) {
const e: D3DragEvent<SVGGElement, Node, Node> = event;
n.x = e.x;
n.y = e.y;
self.revise(select(this));
self.onNodeDragging.emit(new NodeDragging(event, n));
};
const dragging = () => {
return drag<SVGGElement, Node>()
.on('drag', callback)
.on('end', (n: Node) => {
const e: D3DragEvent<SVGGElement, Node, Node> = event;
self.onNodeDragged.emit(new NodeDragged(e, n));
});
};
if (this.draggingEnabled) {
node_merge.call(dragging());
}
nodes_selection
node
.exit()
.remove();
if (this.draggingEnabled) {
this.draggable.call(merge);
}
}
private selectNode(view: SVGSelection, node: Node) {
return view.selectAll<SVGGElement, Node>(`g.node[node_id="${node.node_id}"]`);
}
}

View File

@ -10,7 +10,9 @@
[selection-tool]="tools.selection"
[moving-tool]="tools.moving"
[draw-link-tool]="tools.draw_link"
(onNodeDragged)="onNodeDragged($event)"
[readonly]="inReadOnlyMode"
(nodeDragged)="onNodeDragged($event)"
(drawingDragged)="onDrawingDragged($event)"
(onLinkCreated)="onLinkCreated($event)"
></app-map>
<div class="project-toolbar">

View File

@ -23,12 +23,14 @@ import { NodesDataSource } from "../../cartography/datasources/nodes-datasource"
import { LinksDataSource } from "../../cartography/datasources/links-datasource";
import { ProjectWebServiceHandler } from "../../handlers/project-web-service-handler";
import { SelectionManager } from "../../cartography/managers/selection-manager";
import { InRectangleHelper } from "../../cartography/helpers/in-rectangle-helper";
import { DrawingsDataSource } from "../../cartography/datasources/drawings-datasource";
import { ProgressService } from "../../common/progress/progress.service";
import { MapChangeDetectorRef } from '../../cartography/services/map-change-detector-ref';
import { NodeContextMenu, NodeDragged } from '../../cartography/events/nodes';
import { NodeContextMenu } from '../../cartography/events/nodes';
import { LinkCreated } from '../../cartography/events/links';
import { NodeWidget } from '../../cartography/widgets/node';
import { DraggedDataEvent } from '../../cartography/events/event-source';
import { DrawingService } from '../../services/drawing.service';
@Component({
@ -56,33 +58,29 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
private inReadOnlyMode = false;
protected selectionManager: SelectionManager;
@ViewChild(MapComponent) mapChild: MapComponent;
@ViewChild(NodeContextMenuComponent) nodeContextMenu: NodeContextMenuComponent;
private subscriptions: Subscription[];
private subscriptions: Subscription[] = [];
constructor(
private route: ActivatedRoute,
private serverService: ServerService,
private projectService: ProjectService,
private symbolService: SymbolService,
private nodeService: NodeService,
private linkService: LinkService,
private progressService: ProgressService,
private projectWebServiceHandler: ProjectWebServiceHandler,
private mapChangeDetectorRef: MapChangeDetectorRef,
protected nodesDataSource: NodesDataSource,
protected linksDataSource: LinksDataSource,
protected drawingsDataSource: DrawingsDataSource,
) {
this.selectionManager = new SelectionManager(
this.nodesDataSource, this.linksDataSource, this.drawingsDataSource, new InRectangleHelper());
this.subscriptions = [];
}
private route: ActivatedRoute,
private serverService: ServerService,
private projectService: ProjectService,
private symbolService: SymbolService,
private nodeService: NodeService,
private linkService: LinkService,
private drawingService: DrawingService,
private progressService: ProgressService,
private projectWebServiceHandler: ProjectWebServiceHandler,
private mapChangeDetectorRef: MapChangeDetectorRef,
private nodeWidget: NodeWidget,
private selectionManager: SelectionManager,
protected nodesDataSource: NodesDataSource,
protected linksDataSource: LinksDataSource,
protected drawingsDataSource: DrawingsDataSource,
) {}
ngOnInit() {
this.progressService.activate();
@ -188,9 +186,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
}
setUpMapCallbacks(project: Project) {
this.mapChild.graphLayout.getNodesWidget().setDraggingEnabled(!this.readonly);
const onContextMenu = this.mapChild.graphLayout.getNodesWidget().onContextMenu.subscribe((eventNode: NodeContextMenu) => {
const onContextMenu = this.nodeWidget.onContextMenu.subscribe((eventNode: NodeContextMenu) => {
this.nodeContextMenu.open(
eventNode.node,
eventNode.event.clientY,
@ -220,12 +216,21 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
});
}
onNodeDragged(nodeEvent: NodeDragged) {
this.nodesDataSource.update(nodeEvent.node);
onNodeDragged(draggedEvent: DraggedDataEvent<Node>) {
this.nodesDataSource.update(draggedEvent.datum);
this.nodeService
.updatePosition(this.server, nodeEvent.node, nodeEvent.node.x, nodeEvent.node.y)
.subscribe((n: Node) => {
this.nodesDataSource.update(n);
.updatePosition(this.server, draggedEvent.datum, draggedEvent.datum.x, draggedEvent.datum.y)
.subscribe((node: Node) => {
this.nodesDataSource.update(node);
});
}
onDrawingDragged(draggedEvent: DraggedDataEvent<Drawing>) {
this.drawingsDataSource.update(draggedEvent.datum);
this.drawingService
.updatePosition(this.server, draggedEvent.datum, draggedEvent.datum.x, draggedEvent.datum.y)
.subscribe((drawing: Drawing) => {
this.drawingsDataSource.update(drawing);
});
}

View File

@ -41,7 +41,7 @@ describe('ApplianceService', () => {
server.port = 3080;
server.authorization = "none";
service.list(server).subscribe();
service.list(server).subscribe(() => {});
httpTestingController.expectOne('http://127.0.0.1:3080/v2/appliances');

View File

@ -0,0 +1,95 @@
import { TestBed, inject } from '@angular/core/testing';
import { HttpClient } from '@angular/common/http';
import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing';
import { HttpServer } from './http-server.service';
import { Server } from '../models/server';
import { Drawing } from '../cartography/models/drawing';
import { getTestServer } from './testing';
import { DrawingService } from './drawing.service';
import { AppTestingModule } from "../testing/app-testing/app-testing.module";
describe('DrawingService', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
let httpServer: HttpServer;
let service: DrawingService;
let server: Server;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
AppTestingModule
],
providers: [
HttpServer,
DrawingService
]
});
httpClient = TestBed.get(HttpClient);
httpTestingController = TestBed.get(HttpTestingController);
httpServer = TestBed.get(HttpServer);
service = TestBed.get(DrawingService);
server = getTestServer();
});
afterEach(() => {
httpTestingController.verify();
});
it('should be created', inject([DrawingService], (service: DrawingService) => {
expect(service).toBeTruthy();
}));
it('should updatePosition of drawing', inject([DrawingService], (service: DrawingService) => {
const drawing = new Drawing();
drawing.project_id = "myproject";
drawing.drawing_id = "id";
service.updatePosition(server, drawing, 10, 20).subscribe();
const req = httpTestingController.expectOne(
'http://127.0.0.1:3080/v2/projects/myproject/drawings/id');
expect(req.request.method).toEqual("PUT");
expect(req.request.body).toEqual({
'x': 10,
'y': 20
});
}));
it('should update drawing', inject([DrawingService], (service: DrawingService) => {
const drawing = new Drawing();
drawing.project_id = "myproject";
drawing.drawing_id = "id";
drawing.x = 10;
drawing.y = 20;
drawing.z = 30;
service.update(server, drawing).subscribe();
const req = httpTestingController.expectOne(
'http://127.0.0.1:3080/v2/projects/myproject/drawings/id');
expect(req.request.method).toEqual("PUT");
expect(req.request.body).toEqual({
'x': 10,
'y': 20,
'z': 30
});
}));
it('should delete drawing', inject([DrawingService], (service: DrawingService) => {
const drawing = new Drawing();
drawing.project_id = "myproject";
drawing.drawing_id = "id";
service.delete(server, drawing).subscribe();
const req = httpTestingController.expectOne(
'http://127.0.0.1:3080/v2/projects/myproject/drawings/id');
expect(req.request.method).toEqual("DELETE");
}));
});

View File

@ -0,0 +1,36 @@
import { Injectable } from '@angular/core';
import { Drawing } from '../cartography/models/drawing';
import { Observable } from 'rxjs';
import 'rxjs/add/operator/map';
import { Server } from "../models/server";
import { HttpServer } from "./http-server.service";
@Injectable()
export class DrawingService {
constructor(private httpServer: HttpServer) { }
updatePosition(server: Server, drawing: Drawing, x: number, y: number): Observable<Drawing> {
return this.httpServer
.put<Drawing>(server, `/projects/${drawing.project_id}/drawings/${drawing.drawing_id}`, {
'x': x,
'y': y
});
}
update(server: Server, drawing: Drawing): Observable<Drawing> {
return this.httpServer
.put<Drawing>(server, `/projects/${drawing.project_id}/drawings/${drawing.drawing_id}`, {
'x': drawing.x,
'y': drawing.y,
'z': drawing.z
});
}
delete(server: Server, drawing: Drawing) {
return this.httpServer.delete<Drawing>(server, `/projects/${drawing.project_id}/drawings/${drawing.drawing_id}`);
}
}

View File

@ -1,4 +1,4 @@
import { TestBed, inject } from '@angular/core/testing';
import { TestBed, inject, fakeAsync } from '@angular/core/testing';
import { PersistenceService, StorageType } from "angular-persistence";
import { Settings, SettingsService } from './settings.service';
@ -23,7 +23,7 @@ describe('SettingsService', () => {
persistenceService = TestBed.get(PersistenceService);
});
afterEach(() => {
beforeEach(() => {
persistenceService.removeAll(StorageType.LOCAL);
});
@ -69,7 +69,7 @@ describe('SettingsService', () => {
});
}));
it('should execute subscriber', inject([SettingsService], (service: SettingsService) => {
it('should execute subscriber', inject([SettingsService], fakeAsync((service: SettingsService) => {
let changedSettings: Settings;
service.set('crash_reports', true);
@ -79,7 +79,7 @@ describe('SettingsService', () => {
service.set('crash_reports', false);
expect(changedSettings.crash_reports).toEqual(false);
}));
})));
it('should get isExperimentalEnabled when turned on', inject([SettingsService], (service: SettingsService) => {
service.set('experimental_features', true);