From a1fa0141abdd33171dfe432a51a601e0d0afd3b6 Mon Sep 17 00:00:00 2001 From: mhg <73169169+Marcel-Haag@users.noreply.github.com> Date: Sat, 29 Jan 2022 03:09:02 +0100 Subject: [PATCH] feat: added delete project option and generic confirm dialog --- security-c4po-angular/src/app/app.module.ts | 2 + .../project-overview.component.html | 4 +- .../project-overview.component.scss | 8 +++ .../project-overview.component.ts | 29 ++++++++-- .../src/assets/@theme/styles/_dialog.scss | 18 ++++++ .../src/assets/@theme/styles/pace.theme.scss | 6 -- .../src/assets/i18n/de-DE.json | 11 +++- .../src/assets/i18n/en-US.json | 11 +++- .../confirm-dialog.component.html | 21 +++++++ .../confirm-dialog.component.scss | 1 + .../confirm-dialog.component.spec.ts | 56 +++++++++++++++++++ .../confirm-dialog.component.ts | 28 ++++++++++ .../confirm-dialog/confirm-dialog.module.ts | 23 ++++++++ .../project-dialog.component.html | 6 +- .../project-dialog.component.scss | 2 + .../services/dialog-service/dialog-message.ts | 6 ++ .../dialog-service/dialog.service.mock.ts | 5 ++ .../services/dialog-service/dialog.service.ts | 17 ++++++ .../shared/services/project.service.mock.ts | 8 ++- .../shared/services/project.service.spec.ts | 31 +++++++++- .../src/shared/services/project.service.ts | 11 ++++ 21 files changed, 283 insertions(+), 21 deletions(-) create mode 100644 security-c4po-angular/src/assets/@theme/styles/_dialog.scss create mode 100644 security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.component.html create mode 100644 security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.component.scss create mode 100644 security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.component.spec.ts create mode 100644 security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.component.ts create mode 100644 security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.module.ts create mode 100644 security-c4po-angular/src/shared/services/dialog-service/dialog-message.ts diff --git a/security-c4po-angular/src/app/app.module.ts b/security-c4po-angular/src/app/app.module.ts index 19e02b6..1d4594a 100644 --- a/security-c4po-angular/src/app/app.module.ts +++ b/security-c4po-angular/src/app/app.module.ts @@ -27,6 +27,7 @@ import {KeycloakService} from 'keycloak-angular'; import {httpInterceptorProviders} from '@shared/interceptors'; import {FlexLayoutModule} from '@angular/flex-layout'; import {DialogService} from '@shared/services/dialog-service/dialog.service'; +import {ConfirmDialogModule} from '@shared/modules/confirm-dialog/confirm-dialog.module'; @NgModule({ declarations: [ @@ -45,6 +46,7 @@ import {DialogService} from '@shared/services/dialog-service/dialog.service'; NbIconModule, NbButtonModule, NbEvaIconsModule, + ConfirmDialogModule, NgxsModule.forRoot([SessionState], {developmentMode: !environment.production}), HttpClientModule, TranslateModule.forRoot({ diff --git a/security-c4po-angular/src/app/project-overview/project-overview.component.html b/security-c4po-angular/src/app/project-overview/project-overview.component.html index 41cb515..2582c21 100644 --- a/security-c4po-angular/src/app/project-overview/project-overview.component.html +++ b/security-c4po-angular/src/app/project-overview/project-overview.component.html @@ -1,6 +1,6 @@
- + + (click)="onClickDeleteProject(project)">
diff --git a/security-c4po-angular/src/app/project-overview/project-overview.component.scss b/security-c4po-angular/src/app/project-overview/project-overview.component.scss index c411834..20f6bb7 100644 --- a/security-c4po-angular/src/app/project-overview/project-overview.component.scss +++ b/security-c4po-angular/src/app/project-overview/project-overview.component.scss @@ -1,3 +1,5 @@ +@import '../../assets/@theme/styles/themes'; + .project-card { max-width: 22rem; width: 22rem; @@ -33,6 +35,12 @@ } } +.project-card:hover { + background-color: nb-theme(color-info-transparent-default); + margin-top: +0.625rem; + transform: scale(1.025) +} + .project-link:hover { cursor: pointer !important; } diff --git a/security-c4po-angular/src/app/project-overview/project-overview.component.ts b/security-c4po-angular/src/app/project-overview/project-overview.component.ts index 75704e7..200a6ae 100644 --- a/security-c4po-angular/src/app/project-overview/project-overview.component.ts +++ b/security-c4po-angular/src/app/project-overview/project-overview.component.ts @@ -5,10 +5,9 @@ import {BehaviorSubject, Observable} from 'rxjs'; import {untilDestroyed} from 'ngx-take-until-destroy'; import {ProjectService} from '@shared/services/project.service'; import {NotificationService, PopupType} from '@shared/services/notification.service'; -import {filter, mergeMap, tap} from 'rxjs/operators'; +import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators'; import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {ProjectDialogComponent} from '@shared/modules/project-dialog/project-dialog.component'; -import {NB_DIALOG_CONFIG} from '@nebular/theme/components/dialog/dialog-config'; @Component({ selector: 'app-project-overview', @@ -80,8 +79,30 @@ export class ProjectOverviewComponent implements OnInit, OnDestroy { console.log('to be implemented...'); } - onClickDeleteProject(): void { - console.log('to be implemented...'); + onClickDeleteProject(project: Project): void { + const message = { + title: 'project.delete.title', + key: 'project.delete.key', + data: {name: project.title}, + }; + this.dialogService.openConfirmDialog( + message + ).onClose.pipe( + filter((confirm) => !!confirm), + switchMap(() => this.projectService.deleteProjectById(project.id)), + catchError(() => { + this.notificationService.showPopup('project.popup.delete.failed', PopupType.FAILURE); + return []; + }), + untilDestroyed(this) + ).subscribe({ + next: () => { + this.loadProjects(); + this.notificationService.showPopup('project.popup.delete.success', PopupType.SUCCESS); + }, error: error => { + console.error(error); + } + }); } isLoading(): Observable { diff --git a/security-c4po-angular/src/assets/@theme/styles/_dialog.scss b/security-c4po-angular/src/assets/@theme/styles/_dialog.scss new file mode 100644 index 0000000..0e35a53 --- /dev/null +++ b/security-c4po-angular/src/assets/@theme/styles/_dialog.scss @@ -0,0 +1,18 @@ +.dialog-header { + height: 6.75vh; + font-size: 1.5rem; + + .dialog-headline { + margin-top: 2rem; + } +} + +.dialog-body { + font-size: 1.15rem; +} + +.dialog-button { + width: 4.5rem; + height: 2.5rem; + font-size: 1.5rem; +} diff --git a/security-c4po-angular/src/assets/@theme/styles/pace.theme.scss b/security-c4po-angular/src/assets/@theme/styles/pace.theme.scss index c14f331..8343514 100644 --- a/security-c4po-angular/src/assets/@theme/styles/pace.theme.scss +++ b/security-c4po-angular/src/assets/@theme/styles/pace.theme.scss @@ -1,9 +1,3 @@ -/** - * @license - * Copyright Akveo. All Rights Reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ - @mixin ngx-pace-theme() { .pace .pace-progress { diff --git a/security-c4po-angular/src/assets/i18n/de-DE.json b/security-c4po-angular/src/assets/i18n/de-DE.json index 1fbf84b..c81a822 100644 --- a/security-c4po-angular/src/assets/i18n/de-DE.json +++ b/security-c4po-angular/src/assets/i18n/de-DE.json @@ -3,7 +3,10 @@ "action.login": "Einloggen", "action.retry": "Erneut Versuchen", "action.save": "Speichern", + "action.confirm": "Bestätigen", "action.cancel": "Abbrechen", + "action.yes": "Ja", + "action.no": "Nein", "username": "Nutzername", "password": "Passwort" }, @@ -33,10 +36,16 @@ "create": { "header": "Neues Projekt erstellen" }, + "delete": { + "title": "Projekt löschen", + "key": "Möchten Sie das Projekt \"{{name}}\" unwiderruflich löschen?" + }, "popup": { "not.found": "Keine Projekte gefunden", "save.success": "Projekt erfolgreich gespeichert", - "save.failed": "Projekt konnte nicht gespeichert werden" + "save.failed": "Projekt konnte nicht gespeichert werden", + "delete.success": "Projekt erfolgreich gelöscht", + "delete.failed": "Projekt konnte nicht gelöscht werden" }, "title.label": "Projekt Titel", "client.label": "Name des Auftraggebers", diff --git a/security-c4po-angular/src/assets/i18n/en-US.json b/security-c4po-angular/src/assets/i18n/en-US.json index b7c1f4e..a1cb888 100644 --- a/security-c4po-angular/src/assets/i18n/en-US.json +++ b/security-c4po-angular/src/assets/i18n/en-US.json @@ -2,8 +2,11 @@ "global": { "action.login": "Login", "action.retry": "Try again", + "action.confirm": "Confirm", "action.save": "Save", "action.cancel": "Cancel", + "action.yes": "Yes", + "action.no": "No", "username": "Username", "password": "Password" }, @@ -33,10 +36,16 @@ "create": { "header": "Create New Project" }, + "delete": { + "title": "Delete Project", + "key": "Do you want to permanently delete the project \"{{name}}\"?" + }, "popup": { "not.found": "No projects found", "save.success": "Project saved successfully", - "save.failed": "Project could not be saved" + "save.failed": "Project could not be saved", + "delete.success": "Project deleted successfully", + "delete.failed": "Project could not be deleted" }, "title.label": "Project Title", "client.label": "Name of Client", diff --git a/security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.component.html b/security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.component.html new file mode 100644 index 0000000..72597bc --- /dev/null +++ b/security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.component.html @@ -0,0 +1,21 @@ + + + {{ data?.title | translate }} + + + {{ data?.key | translate: data?.data }} + + + + + + diff --git a/security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.component.scss b/security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.component.scss new file mode 100644 index 0000000..0878515 --- /dev/null +++ b/security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.component.scss @@ -0,0 +1 @@ +@import "../../../assets/@theme/styles/_dialog.scss"; diff --git a/security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.component.spec.ts b/security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.component.spec.ts new file mode 100644 index 0000000..747788c --- /dev/null +++ b/security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.component.spec.ts @@ -0,0 +1,56 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConfirmDialogComponent } from './confirm-dialog.component'; +import {DialogService} from '@shared/services/dialog-service/dialog.service'; +import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock'; +import {NbButtonModule, NbCardModule, NbDialogRef, NbLayoutModule} from '@nebular/theme'; +import {CommonModule} from '@angular/common'; +import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; +import {HttpLoaderFactory} from '../../../app/common-app.module'; +import {HttpClient, HttpClientModule} from '@angular/common/http'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {FlexLayoutModule} from '@angular/flex-layout'; + +describe('ConfirmDialogComponent', () => { + let component: ConfirmDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + ConfirmDialogComponent + ], + imports: [ + CommonModule, + NbLayoutModule, + NbCardModule, + NbButtonModule, + FlexLayoutModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient] + } + }), + HttpClientModule, + HttpClientTestingModule + ], + providers: [ + {provide: DialogService, useClass: DialogServiceMock}, + {provide: NbDialogRef, useValue: {}} + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ConfirmDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.component.ts b/security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.component.ts new file mode 100644 index 0000000..a5de5b6 --- /dev/null +++ b/security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.component.ts @@ -0,0 +1,28 @@ +import {Component, Input} from '@angular/core'; +import {NbDialogRef} from '@nebular/theme'; + +@Component({ + selector: 'app-confirm-dialog', + templateUrl: './confirm-dialog.component.html', + styleUrls: ['./confirm-dialog.component.scss'] +}) +export class ConfirmDialogComponent { + /** + * @param data contains all relevant information the dialog needs + * @param data.title The translation key for the dialog title + * @param data.key The translation key for the shown message + * @param data.data The data that may be used in the message translation key + */ + @Input() data: any; + + constructor(protected dialogRef: NbDialogRef) { + } + + onClickConfirm(): void { + this.dialogRef.close({confirm: true}); + } + + onClickClose(): void { + this.dialogRef.close(); + } +} diff --git a/security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.module.ts b/security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.module.ts new file mode 100644 index 0000000..cddddb6 --- /dev/null +++ b/security-c4po-angular/src/shared/modules/confirm-dialog/confirm-dialog.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import {ConfirmDialogComponent} from '@shared/modules/confirm-dialog/confirm-dialog.component'; +import {NbButtonModule, NbCardModule} from '@nebular/theme'; +import {FlexLayoutModule} from '@angular/flex-layout'; +import {TranslateModule} from '@ngx-translate/core'; + +@NgModule({ + declarations: [ + ConfirmDialogComponent + ], + imports: [ + CommonModule, + NbCardModule, + NbButtonModule, + FlexLayoutModule, + TranslateModule + ], + entryComponents: [ + ConfirmDialogComponent + ] +}) +export class ConfirmDialogModule { } diff --git a/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.html b/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.html index a4f6b5a..0b84d7b 100644 --- a/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.html +++ b/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.html @@ -1,5 +1,5 @@ - + {{ 'project.create.header' | translate }} @@ -39,10 +39,10 @@ - - diff --git a/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.scss b/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.scss index a1ef017..a8fa406 100644 --- a/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.scss +++ b/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.scss @@ -1,3 +1,5 @@ +@import "../../../assets/@theme/styles/_dialog.scss"; + .project-dialog { width: 24rem; height: 31rem; diff --git a/security-c4po-angular/src/shared/services/dialog-service/dialog-message.ts b/security-c4po-angular/src/shared/services/dialog-service/dialog-message.ts new file mode 100644 index 0000000..11f9aaa --- /dev/null +++ b/security-c4po-angular/src/shared/services/dialog-service/dialog-message.ts @@ -0,0 +1,6 @@ +export interface DialogMessage { + key: string; + data?: any; + title?: string; + inputPlaceholderKey?: string; +} diff --git a/security-c4po-angular/src/shared/services/dialog-service/dialog.service.mock.ts b/security-c4po-angular/src/shared/services/dialog-service/dialog.service.mock.ts index c37ba51..20ec024 100644 --- a/security-c4po-angular/src/shared/services/dialog-service/dialog.service.mock.ts +++ b/security-c4po-angular/src/shared/services/dialog-service/dialog.service.mock.ts @@ -2,6 +2,7 @@ import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {ComponentType} from '@angular/cdk/overlay'; import {TemplateRef} from '@angular/core'; import {NbDialogConfig, NbDialogRef} from '@nebular/theme'; +import {DialogMessage} from '@shared/services/dialog-service/dialog-message'; export class DialogServiceMock implements Required { @@ -13,4 +14,8 @@ export class DialogServiceMock implements Required { ): NbDialogRef { return null; } + + openConfirmDialog(message: DialogMessage): NbDialogRef { + return null; + } } diff --git a/security-c4po-angular/src/shared/services/dialog-service/dialog.service.ts b/security-c4po-angular/src/shared/services/dialog-service/dialog.service.ts index 9d04a9a..1aa701c 100644 --- a/security-c4po-angular/src/shared/services/dialog-service/dialog.service.ts +++ b/security-c4po-angular/src/shared/services/dialog-service/dialog.service.ts @@ -1,6 +1,8 @@ import {Injectable, TemplateRef} from '@angular/core'; import {NbDialogConfig, NbDialogRef, NbDialogService} from '@nebular/theme'; import {ComponentType} from '@angular/cdk/overlay'; +import {DialogMessage} from '@shared/services/dialog-service/dialog-message'; +import {ConfirmDialogComponent} from '@shared/modules/confirm-dialog/confirm-dialog.component'; @Injectable({ providedIn: 'root' @@ -25,4 +27,19 @@ export class DialogService { closeOnBackdropClick: config?.closeOnBackdropClick || false, }); } + + /** + * @param message.key The translation key for the shown message + * @param message.data The data that may be used in the message translation key (Set it null if it's not required in the key) + * @param message.title The translation key for the dialog title + */ + openConfirmDialog(message: DialogMessage): NbDialogRef { + return this.dialog.open(ConfirmDialogComponent, { + closeOnEsc: false, + hasScroll: false, + autoFocus: false, + closeOnBackdropClick: false, + context: {data: message} + }); + } } diff --git a/security-c4po-angular/src/shared/services/project.service.mock.ts b/security-c4po-angular/src/shared/services/project.service.mock.ts index d2c7833..c4e198e 100644 --- a/security-c4po-angular/src/shared/services/project.service.mock.ts +++ b/security-c4po-angular/src/shared/services/project.service.mock.ts @@ -1,7 +1,7 @@ import {ProjectService} from '@shared/services/project.service'; import {HttpClient} from '@angular/common/http'; import {Observable, of} from 'rxjs'; -import {Project} from '@shared/models/project.model'; +import {Project, SaveProjectDialogBody} from '@shared/models/project.model'; export class ProjectServiceMock implements Required { @@ -12,7 +12,11 @@ export class ProjectServiceMock implements Required { return of([]); } - saveProject(): Observable { + saveProject(saveProject: SaveProjectDialogBody): Observable { + return of(); + } + + deleteProjectById(projectId: string): Observable { return of(); } } diff --git a/security-c4po-angular/src/shared/services/project.service.spec.ts b/security-c4po-angular/src/shared/services/project.service.spec.ts index 9943c95..ca8e4c6 100644 --- a/security-c4po-angular/src/shared/services/project.service.spec.ts +++ b/security-c4po-angular/src/shared/services/project.service.spec.ts @@ -33,6 +33,7 @@ describe('ProjectService', () => { }); describe('getProjects', () => { + // arrange const mockProject: Project = { id: '56c47c56-3bcd-45f1-a05b-c197dbd33111', client: 'E Corp', @@ -52,6 +53,7 @@ describe('ProjectService', () => { }]; it('should get Projects', (done) => { + // act service.getProjects().subscribe((projects) => { expect(projects[0].id).toEqual(mockProject.id); expect(projects[0].client).toEqual(mockProject.client); @@ -62,6 +64,7 @@ describe('ProjectService', () => { done(); }); + // assert const mockReq = httpMock.expectOne(`${apiBaseURL}`); expect(mockReq.cancelled).toBe(false); expect(mockReq.request.responseType).toEqual('json'); @@ -72,6 +75,7 @@ describe('ProjectService', () => { }); describe('saveProject', () => { + // arrange const mockSaveProjectDialogBody: SaveProjectDialogBody = { client: 'E Corp', title: 'Some Mock API (v1.0) Scanning', @@ -97,17 +101,40 @@ describe('ProjectService', () => { }; it('should save project', (done) => { - + // act service.saveProject(mockSaveProjectDialogBody).subscribe( value => { expect(value).toEqual(mockProject); done(); }, fail); - + // assert const req = httpMock.expectOne(`${apiBaseURL}`); expect(req.request.method).toBe('POST'); req.flush(mockProject); }); }); + + describe('deleteProject', () => { + // arrange + const mockProjectId = '56c47c56-3bcd-45f1-a05b-c197dbd33111'; + + const httpResponse = { + id: '56c47c56-3bcd-45f1-a05b-c197dbd33111' + }; + + it('should delete project', (done) => { + // act + service.deleteProjectById(mockProjectId).subscribe( + value => { + expect(value).toEqual(httpResponse.id); + done(); + }, + fail); + // assert + const req = httpMock.expectOne(`${apiBaseURL}/${mockProjectId}`); + expect(req.request.method).toBe('DELETE'); + req.flush(httpResponse.id); + }); + }); }); diff --git a/security-c4po-angular/src/shared/services/project.service.ts b/security-c4po-angular/src/shared/services/project.service.ts index 2ebc3ee..beb0678 100644 --- a/security-c4po-angular/src/shared/services/project.service.ts +++ b/security-c4po-angular/src/shared/services/project.service.ts @@ -14,6 +14,9 @@ export class ProjectService { constructor(private http: HttpClient) { } + /** + * Get Projects + */ public getProjects(): Observable { return this.http.get(`${this.apiBaseURL}`); } @@ -25,4 +28,12 @@ export class ProjectService { public saveProject(project: SaveProjectDialogBody): Observable { return this.http.post(`${this.apiBaseURL}`, project); } + + /** + * Delete Project + * @param projectId the id of the project + */ + public deleteProjectById(projectId: string): Observable { + return this.http.delete(`${this.apiBaseURL}/${projectId}`); + } }