diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss index 6f5bbb47b..f3c09959a 100644 --- a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss +++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss @@ -188,22 +188,7 @@ $form-min-width: 285px; } .device-qualification-form-actions { - height: 80px; - padding: 16px; - width: calc(100% - 64px); - box-sizing: border-box; - position: sticky; - left: 32px; - bottom: 25px; - background: colors.$surface; - border-radius: 32px; - box-shadow: - 0px 1px 2px 0px rgba(0, 0, 0, 0.3), - 0px 1px 3px 1px rgba(0, 0, 0, 0.15); - display: flex; - align-items: center; - gap: 10px; - justify-content: space-between; + @include mixins.form-actions; div { display: flex; @@ -215,13 +200,11 @@ $form-min-width: 285px; } .delete-button:not(.mat-mdc-button-disabled) { - background-color: colors.$error; - color: colors.$on-error; + @include mixins.delete-red-button; } .close-button:not(.mat-mdc-button-disabled) { - background-color: colors.$secondary-container; - color: colors.$on-secondary-container; + @include mixins.secondary-button; } } diff --git a/modules/ui/src/app/pages/devices/devices.component.scss b/modules/ui/src/app/pages/devices/devices.component.scss index 9256ecf33..a031674bd 100644 --- a/modules/ui/src/app/pages/devices/devices.component.scss +++ b/modules/ui/src/app/pages/devices/devices.component.scss @@ -13,14 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@use 'variables'; @use 'mixins'; -::ng-deep .delete-device app-simple-dialog, -::ng-deep .close-device app-simple-dialog { - --mat-dialog-container-max-width: 329px !important; -} - .device-add-button { @include mixins.add-button; } diff --git a/modules/ui/src/app/pages/devices/devices.component.ts b/modules/ui/src/app/pages/devices/devices.component.ts index c5c89b52f..435f8d69c 100644 --- a/modules/ui/src/app/pages/devices/devices.component.ts +++ b/modules/ui/src/app/pages/devices/devices.component.ts @@ -231,7 +231,7 @@ export class DevicesComponent autoFocus: true, hasBackdrop: true, disableClose: true, - panelClass: ['simple-dialog', 'delete-device'], + panelClass: ['simple-dialog', 'delete-dialog'], }); dialogRef?.beforeClosed().subscribe(deleteDevice => { if (deleteDevice) { @@ -272,7 +272,7 @@ export class DevicesComponent autoFocus: true, hasBackdrop: true, disableClose: true, - panelClass: ['simple-dialog', 'close-device'], + panelClass: ['simple-dialog', 'discard-dialog'], }); dialogRef?.beforeClosed().subscribe(close => { diff --git a/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.html b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.html index debcfa36c..341041168 100644 --- a/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.html +++ b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.html @@ -13,10 +13,10 @@ See the License for the specific language governing permissions and limitations under the License. --> -Risk Assessment Profile Completed +Risk assessment completed

- It has been saved as "{{ data.profile.name }}" and can now be attached to - reports. + The risk profile has been saved as "{{ data.profile.name }}" and can now be + attached to test reports.

{{ data.profile.risk }} risk - + +
{{ getRiskExplanation(data.profile.risk) }} The full report can be found in the zip file. Please share with the lab to validate this profile and diff --git a/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.scss b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.scss index c1aac6bbd..4842b04e0 100644 --- a/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.scss +++ b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.scss @@ -18,38 +18,32 @@ @use 'mixins'; ::ng-deep :root { - --mat-dialog-container-max-width: 570px; + --mat-dialog-container-max-width: 560px; } :host { @include mixins.dialog; - padding: 24px 0 8px 0; + padding: 24px 0 16px 0; + gap: 16px; > * { - padding: 0 16px 0 24px; + padding: 0 24px; } } .simple-dialog-title { font-family: variables.$font-primary; - font-size: 18px; - font-weight: 400; - line-height: 24px; - text-align: left; -} - -.simple-dialog-title + .simple-dialog-content { - margin-top: 0; - padding-top: 0; - border-bottom: 1px solid colors.$lighter-grey; + font-size: 24px; + line-height: 32px; + text-align: center; + color: colors.$on-surface; } .simple-dialog-content { - font-family: Roboto, sans-serif; + font-family: variables.$font-text; font-size: 14px; line-height: 20px; - letter-spacing: 0.2px; - color: colors.$grey-800; - padding: 16px 16px 16px 24px; + letter-spacing: 0; + color: colors.$on-surface-variant; margin: 0; } diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html index 7e0bf87db..ec8664568 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html @@ -44,34 +44,42 @@

+
+ + + + +
- - -
diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss index f54bc0138..d604be613 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss @@ -16,6 +16,7 @@ @use '@angular/material' as mat; @use 'colors'; @use 'variables'; +@use 'mixins'; :host { height: 100%; @@ -47,14 +48,22 @@ } .form-actions { - display: flex; - gap: 16px; - padding: 8px 24px 24px 24px; -} + @include mixins.form-actions; -.save-draft-button:not(.mat-mdc-button-disabled), -.discard-button:not(.mat-mdc-button-disabled) { - color: colors.$primary; + div { + display: flex; + gap: 12px; + } + + .save-draft-button:not(.mat-mdc-button-disabled), + .copy-button:not(.mat-mdc-button-disabled), + .discard-button:not(.mat-mdc-button-disabled) { + @include mixins.secondary-button; + } + + .delete-button:not(.mat-mdc-button-disabled) { + @include mixins.delete-red-button; + } } .save-profile-button:not(.mat-mdc-button-disabled), diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts index 114bd6e0f..5a8be53be 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts @@ -113,6 +113,8 @@ export class ProfileFormComponent implements OnInit, AfterViewInit { @Output() saveProfile = new EventEmitter(); @Output() deleteCopy = new EventEmitter(); @Output() discard = new EventEmitter(); + @Output() delete = new EventEmitter(); + @Output() copyProfile = new EventEmitter(); ngOnInit() { this.profileForm = this.createProfileForm(); } @@ -123,8 +125,91 @@ export class ProfileFormComponent implements OnInit, AfterViewInit { } } - get isDraftDisabled(): boolean { - return !this.nameControl.valid || this.fieldsHasError; + get isDraftDisabled(): boolean | null { + return ( + !this.nameControl.valid || + this.fieldsHasError || + this.profileHasNoChanges() + ); + } + + profileHasNoChanges() { + const oldProfile = this.profile; + const newProfile = oldProfile + ? this.buildResponseFromForm( + oldProfile.status as ProfileStatus, + oldProfile + ) + : this.buildResponseFromForm('', oldProfile); + return ( + (oldProfile === null && this.profileIsEmpty(newProfile)) || + (oldProfile && this.compareProfiles(oldProfile, newProfile)) + ); + } + + private profileIsEmpty(profile: Profile) { + if (profile.name && profile.name !== '') { + return false; + } + + if (profile.questions) { + for (const question of profile.questions) { + if (question.answer && question.answer !== '') { + return false; + } + } + } else { + return false; + } + return true; + } + + private compareProfiles(profile1: Profile, profile2: Profile) { + if (profile1.name !== profile2.name) { + return false; + } + if ( + (!profile1.rename && + profile2.rename && + profile2.rename !== profile1.name) || + (profile1.rename && + profile2.rename && + profile1.rename !== profile2.rename) + ) { + return false; + } + + if (profile1.status !== profile2.status) { + return false; + } + + for (const question of profile1.questions) { + const answer1 = question.answer; + const answer2 = profile2.questions?.find( + question2 => question2.question === question.question + )?.answer; + if (answer1 !== undefined && answer2 !== undefined) { + if (typeof question.answer === 'string') { + if (answer1 !== answer2) { + return false; + } + } else { + //the type of answer is array + if (answer1?.length !== answer2?.length) { + return false; + } + if ( + (answer1 as number[]).some( + answer => !(answer2 as number[]).includes(answer) + ) + ) + return false; + } + } else { + return !!answer1 == !!answer2; + } + } + return true; } private get fieldsHasError(): boolean { @@ -167,7 +252,8 @@ export class ProfileFormComponent implements OnInit, AfterViewInit { } fillProfileForm(profileFormat: ProfileFormat[], profile: Profile): void { - this.nameControl.setValue(profile.name); + const profileName = profile.rename ? profile.rename : profile.name; + this.nameControl.setValue(profileName); profileFormat.forEach((question, index) => { const answer = profile.questions.find( answers => answers.question === question.question @@ -189,23 +275,24 @@ export class ProfileFormComponent implements OnInit, AfterViewInit { } onSaveClick(status: ProfileStatus) { - const response = this.buildResponseFromForm( - this.profileFormat, - this.profileForm, - status, - this.selectedProfile - ); + const response = this.buildResponseFromForm(status, this.selectedProfile); this.saveProfile.emit(response); } onDiscardClick() { - this.discard.emit(); + this.discard.emit(this.selectedProfile!); + } + + onDeleteClick(): void { + this.delete.emit(this.selectedProfile!); + } + + onCopyClick(): void { + this.copyProfile.emit(this.selectedProfile!); } private buildResponseFromForm( - initialQuestions: ProfileFormat[], - profileForm: FormGroup, - status: ProfileStatus, + status: ProfileStatus | '', profile: Profile | null ): Profile { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -220,21 +307,21 @@ export class ProfileFormComponent implements OnInit, AfterViewInit { } const questions: Question[] = []; - initialQuestions.forEach((initialQuestion, index) => { + this.profileFormat.forEach((initialQuestion, index) => { const question: Question = {}; question.question = initialQuestion.question; if (initialQuestion.type === FormControlType.SELECT_MULTIPLE) { const answer: number[] = []; initialQuestion.options?.forEach((_, idx) => { - const value = profileForm.value[index][idx]; + const value = this.profileForm.value[index][idx]; if (value) { answer.push(idx); } }); question.answer = answer; } else { - question.answer = profileForm.value[index]?.trim(); + question.answer = this.profileForm.value[index]?.trim(); } questions.push(question); }); diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html index 1f908bf6b..e353615a4 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html @@ -44,7 +44,9 @@ [profileFormat]="vm.profileFormat" (saveProfile)="saveProfileClicked($event, vm.selectedProfile)" (deleteCopy)="deleteCopy($event, vm.profiles)" - (discard)="discard(vm.selectedProfile)"> + (delete)="deleteProfile($event)" + (copyProfile)="copyProfile($event, vm.profiles)" + (discard)="discard($event)"> diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts index 286a4e081..14e8a11ec 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts @@ -200,6 +200,7 @@ describe('RiskAssessmentComponent', () => { autoFocus: 'dialog', hasBackdrop: true, disableClose: true, + panelClass: ['simple-dialog', 'delete-dialog'], }); openSpy.calls.reset(); @@ -387,8 +388,35 @@ describe('RiskAssessmentComponent', () => { }); describe('#discard', () => { + it('should open discard modal', fakeAsync(() => { + const openSpy = spyOn(component.dialog, 'open').and.returnValue({ + afterClosed: () => of(true), + } as MatDialogRef); + + component.discard(null); + + expect(openSpy).toHaveBeenCalledWith(SimpleDialogComponent, { + ariaLabel: 'Discard the Risk Assessment changes', + data: { + title: 'Discard changes?', + content: `You have unsaved changes that would be permanently lost.`, + confirmName: 'Discard', + }, + autoFocus: true, + hasBackdrop: true, + disableClose: true, + panelClass: ['simple-dialog', 'discard-dialog'], + }); + + openSpy.calls.reset(); + })); + describe('with no selected profile', () => { beforeEach(() => { + spyOn(component.dialog, 'open').and.returnValue({ + afterClosed: () => of(true), + } as MatDialogRef); + component.discard(null); }); @@ -405,6 +433,10 @@ describe('RiskAssessmentComponent', () => { describe('with selected profile', () => { beforeEach(fakeAsync(() => { + spyOn(component.dialog, 'open').and.returnValue({ + afterClosed: () => of(true), + } as MatDialogRef); + component.discard(PROFILE_MOCK); tick(100); })); diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts index 4eef3dcff..fa2d9b7c5 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts @@ -20,6 +20,7 @@ import { OnInit, ViewContainerRef, inject, + ChangeDetectorRef, } from '@angular/core'; import { RiskAssessmentStore } from './risk-assessment.store'; import { SimpleDialogComponent } from '../../components/simple-dialog/simple-dialog.component'; @@ -85,6 +86,7 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { readonly ProfileStatus = ProfileStatus; private store = inject(RiskAssessmentStore); private liveAnnouncer = inject(LiveAnnouncer); + private cd = inject(ChangeDetectorRef); private destroy$: Subject = new Subject(); dialog = inject(MatDialog); element = inject(ViewContainerRef); @@ -141,6 +143,7 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { autoFocus: 'dialog', hasBackdrop: true, disableClose: true, + panelClass: ['simple-dialog', 'delete-dialog'], }); dialogRef @@ -186,16 +189,45 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { discard(selectedProfile: Profile | null) { this.liveAnnouncer.clear(); - this.isOpenProfileForm = false; - this.isCopyProfile = false; - if (selectedProfile) { - timer(100).subscribe(() => { - this.store.setFocusOnSelectedProfile(); - this.store.updateSelectedProfile(null); + this.openCloseDialog(selectedProfile); + } + + copyProfile(profile: Profile, profiles: Profile[]) { + this.copyProfileAndOpenForm(profile, profiles); + } + + private openCloseDialog(selectedProfile: Profile | null) { + const dialogRef = this.dialog.open(SimpleDialogComponent, { + ariaLabel: 'Discard the Risk Assessment changes', + data: { + title: 'Discard changes?', + content: `You have unsaved changes that would be permanently lost.`, + confirmName: 'Discard', + }, + autoFocus: true, + hasBackdrop: true, + disableClose: true, + panelClass: ['simple-dialog', 'discard-dialog'], + }); + + dialogRef + ?.afterClosed() + .pipe(takeUntil(this.destroy$)) + .subscribe(close => { + if (close) { + if (selectedProfile) { + timer(100).subscribe(() => { + this.store.setFocusOnSelectedProfile(); + }); + } else { + this.store.setFocusOnCreateButton(); + } + this.isCopyProfile = false; + this.isOpenProfileForm = false; + this.store.updateSelectedProfile(null); + this.cd.markForCheck(); + } }); - } else { - this.store.setFocusOnCreateButton(); - } } deleteCopy(copyOfProfile: Profile, profiles: Profile[]) { @@ -296,9 +328,9 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { } else { focusElement(); } + this.store.updateSelectedProfile(profile); }, }); - this.isOpenProfileForm = false; this.isCopyProfile = false; } @@ -332,7 +364,7 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { private openSuccessDialog(profile: Profile, focusElement: () => void): void { const dialogRef = this.dialog.open(SuccessDialogComponent, { - ariaLabel: 'Risk Assessment Profile Completed', + ariaLabel: 'Risk assessment completed', data: { profile, }, diff --git a/modules/ui/src/styles.scss b/modules/ui/src/styles.scss index 49caf7229..548ab4751 100644 --- a/modules/ui/src/styles.scss +++ b/modules/ui/src/styles.scss @@ -129,6 +129,11 @@ body { } } +.delete-dialog app-simple-dialog, +.discard-dialog app-simple-dialog { + --mat-dialog-container-max-width: 329px; +} + .device-form-dialog .mat-mdc-dialog-container .mdc-dialog__surface { overflow: hidden; display: grid; diff --git a/modules/ui/src/theming/mixins.scss b/modules/ui/src/theming/mixins.scss index 987875d86..ba402bd1d 100644 --- a/modules/ui/src/theming/mixins.scss +++ b/modules/ui/src/theming/mixins.scss @@ -14,6 +14,7 @@ * limitations under the License. */ @use 'variables'; +@use 'colors'; @mixin dialog { display: grid; @@ -78,3 +79,32 @@ font-size: 16px; font-weight: 500; } + +@mixin delete-red-button { + background-color: colors.$error; + color: colors.$on-error; +} + +@mixin secondary-button { + background-color: colors.$secondary-container; + color: colors.$on-secondary-container; +} + +@mixin form-actions { + position: sticky; + left: 32px; + bottom: 25px; + display: flex; + align-items: center; + gap: 10px; + justify-content: space-between; + width: calc(100% - 64px); + height: 80px; + padding: 16px; + box-sizing: border-box; + background: colors.$surface; + border-radius: 32px; + box-shadow: + 0px 1px 2px 0px rgba(0, 0, 0, 0.3), + 0px 1px 3px 1px rgba(0, 0, 0, 0.15); +}