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);
+}