feat: added update project option and refactored project-dialog

This commit is contained in:
Marcel Haag 2022-02-23 17:13:39 +01:00
parent 203b376ef1
commit b8a57aa103
25 changed files with 475 additions and 137 deletions

View File

@ -45,7 +45,7 @@
status="primary"
size="small"
class="project-button"
(click)="onClickEditProject()">
(click)="onClickEditProject(project)">
<fa-icon [icon]="fa.faPencilAlt"></fa-icon>
</button>
<button nbButton
@ -61,7 +61,7 @@
</div>
</div>
<div *ngIf="projects.getValue().length === 0 && !isLoading()" fxLayout="row" fxLayoutAlign="center center">
<div *ngIf="projects.getValue().length === 0 || !isLoading()" fxLayout="row" fxLayoutAlign="center center">
<p class="error-text">
{{'project.overview.no.projects' | translate}}
</p>

View File

@ -36,9 +36,13 @@
}
.project-card:hover {
background-color: nb-theme(color-info-transparent-default);
background-color: nb-theme(color-basic-transparent-focus);
// Increases element size on hover
// Decreases usability which is why it is commented out
/*
margin-top: +0.625rem;
transform: scale(1.025)
transform: scale(1.025);
*/
}
.project-link:hover {

View File

@ -24,6 +24,8 @@ import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-s
import {KeycloakService} from 'keycloak-angular';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
import {ProjectDialogServiceMock} from '@shared/modules/project-dialog/service/project-dialog.service.mock';
describe('ProjectOverviewComponent', () => {
let component: ProjectOverviewComponent;
@ -63,6 +65,7 @@ describe('ProjectOverviewComponent', () => {
providers: [
KeycloakService,
{provide: ProjectService, useValue: new ProjectServiceMock()},
{provide: ProjectDialogService, useClass: ProjectDialogServiceMock},
{provide: DialogService, useClass: DialogServiceMock},
{provide: NotificationService, useValue: new NotificationServiceMock()}
]

View File

@ -1,6 +1,6 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {Project, SaveProjectDialogBody} from '@shared/models/project.model';
import {Project, ProjectDialogBody} from '@shared/models/project.model';
import {BehaviorSubject, Observable} from 'rxjs';
import {untilDestroyed} from 'ngx-take-until-destroy';
import {ProjectService} from '@shared/services/project.service';
@ -8,6 +8,7 @@ import {NotificationService, PopupType} from '@shared/services/notification.serv
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 {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
@Component({
selector: 'app-project-overview',
@ -24,6 +25,7 @@ export class ProjectOverviewComponent implements OnInit, OnDestroy {
constructor(
private readonly projectService: ProjectService,
private readonly dialogService: DialogService,
private readonly projectDialogService: ProjectDialogService,
private readonly notificationService: NotificationService) {
}
@ -51,17 +53,18 @@ export class ProjectOverviewComponent implements OnInit, OnDestroy {
}
onClickAddProject(): void {
this.dialogService.openCustomDialog(
this.projectDialogService.openProjectDialog(
ProjectDialogComponent,
null,
{
closeOnEsc: false,
hasScroll: false,
autoFocus: false,
closeOnBackdropClick: false
}
).onClose.pipe(
).pipe(
filter(value => !!value),
mergeMap((value: SaveProjectDialogBody) => this.projectService.saveProject(value)),
mergeMap((value: ProjectDialogBody) => this.projectService.saveProject(value)),
untilDestroyed(this)
).subscribe({
next: () => {
@ -75,8 +78,30 @@ export class ProjectOverviewComponent implements OnInit, OnDestroy {
});
}
onClickEditProject(): void {
console.log('to be implemented...');
onClickEditProject(project: Project): void {
this.projectDialogService.openProjectDialog(
ProjectDialogComponent,
project,
{
closeOnEsc: false,
hasScroll: false,
autoFocus: false,
closeOnBackdropClick: false
}
).pipe(
filter(value => !!value),
mergeMap((value: ProjectDialogBody) => this.projectService.updateProject(project.id, value)),
untilDestroyed(this)
).subscribe({
next: () => {
this.loadProjects();
this.notificationService.showPopup('project.popup.update.success', PopupType.SUCCESS);
},
error: error => {
console.error(error);
this.notificationService.showPopup('project.popup.update.failed', PopupType.FAILURE);
}
});
}
onClickDeleteProject(project: Project): void {

View File

@ -3,6 +3,7 @@
"action.login": "Einloggen",
"action.retry": "Erneut Versuchen",
"action.save": "Speichern",
"action.update": "Aktualisieren",
"action.confirm": "Bestätigen",
"action.cancel": "Abbrechen",
"action.yes": "Ja",
@ -14,7 +15,7 @@
"success": "✔",
"failure": "✘",
"warning": "!",
"info": "",
"info": "",
"error.position": {
"permissionDenied": "Berechtigung verweigert",
"timeout": "Zeitüberschreitung"
@ -29,6 +30,13 @@
"unauthorized": "Benutzer nicht gefunden. Bitte registrieren und erneut versuchen"
},
"project": {
"title.label": "Projekt Titel",
"client.label": "Name des Auftraggebers",
"tester.label": "Name des Pentester",
"title": "Titel",
"client": "Klient",
"tester": "Tester",
"createdAt": "Erstellt am",
"overview": {
"add.project": "Projekt hinzufügen",
"no.projects": "Keine Projekte verfügbar"
@ -36,23 +44,26 @@
"create": {
"header": "Neues Projekt erstellen"
},
"edit": {
"header": "Projekt editieren"
},
"delete": {
"title": "Projekt löschen",
"key": "Möchten Sie das Projekt \"{{name}}\" unwiderruflich löschen?"
},
"validationMessage": {
"titleRequired": "Titel ist erforderlich.",
"clientRequired": "Klient ist erforderlich.",
"testerRequired": "Tester ist erforderlich."
},
"popup": {
"not.found": "Keine Projekte gefunden",
"save.success": "Projekt erfolgreich gespeichert",
"save.failed": "Projekt konnte nicht gespeichert werden",
"update.success": "Projekt erfolgreich aktualisiert",
"update.failed": "Projekt konnte nicht aktualisiert werden",
"delete.success": "Projekt erfolgreich gelöscht",
"delete.failed": "Projekt konnte nicht gelöscht werden"
},
"title.label": "Projekt Titel",
"client.label": "Name des Auftraggebers",
"tester.label": "Name des Pentester",
"title": "Titel",
"client": "Klient",
"tester": "Tester",
"createdAt": "Erstellt am"
}
}
}

View File

@ -4,6 +4,7 @@
"action.retry": "Try again",
"action.confirm": "Confirm",
"action.save": "Save",
"action.update": "Update",
"action.cancel": "Cancel",
"action.yes": "Yes",
"action.no": "No",
@ -14,7 +15,7 @@
"success": "✔",
"failure": "✘",
"warning": "!",
"info": "",
"info": "",
"error.position": {
"permissionDenied": "Permission denied",
"timeout": "Timeout"
@ -29,30 +30,40 @@
"unauthorized": "User not found. Please register and try again"
},
"project": {
"overview": {
"add.project": "Add project",
"no.projects": "No projects available"
},
"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",
"delete.success": "Project deleted successfully",
"delete.failed": "Project could not be deleted"
},
"title.label": "Project Title",
"client.label": "Name of Client",
"tester.label": "Name of Pentester",
"title": "Title",
"client": "Client",
"tester": "Tester",
"createdAt": "Created at"
"createdAt": "Created at",
"overview": {
"add.project": "Add Project",
"no.projects": "No projects available"
},
"create": {
"header": "Create New Project"
},
"edit": {
"header": "Edit Project"
},
"delete": {
"title": "Delete Project",
"key": "Do you want to permanently delete the project \"{{name}}\"?"
},
"validationMessage": {
"titleRequired": "Title is required.",
"clientRequired": "Client is required.",
"testerRequired": "Tester is required."
},
"popup": {
"not.found": "No projects found",
"save.success": "Project saved successfully",
"save.failed": "Project could not be saved",
"update.success": "Project updated successfully",
"update.failed": "Project could not be updated",
"delete.success": "Project deleted successfully",
"delete.failed": "Project could not be deleted"
}
}
}

View File

@ -2,10 +2,10 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>SecurityC4POAngular</title>
<title>Security C4PO</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="assets/images/favicons/favicon.ico">
<link rel="icon" type="image/x-icon" href="assets/images/favicons/corporate_favicon.ico">
</head>
<body id="loader-wrapper">
<app-root id="loader"></app-root>

View File

@ -1,4 +1,4 @@
export const GlobalTitlesVariables = {
SECURITYC4PO_TITLE: 'SecurityC4PO',
SECURITYC4PO_TITLE: 'Security C4PO',
NOVATEC_NAME: 'Novatec'
};

View File

@ -0,0 +1,26 @@
export interface ProjectDialogData {
form: {
[key: string]: GenericFormFieldConfig // key is property name, e.g. title
};
options?: GenericFormFieldOption[];
}
export interface GenericFormFieldConfig {
fieldName: string;
type: string; // text, password, email
labelKey: string; // translation key of field label
placeholder: string; // translation key of placeholder text
controlsConfig: { [key: string]: any };
errors: GenericFormFieldError[];
}
export interface GenericFormFieldError {
errorCode: string;
translationKey: string;
}
export interface GenericFormFieldOption {
headerLabelKey: string;
buttonKey: string;
accentColor: string;
}

View File

@ -21,7 +21,7 @@ export class Project {
}
}
export interface SaveProjectDialogBody {
export interface ProjectDialogBody {
title: string;
client: string;
tester: string;

View File

@ -1,48 +1,43 @@
<nb-card #dialog accent="primary" class="project-dialog">
<nb-card #dialog accent="{{dialogData?.options[0].accentColor}}" class="project-dialog">
<nb-card-header fxLayoutAlign="start center" class="dialog-header">
{{ 'project.create.header' | translate }}
{{ dialogData?.options[0].headerLabelKey | translate }}
</nb-card-header>
<nb-card-body>
<form [formGroup]="projectFormGroup" fxLayout="column" fxLayoutGap="1rem" fxLayoutAlign="start start">
<nb-form-field class="project-form-field">
<label for="projectTitleInput" class="label">
{{'project.title.label' | translate}}:
<form *ngIf="formArray" [formGroup]="projectFormGroup" fxLayout="column" fxLayoutGap="1rem"
fxLayoutAlign="start start">
<ng-template ngFor let-fieldConfig [ngForOf]="formArray">
<!-- TYPE select -->
<ng-container [ngSwitch]="fieldConfig.type">
<!-- Default styles -->
<nb-form-field *ngSwitchCase="'text'" class="project-form-field">
<label for="{{fieldConfig.fieldName}}" class="label">
{{fieldConfig.labelKey | translate}}
</label>
<input formControlName="projectTitle" required
id="projectTitleInput" nbInput
class="input" type="text" fullWidth
status="{{formCtrlStatus}}"
placeholder="{{'project.title' | translate}} *">
</nb-form-field>
<nb-form-field class="project-form-field">
<label for="projectClientInput" class="label">
{{'project.client.label' | translate}}:
</label>
<input formControlName="projectClient" required
id="projectClientInput" nbInput
class="input" type="text" fullWidth
status="{{formCtrlStatus}}"
placeholder="{{'project.client' | translate}} *">
</nb-form-field>
<nb-form-field class="project-form-field">
<label for="projectTesterInput" class="label">
{{'project.tester.label' | translate}}:
</label>
<input formControlName="projectTester" required
id="projectTesterInput" nbInput
class="input" type="text" fullWidth
status="{{formCtrlStatus}}"
placeholder="{{'project.tester' | translate}} *">
<input formControlName="{{fieldConfig.fieldName}}"
type="text" required fullWidth
id="{{fieldConfig.fieldName}}" nbInput
class="input"
[status]="projectFormGroup.get(fieldConfig.fieldName).dirty ? (projectFormGroup.get(fieldConfig.fieldName).invalid ? 'danger' : 'basic') : 'basic'"
placeholder="{{fieldConfig.placeholder | translate}} *">
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
<ng-template ngFor let-error [ngForOf]="fieldConfig.errors"
*ngIf="projectFormGroup.get(fieldConfig.fieldName).dirty">
<span class="error-text"
*ngIf="projectFormGroup.get(fieldConfig.fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
{{error.translationKey | translate}}
</span>
</ng-template>
</nb-form-field>
</ng-container>
</ng-template>
</form>
</nb-card-body>
<nb-card-footer fxLayout="row" fxLayoutGap="1.5rem" fxLayoutAlign="end end">
<button nbButton status="success" size="small" class="dialog-button" [disabled]="formIsEmptyOrInvalid()" (click)="onClickSave(projectFormGroup.value)">
{{ 'global.action.save' | translate}}
<button nbButton status="success" size="small" class="dialog-button" [disabled]="!allowSave()"
(click)="onClickSave(projectFormGroup.value)">
{{ dialogData?.options[0].buttonKey | translate}}
</button>
<button nbButton status="danger" size="small" class="dialog-button"(click)="onClickClose()">
<button nbButton status="danger" size="small" class="dialog-button" (click)="onClickClose()">
{{ 'global.action.cancel' | translate }}
</button>
</nb-card-footer>

View File

@ -1,8 +1,9 @@
@import "../../../assets/@theme/styles/_dialog.scss";
@import '../../../assets/@theme/styles/themes';
.project-dialog {
width: 24rem;
height: 31rem;
width: 25.25rem;
height: 35rem;
.project-dialog-header {
height: 8vh;
@ -20,6 +21,12 @@
}
.input {
width: 15rem;
width: 18rem;
margin-bottom: 0.5rem;
}
.error-text {
float: left;
color: nb-theme(color-danger-default);;
}
}

View File

@ -1,8 +1,15 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ProjectDialogComponent} from './project-dialog.component';
import {CommonModule} from '@angular/common';
import {NbButtonModule, NbCardModule, NbDialogRef, NbFormFieldModule, NbInputModule, NbLayoutModule} from '@nebular/theme';
import {
NB_DIALOG_CONFIG,
NbButtonModule,
NbCardModule,
NbDialogRef,
NbFormFieldModule,
NbInputModule,
NbLayoutModule
} from '@nebular/theme';
import {FlexLayoutModule} from '@angular/flex-layout';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {ThemeModule} from '@assets/@theme/theme.module';
@ -14,13 +21,18 @@ import {NotificationService} from '@shared/services/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {ReactiveFormsModule} from '@angular/forms';
import {ReactiveFormsModule, Validators} from '@angular/forms';
import {Project} from '@shared/models/project.model';
import Mock = jest.Mock;
import deepEqual from 'deep-equal';
describe('ProjectDialogComponent', () => {
let component: ProjectDialogComponent;
let fixture: ComponentFixture<ProjectDialogComponent>;
beforeEach(async () => {
const dialogSpy = createSpyObj('NbDialogRef', ['close']);
await TestBed.configureTestingModule({
declarations: [
ProjectDialogComponent
@ -49,12 +61,14 @@ describe('ProjectDialogComponent', () => {
providers: [
{provide: NotificationService, useValue: new NotificationServiceMock()},
{provide: DialogService, useClass: DialogServiceMock},
{provide: NbDialogRef, useValue: {}}
{provide: NbDialogRef, useValue: dialogSpy},
{provide: NB_DIALOG_CONFIG, useValue: mockedDialogData}
]
}).compileComponents();
});
beforeEach(() => {
TestBed.overrideProvider(NB_DIALOG_CONFIG, {useValue: mockedDialogData});
fixture = TestBed.createComponent(ProjectDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
@ -64,3 +78,71 @@ describe('ProjectDialogComponent', () => {
expect(component).toBeTruthy();
});
});
export const createSpyObj = (baseName, methodNames): { [key: string]: Mock<any> } => {
const obj: any = {};
for (const i of methodNames) {
obj[i] = jest.fn();
}
return obj;
};
export const mockProject: Project = {
id: '11-22-33',
title: 'Test Project',
client: 'Testclient',
tester: 'Testpentester',
createdAt: new Date(),
createdBy: 'UID-11-12-13'
};
export const mockedDialogData = {
form: {
projectTitle: {
fieldName: 'projectTitle',
type: 'text',
labelKey: 'project.title.label',
placeholder: 'project.title',
controlsConfig: [
{value: mockProject ? mockProject.title : '', disabled: false},
[Validators.required]
],
errors: [
{errorCode: 'required', translationKey: 'project.validationMessage.titleRequired'}
]
},
projectClient: {
fieldName: 'projectClient',
type: 'text',
labelKey: 'project.client.label',
placeholder: 'project.client',
controlsConfig: [
{value: mockProject ? mockProject.client : '', disabled: false},
[Validators.required]
],
errors: [
{errorCode: 'required', translationKey: 'project.validationMessage.clientRequired'}
]
},
projectTester: {
fieldName: 'projectTester',
type: 'text',
labelKey: 'project.tester.label',
placeholder: 'project.tester',
controlsConfig: [
{value: mockProject ? mockProject.tester : '', disabled: false},
[Validators.required]
],
errors: [
{errorCode: 'required', translationKey: 'project.validationMessage.testerRequired'}
]
}
},
options: [
{
headerLabelKey: 'project.edit.header',
buttonKey: 'global.action.update',
accentColor: 'warning'
},
]
};

View File

@ -1,8 +1,8 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {NbDialogRef} from '@nebular/theme';
import {AbstractControl, FormBuilder, FormGroup, Validators} from '@angular/forms';
import {FieldStatus} from '@shared/models/form-field-status.model';
import {untilDestroyed} from 'ngx-take-until-destroy';
import {Component, Inject, OnDestroy, OnInit} from '@angular/core';
import {NB_DIALOG_CONFIG, NbDialogRef} from '@nebular/theme';
import {FormBuilder, FormGroup} from '@angular/forms';
import {GenericFormFieldConfig, ProjectDialogData} from '@shared/models/project-dialog-data';
import deepEqual from 'deep-equal';
@Component({
selector: 'app-project-dialog',
@ -12,40 +12,29 @@ import {untilDestroyed} from 'ngx-take-until-destroy';
export class ProjectDialogComponent implements OnInit, OnDestroy {
// form control elements
projectFormGroup: FormGroup;
projectTitleCtrl: AbstractControl;
projectClientCtrl: AbstractControl;
projectTesterCtrl: AbstractControl;
formArray: GenericFormFieldConfig[];
formCtrlStatus = FieldStatus.BASIC;
invalidProjectTitle: string;
invalidProjectClient: string;
invalidProjectTester: string;
readonly MIN_LENGTH: number = 2;
dialogData: ProjectDialogData;
constructor(
@Inject(NB_DIALOG_CONFIG) private data: ProjectDialogData,
private fb: FormBuilder,
protected dialogRef: NbDialogRef<ProjectDialogComponent>
) {
}
ngOnInit(): void {
this.projectFormGroup = this.fb.group({
projectTitle: ['', [Validators.required, Validators.minLength(this.MIN_LENGTH)]],
projectClient: ['', [Validators.required, Validators.minLength(this.MIN_LENGTH)]],
projectTester: ['', [Validators.required, Validators.minLength(this.MIN_LENGTH)]]
});
this.projectFormGroup = this.generateFormCreationFieldArray();
this.dialogData = this.data;
}
this.projectTitleCtrl = this.projectFormGroup.get('projectTitle');
this.projectClientCtrl = this.projectFormGroup.get('projectClient');
this.projectTesterCtrl = this.projectFormGroup.get('projectTester');
this.projectFormGroup.valueChanges
.pipe(untilDestroyed(this))
.subscribe(() => {
this.formCtrlStatus = FieldStatus.BASIC;
});
generateFormCreationFieldArray(): FormGroup {
this.formArray = Object.values(this.data.form);
const config = this.formArray?.reduce((accumulator: {}, currentValue: GenericFormFieldConfig) => ({
...accumulator,
[currentValue?.fieldName]: currentValue?.controlsConfig
}), {});
return this.fb.group(config);
}
onClickSave(value): void {
@ -60,23 +49,41 @@ export class ProjectDialogComponent implements OnInit, OnDestroy {
this.dialogRef.close();
}
formIsEmptyOrInvalid(): boolean {
return this.isEmpty(this.projectTitleCtrl.value)
|| this.isEmpty(this.projectClientCtrl.value)
|| this.isEmpty(this.projectTesterCtrl.value)
|| this.projectTitleCtrl.invalid
|| this.projectClientCtrl.invalid
|| this.projectTesterCtrl.invalid;
allowSave(): boolean {
return this.projectFormGroup.valid && this.projectDataChanged();
}
/**
* @param ctrlValue of type string
* @return if ctrlValue is empty or not
* @return true if project data is different from initial value
*/
isEmpty(ctrlValue: string): boolean {
return ctrlValue === '';
private projectDataChanged(): boolean {
const oldProjectData = this.parseInitializedProjectDialogData(this.dialogData);
const newProjectData = this.projectFormGroup.getRawValue();
Object.entries(newProjectData).forEach(entry => {
const [key, value] = entry;
if (value === null) {
newProjectData[key] = '';
}
});
const didChange = !deepEqual(oldProjectData, newProjectData);
return didChange;
}
/**
* @param dialogData of type ProjectDialogData
* @return parsed projectData
*/
private parseInitializedProjectDialogData(dialogData: ProjectDialogData): any {
const projectData = {};
Object.entries(dialogData.form).forEach(entry => {
const [key, value] = entry;
projectData[key] = value.controlsConfig[0] ?
(value.controlsConfig[0].value ? value.controlsConfig[0].value : value.controlsConfig[0]) : '';
});
return projectData;
}
ngOnDestroy(): void {
// ToDo: Remove this after Angular upgrade and use @UnitDestroy() instead
}
}

View File

@ -7,6 +7,7 @@ import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {TranslateModule} from '@ngx-translate/core';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {ReactiveFormsModule} from '@angular/forms';
import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
@NgModule({
declarations: [
@ -25,6 +26,7 @@ import {ReactiveFormsModule} from '@angular/forms';
],
providers: [
DialogService,
ProjectDialogService,
NbDialogService
],
entryComponents: [

View File

@ -0,0 +1,17 @@
import {ComponentType} from '@angular/cdk/overlay';
import {NbDialogConfig} from '@nebular/theme';
import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
import {Project} from '@shared/models/project.model';
import {Observable, of} from 'rxjs';
export class ProjectDialogServiceMock implements Required<ProjectDialogService> {
dialog: any;
openProjectDialog(
componentOrTemplateRef: ComponentType<any>,
project: Project | undefined,
config: Partial<NbDialogConfig<Partial<any> | string>> | undefined): Observable<any> {
return of(undefined);
}
}

View File

@ -0,0 +1,30 @@
import { TestBed } from '@angular/core/testing';
import { ProjectDialogService } from './project-dialog.service';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {NbDialogModule, NbDialogRef} from '@nebular/theme';
import {ProjectDialogServiceMock} from '@shared/modules/project-dialog/service/project-dialog.service.mock';
describe('ProjectDialogService', () => {
let service: ProjectDialogService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
BrowserAnimationsModule,
NbDialogModule.forRoot()
],
providers: [
{provide: ProjectDialogService, useClass: ProjectDialogServiceMock},
{provide: NbDialogRef, useValue: {}},
]
});
service = TestBed.inject(ProjectDialogService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,102 @@
import {Injectable} from '@angular/core';
import {NbDialogConfig, NbDialogService} from '@nebular/theme';
import {ComponentType} from '@angular/cdk/overlay';
import {Observable} from 'rxjs';
import {Project} from '@shared/models/project.model';
import {ProjectDialogComponent} from '@shared/modules/project-dialog/project-dialog.component';
import {Validators} from '@angular/forms';
import {ProjectDialogData} from '@shared/models/project-dialog-data';
@Injectable()
export class ProjectDialogService {
constructor(private readonly dialog: NbDialogService) {
}
private readonly MIN_LENGTH: number = 4;
static addDataToDialogConfig(
dialogOptions?: Partial<NbDialogConfig<Partial<any> | string>>,
projectData?: ProjectDialogData
): Partial<NbDialogConfig<Partial<any> | string>> {
return {
context: {data: projectData},
closeOnEsc: dialogOptions?.closeOnEsc || false,
hasScroll: dialogOptions?.hasScroll || false,
autoFocus: dialogOptions?.autoFocus || false,
closeOnBackdropClick: dialogOptions?.closeOnBackdropClick || false
};
}
public openProjectDialog(componentOrTemplateRef: ComponentType<any>,
project?: Project,
config?: Partial<NbDialogConfig<Partial<any> | string>>): Observable<any> {
let dialogOptions: Partial<NbDialogConfig<Partial<any> | string>>;
let dialogData: ProjectDialogData;
// Setup ProjectDialogData
dialogData = {
form: {
projectTitle: {
fieldName: 'projectTitle',
type: 'text',
labelKey: 'project.title.label',
placeholder: 'project.title',
controlsConfig: [
{value: project ? project.title : '', disabled: false},
[Validators.required]
],
errors: [
{errorCode: 'required', translationKey: 'project.validationMessage.titleRequired'}
]
},
projectClient: {
fieldName: 'projectClient',
type: 'text',
labelKey: 'project.client.label',
placeholder: 'project.client',
controlsConfig: [
{value: project ? project.client : '', disabled: false},
[Validators.required]
],
errors: [
{errorCode: 'required', translationKey: 'project.validationMessage.clientRequired'}
]
},
projectTester: {
fieldName: 'projectTester',
type: 'text',
labelKey: 'project.tester.label',
placeholder: 'project.tester',
controlsConfig: [
{value: project ? project.tester : '', disabled: false},
[Validators.required]
],
errors: [
{errorCode: 'required', translationKey: 'project.validationMessage.testerRequired'}
]
}
},
options: []
};
if (project) {
dialogData.options = [
{
headerLabelKey: 'project.edit.header',
buttonKey: 'global.action.update',
accentColor: 'warning'
},
];
} else {
dialogData.options = [
{
headerLabelKey: 'project.create.header',
buttonKey: 'global.action.save',
accentColor: 'primary'
},
];
}
// Merge dialog config with project data
dialogOptions = ProjectDialogService.addDataToDialogConfig(config, dialogData);
return this.dialog.open(ProjectDialogComponent, dialogOptions).onClose;
}
}

View File

@ -24,7 +24,7 @@ export class DialogService {
closeOnEsc: config?.closeOnEsc || false,
hasScroll: config?.hasScroll || false,
autoFocus: config?.autoFocus || false,
closeOnBackdropClick: config?.closeOnBackdropClick || false,
closeOnBackdropClick: config?.closeOnBackdropClick || false
});
}
@ -35,7 +35,7 @@ export class DialogService {
*/
openConfirmDialog(message: DialogMessage): NbDialogRef<ConfirmDialogComponent> {
return this.dialog.open(ConfirmDialogComponent, {
closeOnEsc: false,
closeOnEsc: true,
hasScroll: false,
autoFocus: false,
closeOnBackdropClick: false,

View File

@ -1,7 +1,7 @@
import {ProjectService} from '@shared/services/project.service';
import {HttpClient} from '@angular/common/http';
import {Observable, of} from 'rxjs';
import {Project, SaveProjectDialogBody} from '@shared/models/project.model';
import {Project, ProjectDialogBody} from '@shared/models/project.model';
export class ProjectServiceMock implements Required<ProjectService> {
@ -12,7 +12,11 @@ export class ProjectServiceMock implements Required<ProjectService> {
return of([]);
}
saveProject(saveProject: SaveProjectDialogBody): Observable<Project> {
saveProject(saveProject: ProjectDialogBody): Observable<Project> {
return of();
}
updateProject(projectId: string, project: ProjectDialogBody): Observable<Project> {
return of();
}

View File

@ -4,7 +4,7 @@ import {ProjectService} from './project.service';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {KeycloakService} from 'keycloak-angular';
import {Project, SaveProjectDialogBody} from '@shared/models/project.model';
import {Project, ProjectDialogBody} from '@shared/models/project.model';
import {environment} from '../../environments/environment';
describe('ProjectService', () => {
@ -76,7 +76,7 @@ describe('ProjectService', () => {
describe('saveProject', () => {
// arrange
const mockSaveProjectDialogBody: SaveProjectDialogBody = {
const mockSaveProjectDialogBody: ProjectDialogBody = {
client: 'E Corp',
title: 'Some Mock API (v1.0) Scanning',
tester: 'Novatester',

View File

@ -1,7 +1,7 @@
import {Injectable} from '@angular/core';
import {environment} from '../../environments/environment';
import {HttpClient} from '@angular/common/http';
import {Project, SaveProjectDialogBody} from '../models/project.model';
import {Project, ProjectDialogBody} from '../models/project.model';
import {Observable} from 'rxjs';
@Injectable({
@ -25,10 +25,20 @@ export class ProjectService {
* Save Project
* @param project the information of the project
*/
public saveProject(project: SaveProjectDialogBody): Observable<Project> {
public saveProject(project: ProjectDialogBody): Observable<Project> {
return this.http.post<Project>(`${this.apiBaseURL}`, project);
}
/**
* Update Project
* @param projectId the id of the project
* @param project the information of the project
*/
public updateProject(projectId: string, project: ProjectDialogBody): Observable<Project> {
console.log('update Project');
return this.http.patch<Project>(`${this.apiBaseURL}/${projectId}`, project);
}
/**
* Delete Project
* @param projectId the id of the project

View File

@ -7,6 +7,7 @@
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",

View File

@ -6,6 +6,7 @@
"jest"
],
"module": "commonjs",
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"allowJs": true,
},

View File

@ -12,7 +12,7 @@ services:
- ../cfg/keycloak.env
c4po-keycloak-postgress:
container_name: c4po-keycloak-postgres
image: postgres:latest
image: postgres:10.16-alpine
env_file:
- ../cfg/keycloakdb.env
ports: