From 4084fb39e0450a0eae3fbde9d3f2195521027edd Mon Sep 17 00:00:00 2001 From: PiotrP Date: Thu, 25 Oct 2018 02:40:37 -0700 Subject: [PATCH 01/15] initial implementation of projects importing --- package.json | 3 +- src/app/app.module.ts | 12 +- .../import-project-dialog.component.css | 9 ++ .../import-project-dialog.component.html | 26 ++++ .../import-project-dialog.component.spec.ts | 139 ++++++++++++++++++ .../import-project-dialog.component.ts | 79 ++++++++++ .../projects/projects.component.css | 3 + .../projects/projects.component.html | 3 + .../projects/projects.component.spec.ts | 3 +- .../components/projects/projects.component.ts | 18 ++- 10 files changed, 288 insertions(+), 7 deletions(-) create mode 100644 src/app/components/projects/import-project-dialog/import-project-dialog.component.css create mode 100644 src/app/components/projects/import-project-dialog/import-project-dialog.component.html create mode 100644 src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts create mode 100644 src/app/components/projects/import-project-dialog/import-project-dialog.component.ts diff --git a/package.json b/package.json index 54d7464a..6dc43fb3 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "d3-ng2-service": "^2.1.0", "electron-settings": "^3.2.0", "material-design-icons": "^3.0.1", + "ng2-file-upload": "^1.3.0", "ngx-electron": "^1.0.4", "notosans-fontface": "^1.1.0", "npm-check-updates": "^2.14.1", @@ -62,10 +63,10 @@ "@angular/cli": "^6.0.8", "@angular/compiler-cli": "^6.0.7", "@angular/language-service": "^6.0.7", + "@sentry/electron": "^0.7.0", "@types/jasmine": "~2.8.8", "@types/jasminewd2": "~2.0.2", "@types/node": "~10.5.2", - "@sentry/electron": "^0.7.0", "codelyzer": "~4.4.2", "electron": "2.0.4", "electron-builder": "^20.19.2", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e3c4580f..b9426bae 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -11,6 +11,7 @@ import { MatCardModule, MatMenuModule, MatToolbarModule, + MatStepperModule, MatIconModule, MatFormFieldModule, MatInputModule, @@ -32,7 +33,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { HotkeyModule } from 'angular2-hotkeys'; import { PersistenceModule } from 'angular-persistence'; import { NgxElectronModule } from 'ngx-electron'; - +import { FileUploadModule } from 'ng2-file-upload'; import { AppRoutingModule } from './app-routing.module'; import { VersionService } from './services/version.service'; @@ -48,6 +49,7 @@ import { ApplianceService } from "./services/appliance.service"; import { LinkService } from "./services/link.service"; import { ProjectsComponent } from './components/projects/projects.component'; +import { ImportProjectDialogComponent } from './components/projects/import-project-dialog/import-project-dialog.component'; import { DefaultLayoutComponent } from './layouts/default-layout/default-layout.component'; import { ProgressDialogComponent } from './common/progress-dialog/progress-dialog.component'; import { AppComponent } from './app.component'; @@ -106,6 +108,7 @@ if (environment.production) { AddServerDialogComponent, CreateSnapshotDialogComponent, ProjectsComponent, + ImportProjectDialogComponent, DefaultLayoutComponent, ProgressDialogComponent, NodeContextMenuComponent, @@ -148,10 +151,12 @@ if (environment.production) { MatSortModule, MatSelectModule, MatTooltipModule, + MatStepperModule, CartographyModule, HotkeyModule.forRoot(), PersistenceModule, - NgxElectronModule + NgxElectronModule, + FileUploadModule ], providers: [ SettingsService, @@ -184,7 +189,8 @@ if (environment.production) { AddServerDialogComponent, CreateSnapshotDialogComponent, ProgressDialogComponent, - ApplianceListDialogComponent + ApplianceListDialogComponent, + ImportProjectDialogComponent ], bootstrap: [ AppComponent ] }) diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.css b/src/app/components/projects/import-project-dialog/import-project-dialog.component.css new file mode 100644 index 00000000..3ae208bc --- /dev/null +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.css @@ -0,0 +1,9 @@ +.file-name-form { + margin-left: 10px; + margin-right: 10px; + width:250px; +} + +.non-visible { + display: none; +} \ No newline at end of file diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.html b/src/app/components/projects/import-project-dialog/import-project-dialog.component.html new file mode 100644 index 00000000..f3fd3a31 --- /dev/null +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.html @@ -0,0 +1,26 @@ +

Import project

+ + + + + + + + +
+ + +
+
+ +
+
+
+
+ {{errorMessage}} +
+
+ +
+
+
\ No newline at end of file diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts b/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts new file mode 100644 index 00000000..cd174be8 --- /dev/null +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts @@ -0,0 +1,139 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ImportProjectDialogComponent } from "./import-project-dialog.component"; +import { Server } from "../../../models/server"; +import { MatInputModule, MatIconModule, MatSortModule, MatTableModule, MatTooltipModule, MatDialogModule, MatStepperModule, MatFormFieldModule, MatDialogRef, MatDialog, MAT_DIALOG_DATA } from "@angular/material"; +import { RouterTestingModule } from "@angular/router/testing"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { FileUploadModule, FileSelectDirective, FileItem, FileUploader } from "ng2-file-upload"; +import { FormsModule } from '@angular/forms'; +import { DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; + +describe('ImportProjectDialogComponent', () => { + let component: ImportProjectDialogComponent; + let fixture: ComponentFixture; + let server: Server; + let dialog: MatDialog; + let debugElement: DebugElement; + let fileSelectDirective: FileSelectDirective; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + MatTableModule, + MatTooltipModule, + MatIconModule, + MatSortModule, + MatDialogModule, + MatStepperModule, + MatFormFieldModule, + MatInputModule, + NoopAnimationsModule, + FileUploadModule, + FormsModule, + RouterTestingModule.withRoutes([]), + ], + providers: [ + { provide: MatDialogRef }, + { provide: MAT_DIALOG_DATA } + ], + declarations : [ImportProjectDialogComponent] + }) + .compileComponents(); + + dialog = TestBed.get(MatDialog); + server = new Server(); + server.ip = "localhost"; + server.port = 80; + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ImportProjectDialogComponent); + debugElement = fixture.debugElement; + component = fixture.componentInstance; + component.server = server; + + fixture.detectChanges(); + + debugElement = fixture.debugElement.query(By.directive(FileSelectDirective)); + fileSelectDirective = debugElement.injector.get(FileSelectDirective) as FileSelectDirective; + }); + + it('should be created', () => { + expect(fixture).toBeDefined(); + expect(component).toBeTruthy(); + }); + + it('should set file uploader', () => { + expect(fileSelectDirective).toBeDefined(); + expect(fileSelectDirective.uploader).toBe(component.uploader); + }); + + it('should handle file adding', () => { + spyOn(fileSelectDirective.uploader, 'addToQueue'); + + fileSelectDirective.onChange(); + + const expectedArguments = [ debugElement.nativeElement.files, + fileSelectDirective.getOptions(), + fileSelectDirective.getFilters() ]; + expect(fileSelectDirective.uploader.addToQueue).toHaveBeenCalledWith(...expectedArguments); + }); + + it('should call uploading item', () => { + spyOn(fileSelectDirective.uploader, 'uploadItem'); + + component.onImportClick(); + + expect(fileSelectDirective.uploader.uploadItem).toHaveBeenCalled(); + }); + + it('should call uploading item with correct arguments', () => { + let fileItem = new FileItem(fileSelectDirective.uploader,new File([],"fileName"),{}); + fileSelectDirective.uploader.queue.push(fileItem); + spyOn(fileSelectDirective.uploader, 'uploadItem'); + + component.onImportClick(); + + expect(fileSelectDirective.uploader.uploadItem).toHaveBeenCalledWith(fileItem); + }); + + it('should handle file change event', () => { + let input = fixture.debugElement.query(By.css('input[type=file]')).nativeElement; + spyOn(component, 'uploadProjectFile'); + + input.dispatchEvent(new Event('change')); + + expect(component.uploadProjectFile).toHaveBeenCalled(); + }); + + it('should clear queue after calling delete', () => { + fileSelectDirective.uploader.queue.push(new FileItem(fileSelectDirective.uploader,new File([],"fileName"),{})); + spyOn(fileSelectDirective.uploader.queue, "pop"); + + component.onDeleteClick(); + + expect(fileSelectDirective.uploader.queue.pop).toHaveBeenCalled(); + expect(fileSelectDirective.uploader.queue[0]).toBeNull; + }); + + it('should prepare correct upload path for file', () => { + fileSelectDirective.uploader.queue.push(new FileItem(fileSelectDirective.uploader,new File([],"fileName"),{})); + component.projectName = "newProject.gns3"; + + component.prepareUploadPath(); + + expect(fileSelectDirective.uploader.queue[0].url).toContain("localhost:80"); + expect(fileSelectDirective.uploader.queue[0].url).toContain("newProject"); + }); + + it('should navigate to next step after clicking import', () => { + let fileItem = new FileItem(fileSelectDirective.uploader,new File([],"fileName"),{}); + fileSelectDirective.uploader.queue.push(fileItem); + spyOn(component.stepper, "next"); + + component.onImportClick(); + + expect(component.stepper.next).toHaveBeenCalled(); + }); +}); diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts b/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts new file mode 100644 index 00000000..909baf72 --- /dev/null +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts @@ -0,0 +1,79 @@ +import { Component, OnInit, Inject, ViewChild } from '@angular/core'; +import { MatStepper, MatDialogRef, MAT_DIALOG_DATA } from "@angular/material"; +import { FileUploader, ParsedResponseHeaders, FileItem } from 'ng2-file-upload'; +import { Server } from '../../../models/server'; +import { v4 as uuid } from 'uuid'; + +@Component({ + selector: 'app-import-project-dialog', + templateUrl: 'import-project-dialog.component.html', + styleUrls: ['import-project-dialog.component.css'], +}) +export class ImportProjectDialogComponent implements OnInit { + uploader: FileUploader; + server : Server; + projectName : string; + isImportEnabled : boolean = false; + isFinishEnabled : boolean = false; + errorMessage : string; + + @ViewChild('stepper') stepper: MatStepper; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any){} + + ngOnInit(){ + this.uploader = new FileUploader({}); + this.uploader.onAfterAddingFile = (file) => { file.withCredentials = false; }; + } + + uploadProjectFile(event) : void{ + this.projectName = event.target.files[0].name.split(".")[0]; + this.isImportEnabled = true; + } + + onImportClick() : void{ + if(this.validateProjectName()){ + this.prepareUploadPath(); + this.stepper.selected.completed = true; + this.stepper.next(); + let itemToUpload = this.uploader.queue[0]; + this.uploader.uploadItem(itemToUpload); + + this.uploader.onErrorItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => { + this.errorMessage = response; + this.isFinishEnabled = true; + }; + + this.uploader.onSuccessItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => { + this.isFinishEnabled = true; + }; + } + } + + onNoClick() : void{ + this.uploader.cancelAll(); + this.dialogRef.close(); + } + + onFinishClick() : void{ + this.dialogRef.close(); + } + + onDeleteClick() : void{ + this.uploader.queue.pop(); + this.isImportEnabled = false; + this.projectName = ""; + } + + prepareUploadPath() : void{ + let url = `http://${this.server.ip}:${this.server.port}/v2/projects/${uuid()}/import?name=${this.projectName}`; + this.uploader.queue.forEach(elem => elem.url = url); + } + + validateProjectName() : boolean{ + var pattern = new RegExp(/[~`!#$%\^&*+=\-\[\]\\';,/{}|\\":<>\?]/); + return !pattern.test(this.projectName); + } +} diff --git a/src/app/components/projects/projects.component.css b/src/app/components/projects/projects.component.css index e69de29b..6884375a 100644 --- a/src/app/components/projects/projects.component.css +++ b/src/app/components/projects/projects.component.css @@ -0,0 +1,3 @@ +.import-button { + margin-right:10px +} \ No newline at end of file diff --git a/src/app/components/projects/projects.component.html b/src/app/components/projects/projects.component.html index 6716c27a..4ec801cc 100644 --- a/src/app/components/projects/projects.component.html +++ b/src/app/components/projects/projects.component.html @@ -34,5 +34,8 @@ +
+ +
diff --git a/src/app/components/projects/projects.component.spec.ts b/src/app/components/projects/projects.component.spec.ts index c9fc70f5..24bcf1bd 100644 --- a/src/app/components/projects/projects.component.spec.ts +++ b/src/app/components/projects/projects.component.spec.ts @@ -1,5 +1,5 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatIconModule, MatSortModule, MatTableModule, MatTooltipModule } from "@angular/material"; +import { MatIconModule, MatSortModule, MatTableModule, MatTooltipModule, MatDialogModule } from "@angular/material"; import { RouterTestingModule } from "@angular/router/testing"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; @@ -34,6 +34,7 @@ describe('ProjectsComponent', () => { MatTooltipModule, MatIconModule, MatSortModule, + MatDialogModule, NoopAnimationsModule, RouterTestingModule.withRoutes([]), ], diff --git a/src/app/components/projects/projects.component.ts b/src/app/components/projects/projects.component.ts index 791f2398..571d8364 100644 --- a/src/app/components/projects/projects.component.ts +++ b/src/app/components/projects/projects.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; -import { MatSort, MatSortable } from "@angular/material"; +import { MatSort, MatSortable, MatDialog } from "@angular/material"; import { DataSource } from "@angular/cdk/collections"; @@ -14,6 +14,7 @@ import { ServerService } from "../../services/server.service"; import { SettingsService, Settings } from "../../services/settings.service"; import { ProgressService } from "../../common/progress/progress.service"; +import { ImportProjectDialogComponent } from './import-project-dialog/import-project-dialog.component'; @Component({ selector: 'app-projects', @@ -33,7 +34,8 @@ export class ProjectsComponent implements OnInit { private serverService: ServerService, private projectService: ProjectService, private settingsService: SettingsService, - private progressService: ProgressService + private progressService: ProgressService, + private dialog: MatDialog ) { } @@ -96,6 +98,18 @@ export class ProjectsComponent implements OnInit { this.progressService.deactivate(); }); } + + importProject(){ + const dialogRef = this.dialog.open(ImportProjectDialogComponent, { + width: '550px', + }); + let instance = dialogRef.componentInstance; + instance.server = this.server; + + dialogRef.afterClosed().subscribe(() => { + this.refresh(); + }); + } } From 668235936d9bcecd63bac0b89e0ea79f7cd80cf5 Mon Sep 17 00:00:00 2001 From: PiotrP Date: Fri, 26 Oct 2018 05:35:02 -0700 Subject: [PATCH 02/15] Fixes after review --- src/app/app.module.ts | 3 +- .../import-project-dialog.component.css | 30 ++++++++- .../import-project-dialog.component.html | 32 ++++++---- .../import-project-dialog.component.spec.ts | 46 ++++++++++--- .../import-project-dialog.component.ts | 64 +++++++++++++------ 5 files changed, 131 insertions(+), 44 deletions(-) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b9426bae..500b3518 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,7 +1,7 @@ import * as Raven from 'raven-js'; import { BrowserModule } from '@angular/platform-browser'; import { NgModule, ErrorHandler } from '@angular/core'; -import { FormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { CdkTableModule } from "@angular/cdk/table"; import { HttpClientModule } from '@angular/common/http'; @@ -131,6 +131,7 @@ if (environment.production) { HttpClientModule, AppRoutingModule, FormsModule, + ReactiveFormsModule, BrowserAnimationsModule, CdkTableModule, MatButtonModule, diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.css b/src/app/components/projects/import-project-dialog/import-project-dialog.component.css index 3ae208bc..b47db760 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.css +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.css @@ -1,9 +1,33 @@ +.non-visible { + display: none; +} + +.file-button{ + height: 50px; + width: 120px; + margin-top: 10px; +} + .file-name-form { + float: right; +} + +.file-name-form-field { margin-left: 10px; margin-right: 10px; width:250px; } -.non-visible { - display: none; -} \ No newline at end of file +.delete-button { + background: transparent; + border: none; + outline: 0 +} + +.delete-icon { + vertical-align: "middle"; +} + +.result-message-box { + margin-top: 10px; +} diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.html b/src/app/components/projects/import-project-dialog/import-project-dialog.component.html index f3fd3a31..b27d2c13 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.html +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.html @@ -1,26 +1,34 @@

Import project

- - - - - - -
- - +
+ + +
+ + + Project name is required + Project name is incorrect + + +
+ + +
+
-
- {{errorMessage}} +
+ {{resultMessage}}
- +
\ No newline at end of file diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts b/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts index cd174be8..7aa20d6a 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts @@ -1,11 +1,11 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { ImportProjectDialogComponent } from "./import-project-dialog.component"; +import { ImportProjectDialogComponent, Validator } from "./import-project-dialog.component"; import { Server } from "../../../models/server"; import { MatInputModule, MatIconModule, MatSortModule, MatTableModule, MatTooltipModule, MatDialogModule, MatStepperModule, MatFormFieldModule, MatDialogRef, MatDialog, MAT_DIALOG_DATA } from "@angular/material"; import { RouterTestingModule } from "@angular/router/testing"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { FileUploadModule, FileSelectDirective, FileItem, FileUploader } from "ng2-file-upload"; -import { FormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule, FormBuilder, FormControl, Validators } from '@angular/forms'; import { DebugElement } from '@angular/core'; import { By } from '@angular/platform-browser'; @@ -13,9 +13,9 @@ describe('ImportProjectDialogComponent', () => { let component: ImportProjectDialogComponent; let fixture: ComponentFixture; let server: Server; - let dialog: MatDialog; let debugElement: DebugElement; let fileSelectDirective: FileSelectDirective; + let formBuilder: FormBuilder; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -31,6 +31,7 @@ describe('ImportProjectDialogComponent', () => { NoopAnimationsModule, FileUploadModule, FormsModule, + ReactiveFormsModule, RouterTestingModule.withRoutes([]), ], providers: [ @@ -41,10 +42,10 @@ describe('ImportProjectDialogComponent', () => { }) .compileComponents(); - dialog = TestBed.get(MatDialog); server = new Server(); server.ip = "localhost"; server.port = 80; + formBuilder = new FormBuilder(); })); beforeEach(() => { @@ -52,7 +53,10 @@ describe('ImportProjectDialogComponent', () => { debugElement = fixture.debugElement; component = fixture.componentInstance; component.server = server; - + component.projectNameForm = formBuilder.group({ + projectName: new FormControl(null, [Validators.required, Validator.projectNameValidator]) + }); + component.projectNameForm.controls['projectName'].setValue("ValidName"); fixture.detectChanges(); debugElement = fixture.debugElement.query(By.directive(FileSelectDirective)); @@ -119,16 +123,16 @@ describe('ImportProjectDialogComponent', () => { it('should prepare correct upload path for file', () => { fileSelectDirective.uploader.queue.push(new FileItem(fileSelectDirective.uploader,new File([],"fileName"),{})); - component.projectName = "newProject.gns3"; + component.projectNameForm.controls['projectName'].setValue("newProject"); - component.prepareUploadPath(); + component.onImportClick(); expect(fileSelectDirective.uploader.queue[0].url).toContain("localhost:80"); expect(fileSelectDirective.uploader.queue[0].url).toContain("newProject"); }); it('should navigate to next step after clicking import', () => { - let fileItem = new FileItem(fileSelectDirective.uploader,new File([],"fileName"),{}); + let fileItem = new FileItem(fileSelectDirective.uploader, new File([],"fileName"),{}); fileSelectDirective.uploader.queue.push(fileItem); spyOn(component.stepper, "next"); @@ -136,4 +140,30 @@ describe('ImportProjectDialogComponent', () => { expect(component.stepper.next).toHaveBeenCalled(); }); + + it('should detect if file input is empty', () => { + component.projectNameForm.controls['projectName'].setValue(""); + fixture.detectChanges(); + spyOn(component.stepper, "next"); + spyOn(fileSelectDirective.uploader, 'uploadItem'); + + component.onImportClick(); + + expect(component.stepper.next).not.toHaveBeenCalled(); + expect(fileSelectDirective.uploader.uploadItem).not.toHaveBeenCalled(); + expect(component.projectNameForm.valid).toBeFalsy(); + }); + + it('should sanitize file name input', () => { + component.projectNameForm.controls['projectName'].setValue("[][]"); + fixture.detectChanges(); + spyOn(component.stepper, "next"); + spyOn(fileSelectDirective.uploader, 'uploadItem'); + + component.onImportClick(); + + expect(component.stepper.next).not.toHaveBeenCalled(); + expect(fileSelectDirective.uploader.uploadItem).not.toHaveBeenCalled(); + expect(component.projectNameForm.valid).toBeFalsy(); + }); }); diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts b/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts index 909baf72..c8f23d58 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts @@ -3,51 +3,80 @@ import { MatStepper, MatDialogRef, MAT_DIALOG_DATA } from "@angular/material"; import { FileUploader, ParsedResponseHeaders, FileItem } from 'ng2-file-upload'; import { Server } from '../../../models/server'; import { v4 as uuid } from 'uuid'; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; + +export class Validator { + static projectNameValidator(projectName) { + var pattern = new RegExp(/[~`!#$%\^&*+=\-\[\]\\';,/{}|\\":<>\?]/); + + if(!pattern.test(projectName.value)) { + return null; + } + + return { invalidName: true } + } +} @Component({ selector: 'app-import-project-dialog', templateUrl: 'import-project-dialog.component.html', - styleUrls: ['import-project-dialog.component.css'], + styleUrls: ['import-project-dialog.component.css'] }) export class ImportProjectDialogComponent implements OnInit { uploader: FileUploader; server : Server; - projectName : string; isImportEnabled : boolean = false; isFinishEnabled : boolean = false; - errorMessage : string; + resultMessage : string = "The project is being imported... Please wait"; + projectNameForm: FormGroup; + submitted: boolean = false; @ViewChild('stepper') stepper: MatStepper; constructor( public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: any){} + @Inject(MAT_DIALOG_DATA) public data: any, + private formBuilder: FormBuilder){ + this.projectNameForm = this.formBuilder.group({ + projectName: new FormControl(null, [Validators.required, Validator.projectNameValidator]) + }); + } ngOnInit(){ this.uploader = new FileUploader({}); this.uploader.onAfterAddingFile = (file) => { file.withCredentials = false; }; } + + get form() { + return this.projectNameForm.controls; + } uploadProjectFile(event) : void{ - this.projectName = event.target.files[0].name.split(".")[0]; + this.projectNameForm.controls['projectName'].setValue(event.target.files[0].name.split(".")[0]); this.isImportEnabled = true; } onImportClick() : void{ - if(this.validateProjectName()){ - this.prepareUploadPath(); + if (this.projectNameForm.invalid){ + this.submitted = true; + } else { + const url = this.prepareUploadPath(); + this.uploader.queue.forEach(elem => elem.url = url); + this.stepper.selected.completed = true; this.stepper.next(); - let itemToUpload = this.uploader.queue[0]; + + const itemToUpload = this.uploader.queue[0]; this.uploader.uploadItem(itemToUpload); this.uploader.onErrorItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => { - this.errorMessage = response; - this.isFinishEnabled = true; + this.resultMessage = response; + this.isFinishEnabled = true; }; this.uploader.onSuccessItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => { - this.isFinishEnabled = true; + this.resultMessage = "Project was imported succesfully!"; + this.isFinishEnabled = true; }; } } @@ -64,16 +93,11 @@ export class ImportProjectDialogComponent implements OnInit { onDeleteClick() : void{ this.uploader.queue.pop(); this.isImportEnabled = false; - this.projectName = ""; + this.projectNameForm.controls['projectName'].setValue(""); } - prepareUploadPath() : void{ - let url = `http://${this.server.ip}:${this.server.port}/v2/projects/${uuid()}/import?name=${this.projectName}`; - this.uploader.queue.forEach(elem => elem.url = url); - } - - validateProjectName() : boolean{ - var pattern = new RegExp(/[~`!#$%\^&*+=\-\[\]\\';,/{}|\\":<>\?]/); - return !pattern.test(this.projectName); + prepareUploadPath() : string{ + const projectName = this.projectNameForm.controls['projectName'].value; + return `http://${this.server.ip}:${this.server.port}/v2/projects/${uuid()}/import?name=${projectName}`; } } From cc401a584d1c56626bf37347b002a8a371ed6620 Mon Sep 17 00:00:00 2001 From: PiotrP Date: Tue, 30 Oct 2018 04:13:37 -0700 Subject: [PATCH 03/15] Confirmation dialog for existing project added, error message handling added --- src/app/app.module.ts | 5 ++- .../appliance/appliance.component.ts | 1 - ...-project-confirmation-dialog.component.css | 0 ...project-confirmation-dialog.component.html | 10 +++++ ...ject-confirmation-dialog.component.spec.ts | 0 ...t-project-confirmation-dialog.component.ts | 40 +++++++++++++++++++ src/app/models/serverResponse.ts | 4 ++ 7 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.css create mode 100644 src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.html create mode 100644 src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.spec.ts create mode 100644 src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.ts create mode 100644 src/app/models/serverResponse.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 500b3518..331687b8 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -50,6 +50,7 @@ import { LinkService } from "./services/link.service"; import { ProjectsComponent } from './components/projects/projects.component'; import { ImportProjectDialogComponent } from './components/projects/import-project-dialog/import-project-dialog.component'; +import { ImportProjectConfirmationDialogComponent} from './components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component'; import { DefaultLayoutComponent } from './layouts/default-layout/default-layout.component'; import { ProgressDialogComponent } from './common/progress-dialog/progress-dialog.component'; import { AppComponent } from './app.component'; @@ -109,6 +110,7 @@ if (environment.production) { CreateSnapshotDialogComponent, ProjectsComponent, ImportProjectDialogComponent, + ImportProjectConfirmationDialogComponent, DefaultLayoutComponent, ProgressDialogComponent, NodeContextMenuComponent, @@ -191,7 +193,8 @@ if (environment.production) { CreateSnapshotDialogComponent, ProgressDialogComponent, ApplianceListDialogComponent, - ImportProjectDialogComponent + ImportProjectDialogComponent, + ImportProjectConfirmationDialogComponent ], bootstrap: [ AppComponent ] }) diff --git a/src/app/components/appliance/appliance.component.ts b/src/app/components/appliance/appliance.component.ts index 6eb7801f..aaed1faa 100644 --- a/src/app/components/appliance/appliance.component.ts +++ b/src/app/components/appliance/appliance.component.ts @@ -34,4 +34,3 @@ export class ApplianceComponent implements OnInit { }); } } - diff --git a/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.css b/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.css new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.html b/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.html new file mode 100644 index 00000000..74d874d7 --- /dev/null +++ b/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.html @@ -0,0 +1,10 @@ + + {{confirmationMessage}} + +
+ + +
+
+ +
diff --git a/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.spec.ts b/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.ts b/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.ts new file mode 100644 index 00000000..b6b8fe4e --- /dev/null +++ b/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.ts @@ -0,0 +1,40 @@ +import { Component, OnInit, Inject, ViewChild } from '@angular/core'; +import { MatStepper, MatDialogRef, MAT_DIALOG_DATA } from "@angular/material"; +import { FileUploader, ParsedResponseHeaders, FileItem } from 'ng2-file-upload'; +import { v4 as uuid } from 'uuid'; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Project } from '../../../../models/project'; + +@Component({ + selector: 'app-import-project-dialog', + templateUrl: 'import-project-confirmation-dialog.component.html', + styleUrls: ['import-project-confirmation-dialog.component.css'] +}) +export class ImportProjectConfirmationDialogComponent implements OnInit { + private existingProject : Project; + private confirmationMessage : string; + private isOpen : boolean; + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any + ){ + this.existingProject = data['existingProject'] + } + + ngOnInit(){ + if(this.existingProject.status === "opened"){ + this.confirmationMessage = `Project ${this.existingProject.name} is open. You can not overwrite it.` + this.isOpen = true; + } else { + this.confirmationMessage = `Project ${this.existingProject.name} already exist, overwrite it?` + } + } + + onNoClick() : void { + this.dialogRef.close(false); + } + + onYesClick() : void { + this.dialogRef.close(true); + } +} diff --git a/src/app/models/serverResponse.ts b/src/app/models/serverResponse.ts new file mode 100644 index 00000000..faf8e621 --- /dev/null +++ b/src/app/models/serverResponse.ts @@ -0,0 +1,4 @@ +export class ServerResponse { + message: string; + status: number; +} From 606b7fa01c681efbf48bebca5a72cac70a2286b9 Mon Sep 17 00:00:00 2001 From: PiotrP Date: Tue, 30 Oct 2018 07:36:16 -0700 Subject: [PATCH 04/15] Unit tests for confirmation dialog component added --- ...project-confirmation-dialog.component.html | 8 +- ...ject-confirmation-dialog.component.spec.ts | 131 ++++++++++++++++++ .../import-project-dialog.component.spec.ts | 43 +++++- .../import-project-dialog.component.ts | 69 +++++++-- 4 files changed, 231 insertions(+), 20 deletions(-) diff --git a/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.html b/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.html index 74d874d7..10cac4c9 100644 --- a/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.html +++ b/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.html @@ -1,9 +1,7 @@ - - {{confirmationMessage}} - +{{confirmationMessage}}
- - + +
diff --git a/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.spec.ts b/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.spec.ts index e69de29b..b51e949d 100644 --- a/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.spec.ts +++ b/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.spec.ts @@ -0,0 +1,131 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogModule, MatDialog } from "@angular/material"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { Component, NgModule } from '@angular/core'; +import { Project } from '../../../../models/project'; +import { ImportProjectConfirmationDialogComponent } from './import-project-confirmation-dialog.component'; +import { OverlayContainer } from '@angular/cdk/overlay'; + +describe('ImportProjectConfirmationDialogComponent', () => { + let dialog: MatDialog; + let overlayContainerElement: HTMLElement; + + let noop: ComponentFixture; + let existingProject: Project = { + auto_close: false, + auto_open: false, + auto_start: false, + filename: "blank", + name: "blank", + path: "", + project_id: "", + scene_height: 100, + scene_width: 100, + status: "", + readonly: false, + show_interface_labels: false, + show_layers: false, + show_grid: false, + snap_to_grid: false, + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ DialogTestModule ], + providers: [ + { provide: OverlayContainer, useFactory: () => { + overlayContainerElement = document.createElement('div'); + return { getContainerElement: () => overlayContainerElement }; + }} + ] + }); + + dialog = TestBed.get(MatDialog); + + noop = TestBed.createComponent(NoopComponent); + }); + + it('should show correct message if project is open', () => { + existingProject.status = "opened"; + const config = { + data: { + 'existingProject' : existingProject + } + }; + + dialog.open(ImportProjectConfirmationDialogComponent, config); + noop.detectChanges(); + + const message = overlayContainerElement.querySelector('span'); + expect(message.textContent).toBe("Project blank is open. You can not overwrite it."); + }); + + it('should show correct message if project is closed', () => { + existingProject.status = "closed"; + const config = { + data: { + 'existingProject' : existingProject + } + }; + + dialog.open(ImportProjectConfirmationDialogComponent, config); + noop.detectChanges(); + + const message = overlayContainerElement.querySelector('span'); + expect(message.textContent).toBe("Project blank already exist, overwrite it?"); + }); + + it('should return false after closing when project is open', () => { + existingProject.status = "opened"; + const config = { + data: { + 'existingProject' : existingProject + } + }; + + let dialogRef = dialog.open(ImportProjectConfirmationDialogComponent, config); + noop.detectChanges(); + const button = overlayContainerElement.querySelector('button'); + spyOn(dialogRef.componentInstance.dialogRef, 'close'); + button.click(); + + expect(dialogRef.componentInstance.dialogRef.close).toHaveBeenCalledWith(false); + }); + + it('should return true after choosing overriding', () => { + existingProject.status = "closed"; + const config = { + data: { + 'existingProject' : existingProject + } + }; + + let dialogRef = dialog.open(ImportProjectConfirmationDialogComponent, config); + noop.detectChanges(); + const button: HTMLButtonElement = overlayContainerElement.querySelector('.confirmButton'); + spyOn(dialogRef.componentInstance.dialogRef, 'close'); + button.click(); + + expect(dialogRef.componentInstance.dialogRef.close).toHaveBeenCalledWith(true); + }); +}); + +@Component({ + template: '' +}) +class NoopComponent {} + +const TEST_DIRECTIVES = [ + ImportProjectConfirmationDialogComponent, + NoopComponent +]; + +@NgModule({ + imports: [MatDialogModule, NoopAnimationsModule], + exports: TEST_DIRECTIVES, + declarations: TEST_DIRECTIVES, + entryComponents: [ + ImportProjectConfirmationDialogComponent + ], +}) +class DialogTestModule { } diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts b/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts index 7aa20d6a..e89c30fd 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts @@ -4,10 +4,37 @@ import { Server } from "../../../models/server"; import { MatInputModule, MatIconModule, MatSortModule, MatTableModule, MatTooltipModule, MatDialogModule, MatStepperModule, MatFormFieldModule, MatDialogRef, MatDialog, MAT_DIALOG_DATA } from "@angular/material"; import { RouterTestingModule } from "@angular/router/testing"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; -import { FileUploadModule, FileSelectDirective, FileItem, FileUploader } from "ng2-file-upload"; +import { FileUploadModule, FileSelectDirective, FileItem, FileUploader, ParsedResponseHeaders } from "ng2-file-upload"; import { FormsModule, ReactiveFormsModule, FormBuilder, FormControl, Validators } from '@angular/forms'; import { DebugElement } from '@angular/core'; import { By } from '@angular/platform-browser'; +import { ProjectService } from '../../../services/project.service'; +import { of } from 'rxjs/internal/observable/of'; +import { Project } from '../../../models/project'; + +export class MockedProjectService { + public projects: Project[] = [{ + auto_close: false, + auto_open: false, + auto_start: false, + filename: "blank", + name: "blank", + path: "", + project_id: "", + scene_height: 100, + scene_width: 100, + status: "opened", + readonly: false, + show_interface_labels: false, + show_layers: false, + show_grid: false, + snap_to_grid: false, + }]; + + list(server: Server) { + return of(this.projects); + } +} describe('ImportProjectDialogComponent', () => { let component: ImportProjectDialogComponent; @@ -36,7 +63,8 @@ describe('ImportProjectDialogComponent', () => { ], providers: [ { provide: MatDialogRef }, - { provide: MAT_DIALOG_DATA } + { provide: MAT_DIALOG_DATA }, + { provide: ProjectService, useClass: MockedProjectService} ], declarations : [ImportProjectDialogComponent] }) @@ -61,6 +89,7 @@ describe('ImportProjectDialogComponent', () => { debugElement = fixture.debugElement.query(By.directive(FileSelectDirective)); fileSelectDirective = debugElement.injector.get(FileSelectDirective) as FileSelectDirective; + component.uploader.onErrorItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => {}; }); it('should be created', () => { @@ -134,6 +163,7 @@ describe('ImportProjectDialogComponent', () => { it('should navigate to next step after clicking import', () => { let fileItem = new FileItem(fileSelectDirective.uploader, new File([],"fileName"),{}); fileSelectDirective.uploader.queue.push(fileItem); + spyOn(component.stepper, "next"); component.onImportClick(); @@ -166,4 +196,13 @@ describe('ImportProjectDialogComponent', () => { expect(fileSelectDirective.uploader.uploadItem).not.toHaveBeenCalled(); expect(component.projectNameForm.valid).toBeFalsy(); }); + + it('should open confirmation dialog if project with the same exists', () => { + component.projectNameForm.controls['projectName'].setValue("blank"); + spyOn(component, "openConfirmationDialog"); + + component.onImportClick(); + + expect(component.openConfirmationDialog).toHaveBeenCalled(); + }); }); diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts b/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts index c8f23d58..e2ff62f1 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts @@ -1,9 +1,13 @@ import { Component, OnInit, Inject, ViewChild } from '@angular/core'; -import { MatStepper, MatDialogRef, MAT_DIALOG_DATA } from "@angular/material"; +import { MatStepper, MatDialogRef, MAT_DIALOG_DATA, MatDialog } from "@angular/material"; import { FileUploader, ParsedResponseHeaders, FileItem } from 'ng2-file-upload'; import { Server } from '../../../models/server'; import { v4 as uuid } from 'uuid'; import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { ProjectService } from '../../../services/project.service'; +import { Project } from '../../../models/project'; +import { ImportProjectConfirmationDialogComponent } from './import-project-confirmation-dialog/import-project-confirmation-dialog.component'; +import { ServerResponse } from '../../../models/serverResponse'; export class Validator { static projectNameValidator(projectName) { @@ -34,9 +38,11 @@ export class ImportProjectDialogComponent implements OnInit { @ViewChild('stepper') stepper: MatStepper; constructor( + private dialog: MatDialog, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any, - private formBuilder: FormBuilder){ + private formBuilder: FormBuilder, + private projectService: ProjectService){ this.projectNameForm = this.formBuilder.group({ projectName: new FormControl(null, [Validators.required, Validator.projectNameValidator]) }); @@ -45,6 +51,17 @@ export class ImportProjectDialogComponent implements OnInit { ngOnInit(){ this.uploader = new FileUploader({}); this.uploader.onAfterAddingFile = (file) => { file.withCredentials = false; }; + + this.uploader.onErrorItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => { + let serverResponse : ServerResponse = JSON.parse(response); + this.resultMessage = "An error occured: " + serverResponse.message; + this.isFinishEnabled = true; + }; + + this.uploader.onCompleteItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => { + this.resultMessage = "Project was imported succesfully!"; + this.isFinishEnabled = true; + }; } get form() { @@ -60,6 +77,22 @@ export class ImportProjectDialogComponent implements OnInit { if (this.projectNameForm.invalid){ this.submitted = true; } else { + this.projectService + .list(this.server) + .subscribe((projects: Project[]) => { + const projectName = this.projectNameForm.controls['projectName'].value; + let existingProject = projects.find(project => project.name === projectName); + + if (existingProject){ + this.openConfirmationDialog(existingProject); + } else { + this.importProject(); + } + }); + } + } + + importProject(){ const url = this.prepareUploadPath(); this.uploader.queue.forEach(elem => elem.url = url); @@ -67,18 +100,27 @@ export class ImportProjectDialogComponent implements OnInit { this.stepper.next(); const itemToUpload = this.uploader.queue[0]; - this.uploader.uploadItem(itemToUpload); + this.uploader.uploadItem(itemToUpload); + } - this.uploader.onErrorItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => { - this.resultMessage = response; - this.isFinishEnabled = true; - }; - - this.uploader.onSuccessItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => { - this.resultMessage = "Project was imported succesfully!"; - this.isFinishEnabled = true; - }; - } + openConfirmationDialog(existingProject: Project) { + const dialogRef = this.dialog.open(ImportProjectConfirmationDialogComponent, { + width: '300px', + height: '150px', + data: { + 'existingProject': existingProject + } + }); + + dialogRef.afterClosed().subscribe((answer: boolean) => { + if (answer) { + this.projectService.close(this.server, existingProject.project_id).subscribe(() => { + this.projectService.delete(this.server, existingProject.project_id).subscribe(() => { + this.importProject(); + }); + }); + } + }); } onNoClick() : void{ @@ -101,3 +143,4 @@ export class ImportProjectDialogComponent implements OnInit { return `http://${this.server.ip}:${this.server.port}/v2/projects/${uuid()}/import?name=${projectName}`; } } + \ No newline at end of file From 35f16d3eff6c9a35e08424acca87fc003881d5ad Mon Sep 17 00:00:00 2001 From: PiotrP Date: Tue, 30 Oct 2018 07:50:54 -0700 Subject: [PATCH 05/15] Fix for tests --- .../import-project-confirmation-dialog.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.ts b/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.ts index b6b8fe4e..0b640c6d 100644 --- a/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.ts +++ b/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.ts @@ -12,8 +12,8 @@ import { Project } from '../../../../models/project'; }) export class ImportProjectConfirmationDialogComponent implements OnInit { private existingProject : Project; - private confirmationMessage : string; - private isOpen : boolean; + public confirmationMessage : string; + public isOpen : boolean; constructor( public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any From c4486ee1049edc5473c1d54399c760a840877ff0 Mon Sep 17 00:00:00 2001 From: PiotrP Date: Fri, 2 Nov 2018 02:22:04 -0700 Subject: [PATCH 06/15] Fixes after cr --- ...project-confirmation-dialog.component.html | 4 +-- .../import-project-dialog.component.css | 17 ++++++++---- .../import-project-dialog.component.html | 18 ++++++------- .../import-project-dialog.component.spec.ts | 27 +++++++++++++++++++ .../import-project-dialog.component.ts | 6 +++-- .../projects/projects.component.css | 5 ++-- .../projects/projects.component.html | 9 +++---- 7 files changed, 61 insertions(+), 25 deletions(-) diff --git a/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.html b/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.html index 10cac4c9..6d10a1db 100644 --- a/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.html +++ b/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.html @@ -1,8 +1,8 @@ {{confirmationMessage}} -
+
-
+
diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.css b/src/app/components/projects/import-project-dialog/import-project-dialog.component.css index b47db760..6d26582e 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.css +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.css @@ -4,24 +4,26 @@ .file-button{ height: 50px; - width: 120px; + width: 20%; margin-top: 10px; + padding: 0px; } .file-name-form { float: right; + width: 100%; } .file-name-form-field { - margin-left: 10px; - margin-right: 10px; - width:250px; + margin-left: 5%; + width: 65%; } .delete-button { background: transparent; border: none; - outline: 0 + outline: 0; + width: 10%; } .delete-icon { @@ -30,4 +32,9 @@ .result-message-box { margin-top: 10px; + text-align: center; +} + +.progress-bar { + background-color : #0097a7; } diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.html b/src/app/components/projects/import-project-dialog/import-project-dialog.component.html index b27d2c13..99fe4238 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.html +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.html @@ -2,32 +2,32 @@
- -
+ + Project name is required Project name is incorrect - -
+ +
-
+
-
+
{{resultMessage}}
-
+
diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts b/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts index e89c30fd..5c32d7e1 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts @@ -205,4 +205,31 @@ describe('ImportProjectDialogComponent', () => { expect(component.openConfirmationDialog).toHaveBeenCalled(); }); + + it('should show delete button after selecting project', () => { + let fileItem = new FileItem(fileSelectDirective.uploader, new File([],"fileName"),{}); + fileSelectDirective.uploader.queue.push(fileItem); + let event = { + target: { + files: [ {name : "uploadedFile"} ] + } + }; + component.uploadProjectFile(event); + + expect(component.isDeleteVisible).toBe(true); + }); + + it('should hide delete button after deselecting project', () => { + let fileItem = new FileItem(fileSelectDirective.uploader, new File([],"fileName"),{}); + fileSelectDirective.uploader.queue.push(fileItem); + let event = { + target: { + files: [ {name : "uploadedFile"} ] + } + }; + component.uploadProjectFile(event); + component.onDeleteClick(); + + expect(component.isDeleteVisible).toBe(false); + }); }); diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts b/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts index e2ff62f1..9995299b 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts @@ -11,7 +11,7 @@ import { ServerResponse } from '../../../models/serverResponse'; export class Validator { static projectNameValidator(projectName) { - var pattern = new RegExp(/[~`!#$%\^&*+=\-\[\]\\';,/{}|\\":<>\?]/); + var pattern = new RegExp(/[~`!#$%\^&*+=\[\]\\';,/{}|\\":<>\?]/); if(!pattern.test(projectName.value)) { return null; @@ -31,6 +31,7 @@ export class ImportProjectDialogComponent implements OnInit { server : Server; isImportEnabled : boolean = false; isFinishEnabled : boolean = false; + isDeleteVisible : boolean = false; resultMessage : string = "The project is being imported... Please wait"; projectNameForm: FormGroup; submitted: boolean = false; @@ -71,6 +72,7 @@ export class ImportProjectDialogComponent implements OnInit { uploadProjectFile(event) : void{ this.projectNameForm.controls['projectName'].setValue(event.target.files[0].name.split(".")[0]); this.isImportEnabled = true; + this.isDeleteVisible = true; } onImportClick() : void{ @@ -135,6 +137,7 @@ export class ImportProjectDialogComponent implements OnInit { onDeleteClick() : void{ this.uploader.queue.pop(); this.isImportEnabled = false; + this.isDeleteVisible = false; this.projectNameForm.controls['projectName'].setValue(""); } @@ -143,4 +146,3 @@ export class ImportProjectDialogComponent implements OnInit { return `http://${this.server.ip}:${this.server.port}/v2/projects/${uuid()}/import?name=${projectName}`; } } - \ No newline at end of file diff --git a/src/app/components/projects/projects.component.css b/src/app/components/projects/projects.component.css index 6884375a..d49aad69 100644 --- a/src/app/components/projects/projects.component.css +++ b/src/app/components/projects/projects.component.css @@ -1,3 +1,4 @@ .import-button { - margin-right:10px -} \ No newline at end of file + height: 40px; + margin: 20px; +} diff --git a/src/app/components/projects/projects.component.html b/src/app/components/projects/projects.component.html index 4ec801cc..e14ad2d6 100644 --- a/src/app/components/projects/projects.component.html +++ b/src/app/components/projects/projects.component.html @@ -1,6 +1,9 @@
-

Projects

+
+

Projects

+ +
@@ -33,9 +36,5 @@
- -
- -
From 0fe3d0e7cab6a52443c0857b2b9a7366e3124823 Mon Sep 17 00:00:00 2001 From: ziajka Date: Wed, 7 Nov 2018 10:42:22 +0100 Subject: [PATCH 07/15] Separate node rendering from nodes; inital drag multiple nodes --- .../draw-link-tool.component.ts | 6 +- .../components/map/map.component.ts | 28 ++- src/app/cartography/d3-map.imports.ts | 2 + src/app/cartography/widgets/link.ts | 12 +- src/app/cartography/widgets/links.ts | 10 +- src/app/cartography/widgets/node.ts | 140 +++++++++++++ src/app/cartography/widgets/nodes.ts | 187 +++--------------- .../project-map/project-map.component.html | 1 + .../project-map/project-map.component.ts | 6 +- 9 files changed, 202 insertions(+), 190 deletions(-) create mode 100644 src/app/cartography/widgets/node.ts diff --git a/src/app/cartography/components/draw-link-tool/draw-link-tool.component.ts b/src/app/cartography/components/draw-link-tool/draw-link-tool.component.ts index 337dfe59..c3bcdffe 100644 --- a/src/app/cartography/components/draw-link-tool/draw-link-tool.component.ts +++ b/src/app/cartography/components/draw-link-tool/draw-link-tool.component.ts @@ -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, diff --git a/src/app/cartography/components/map/map.component.ts b/src/app/cartography/components/map/map.component.ts index 81de01e3..70d232ce 100644 --- a/src/app/cartography/components/map/map.component.ts +++ b/src/app/cartography/components/map/map.component.ts @@ -20,6 +20,8 @@ import { MapChangeDetectorRef } from '../../services/map-change-detector-ref'; import { NodeDragging, NodeDragged } from '../../events/nodes'; import { LinkCreated } from '../../events/links'; import { CanvasSizeDetector } from '../../helpers/canvas-size-detector'; +import { SelectionManager } from '../../managers/selection-manager'; +import { NodeWidget } from '../../widgets/node'; @Component({ @@ -33,6 +35,8 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { @Input() drawings: Drawing[] = []; @Input() symbols: Symbol[] = []; + @Input('selection-manager') selectionManager: SelectionManager; + @Input() width = 1500; @Input() height = 600; @@ -55,6 +59,7 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { private canvasSizeDetector: CanvasSizeDetector, protected element: ElementRef, protected nodesWidget: NodesWidget, + protected nodeWidget: NodeWidget, protected linksWidget: LinksWidget, protected interfaceLabelWidget: InterfaceLabelWidget, protected selectionToolWidget: SelectionTool, @@ -62,7 +67,7 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { public graphLayout: GraphLayout ) { this.parentNativeElement = element.nativeElement; - this.onNodeDragged = nodesWidget.onNodeDragged; + this.onNodeDragged = nodeWidget.onNodeDragged; } @Input('show-interface-labels') @@ -122,12 +127,23 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { } 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); + this.onNodeDraggingSubscription = this.nodeWidget.onNodeDragging.subscribe((eventNode: NodeDragging) => { + const nodes = this.selectionManager.getSelectedNodes(); - links.forEach((link) => { - this.linksWidget.redrawLink(this.svg, link); + nodes.forEach((node: Node) => { + + node.x += eventNode.event.dx; + node.y += eventNode.event.dy; + + this.nodesWidget.redrawNode(this.svg, node); + + const links = this.links.filter((link) => link.target.node_id === node.node_id || link.source.node_id === node.node_id); + + links.forEach((link) => { + this.linksWidget.redrawLink(this.svg, link); + }); }); + }); this.onChangesDetected = this.mapChangeDetectorRef.changesDetected.subscribe(() => { @@ -193,7 +209,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() { diff --git a/src/app/cartography/d3-map.imports.ts b/src/app/cartography/d3-map.imports.ts index 1cc89ab2..e2f53f42 100644 --- a/src/app/cartography/d3-map.imports.ts +++ b/src/app/cartography/d3-map.imports.ts @@ -14,11 +14,13 @@ 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'; export const D3_MAP_IMPORTS = [ GraphLayout, LinksWidget, NodesWidget, + NodeWidget, DrawingsWidget, DrawingLineWidget, SelectionTool, diff --git a/src/app/cartography/widgets/link.ts b/src/app/cartography/widgets/link.ts index c673c84a..1b3dc965 100644 --- a/src/app/cartography/widgets/link.ts +++ b/src/app/cartography/widgets/link.ts @@ -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("g.link_body") .data((l) => [l]); @@ -51,8 +43,8 @@ export class LinkWidget implements Widget { .select('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); } } diff --git a/src/app/cartography/widgets/links.ts b/src/app/cartography/widgets/links.ts index a0775016..e96e9baf 100644 --- a/src/app/cartography/widgets/links.ts +++ b/src/app/cartography/widgets/links.ts @@ -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() diff --git a/src/app/cartography/widgets/node.ts b/src/app/cartography/widgets/node.ts new file mode 100644 index 00000000..eed25b93 --- /dev/null +++ b/src/app/cartography/widgets/node.ts @@ -0,0 +1,140 @@ +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(); + public onNodeClicked = new EventEmitter(); + public onNodeDragged = new EventEmitter(); + public onNodeDragging = new EventEmitter(); + + private symbols: Symbol[] = []; + private draggingEnabled = false; + + constructor( + private cssFixer: CssFixer, + private fontFixer: FontFixer, + ) {} + + public setSymbols(symbols: Symbol[]) { + this.symbols = symbols; + } + + public setDraggingEnabled(enabled: boolean) { + this.draggingEnabled = enabled; + } + + public draw(view: SVGSelection) { + const self = this; + + const node_body = view.selectAll("g.node_body") + .data((n) => [n]); + + const node_body_enter = node_body.enter() + .append('g') + .attr("class", "node_body"); + + node_body_enter + .append('image'); + + // add label of node + node_body_enter + .append('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('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('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; + }); + + const callback = function (this: SVGGElement, n: Node) { + const e: D3DragEvent = event; + self.onNodeDragging.emit(new NodeDragging(e, n)); + }; + + const dragging = () => { + return drag() + .on('drag', callback) + .on('end', (n: Node) => { + const e: D3DragEvent = event; + self.onNodeDragged.emit(new NodeDragged(e, n)); + }); + }; + + if (this.draggingEnabled) { + node_body_merge.call(dragging()); + } + } +} diff --git a/src/app/cartography/widgets/nodes.ts b/src/app/cartography/widgets/nodes.ts index 114f47b9..a8c78884 100644 --- a/src/app/cartography/widgets/nodes.ts +++ b/src/app/cartography/widgets/nodes.ts @@ -1,190 +1,53 @@ -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"; @Injectable() export class NodesWidget implements Widget { static NODE_LABEL_MARGIN = 3; - private debug = false; - private draggingEnabled = false; - - private symbols: Symbol[] = []; - - public onContextMenu = new EventEmitter(); - public onNodeClicked = new EventEmitter(); - public onNodeDragged = new EventEmitter(); - public onNodeDragging = new EventEmitter(); - 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('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('text.node_point_label') - .text((n: Node) => `(${n.x}, ${n.y})`); - - } - - public draw(view: SVGSelection, nodes?: Node[]) { - const self = this; - - let nodes_selection: Selection = view - .selectAll('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("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('g') - .attr('class', 'node'); + const node_enter = node.enter() + .append('g') + .attr('class', 'node') + .attr('node_id', (n: Node) => n.node_id) - // add image to node - node_enter - .append('image'); + const merge = node.merge(node_enter); - // add label of node - node_enter - .append('text') - .attr('class', 'label'); + this.nodeWidget.draw(merge); - if (this.debug) { - node_enter - .append('circle') - .attr('class', 'node_point') - .attr('r', 2); - - node_enter - .append('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('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 = event; - - n.x = e.x; - n.y = e.y; - - self.revise(select(this)); - self.onNodeDragging.emit(new NodeDragging(event, n)); - }; - - const dragging = () => { - return drag() - .on('drag', callback) - .on('end', (n: Node) => { - const e: D3DragEvent = event; - self.onNodeDragged.emit(new NodeDragged(e, n)); - }); - }; - - if (this.draggingEnabled) { - node_merge.call(dragging()); - } - - nodes_selection + node .exit() .remove(); } + + private selectNode(view: SVGSelection, node: Node) { + return view.selectAll(`g.node[node_id="${node.node_id}"]`); + } + } diff --git a/src/app/components/project-map/project-map.component.html b/src/app/components/project-map/project-map.component.html index 806473b6..aba18853 100644 --- a/src/app/components/project-map/project-map.component.html +++ b/src/app/components/project-map/project-map.component.html @@ -6,6 +6,7 @@ [drawings]="drawings" [width]="project.scene_width" [height]="project.scene_height" + [selection-manager]="selectionManager" [show-interface-labels]="project.show_interface_labels" [selection-tool]="tools.selection" [moving-tool]="tools.moving" diff --git a/src/app/components/project-map/project-map.component.ts b/src/app/components/project-map/project-map.component.ts index 091194f3..b8bf7f25 100644 --- a/src/app/components/project-map/project-map.component.ts +++ b/src/app/components/project-map/project-map.component.ts @@ -29,6 +29,7 @@ import { ProgressService } from "../../common/progress/progress.service"; import { MapChangeDetectorRef } from '../../cartography/services/map-change-detector-ref'; import { NodeContextMenu, NodeDragged } from '../../cartography/events/nodes'; import { LinkCreated } from '../../cartography/events/links'; +import { NodeWidget } from '../../cartography/widgets/node'; @Component({ @@ -74,6 +75,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy { private progressService: ProgressService, private projectWebServiceHandler: ProjectWebServiceHandler, private mapChangeDetectorRef: MapChangeDetectorRef, + private nodeWidget: NodeWidget, protected nodesDataSource: NodesDataSource, protected linksDataSource: LinksDataSource, protected drawingsDataSource: DrawingsDataSource, @@ -188,9 +190,9 @@ export class ProjectMapComponent implements OnInit, OnDestroy { } setUpMapCallbacks(project: Project) { - this.mapChild.graphLayout.getNodesWidget().setDraggingEnabled(!this.readonly); + this.nodeWidget.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, From 2f822580a93ecf041d692b4540ec7fd9f6d96301 Mon Sep 17 00:00:00 2001 From: ziajka Date: Wed, 7 Nov 2018 11:44:33 +0100 Subject: [PATCH 08/15] Ability to drag more than one node at once --- .../components/map/map.component.ts | 38 ++++++++-- src/app/cartography/widgets/links.spec.ts | 10 +-- src/app/cartography/widgets/node.spec.ts | 70 +++++++++++++++++++ src/app/cartography/widgets/nodes.spec.ts | 61 ++-------------- src/app/services/appliance.service.spec.ts | 2 +- src/app/services/settings.service.spec.ts | 8 +-- 6 files changed, 114 insertions(+), 75 deletions(-) create mode 100644 src/app/cartography/widgets/node.spec.ts diff --git a/src/app/cartography/components/map/map.component.ts b/src/app/cartography/components/map/map.component.ts index 70d232ce..fe50656d 100644 --- a/src/app/cartography/components/map/map.component.ts +++ b/src/app/cartography/components/map/map.component.ts @@ -17,7 +17,7 @@ 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 { SelectionManager } from '../../managers/selection-manager'; @@ -40,13 +40,16 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { @Input() width = 1500; @Input() height = 600; - @Output() onNodeDragged: EventEmitter; + @Output() onNodeDragged = new EventEmitter(); @Output() onLinkCreated = new EventEmitter(); private parentNativeElement: any; private svg: Selection; private onNodeDraggingSubscription: Subscription; + private onNodeClickedSubscription: Subscription; + private onNodeDraggedSubscription: Subscription; + private onChangesDetected: Subscription; protected settings = { @@ -67,7 +70,6 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { public graphLayout: GraphLayout ) { this.parentNativeElement = element.nativeElement; - this.onNodeDragged = nodeWidget.onNodeDragged; } @Input('show-interface-labels') @@ -118,6 +120,8 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { ngOnDestroy() { this.graphLayout.disconnect(this.svg); this.onNodeDraggingSubscription.unsubscribe(); + this.onNodeClickedSubscription.unsubscribe(); + this.onNodeDraggedSubscription.unsubscribe(); this.onChangesDetected.unsubscribe(); } @@ -128,17 +132,19 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { this.context.size = this.getSize(); this.onNodeDraggingSubscription = this.nodeWidget.onNodeDragging.subscribe((eventNode: NodeDragging) => { - const nodes = this.selectionManager.getSelectedNodes(); + let nodes = this.selectionManager.getSelectedNodes(); + + if (nodes.filter((n: Node) => n.node_id === eventNode.node.node_id).length === 0) { + this.selectionManager.setSelectedNodes([eventNode.node]); + nodes = this.selectionManager.getSelectedNodes(); + } nodes.forEach((node: Node) => { - node.x += eventNode.event.dx; node.y += eventNode.event.dy; this.nodesWidget.redrawNode(this.svg, node); - const links = this.links.filter((link) => link.target.node_id === node.node_id || link.source.node_id === node.node_id); - links.forEach((link) => { this.linksWidget.redrawLink(this.svg, link); }); @@ -146,6 +152,24 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { }); + this.onNodeDraggedSubscription = this.nodeWidget.onNodeDragged.subscribe((eventNode: NodeDragged) => { + let nodes = this.selectionManager.getSelectedNodes(); + + if (nodes.filter((n: Node) => n.node_id === eventNode.node.node_id).length === 0) { + this.selectionManager.setSelectedNodes([eventNode.node]); + nodes = this.selectionManager.getSelectedNodes(); + } + + nodes.forEach((node) => { + this.onNodeDragged.emit(new NodeDragged(eventNode.event, node)); + }); + + }); + + this.onNodeClickedSubscription = this.nodeWidget.onNodeClicked.subscribe((nodeClickedEvent: NodeClicked) => { + this.selectionManager.setSelectedNodes([nodeClickedEvent.node]); + }); + this.onChangesDetected = this.mapChangeDetectorRef.changesDetected.subscribe(() => { if (this.mapChangeDetectorRef.hasBeenDrawn) { this.reload(); diff --git a/src/app/cartography/widgets/links.spec.ts b/src/app/cartography/widgets/links.spec.ts index 554b957c..705daba3 100644 --- a/src/app/cartography/widgets/links.spec.ts +++ b/src/app/cartography/widgets/links.spec.ts @@ -16,12 +16,12 @@ describe('LinksWidget', () => { let widget: LinksWidget; let layersEnter: Selection; 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('g.link'); diff --git a/src/app/cartography/widgets/node.spec.ts b/src/app/cartography/widgets/node.spec.ts new file mode 100644 index 00000000..49edd7b2 --- /dev/null +++ b/src/app/cartography/widgets/node.spec.ts @@ -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('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); + // }); + + }); +}); diff --git a/src/app/cartography/widgets/nodes.spec.ts b/src/app/cartography/widgets/nodes.spec.ts index a1a57781..f26236e4 100644 --- a/src/app/cartography/widgets/nodes.spec.ts +++ b/src/app/cartography/widgets/nodes.spec.ts @@ -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('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); - }); - }); - - }); diff --git a/src/app/services/appliance.service.spec.ts b/src/app/services/appliance.service.spec.ts index 0f749331..8844c7b6 100644 --- a/src/app/services/appliance.service.spec.ts +++ b/src/app/services/appliance.service.spec.ts @@ -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'); diff --git a/src/app/services/settings.service.spec.ts b/src/app/services/settings.service.spec.ts index c01d45de..941158f9 100644 --- a/src/app/services/settings.service.spec.ts +++ b/src/app/services/settings.service.spec.ts @@ -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); From ce13d3a68cd86d84d6c06409c8f73fbef6000e81 Mon Sep 17 00:00:00 2001 From: PiotrP Date: Wed, 7 Nov 2018 05:43:19 -0800 Subject: [PATCH 09/15] Removing stepper --- ...t-project-confirmation-dialog.component.ts | 7 +-- .../import-project-dialog.component.html | 58 +++++++++---------- .../import-project-dialog.component.spec.ts | 12 +--- .../import-project-dialog.component.ts | 6 +- 4 files changed, 34 insertions(+), 49 deletions(-) diff --git a/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.ts b/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.ts index 0b640c6d..48036d07 100644 --- a/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.ts +++ b/src/app/components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component.ts @@ -1,8 +1,5 @@ -import { Component, OnInit, Inject, ViewChild } from '@angular/core'; -import { MatStepper, MatDialogRef, MAT_DIALOG_DATA } from "@angular/material"; -import { FileUploader, ParsedResponseHeaders, FileItem } from 'ng2-file-upload'; -import { v4 as uuid } from 'uuid'; -import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Component, OnInit, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material"; import { Project } from '../../../../models/project'; @Component({ diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.html b/src/app/components/projects/import-project-dialog/import-project-dialog.component.html index 99fe4238..e9eedd0b 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.html +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.html @@ -1,34 +1,30 @@

Import project

- - -
-
- - - - - Project name is required - Project name is incorrect - - -
- - -
-
-
-
- -
-
-
-
- {{resultMessage}} -
+
+
+ + + + + Project name is required + Project name is incorrect + +
- + +
- - \ No newline at end of file +
+
+
+
+
+
+
+ {{resultMessage}} +
+
+ +
+
diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts b/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts index 5c32d7e1..45d3628e 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts @@ -36,7 +36,7 @@ export class MockedProjectService { } } -describe('ImportProjectDialogComponent', () => { +fdescribe('ImportProjectDialogComponent', () => { let component: ImportProjectDialogComponent; let fixture: ComponentFixture; let server: Server; @@ -160,26 +160,22 @@ describe('ImportProjectDialogComponent', () => { expect(fileSelectDirective.uploader.queue[0].url).toContain("newProject"); }); - it('should navigate to next step after clicking import', () => { + it('should navigate to progress view after clicking import', () => { let fileItem = new FileItem(fileSelectDirective.uploader, new File([],"fileName"),{}); fileSelectDirective.uploader.queue.push(fileItem); - spyOn(component.stepper, "next"); - component.onImportClick(); - expect(component.stepper.next).toHaveBeenCalled(); + expect(component.isFirstStepCompleted).toBe(true); }); it('should detect if file input is empty', () => { component.projectNameForm.controls['projectName'].setValue(""); fixture.detectChanges(); - spyOn(component.stepper, "next"); spyOn(fileSelectDirective.uploader, 'uploadItem'); component.onImportClick(); - expect(component.stepper.next).not.toHaveBeenCalled(); expect(fileSelectDirective.uploader.uploadItem).not.toHaveBeenCalled(); expect(component.projectNameForm.valid).toBeFalsy(); }); @@ -187,12 +183,10 @@ describe('ImportProjectDialogComponent', () => { it('should sanitize file name input', () => { component.projectNameForm.controls['projectName'].setValue("[][]"); fixture.detectChanges(); - spyOn(component.stepper, "next"); spyOn(fileSelectDirective.uploader, 'uploadItem'); component.onImportClick(); - expect(component.stepper.next).not.toHaveBeenCalled(); expect(fileSelectDirective.uploader.uploadItem).not.toHaveBeenCalled(); expect(component.projectNameForm.valid).toBeFalsy(); }); diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts b/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts index 9995299b..e6a3ddbc 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts @@ -35,8 +35,7 @@ export class ImportProjectDialogComponent implements OnInit { resultMessage : string = "The project is being imported... Please wait"; projectNameForm: FormGroup; submitted: boolean = false; - - @ViewChild('stepper') stepper: MatStepper; + isFirstStepCompleted: boolean = false; constructor( private dialog: MatDialog, @@ -98,8 +97,7 @@ export class ImportProjectDialogComponent implements OnInit { const url = this.prepareUploadPath(); this.uploader.queue.forEach(elem => elem.url = url); - this.stepper.selected.completed = true; - this.stepper.next(); + this.isFirstStepCompleted = true; const itemToUpload = this.uploader.queue[0]; this.uploader.uploadItem(itemToUpload); From 58a4ed08b1e8eedc50332a90dd8d68199e3fffa2 Mon Sep 17 00:00:00 2001 From: PiotrP Date: Wed, 7 Nov 2018 05:43:57 -0800 Subject: [PATCH 10/15] Code cleaned up --- .../import-project-dialog.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts b/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts index 45d3628e..7ee72a8a 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts @@ -36,7 +36,7 @@ export class MockedProjectService { } } -fdescribe('ImportProjectDialogComponent', () => { +describe('ImportProjectDialogComponent', () => { let component: ImportProjectDialogComponent; let fixture: ComponentFixture; let server: Server; From b57bc7370b8dd538a9bfcf2df8e693b70a5e8c14 Mon Sep 17 00:00:00 2001 From: ziajka Date: Wed, 7 Nov 2018 14:44:17 +0100 Subject: [PATCH 11/15] New approach for draggable --- src/app/cartography/cartography.module.ts | 2 + .../components/map/map.component.ts | 8 ++- src/app/cartography/events/draggable.ts | 64 +++++++++++++++++++ src/app/cartography/listeners/map-listener.ts | 47 ++++++++++++++ src/app/cartography/widgets/drawings.ts | 13 +++- .../project-map/project-map.component.html | 1 - .../project-map/project-map.component.ts | 38 +++++------ 7 files changed, 147 insertions(+), 26 deletions(-) create mode 100644 src/app/cartography/events/draggable.ts create mode 100644 src/app/cartography/listeners/map-listener.ts diff --git a/src/app/cartography/cartography.module.ts b/src/app/cartography/cartography.module.ts index c276d253..0161de52 100644 --- a/src/app/cartography/cartography.module.ts +++ b/src/app/cartography/cartography.module.ts @@ -16,6 +16,7 @@ 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 { MapListener } from './listeners/map-listener'; @NgModule({ @@ -39,6 +40,7 @@ import { CanvasSizeDetector } from './helpers/canvas-size-detector'; MapChangeDetectorRef, CanvasSizeDetector, Context, + MapListener, ...D3_MAP_IMPORTS ], exports: [ MapComponent ] diff --git a/src/app/cartography/components/map/map.component.ts b/src/app/cartography/components/map/map.component.ts index fe50656d..e120afcb 100644 --- a/src/app/cartography/components/map/map.component.ts +++ b/src/app/cartography/components/map/map.component.ts @@ -22,6 +22,7 @@ import { LinkCreated } from '../../events/links'; import { CanvasSizeDetector } from '../../helpers/canvas-size-detector'; import { SelectionManager } from '../../managers/selection-manager'; import { NodeWidget } from '../../widgets/node'; +import { MapListener } from '../../listeners/map-listener'; @Component({ @@ -35,8 +36,6 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { @Input() drawings: Drawing[] = []; @Input() symbols: Symbol[] = []; - @Input('selection-manager') selectionManager: SelectionManager; - @Input() width = 1500; @Input() height = 600; @@ -60,6 +59,8 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { private context: Context, private mapChangeDetectorRef: MapChangeDetectorRef, private canvasSizeDetector: CanvasSizeDetector, + private mapListener: MapListener, + private selectionManager: SelectionManager, protected element: ElementRef, protected nodesWidget: NodesWidget, protected nodeWidget: NodeWidget, @@ -123,6 +124,7 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { this.onNodeClickedSubscription.unsubscribe(); this.onNodeDraggedSubscription.unsubscribe(); this.onChangesDetected.unsubscribe(); + this.mapListener.onDestroy(); } ngOnInit() { @@ -175,6 +177,8 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { this.reload(); } }); + + this.mapListener.onInit(this.svg); } public createGraph(domElement: HTMLElement) { diff --git a/src/app/cartography/events/draggable.ts b/src/app/cartography/events/draggable.ts new file mode 100644 index 00000000..3cefb1ee --- /dev/null +++ b/src/app/cartography/events/draggable.ts @@ -0,0 +1,64 @@ +import { EventEmitter } from "@angular/core"; +import { drag, DraggedElementBaseType } from "d3-drag"; +import { event } from "d3-selection"; + +class DraggableEvent { + public dx: number; + public dy: number; +} + +export class DraggableStart extends DraggableEvent { + constructor( + public datum: T + ){ + super(); + } +} + +export class DraggableDrag extends DraggableEvent { + constructor( + public datum: T + ){ + super(); + } +} + +export class DraggableEnd extends DraggableEvent { + constructor( + public datum: T + ){ + super(); + } +} + +export class Draggable { + public start = new EventEmitter>(); + public drag = new EventEmitter>(); + public end = new EventEmitter>(); + + public call(selection) { + selection.call(this.behaviour()); + } + + private behaviour() { + return drag() + .on('start', (datum: Datum) => { + const evt = new DraggableStart(datum); + evt.dx = event.dx; + evt.dy = event.dy; + this.start.emit(evt); + }) + .on('drag', (datum: Datum) => { + const evt = new DraggableDrag(datum); + evt.dx = event.dx; + evt.dy = event.dy; + this.drag.emit(evt); + }) + .on('end', (datum: Datum) => { + const evt = new DraggableEnd(datum); + evt.dx = event.dx; + evt.dy = event.dy; + this.end.emit(evt); + }); + } +} \ No newline at end of file diff --git a/src/app/cartography/listeners/map-listener.ts b/src/app/cartography/listeners/map-listener.ts new file mode 100644 index 00000000..b6dd2aad --- /dev/null +++ b/src/app/cartography/listeners/map-listener.ts @@ -0,0 +1,47 @@ +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"; + + +@Injectable() +export class MapListener { + private start: Subscription; + private drag: Subscription; + private end: Subscription; + + constructor( + private drawingsWidget: DrawingsWidget, + private selectionManager: SelectionManager + ) { + } + + public onInit(svg: any) { + this.start = this.drawingsWidget.draggable.start.subscribe((evt: DraggableStart) => { + 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.start.subscribe((evt: DraggableStart) => { + let drawings = this.selectionManager.getSelectedDrawings(); + drawings.forEach((drawing: Drawing) => { + drawing.x += evt.dx; + drawing.y += evt.dy; + // this.drawingsWidget.redrawDrawing(svg, drawing); + }); + }); + + } + + public onDestroy() { + this.start.unsubscribe(); + this.drag.unsubscribe(); + this.end.unsubscribe(); + } +} \ No newline at end of file diff --git a/src/app/cartography/widgets/drawings.ts b/src/app/cartography/widgets/drawings.ts index 7f08d6ee..a70aa955 100644 --- a/src/app/cartography/widgets/drawings.ts +++ b/src/app/cartography/widgets/drawings.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@angular/core"; +import { Injectable, EventEmitter } from "@angular/core"; import { Widget } from "./widget"; import { Drawing } from "../models/drawing"; @@ -11,12 +11,22 @@ 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 { event } from "d3-selection"; +import { D3DragEvent, drag } from "d3-drag"; +import { Draggable } from "../events/draggable"; @Injectable() export class DrawingsWidget implements Widget { private drawingWidgets: DrawingWidget[] = []; + public draggable = new Draggable(); + + // public onContextMenu = new EventEmitter(); + // public onDrawingClicked = new EventEmitter(); + // public onDrawingDragged = new EventEmitter(); + // public onDrawingDragging = new EventEmitter(); + constructor( private svgToDrawingConverter: SvgToDrawingConverter, private textDrawingWidget: TextDrawingWidget, @@ -69,5 +79,6 @@ export class DrawingsWidget implements Widget { .exit() .remove(); + this.draggable.call(drawing_merge); } } diff --git a/src/app/components/project-map/project-map.component.html b/src/app/components/project-map/project-map.component.html index aba18853..806473b6 100644 --- a/src/app/components/project-map/project-map.component.html +++ b/src/app/components/project-map/project-map.component.html @@ -6,7 +6,6 @@ [drawings]="drawings" [width]="project.scene_width" [height]="project.scene_height" - [selection-manager]="selectionManager" [show-interface-labels]="project.show_interface_labels" [selection-tool]="tools.selection" [moving-tool]="tools.moving" diff --git a/src/app/components/project-map/project-map.component.ts b/src/app/components/project-map/project-map.component.ts index b8bf7f25..b262ad92 100644 --- a/src/app/components/project-map/project-map.component.ts +++ b/src/app/components/project-map/project-map.component.ts @@ -57,34 +57,28 @@ 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, - private nodeWidget: NodeWidget, - 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 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(); From 6fcbf0da28105034f83b0b37cc8819055d22736b Mon Sep 17 00:00:00 2001 From: ziajka Date: Thu, 8 Nov 2018 08:28:19 +0100 Subject: [PATCH 12/15] Rename DrawingWidget to DrawingShapeWidget --- src/app/cartography/listeners/map-listener.ts | 6 ++++++ src/app/cartography/widgets/drawings.ts | 4 ++-- .../drawings/{drawing-widget.ts => drawing-shape-widget.ts} | 2 +- src/app/cartography/widgets/drawings/ellipse-drawing.ts | 4 ++-- src/app/cartography/widgets/drawings/image-drawing.ts | 4 ++-- src/app/cartography/widgets/drawings/line-drawing.ts | 4 ++-- src/app/cartography/widgets/drawings/rect-drawing.ts | 4 ++-- src/app/cartography/widgets/drawings/text-drawing.ts | 4 ++-- 8 files changed, 19 insertions(+), 13 deletions(-) rename src/app/cartography/widgets/drawings/{drawing-widget.ts => drawing-shape-widget.ts} (68%) diff --git a/src/app/cartography/listeners/map-listener.ts b/src/app/cartography/listeners/map-listener.ts index b6dd2aad..ea4f524e 100644 --- a/src/app/cartography/listeners/map-listener.ts +++ b/src/app/cartography/listeners/map-listener.ts @@ -37,6 +37,12 @@ export class MapListener { }); }); + this.end = this.drawingsWidget.draggable.end.subscribe((evt: DraggableStart) => { + let drawings = this.selectionManager.getSelectedDrawings(); + drawings.forEach((drawing: Drawing) => { + + }); + }); } public onDestroy() { diff --git a/src/app/cartography/widgets/drawings.ts b/src/app/cartography/widgets/drawings.ts index a70aa955..e68967fa 100644 --- a/src/app/cartography/widgets/drawings.ts +++ b/src/app/cartography/widgets/drawings.ts @@ -10,7 +10,7 @@ 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 { DrawingShapeWidget } from "./drawings/drawing-widget"; import { event } from "d3-selection"; import { D3DragEvent, drag } from "d3-drag"; import { Draggable } from "../events/draggable"; @@ -18,7 +18,7 @@ import { Draggable } from "../events/draggable"; @Injectable() export class DrawingsWidget implements Widget { - private drawingWidgets: DrawingWidget[] = []; + private drawingWidgets: DrawingShapeWidget[] = []; public draggable = new Draggable(); diff --git a/src/app/cartography/widgets/drawings/drawing-widget.ts b/src/app/cartography/widgets/drawings/drawing-shape-widget.ts similarity index 68% rename from src/app/cartography/widgets/drawings/drawing-widget.ts rename to src/app/cartography/widgets/drawings/drawing-shape-widget.ts index 2e950eee..f18a0950 100644 --- a/src/app/cartography/widgets/drawings/drawing-widget.ts +++ b/src/app/cartography/widgets/drawings/drawing-shape-widget.ts @@ -1,5 +1,5 @@ import { SVGSelection } from "../../models/types"; -export interface DrawingWidget { +export interface DrawingShapeWidget { draw(view: SVGSelection); } diff --git a/src/app/cartography/widgets/drawings/ellipse-drawing.ts b/src/app/cartography/widgets/drawings/ellipse-drawing.ts index 8d394773..b035a724 100644 --- a/src/app/cartography/widgets/drawings/ellipse-drawing.ts +++ b/src/app/cartography/widgets/drawings/ellipse-drawing.ts @@ -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-widget"; import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer"; @Injectable() -export class EllipseDrawingWidget implements DrawingWidget { +export class EllipseDrawingWidget implements DrawingShapeWidget { constructor( private qtDasharrayFixer: QtDasharrayFixer diff --git a/src/app/cartography/widgets/drawings/image-drawing.ts b/src/app/cartography/widgets/drawings/image-drawing.ts index 66a2cd23..464b608a 100644 --- a/src/app/cartography/widgets/drawings/image-drawing.ts +++ b/src/app/cartography/widgets/drawings/image-drawing.ts @@ -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-widget"; @Injectable() -export class ImageDrawingWidget implements DrawingWidget { +export class ImageDrawingWidget implements DrawingShapeWidget { public draw(view: SVGSelection) { const drawing = view .selectAll('image.image_element') diff --git a/src/app/cartography/widgets/drawings/line-drawing.ts b/src/app/cartography/widgets/drawings/line-drawing.ts index fa4ee6fb..074c35d7 100644 --- a/src/app/cartography/widgets/drawings/line-drawing.ts +++ b/src/app/cartography/widgets/drawings/line-drawing.ts @@ -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-widget"; import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer"; @Injectable() -export class LineDrawingWidget implements DrawingWidget { +export class LineDrawingWidget implements DrawingShapeWidget { constructor( private qtDasharrayFixer: QtDasharrayFixer diff --git a/src/app/cartography/widgets/drawings/rect-drawing.ts b/src/app/cartography/widgets/drawings/rect-drawing.ts index 42f3da01..35bf47f6 100644 --- a/src/app/cartography/widgets/drawings/rect-drawing.ts +++ b/src/app/cartography/widgets/drawings/rect-drawing.ts @@ -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-widget"; import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer"; @Injectable() -export class RectDrawingWidget implements DrawingWidget { +export class RectDrawingWidget implements DrawingShapeWidget { constructor( private qtDasharrayFixer: QtDasharrayFixer ) {} diff --git a/src/app/cartography/widgets/drawings/text-drawing.ts b/src/app/cartography/widgets/drawings/text-drawing.ts index 2b6384a8..75175023 100644 --- a/src/app/cartography/widgets/drawings/text-drawing.ts +++ b/src/app/cartography/widgets/drawings/text-drawing.ts @@ -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-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( From b7fb7521f2c06e7b7557e1d475173665e580ddda Mon Sep 17 00:00:00 2001 From: ziajka Date: Thu, 8 Nov 2018 08:41:28 +0100 Subject: [PATCH 13/15] Drawings can be dragged --- src/app/cartography/d3-map.imports.ts | 2 + src/app/cartography/listeners/map-listener.ts | 4 +- src/app/cartography/widgets/drawing.ts | 52 +++++++++++++++ src/app/cartography/widgets/drawings.ts | 63 +++++++------------ .../widgets/drawings/ellipse-drawing.ts | 2 +- .../widgets/drawings/image-drawing.ts | 2 +- .../widgets/drawings/line-drawing.ts | 2 +- .../widgets/drawings/rect-drawing.ts | 2 +- .../widgets/drawings/text-drawing.ts | 2 +- 9 files changed, 84 insertions(+), 47 deletions(-) create mode 100644 src/app/cartography/widgets/drawing.ts diff --git a/src/app/cartography/d3-map.imports.ts b/src/app/cartography/d3-map.imports.ts index e2f53f42..4fbe4f32 100644 --- a/src/app/cartography/d3-map.imports.ts +++ b/src/app/cartography/d3-map.imports.ts @@ -15,6 +15,7 @@ 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, @@ -34,4 +35,5 @@ export const D3_MAP_IMPORTS = [ LineDrawingWidget, RectDrawingWidget, TextDrawingWidget, + DrawingWidget ]; diff --git a/src/app/cartography/listeners/map-listener.ts b/src/app/cartography/listeners/map-listener.ts index ea4f524e..61260ef7 100644 --- a/src/app/cartography/listeners/map-listener.ts +++ b/src/app/cartography/listeners/map-listener.ts @@ -28,12 +28,12 @@ export class MapListener { } }); - this.drag = this.drawingsWidget.draggable.start.subscribe((evt: DraggableStart) => { + this.drag = this.drawingsWidget.draggable.drag.subscribe((evt: DraggableStart) => { let drawings = this.selectionManager.getSelectedDrawings(); drawings.forEach((drawing: Drawing) => { drawing.x += evt.dx; drawing.y += evt.dy; - // this.drawingsWidget.redrawDrawing(svg, drawing); + this.drawingsWidget.redrawDrawing(svg, drawing); }); }); diff --git a/src/app/cartography/widgets/drawing.ts b/src/app/cartography/widgets/drawing.ts new file mode 100644 index 00000000..ca4e0fe4 --- /dev/null +++ b/src/app/cartography/widgets/drawing.ts @@ -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("g.drawing_body") + .data((l) => [l]); + + const drawing_body_enter = drawing_body.enter() + .append('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); + }); + + } +} diff --git a/src/app/cartography/widgets/drawings.ts b/src/app/cartography/widgets/drawings.ts index e68967fa..6e5812a7 100644 --- a/src/app/cartography/widgets/drawings.ts +++ b/src/app/cartography/widgets/drawings.ts @@ -1,25 +1,16 @@ -import { Injectable, EventEmitter } from "@angular/core"; +import { Injectable } from "@angular/core"; 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 { DrawingShapeWidget } from "./drawings/drawing-widget"; -import { event } from "d3-selection"; -import { D3DragEvent, drag } from "d3-drag"; import { Draggable } from "../events/draggable"; +import { DrawingWidget } from "./drawing"; @Injectable() export class DrawingsWidget implements Widget { - private drawingWidgets: DrawingShapeWidget[] = []; - public draggable = new Draggable(); // public onContextMenu = new EventEmitter(); @@ -28,57 +19,49 @@ export class DrawingsWidget implements Widget { // public onDrawingDragging = new EventEmitter(); 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('g.drawing') - .data((l: Layer) => { - l.drawings.forEach((d: Drawing) => { + .selectAll("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('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(); - this.draggable.call(drawing_merge); + this.draggable.call(merge); + } + + private selectDrawing(view: SVGSelection, drawing: Drawing) { + return view.selectAll(`g.drawing[drawing_id="${drawing.drawing_id}"]`); } } diff --git a/src/app/cartography/widgets/drawings/ellipse-drawing.ts b/src/app/cartography/widgets/drawings/ellipse-drawing.ts index b035a724..576578f2 100644 --- a/src/app/cartography/widgets/drawings/ellipse-drawing.ts +++ b/src/app/cartography/widgets/drawings/ellipse-drawing.ts @@ -3,7 +3,7 @@ import { Injectable } from "@angular/core"; import { SVGSelection } from "../../models/types"; import { Drawing } from "../../models/drawing"; import { EllipseElement } from "../../models/drawings/ellipse-element"; -import { DrawingShapeWidget } from "./drawing-widget"; +import { DrawingShapeWidget } from "./drawing-shape-widget"; import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer"; diff --git a/src/app/cartography/widgets/drawings/image-drawing.ts b/src/app/cartography/widgets/drawings/image-drawing.ts index 464b608a..1dcf50ed 100644 --- a/src/app/cartography/widgets/drawings/image-drawing.ts +++ b/src/app/cartography/widgets/drawings/image-drawing.ts @@ -3,7 +3,7 @@ import { Injectable } from "@angular/core"; import { SVGSelection } from "../../models/types"; import { Drawing } from "../../models/drawing"; import { ImageElement } from "../../models/drawings/image-element"; -import { DrawingShapeWidget } from "./drawing-widget"; +import { DrawingShapeWidget } from "./drawing-shape-widget"; @Injectable() diff --git a/src/app/cartography/widgets/drawings/line-drawing.ts b/src/app/cartography/widgets/drawings/line-drawing.ts index 074c35d7..e3816f50 100644 --- a/src/app/cartography/widgets/drawings/line-drawing.ts +++ b/src/app/cartography/widgets/drawings/line-drawing.ts @@ -3,7 +3,7 @@ import { Injectable } from "@angular/core"; import { SVGSelection } from "../../models/types"; import { Drawing } from "../../models/drawing"; import { LineElement } from "../../models/drawings/line-element"; -import { DrawingShapeWidget } from "./drawing-widget"; +import { DrawingShapeWidget } from "./drawing-shape-widget"; import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer"; diff --git a/src/app/cartography/widgets/drawings/rect-drawing.ts b/src/app/cartography/widgets/drawings/rect-drawing.ts index 35bf47f6..f3c768b9 100644 --- a/src/app/cartography/widgets/drawings/rect-drawing.ts +++ b/src/app/cartography/widgets/drawings/rect-drawing.ts @@ -3,7 +3,7 @@ import { Injectable } from "@angular/core"; import { SVGSelection } from "../../models/types"; import { Drawing } from "../../models/drawing"; import { RectElement } from "../../models/drawings/rect-element"; -import { DrawingShapeWidget } from "./drawing-widget"; +import { DrawingShapeWidget } from "./drawing-shape-widget"; import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer"; diff --git a/src/app/cartography/widgets/drawings/text-drawing.ts b/src/app/cartography/widgets/drawings/text-drawing.ts index 75175023..3522fb61 100644 --- a/src/app/cartography/widgets/drawings/text-drawing.ts +++ b/src/app/cartography/widgets/drawings/text-drawing.ts @@ -3,7 +3,7 @@ import { Injectable } from "@angular/core"; import { SVGSelection } from "../../models/types"; import { TextElement } from "../../models/drawings/text-element"; import { Drawing } from "../../models/drawing"; -import { DrawingShapeWidget } from "./drawing-widget"; +import { DrawingShapeWidget } from "./drawing-shape-widget"; import { FontFixer } from "../../helpers/font-fixer"; import { select } from "d3-selection"; From 69163e7b6d7ac792bdec1a15fea52d2bc0cfe61c Mon Sep 17 00:00:00 2001 From: ziajka Date: Thu, 8 Nov 2018 10:29:00 +0100 Subject: [PATCH 14/15] Drawings can communicate with server after dragged --- src/app/app.module.ts | 2 + src/app/cartography/cartography.module.ts | 12 ++- .../components/map/map.component.ts | 78 ++++----------- src/app/cartography/events/draggable.ts | 8 ++ .../events/drawings-event-source.ts | 9 ++ src/app/cartography/events/event-source.ts | 9 ++ .../cartography/events/nodes-event-source.ts | 9 ++ .../listeners/drawings-draggable-listener.ts | 55 +++++++++++ src/app/cartography/listeners/map-listener.ts | 55 +---------- .../cartography/listeners/map-listeners.ts | 29 ++++++ .../listeners/nodes-draggable-listener.ts | 64 +++++++++++++ src/app/cartography/widgets/graph-layout.ts | 4 + src/app/cartography/widgets/node.ts | 18 ---- src/app/cartography/widgets/nodes.ts | 5 + .../project-map/project-map.component.html | 3 +- .../project-map/project-map.component.ts | 22 ++++- src/app/services/drawing.service.spec.ts | 95 +++++++++++++++++++ src/app/services/drawing.service.ts | 36 +++++++ 18 files changed, 376 insertions(+), 137 deletions(-) create mode 100644 src/app/cartography/events/drawings-event-source.ts create mode 100644 src/app/cartography/events/event-source.ts create mode 100644 src/app/cartography/events/nodes-event-source.ts create mode 100644 src/app/cartography/listeners/drawings-draggable-listener.ts create mode 100644 src/app/cartography/listeners/map-listeners.ts create mode 100644 src/app/cartography/listeners/nodes-draggable-listener.ts create mode 100644 src/app/services/drawing.service.spec.ts create mode 100644 src/app/services/drawing.service.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 962c7e1e..28df5878 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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, diff --git a/src/app/cartography/cartography.module.ts b/src/app/cartography/cartography.module.ts index 0161de52..43173aa4 100644 --- a/src/app/cartography/cartography.module.ts +++ b/src/app/cartography/cartography.module.ts @@ -16,7 +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 { MapListener } from './listeners/map-listener'; +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({ @@ -40,7 +44,11 @@ import { MapListener } from './listeners/map-listener'; MapChangeDetectorRef, CanvasSizeDetector, Context, - MapListener, + MapListeners, + DrawingsDraggableListener, + NodesDraggableListener, + DrawingsEventSource, + NodesEventSource, ...D3_MAP_IMPORTS ], exports: [ MapComponent ] diff --git a/src/app/cartography/components/map/map.component.ts b/src/app/cartography/components/map/map.component.ts index e120afcb..9bc1a6f2 100644 --- a/src/app/cartography/components/map/map.component.ts +++ b/src/app/cartography/components/map/map.component.ts @@ -20,9 +20,11 @@ import { MapChangeDetectorRef } from '../../services/map-change-detector-ref'; import { NodeDragging, NodeDragged, NodeClicked } from '../../events/nodes'; import { LinkCreated } from '../../events/links'; import { CanvasSizeDetector } from '../../helpers/canvas-size-detector'; -import { SelectionManager } from '../../managers/selection-manager'; import { NodeWidget } from '../../widgets/node'; -import { MapListener } from '../../listeners/map-listener'; +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'; @Component({ @@ -39,16 +41,13 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { @Input() width = 1500; @Input() height = 600; - @Output() onNodeDragged = new EventEmitter(); + @Output() nodeDragged: EventEmitter>; + @Output() drawingDragged: EventEmitter>; @Output() onLinkCreated = new EventEmitter(); private parentNativeElement: any; private svg: Selection; - private onNodeDraggingSubscription: Subscription; - private onNodeClickedSubscription: Subscription; - private onNodeDraggedSubscription: Subscription; - private onChangesDetected: Subscription; protected settings = { @@ -59,8 +58,7 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { private context: Context, private mapChangeDetectorRef: MapChangeDetectorRef, private canvasSizeDetector: CanvasSizeDetector, - private mapListener: MapListener, - private selectionManager: SelectionManager, + private mapListeners: MapListeners, protected element: ElementRef, protected nodesWidget: NodesWidget, protected nodeWidget: NodeWidget, @@ -68,9 +66,13 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { protected interfaceLabelWidget: InterfaceLabelWidget, protected selectionToolWidget: SelectionTool, protected movingToolWidget: MovingTool, - public graphLayout: GraphLayout + public graphLayout: GraphLayout, + nodesEventSource: NodesEventSource, + drawingsEventSource: DrawingsEventSource, ) { this.parentNativeElement = element.nativeElement; + this.nodeDragged = nodesEventSource.dragged; + this.drawingDragged = drawingsEventSource.dragged; } @Input('show-interface-labels') @@ -118,67 +120,25 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { } } - ngOnDestroy() { - this.graphLayout.disconnect(this.svg); - this.onNodeDraggingSubscription.unsubscribe(); - this.onNodeClickedSubscription.unsubscribe(); - this.onNodeDraggedSubscription.unsubscribe(); - this.onChangesDetected.unsubscribe(); - this.mapListener.onDestroy(); - } - ngOnInit() { if (this.parentNativeElement !== null) { this.createGraph(this.parentNativeElement); } this.context.size = this.getSize(); - this.onNodeDraggingSubscription = this.nodeWidget.onNodeDragging.subscribe((eventNode: NodeDragging) => { - let nodes = this.selectionManager.getSelectedNodes(); - - if (nodes.filter((n: Node) => n.node_id === eventNode.node.node_id).length === 0) { - this.selectionManager.setSelectedNodes([eventNode.node]); - nodes = this.selectionManager.getSelectedNodes(); - } - - nodes.forEach((node: Node) => { - node.x += eventNode.event.dx; - node.y += eventNode.event.dy; - - this.nodesWidget.redrawNode(this.svg, node); - const links = this.links.filter((link) => link.target.node_id === node.node_id || link.source.node_id === node.node_id); - links.forEach((link) => { - this.linksWidget.redrawLink(this.svg, link); - }); - }); - - }); - - this.onNodeDraggedSubscription = this.nodeWidget.onNodeDragged.subscribe((eventNode: NodeDragged) => { - let nodes = this.selectionManager.getSelectedNodes(); - - if (nodes.filter((n: Node) => n.node_id === eventNode.node.node_id).length === 0) { - this.selectionManager.setSelectedNodes([eventNode.node]); - nodes = this.selectionManager.getSelectedNodes(); - } - - nodes.forEach((node) => { - this.onNodeDragged.emit(new NodeDragged(eventNode.event, node)); - }); - - }); - - this.onNodeClickedSubscription = this.nodeWidget.onNodeClicked.subscribe((nodeClickedEvent: NodeClicked) => { - this.selectionManager.setSelectedNodes([nodeClickedEvent.node]); - }); - this.onChangesDetected = this.mapChangeDetectorRef.changesDetected.subscribe(() => { if (this.mapChangeDetectorRef.hasBeenDrawn) { this.reload(); } }); - this.mapListener.onInit(this.svg); + this.mapListeners.onInit(this.svg); + } + + ngOnDestroy() { + this.graphLayout.disconnect(this.svg); + this.onChangesDetected.unsubscribe(); + this.mapListeners.onDestroy(); } public createGraph(domElement: HTMLElement) { diff --git a/src/app/cartography/events/draggable.ts b/src/app/cartography/events/draggable.ts index 3cefb1ee..e121b8e4 100644 --- a/src/app/cartography/events/draggable.ts +++ b/src/app/cartography/events/draggable.ts @@ -3,6 +3,8 @@ 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; } @@ -46,18 +48,24 @@ export class Draggable { const evt = new DraggableStart(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); 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); evt.dx = event.dx; evt.dy = event.dy; + evt.x = event.x; + evt.y = event.y; this.end.emit(evt); }); } diff --git a/src/app/cartography/events/drawings-event-source.ts b/src/app/cartography/events/drawings-event-source.ts new file mode 100644 index 00000000..a24f2b6b --- /dev/null +++ b/src/app/cartography/events/drawings-event-source.ts @@ -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>(); +} \ No newline at end of file diff --git a/src/app/cartography/events/event-source.ts b/src/app/cartography/events/event-source.ts new file mode 100644 index 00000000..26bc15df --- /dev/null +++ b/src/app/cartography/events/event-source.ts @@ -0,0 +1,9 @@ +export class DataEventSource { + constructor( + public datum: T + ) {} +} + + +// class CreatedDataEvent extends DataEventSource {} +export class DraggedDataEvent extends DataEventSource {} diff --git a/src/app/cartography/events/nodes-event-source.ts b/src/app/cartography/events/nodes-event-source.ts new file mode 100644 index 00000000..bd237506 --- /dev/null +++ b/src/app/cartography/events/nodes-event-source.ts @@ -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>(); +} \ No newline at end of file diff --git a/src/app/cartography/listeners/drawings-draggable-listener.ts b/src/app/cartography/listeners/drawings-draggable-listener.ts new file mode 100644 index 00000000..420b840b --- /dev/null +++ b/src/app/cartography/listeners/drawings-draggable-listener.ts @@ -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) => { + 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) => { + 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) => { + let drawings = this.selectionManager.getSelectedDrawings(); + drawings.forEach((drawing: Drawing) => { + this.drawingsEventSource.dragged.emit(new DraggedDataEvent(drawing)); + }); + }); + } + + public onDestroy() { + this.start.unsubscribe(); + this.drag.unsubscribe(); + this.end.unsubscribe(); + } +} \ No newline at end of file diff --git a/src/app/cartography/listeners/map-listener.ts b/src/app/cartography/listeners/map-listener.ts index 61260ef7..50c7a2bb 100644 --- a/src/app/cartography/listeners/map-listener.ts +++ b/src/app/cartography/listeners/map-listener.ts @@ -1,53 +1,4 @@ -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"; - - -@Injectable() -export class MapListener { - private start: Subscription; - private drag: Subscription; - private end: Subscription; - - constructor( - private drawingsWidget: DrawingsWidget, - private selectionManager: SelectionManager - ) { - } - - public onInit(svg: any) { - this.start = this.drawingsWidget.draggable.start.subscribe((evt: DraggableStart) => { - 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) => { - 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) => { - let drawings = this.selectionManager.getSelectedDrawings(); - drawings.forEach((drawing: Drawing) => { - - }); - }); - } - - public onDestroy() { - this.start.unsubscribe(); - this.drag.unsubscribe(); - this.end.unsubscribe(); - } +export interface MapListener { + onInit(svg: any); + onDestroy(); } \ No newline at end of file diff --git a/src/app/cartography/listeners/map-listeners.ts b/src/app/cartography/listeners/map-listeners.ts new file mode 100644 index 00000000..1147fe8e --- /dev/null +++ b/src/app/cartography/listeners/map-listeners.ts @@ -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(); + }); + } +} \ No newline at end of file diff --git a/src/app/cartography/listeners/nodes-draggable-listener.ts b/src/app/cartography/listeners/nodes-draggable-listener.ts new file mode 100644 index 00000000..8c339976 --- /dev/null +++ b/src/app/cartography/listeners/nodes-draggable-listener.ts @@ -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) => { + 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) => { + 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) => { + let nodes = this.selectionManager.getSelectedNodes(); + nodes.forEach((node: Node) => { + this.nodesEventSource.dragged.emit(new DraggedDataEvent(node)); + }); + }); + } + + public onDestroy() { + this.start.unsubscribe(); + this.drag.unsubscribe(); + this.end.unsubscribe(); + } +} \ No newline at end of file diff --git a/src/app/cartography/widgets/graph-layout.ts b/src/app/cartography/widgets/graph-layout.ts index d0e75077..233311bb 100644 --- a/src/app/cartography/widgets/graph-layout.ts +++ b/src/app/cartography/widgets/graph-layout.ts @@ -36,6 +36,10 @@ export class GraphLayout implements Widget { this.links = links; } + public getLinks() { + return this.links; + } + public setDrawings(drawings: Drawing[]) { this.drawings = drawings; } diff --git a/src/app/cartography/widgets/node.ts b/src/app/cartography/widgets/node.ts index eed25b93..60e771b1 100644 --- a/src/app/cartography/widgets/node.ts +++ b/src/app/cartography/widgets/node.ts @@ -118,23 +118,5 @@ export class NodeWidget implements Widget { } return n.label.y + bbox.height - NodeWidget.NODE_LABEL_MARGIN; }); - - const callback = function (this: SVGGElement, n: Node) { - const e: D3DragEvent = event; - self.onNodeDragging.emit(new NodeDragging(e, n)); - }; - - const dragging = () => { - return drag() - .on('drag', callback) - .on('end', (n: Node) => { - const e: D3DragEvent = event; - self.onNodeDragged.emit(new NodeDragged(e, n)); - }); - }; - - if (this.draggingEnabled) { - node_body_merge.call(dragging()); - } } } diff --git a/src/app/cartography/widgets/nodes.ts b/src/app/cartography/widgets/nodes.ts index a8c78884..849e8902 100644 --- a/src/app/cartography/widgets/nodes.ts +++ b/src/app/cartography/widgets/nodes.ts @@ -5,12 +5,15 @@ import { Node } from "../models/node"; import { SVGSelection } from "../models/types"; import { Layer } from "../models/layer"; import { NodeWidget } from "./node"; +import { Draggable } from "../events/draggable"; @Injectable() export class NodesWidget implements Widget { static NODE_LABEL_MARGIN = 3; + public draggable = new Draggable(); + constructor( private nodeWidget: NodeWidget ) { @@ -44,6 +47,8 @@ export class NodesWidget implements Widget { node .exit() .remove(); + + this.draggable.call(merge); } private selectNode(view: SVGSelection, node: Node) { diff --git a/src/app/components/project-map/project-map.component.html b/src/app/components/project-map/project-map.component.html index 806473b6..7ff2ce48 100644 --- a/src/app/components/project-map/project-map.component.html +++ b/src/app/components/project-map/project-map.component.html @@ -10,7 +10,8 @@ [selection-tool]="tools.selection" [moving-tool]="tools.moving" [draw-link-tool]="tools.draw_link" - (onNodeDragged)="onNodeDragged($event)" + (nodeDragged)="onNodeDragged($event)" + (drawingDragged)="onDrawingDragged($event)" (onLinkCreated)="onLinkCreated($event)" >
diff --git a/src/app/components/project-map/project-map.component.ts b/src/app/components/project-map/project-map.component.ts index b262ad92..e3873c93 100644 --- a/src/app/components/project-map/project-map.component.ts +++ b/src/app/components/project-map/project-map.component.ts @@ -30,6 +30,8 @@ import { MapChangeDetectorRef } from '../../cartography/services/map-change-dete import { NodeContextMenu, NodeDragged } 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({ @@ -70,6 +72,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy { private symbolService: SymbolService, private nodeService: NodeService, private linkService: LinkService, + private drawingService: DrawingService, private progressService: ProgressService, private projectWebServiceHandler: ProjectWebServiceHandler, private mapChangeDetectorRef: MapChangeDetectorRef, @@ -216,12 +219,21 @@ export class ProjectMapComponent implements OnInit, OnDestroy { }); } - onNodeDragged(nodeEvent: NodeDragged) { - this.nodesDataSource.update(nodeEvent.node); + onNodeDragged(draggedEvent: DraggedDataEvent) { + 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) { + 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); }); } diff --git a/src/app/services/drawing.service.spec.ts b/src/app/services/drawing.service.spec.ts new file mode 100644 index 00000000..85a5c3f7 --- /dev/null +++ b/src/app/services/drawing.service.spec.ts @@ -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"); + })); + +}); diff --git a/src/app/services/drawing.service.ts b/src/app/services/drawing.service.ts new file mode 100644 index 00000000..31ef46a7 --- /dev/null +++ b/src/app/services/drawing.service.ts @@ -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 { + return this.httpServer + .put(server, `/projects/${drawing.project_id}/drawings/${drawing.drawing_id}`, { + 'x': x, + 'y': y + }); + } + + update(server: Server, drawing: Drawing): Observable { + return this.httpServer + .put(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(server, `/projects/${drawing.project_id}/drawings/${drawing.drawing_id}`); + } + +} From b89e9f143487f0bbbf7959202759cc7ff6448d89 Mon Sep 17 00:00:00 2001 From: ziajka Date: Thu, 8 Nov 2018 10:51:56 +0100 Subject: [PATCH 15/15] Disable dragging on readonly mode --- src/app/cartography/components/map/map.component.ts | 7 +++++++ src/app/cartography/widgets/drawings.ts | 5 ++++- src/app/cartography/widgets/node.ts | 5 ----- src/app/cartography/widgets/nodes.ts | 7 +++++-- src/app/components/project-map/project-map.component.html | 1 + src/app/components/project-map/project-map.component.ts | 5 +---- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/app/cartography/components/map/map.component.ts b/src/app/cartography/components/map/map.component.ts index 9bc1a6f2..902d658e 100644 --- a/src/app/cartography/components/map/map.component.ts +++ b/src/app/cartography/components/map/map.component.ts @@ -25,6 +25,7 @@ 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({ @@ -63,6 +64,7 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { protected nodesWidget: NodesWidget, protected nodeWidget: NodeWidget, protected linksWidget: LinksWidget, + protected drawingsWidget: DrawingsWidget, protected interfaceLabelWidget: InterfaceLabelWidget, protected selectionToolWidget: SelectionTool, protected movingToolWidget: MovingTool, @@ -95,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 ( diff --git a/src/app/cartography/widgets/drawings.ts b/src/app/cartography/widgets/drawings.ts index 6e5812a7..3f3b0f80 100644 --- a/src/app/cartography/widgets/drawings.ts +++ b/src/app/cartography/widgets/drawings.ts @@ -12,6 +12,7 @@ import { DrawingWidget } from "./drawing"; @Injectable() export class DrawingsWidget implements Widget { public draggable = new Draggable(); + public draggingEnabled = false; // public onContextMenu = new EventEmitter(); // public onDrawingClicked = new EventEmitter(); @@ -58,7 +59,9 @@ export class DrawingsWidget implements Widget { .exit() .remove(); - this.draggable.call(merge); + if (this.draggingEnabled) { + this.draggable.call(merge); + } } private selectDrawing(view: SVGSelection, drawing: Drawing) { diff --git a/src/app/cartography/widgets/node.ts b/src/app/cartography/widgets/node.ts index 60e771b1..e879786e 100644 --- a/src/app/cartography/widgets/node.ts +++ b/src/app/cartography/widgets/node.ts @@ -21,7 +21,6 @@ export class NodeWidget implements Widget { public onNodeDragging = new EventEmitter(); private symbols: Symbol[] = []; - private draggingEnabled = false; constructor( private cssFixer: CssFixer, @@ -32,10 +31,6 @@ export class NodeWidget implements Widget { this.symbols = symbols; } - public setDraggingEnabled(enabled: boolean) { - this.draggingEnabled = enabled; - } - public draw(view: SVGSelection) { const self = this; diff --git a/src/app/cartography/widgets/nodes.ts b/src/app/cartography/widgets/nodes.ts index 849e8902..60fed63b 100644 --- a/src/app/cartography/widgets/nodes.ts +++ b/src/app/cartography/widgets/nodes.ts @@ -13,7 +13,8 @@ export class NodesWidget implements Widget { static NODE_LABEL_MARGIN = 3; public draggable = new Draggable(); - + public draggingEnabled = false; + constructor( private nodeWidget: NodeWidget ) { @@ -48,7 +49,9 @@ export class NodesWidget implements Widget { .exit() .remove(); - this.draggable.call(merge); + if (this.draggingEnabled) { + this.draggable.call(merge); + } } private selectNode(view: SVGSelection, node: Node) { diff --git a/src/app/components/project-map/project-map.component.html b/src/app/components/project-map/project-map.component.html index 7ff2ce48..a62f2834 100644 --- a/src/app/components/project-map/project-map.component.html +++ b/src/app/components/project-map/project-map.component.html @@ -10,6 +10,7 @@ [selection-tool]="tools.selection" [moving-tool]="tools.moving" [draw-link-tool]="tools.draw_link" + [readonly]="inReadOnlyMode" (nodeDragged)="onNodeDragged($event)" (drawingDragged)="onDrawingDragged($event)" (onLinkCreated)="onLinkCreated($event)" diff --git a/src/app/components/project-map/project-map.component.ts b/src/app/components/project-map/project-map.component.ts index e3873c93..4596861c 100644 --- a/src/app/components/project-map/project-map.component.ts +++ b/src/app/components/project-map/project-map.component.ts @@ -23,11 +23,10 @@ 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'; @@ -187,8 +186,6 @@ export class ProjectMapComponent implements OnInit, OnDestroy { } setUpMapCallbacks(project: Project) { - this.nodeWidget.setDraggingEnabled(!this.readonly); - const onContextMenu = this.nodeWidget.onContextMenu.subscribe((eventNode: NodeContextMenu) => { this.nodeContextMenu.open( eventNode.node,