From f65d67029f0ecd85340c44b5848c74dcfceab4ff Mon Sep 17 00:00:00 2001 From: Volha Mardvilka Date: Tue, 11 Jun 2024 13:49:20 +0000 Subject: [PATCH] 340859666: (feat): display risk profile form with name field --- .../profile-form/profile-form.component.html | 54 +++++++++++++ .../profile-form/profile-form.component.scss | 47 +++++++++++ .../profile-form.component.spec.ts | 40 ++++++++++ .../profile-form/profile-form.component.ts | 80 +++++++++++++++++++ .../profile-form/profile.validators.ts | 40 ++++++++++ .../risk-assessment.component.html | 33 ++++++-- .../risk-assessment.component.scss | 18 +++++ .../risk-assessment.component.spec.ts | 33 +++++++- .../risk-assessment.component.ts | 5 ++ .../risk-assessment/risk-assessment.module.ts | 22 +++++ 10 files changed, 362 insertions(+), 10 deletions(-) create mode 100644 modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html create mode 100644 modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss create mode 100644 modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.spec.ts create mode 100644 modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts create mode 100644 modules/ui/src/app/pages/risk-assessment/profile-form/profile.validators.ts 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 new file mode 100644 index 000000000..33c70f809 --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html @@ -0,0 +1,54 @@ + +
+
+

Profile name *

+ + Specify risk assessment profile name + + + Please, check. The Profile name must be a maximum of 28 characters. + Only letters, numbers, and accented letters are permitted. + + + The Profile name is required + + + This Profile name is already used for another Risk Assessment + profile + + +
+
+
+ + + +
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 new file mode 100644 index 000000000..003ed84a8 --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss @@ -0,0 +1,47 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import 'src/theming/colors'; +@import 'src/theming/variables'; + +.profile-form { + .field-container { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; + padding: 8px 16px 8px 24px; + } + .field-label { + margin: 0; + color: $grey-800; + font-size: 18px; + line-height: 24px; + } + mat-form-field { + width: 100%; + } +} + +.form-actions { + display: flex; + gap: 16px; + padding: 0 24px; +} + +.save-draft-button, +.discard-button { + color: $primary; +} diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.spec.ts b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.spec.ts new file mode 100644 index 000000000..f72f41735 --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.spec.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProfileFormComponent } from './profile-form.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +describe('ProfileFormComponent', () => { + let component: ProfileFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProfileFormComponent, BrowserAnimationsModule], + }).compileComponents(); + + fixture = TestBed.createComponent(ProfileFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + // TODO: Add more unit tests +}); 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 new file mode 100644 index 000000000..bb8e5b12e --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts @@ -0,0 +1,80 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + ChangeDetectionStrategy, + Component, + Input, + OnInit, +} from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { CommonModule } from '@angular/common'; +import { + AbstractControl, + FormBuilder, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { MatInputModule } from '@angular/material/input'; +import { DeviceValidators } from '../../devices/components/device-form/device.validators'; +import { Profile } from '../../../model/profile'; +import { ProfileValidators } from './profile.validators'; +import { MatError } from '@angular/material/form-field'; + +@Component({ + selector: 'app-profile-form', + standalone: true, + imports: [ + MatButtonModule, + CommonModule, + ReactiveFormsModule, + MatInputModule, + MatError, + ], + templateUrl: './profile-form.component.html', + styleUrl: './profile-form.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProfileFormComponent implements OnInit { + profileForm!: FormGroup; + @Input() profiles!: Profile[]; + constructor( + private deviceValidators: DeviceValidators, + private profileValidators: ProfileValidators, + private fb: FormBuilder + ) {} + + get nameControl() { + return this.profileForm.get('name') as AbstractControl; + } + + ngOnInit() { + this.createProfileForm(); + } + + createProfileForm() { + this.profileForm = this.fb.group({ + name: [ + '', + [ + Validators.required, + this.deviceValidators.deviceStringFormat(), + this.profileValidators.differentProfileName(this.profiles), + ], + ], + }); + } +} diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile.validators.ts b/modules/ui/src/app/pages/risk-assessment/profile-form/profile.validators.ts new file mode 100644 index 000000000..7de2cd353 --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile.validators.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable } from '@angular/core'; +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; +import { Profile } from '../../../model/profile'; +@Injectable({ providedIn: 'root' }) +export class ProfileValidators { + public differentProfileName(profiles: Profile[]): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value?.trim(); + if (value && profiles.length) { + const isSameProfileName = this.hasSameProfileName(value, profiles); + return isSameProfileName ? { has_same_profile_name: true } : null; + } + return null; + }; + } + + private hasSameProfileName( + profileName: string, + profiles: Profile[] + ): boolean { + return ( + profiles.some(profile => profile.name === profileName.trim()) || false + ); + } +} 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 2cdd350b4..8975b819b 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 @@ -14,14 +14,18 @@ limitations under the License. --> - - - -

Risk assessment

-
-
-
-
+ + + + +

Risk assessment

+
+
+ +
+
+
+
Saved profiles + + +
+ +
+
diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.scss b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.scss index 102e4ba65..0fc2c3784 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.scss +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.scss @@ -16,8 +16,23 @@ @import 'src/theming/colors'; @import 'src/theming/variables'; +.risk-assessment-content-empty { + height: 100%; + width: calc(100%); + display: flex; + align-items: center; + justify-content: center; +} + +:host:has(.profiles-drawer) { + .risk-assessment-content-empty { + width: calc(100% - $profiles-drawer-width); + } +} + .risk-assessment-container, .risk-assessment-content { + height: 100%; background-color: $white; } @@ -25,6 +40,9 @@ display: flex; flex-direction: column; gap: 14px; + width: calc(100% - $profiles-drawer-width); + box-sizing: border-box; + padding-right: 94px; } .risk-assessment-toolbar { 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 db45e728f..059f9888e 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 @@ -57,7 +57,11 @@ describe('RiskAssessmentComponent', () => { ]); await TestBed.configureTestingModule({ - declarations: [RiskAssessmentComponent, FakeProfileItemComponent], + declarations: [ + RiskAssessmentComponent, + FakeProfileItemComponent, + FakeProfileFormComponent, + ], imports: [MatToolbarModule, MatSidenavModule, BrowserAnimationsModule], providers: [ { provide: TestRunService, useValue: mockService }, @@ -88,14 +92,31 @@ describe('RiskAssessmentComponent', () => { fixture.detectChanges(); }); - it('should have toolbar with title', () => { + it('should have "New Risk Assessment" button', () => { + const newRiskAssessmentBtn = compiled.querySelector( + '.risk-assessment-add-button' + ); + + expect(newRiskAssessmentBtn).not.toBeNull(); + }); + + it('should have title and profile form when "New Risk Assessment" button is clicked', () => { + const newRiskAssessmentBtn = compiled.querySelector( + '.risk-assessment-add-button' + ) as HTMLButtonElement; + + newRiskAssessmentBtn.click(); + fixture.detectChanges(); + const toolbarEl = compiled.querySelector('.risk-assessment-toolbar'); const title = compiled.querySelector('h2.title'); const titleContent = title?.innerHTML.trim(); + const profileForm = compiled.querySelectorAll('app-profile-form'); expect(toolbarEl).not.toBeNull(); expect(title).toBeTruthy(); expect(titleContent).toContain('Risk assessment'); + expect(profileForm).toBeTruthy(); }); it('should not have profiles drawer', () => { @@ -160,3 +181,11 @@ describe('RiskAssessmentComponent', () => { class FakeProfileItemComponent { @Input() profile!: Profile; } + +@Component({ + selector: 'app-profile-form', + template: '
', +}) +class FakeProfileFormComponent { + @Input() profiles!: Profile[]; +} 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 ddd9d8b3e..ca5725a59 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 @@ -28,6 +28,7 @@ import { MatDialog } from '@angular/material/dialog'; }) export class RiskAssessmentComponent implements OnDestroy { viewModel$ = this.store.viewModel$; + isOpenProfileForm = false; private destroy$: Subject = new Subject(); constructor( private store: RiskAssessmentStore, @@ -39,6 +40,10 @@ export class RiskAssessmentComponent implements OnDestroy { this.destroy$.unsubscribe(); } + openForm(): void { + this.isOpenProfileForm = true; + } + deleteProfile(profileName: string, index: number): void { const dialogRef = this.dialog.open(DeleteFormComponent, { ariaLabel: 'Delete risk profile', diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.module.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.module.ts index 706760296..97d48989f 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.module.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.module.ts @@ -21,6 +21,18 @@ import { MatToolbarModule } from '@angular/material/toolbar'; import { RiskAssessmentComponent } from './risk-assessment.component'; import { MatSidenavModule } from '@angular/material/sidenav'; import { ProfileItemComponent } from './profile-item/profile-item.component'; +import { MatButtonModule } from '@angular/material/button'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatInputModule } from '@angular/material/input'; +import { + MAT_FORM_FIELD_DEFAULT_OPTIONS, + MatFormFieldDefaultOptions, +} from '@angular/material/form-field'; +import { ProfileFormComponent } from './profile-form/profile-form.component'; + +const matFormFieldDefaultOptions: MatFormFieldDefaultOptions = { + hideRequiredMarker: true, +}; @NgModule({ declarations: [RiskAssessmentComponent], @@ -28,8 +40,18 @@ import { ProfileItemComponent } from './profile-item/profile-item.component'; CommonModule, RiskAssessmentRoutingModule, MatToolbarModule, + MatButtonModule, + ReactiveFormsModule, + MatInputModule, MatSidenavModule, + ProfileFormComponent, ProfileItemComponent, ], + providers: [ + { + provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, + useValue: matFormFieldDefaultOptions, + }, + ], }) export class RiskAssessmentModule {}