diff --git a/src/app/app.module.ts b/src/app/app.module.ts index c4911cc82..ecc93f8b4 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -22,6 +22,12 @@ import { AboutUsComponent } from './component/about-us/about-us.component'; import { DependencyGraphComponent } from './component/dependency-graph/dependency-graph.component'; import { TeamsComponent } from './component/teams/teams.component'; import { ToStringValuePipe } from './pipe/to-string-value.pipe'; +import { ModalMessageComponent } from './component/modal-message/modal-message.component'; +import { + MatDialogModule, + MAT_DIALOG_DATA, + MatDialogRef, +} from '@angular/material/dialog'; @NgModule({ declarations: [ @@ -40,16 +46,23 @@ import { ToStringValuePipe } from './pipe/to-string-value.pipe'; TeamsComponent, ToStringValuePipe, UserdayComponent, + ModalMessageComponent, ], imports: [ BrowserModule, AppRoutingModule, BrowserAnimationsModule, MaterialModule, + MatDialogModule, ReactiveFormsModule, HttpClientModule, ], - providers: [ymlService], + providers: [ + ymlService, + ModalMessageComponent, + { provide: MAT_DIALOG_DATA, useValue: {} }, + { provide: MatDialogRef, useValue: { close: (dialogResult: any) => {} } }, + ], bootstrap: [AppComponent], }) export class AppModule {} diff --git a/src/app/component/circular-heatmap/circular-heatmap.component.html b/src/app/component/circular-heatmap/circular-heatmap.component.html index 6e6347d93..43ba21b35 100644 --- a/src/app/component/circular-heatmap/circular-heatmap.component.html +++ b/src/app/component/circular-heatmap/circular-heatmap.component.html @@ -284,8 +284,8 @@

Nothing to show

class="normal-button" mat-raised-button class="resetButtonClass" - (click)="ResetIsImplemented()"> - Reset Implemented + (click)="deleteLocalTeamsProgress()"> + Delete team progress diff --git a/src/app/component/circular-heatmap/circular-heatmap.component.spec.ts b/src/app/component/circular-heatmap/circular-heatmap.component.spec.ts index a1d1515d0..cb10751cb 100644 --- a/src/app/component/circular-heatmap/circular-heatmap.component.spec.ts +++ b/src/app/component/circular-heatmap/circular-heatmap.component.spec.ts @@ -4,6 +4,7 @@ import { ymlService } from 'src/app/service/yaml-parser/yaml-parser.service'; import { CircularHeatmapComponent } from './circular-heatmap.component'; import { RouterTestingModule } from '@angular/router/testing'; import { MatChip } from '@angular/material/chips'; +import { ModalMessageComponent } from '../modal-message/modal-message.component'; describe('CircularHeatmapComponent', () => { let component: CircularHeatmapComponent; @@ -11,19 +12,17 @@ describe('CircularHeatmapComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - providers: [ymlService, HttpClient, HttpHandler], + declarations: [CircularHeatmapComponent, MatChip], imports: [RouterTestingModule], - declarations: [CircularHeatmapComponent], + providers: [ + ymlService, + HttpClient, + HttpHandler, + { provide: ModalMessageComponent, useValue: {} }, + ], }).compileComponents(); - }); - beforeEach(async () => { - TestBed.configureTestingModule({ - declarations: [MatChip], - }).compileComponents(); - }); - beforeEach(() => { - fixture = TestBed.createComponent(CircularHeatmapComponent); + fixture = TestBed.createComponent(CircularHeatmapComponent); // Create fixture and component here component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/component/circular-heatmap/circular-heatmap.component.ts b/src/app/component/circular-heatmap/circular-heatmap.component.ts index 9a747b519..fa5e33da0 100644 --- a/src/app/component/circular-heatmap/circular-heatmap.component.ts +++ b/src/app/component/circular-heatmap/circular-heatmap.component.ts @@ -11,6 +11,10 @@ import * as yaml from 'js-yaml'; import { Router } from '@angular/router'; import { MatChip } from '@angular/material/chips'; import * as md from 'markdown-it'; +import { + ModalMessageComponent, + DialogInfo, +} from '../modal-message/modal-message.component'; export interface activitySchema { uuid: string; @@ -62,7 +66,7 @@ export class CircularHeatmapComponent implements OnInit { constructor( private yaml: ymlService, private router: Router, - private changeDetector: ChangeDetectorRef + public modal: ModalMessageComponent ) { this.showOverlay = false; } @@ -83,6 +87,14 @@ export class CircularHeatmapComponent implements OnInit { @ViewChildren(MatChip) chips!: QueryList; matChipsArray: MatChip[] = []; + displayMessage(dialogInfo: DialogInfo) { + // Remove focus from the button that becomes aria unavailable (avoids ugly console error message) + const buttonElement = document.activeElement as HTMLElement; + buttonElement.blur(); + + this.modal.openDialog(dialogInfo); + } + private LoadMaturityDataFromGeneratedYaml() { return new Promise((resolve, reject) => { console.log(`${this.perfNow()}s: LoadMaturityData Fetch`); @@ -843,9 +855,26 @@ export class CircularHeatmapComponent implements OnInit { this.noActivitytoGrey(); } - ResetIsImplemented() { - localStorage.removeItem('dataset'); - this.loadDataset(); + deleteLocalTeamsProgress() { + // Remove focus from the button that becomes aria unavailable (avoids ugly console error message) + const buttonElement = document.activeElement as HTMLElement; + buttonElement.blur(); + + let title: string = 'Delete local browser data'; + let message: string = + 'Do you want to delete all progress for each team?' + + '\n\nThis deletes all progress stored in your local browser, but does ' + + 'not change any progress stored in the yaml file on the server.'; + let buttons: string[] = ['Cancel', 'Delete']; + this.modal + .openDialog({ title, message, buttons, template: '' }) + .afterClosed() + .subscribe(data => { + if (data === 'Delete') { + localStorage.removeItem('dataset'); + location.reload(); // Make sure all load routines are initialized + } + }); } saveDataset() { diff --git a/src/app/component/modal-message/modal-message.component.css b/src/app/component/modal-message/modal-message.component.css new file mode 100644 index 000000000..375caf548 --- /dev/null +++ b/src/app/component/modal-message/modal-message.component.css @@ -0,0 +1,14 @@ +.dialog { + margin: 0.5em; + padding: 1em; +} + +.dialog-buttons { + display: flex; + justify-content: flex-end; +} + +button { + min-width: 5rem; + margin: 0 1rem; +} \ No newline at end of file diff --git a/src/app/component/modal-message/modal-message.component.html b/src/app/component/modal-message/modal-message.component.html new file mode 100644 index 000000000..f1ff005f7 --- /dev/null +++ b/src/app/component/modal-message/modal-message.component.html @@ -0,0 +1,14 @@ +
+

{{ data.title }}

+

+
+
+ +
diff --git a/src/app/component/modal-message/modal-message.component.spec.ts b/src/app/component/modal-message/modal-message.component.spec.ts new file mode 100644 index 000000000..213ea3cb9 --- /dev/null +++ b/src/app/component/modal-message/modal-message.component.spec.ts @@ -0,0 +1,52 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DialogInfo, ModalMessageComponent } from './modal-message.component'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { MatDialogModule } from '@angular/material/dialog'; + +describe('ModalMessageComponent', () => { + let component: ModalMessageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, MatDialogModule], + declarations: [ModalMessageComponent], + providers: [ + { provide: MatDialogRef, useValue: {} }, + { provide: MAT_DIALOG_DATA, useValue: {} }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ModalMessageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render markdown correctly in the dialog', () => { + const dialogInfo: DialogInfo = new DialogInfo('A **test** markdown.'); + const dialogRef: MatDialogRef = + component.openDialog(dialogInfo); + + expect(dialogRef.componentInstance.data.message).toContain( + 'test' + ); + }); + + it('should render markdown correctly in the dialog', () => { + const dialogInfo: DialogInfo = new DialogInfo('A **test** markdown.'); + const dialogRef: MatDialogRef = + component.openDialog(dialogInfo); + + // Check if markdown rendering is applied + expect(dialogRef.componentInstance.data.message).toContain( + 'test' + ); + }); +}); diff --git a/src/app/component/modal-message/modal-message.component.ts b/src/app/component/modal-message/modal-message.component.ts new file mode 100644 index 000000000..2dbb00469 --- /dev/null +++ b/src/app/component/modal-message/modal-message.component.ts @@ -0,0 +1,87 @@ +import { Inject, Component, OnInit } from '@angular/core'; +import { + MAT_DIALOG_DATA, + MatDialogRef, + MatDialog, + MatDialogConfig, +} from '@angular/material/dialog'; +import * as md from 'markdown-it'; + +@Component({ + selector: 'app-modal-message', + templateUrl: './modal-message.component.html', + styleUrls: ['./modal-message.component.css'], +}) +export class ModalMessageComponent implements OnInit { + data: DialogInfo; + markdown: md = md(); + + DSOMM_host: string = 'https://github.com/devsecopsmaturitymodel'; + DSOMM_url: string = `${this.DSOMM_host}/DevSecOps-MaturityModel-data`; + meassageTemplates: Record = { + generated_yaml: new DialogInfo( + `{message}\n\n` + + `Please download the activity template \`generated.yaml\` ` + + `from [DSOMM-data](${this.DSOMM_url}) on GitHub.\n\n` + + 'The DSOMM activities are maintained and distributed ' + + 'separately from the software.', + 'DSOMM startup problems' + ), + }; + + constructor( + public dialog: MatDialog, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) data: DialogInfo + ) { + this.data = data; + } + + // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method + ngOnInit(): void {} + + openDialog( + dialogInfo: DialogInfo | string + ): MatDialogRef { + if (typeof dialogInfo === 'string') { + dialogInfo = new DialogInfo(dialogInfo); + } + if ( + dialogInfo.template && + this.meassageTemplates.hasOwnProperty(dialogInfo.template) + ) { + let template: DialogInfo = this.meassageTemplates[dialogInfo.template]; + dialogInfo.title = dialogInfo.title || template?.title; + dialogInfo.message = template?.message?.replace( + '{message}', + dialogInfo.message + ); + } + + dialogInfo.message = this.markdown.render(dialogInfo.message); + + const dialogConfig = new MatDialogConfig(); + dialogConfig.id = 'modal-message'; + dialogConfig.disableClose = true; + dialogConfig.data = dialogInfo; + dialogConfig.autoFocus = false; + this.dialogRef = this.dialog.open(ModalMessageComponent, dialogConfig); + return this.dialogRef; + } + + closeDialog(buttonName: string) { + this.dialogRef?.close(buttonName); + } +} + +export class DialogInfo { + title: string = ''; + template: string | null = ''; + message: string = ''; + buttons: string[] = ['OK']; + + constructor(msg: string = '', title: string = '') { + this.message = msg; + this.title = title; + } +}