feat: added delete project option and generic confirm dialog
This commit is contained in:
parent
c9987b48b9
commit
c54303b50a
|
@ -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({
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div fxLayout="row" fxLayoutGap="2rem">
|
||||
<div *ngFor="let project of projects | async">
|
||||
<nb-card accent="success" class="project-card">
|
||||
<nb-card class="project-card" accent="success">
|
||||
<nb-card-header fxLayoutAlign="start center"
|
||||
routerLink="id"
|
||||
fragment="{{project.id}}"
|
||||
|
@ -52,7 +52,7 @@
|
|||
status="danger"
|
||||
size="small"
|
||||
class="project-button"
|
||||
(click)="onClickDeleteProject()">
|
||||
(click)="onClickDeleteProject(project)">
|
||||
<fa-icon [icon]="fa.faTrash"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<boolean> {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<nb-card>
|
||||
<nb-card-header fxLayoutAlign="start center" class="dialog-header confirm">
|
||||
{{ data?.title | translate }}
|
||||
</nb-card-header>
|
||||
<nb-card-body class="dialog-body">
|
||||
{{ data?.key | translate: data?.data }}
|
||||
</nb-card-body>
|
||||
<nb-card-footer fxLayout="row" fxLayoutGap="1.5rem" fxLayoutAlign="end end">
|
||||
<button nbButton size="small"
|
||||
class="dialog-button"
|
||||
status="danger"
|
||||
(click)="onClickConfirm()">
|
||||
{{ 'global.action.yes' | translate }}
|
||||
</button>
|
||||
<button nbButton size="small"
|
||||
class="dialog-button"
|
||||
(click)="onClickClose()">
|
||||
{{ 'global.action.no' | translate }}
|
||||
</button>
|
||||
</nb-card-footer>
|
||||
</nb-card>
|
|
@ -0,0 +1 @@
|
|||
@import "../../../assets/@theme/styles/_dialog.scss";
|
|
@ -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<ConfirmDialogComponent>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
|
@ -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<any>) {
|
||||
}
|
||||
|
||||
onClickConfirm(): void {
|
||||
this.dialogRef.close({confirm: true});
|
||||
}
|
||||
|
||||
onClickClose(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
|
@ -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 { }
|
|
@ -1,5 +1,5 @@
|
|||
<nb-card #dialog accent="primary" class="project-dialog">
|
||||
<nb-card-header fxLayoutAlign="start center" class="project-dialog-header">
|
||||
<nb-card-header fxLayoutAlign="start center" class="dialog-header">
|
||||
{{ 'project.create.header' | translate }}
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
|
@ -39,10 +39,10 @@
|
|||
</form>
|
||||
</nb-card-body>
|
||||
<nb-card-footer fxLayout="row" fxLayoutGap="1.5rem" fxLayoutAlign="end end">
|
||||
<button nbButton status="success" [disabled]="formIsEmptyOrInvalid()" (click)="onClickSave(projectFormGroup.value)">
|
||||
<button nbButton status="success" size="small" class="dialog-button" [disabled]="formIsEmptyOrInvalid()" (click)="onClickSave(projectFormGroup.value)">
|
||||
{{ 'global.action.save' | translate}}
|
||||
</button>
|
||||
<button nbButton status="danger" (click)="onClickClose()">
|
||||
<button nbButton status="danger" size="small" class="dialog-button"(click)="onClickClose()">
|
||||
{{ 'global.action.cancel' | translate }}
|
||||
</button>
|
||||
</nb-card-footer>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import "../../../assets/@theme/styles/_dialog.scss";
|
||||
|
||||
.project-dialog {
|
||||
width: 24rem;
|
||||
height: 31rem;
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export interface DialogMessage {
|
||||
key: string;
|
||||
data?: any;
|
||||
title?: string;
|
||||
inputPlaceholderKey?: string;
|
||||
}
|
|
@ -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<DialogService> {
|
||||
|
||||
|
@ -13,4 +14,8 @@ export class DialogServiceMock implements Required<DialogService> {
|
|||
): NbDialogRef<T> {
|
||||
return null;
|
||||
}
|
||||
|
||||
openConfirmDialog(message: DialogMessage): NbDialogRef<any> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ConfirmDialogComponent> {
|
||||
return this.dialog.open(ConfirmDialogComponent, {
|
||||
closeOnEsc: false,
|
||||
hasScroll: false,
|
||||
autoFocus: false,
|
||||
closeOnBackdropClick: false,
|
||||
context: {data: message}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ProjectService> {
|
||||
|
@ -12,7 +12,11 @@ export class ProjectServiceMock implements Required<ProjectService> {
|
|||
return of([]);
|
||||
}
|
||||
|
||||
saveProject(): Observable<Project> {
|
||||
saveProject(saveProject: SaveProjectDialogBody): Observable<Project> {
|
||||
return of();
|
||||
}
|
||||
|
||||
deleteProjectById(projectId: string): Observable<string> {
|
||||
return of();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,6 +14,9 @@ export class ProjectService {
|
|||
constructor(private http: HttpClient) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Projects
|
||||
*/
|
||||
public getProjects(): Observable<Project[]> {
|
||||
return this.http.get<Project[]>(`${this.apiBaseURL}`);
|
||||
}
|
||||
|
@ -25,4 +28,12 @@ export class ProjectService {
|
|||
public saveProject(project: SaveProjectDialogBody): Observable<Project> {
|
||||
return this.http.post<Project>(`${this.apiBaseURL}`, project);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Project
|
||||
* @param projectId the id of the project
|
||||
*/
|
||||
public deleteProjectById(projectId: string): Observable<string> {
|
||||
return this.http.delete<string>(`${this.apiBaseURL}/${projectId}`);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue