From 9da9959055fda2876ec12f194711247a00b04cdb Mon Sep 17 00:00:00 2001 From: kurilova Date: Fri, 16 Aug 2024 07:53:11 +0000 Subject: [PATCH] Adds summary step; add close form pop-up; save is not in scope --- .../components/stepper/stepper.component.html | 2 +- .../components/stepper/stepper.component.scss | 5 +- .../components/stepper/stepper.component.ts | 4 +- .../device-form/device-form.component.spec.ts | 3 +- .../device-form/device-form.component.ts | 11 +-- .../device-qualification-from.component.html | 56 ++++++++----- .../device-qualification-from.component.scss | 24 ++++++ ...evice-qualification-from.component.spec.ts | 28 ++++++- .../device-qualification-from.component.ts | 32 +++++++- .../app/pages/devices/devices.component.html | 2 +- .../pages/devices/devices.component.spec.ts | 65 ++------------- .../app/pages/devices/devices.component.ts | 80 +++++++++---------- 12 files changed, 166 insertions(+), 146 deletions(-) diff --git a/modules/ui/src/app/components/stepper/stepper.component.html b/modules/ui/src/app/components/stepper/stepper.component.html index 487bce262..8571dfd2c 100644 --- a/modules/ui/src/app/components/stepper/stepper.component.html +++ b/modules/ui/src/app/components/stepper/stepper.component.html @@ -34,7 +34,7 @@
diff --git a/modules/ui/src/app/components/stepper/stepper.component.scss b/modules/ui/src/app/components/stepper/stepper.component.scss index 87c7ffecd..e300fb8c6 100644 --- a/modules/ui/src/app/components/stepper/stepper.component.scss +++ b/modules/ui/src/app/components/stepper/stepper.component.scss @@ -29,7 +29,8 @@ display: grid; padding: 24px; gap: 10px; - overflow: auto; + overflow: hidden; + height: 100%; } .form-footer { @@ -78,6 +79,6 @@ } &.hidden { - display: none; + visibility: hidden; } } diff --git a/modules/ui/src/app/components/stepper/stepper.component.ts b/modules/ui/src/app/components/stepper/stepper.component.ts index fdd0c9ce7..6f688d377 100644 --- a/modules/ui/src/app/components/stepper/stepper.component.ts +++ b/modules/ui/src/app/components/stepper/stepper.component.ts @@ -40,10 +40,8 @@ export class StepperComponent extends CdkStepper { @Input() header: TemplateRef | undefined; @Input() activeClass = 'active'; - stepsCount = [1, 2, 3, 4]; //TODO will be removed when all steps are implemented - forwardButtonHidden() { - return this.selectedIndex === this.stepsCount.length - 1; + return this.selectedIndex === this.steps.length - 1; } backButtonHidden() { diff --git a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.spec.ts b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.spec.ts index 6ea72aa52..1911402a0 100644 --- a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.spec.ts +++ b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.spec.ts @@ -20,7 +20,7 @@ import { TestBed, } from '@angular/core/testing'; -import { DeviceFormComponent, FormAction } from './device-form.component'; +import { DeviceFormComponent } from './device-form.component'; import { MatButtonModule } from '@angular/material/button'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -39,6 +39,7 @@ import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; import { DevicesStore } from '../../devices.store'; import { device, MOCK_TEST_MODULES } from '../../../../mocks/device.mock'; import SpyObj = jasmine.SpyObj; +import { FormAction } from '../../devices.component'; describe('DeviceFormComponent', () => { let component: DeviceFormComponent; diff --git a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts index 38ab05ffa..f7aa4f180 100644 --- a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts +++ b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts @@ -29,6 +29,7 @@ import { Subject } from 'rxjs'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { EscapableDialogComponent } from '../../../../components/escapable-dialog/escapable-dialog.component'; import { DevicesStore } from '../../devices.store'; +import { FormAction, FormResponse } from '../../devices.component'; const MAC_ADDRESS_PATTERN = '^[\\s]*[a-fA-F0-9]{2}(?:[:][a-fA-F0-9]{2}){5}[\\s]*$'; @@ -40,16 +41,6 @@ interface DialogData { testModules: TestModule[]; } -export enum FormAction { - Delete = 'Delete', - Save = 'Save', -} - -export interface FormResponse { - device?: Device; - action: FormAction; -} - @Component({ selector: 'app-device-form', templateUrl: './device-form.component.html', diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.html b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.html index 60f7226b0..cf1d60fb6 100644 --- a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.html +++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.html @@ -32,10 +32,11 @@

{{ data.title }}

+ [header]="header" + [selectedIndex]="selectedIndex"> Device Manufacturer @@ -151,26 +152,43 @@

{{ data.title }}

[editable]="true" [formGroupName]="step.step" [stepControl]="getStep(step.step)"> -

- {{ step.title }} -

-

- {{ step.description }} -

- +
+

+ {{ step.title }} +

+

+ {{ step.description }} +

+ +
- - Final step + +
+

Summary

+

+ The device has been configured. Please check the setup. +

+
+
+ + +
+
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 f4c06d5e6..97b92dfbc 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 @@ -145,3 +145,27 @@ $form-height: 993px; margin: 0; padding-top: 8px; } + +.device-qualification-form-step-content { + overflow: scroll; +} + +.device-qualification-form-page { + display: grid; + gap: 10px; + height: 100%; + overflow: hidden; + &:last-of-type { + grid-template-rows: min-content min-content 1fr min-content; + } +} + +.device-qualification-form-actions { + text-align: center; + .close-button { + padding: 0 16px; + } + .save-button { + margin: 0 16px; + } +} diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts index cc320e03f..2dd7263f2 100644 --- a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts +++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts @@ -39,6 +39,7 @@ import { MatIconTestingModule } from '@angular/material/icon/testing'; import { TestRunService } from '../../../../services/test-run.service'; import { DevicesStore } from '../../devices.store'; import { provideMockStore } from '@ngrx/store/testing'; +import { FormAction } from '../../devices.component'; describe('DeviceQualificationFromComponent', () => { let component: DeviceQualificationFromComponent; let fixture: ComponentFixture; @@ -85,6 +86,7 @@ describe('DeviceQualificationFromComponent', () => { component.data = { testModules: MOCK_TEST_MODULES, devices: [], + index: 0, }; testrunServiceMock.fetchQuestionnaireFormat.and.returnValue( of(DEVICES_FORM) @@ -97,13 +99,13 @@ describe('DeviceQualificationFromComponent', () => { expect(component).toBeTruthy(); }); - it('should fetch devices format', () => { + it('should contain device form', () => { const form = compiled.querySelector('.device-qualification-form'); expect(form).toBeTruthy(); }); - it('should contain device form', () => { + it('should fetch devices format', () => { const getQuestionnaireFormatSpy = spyOn( component.devicesStore, 'getQuestionnaireFormat' @@ -122,7 +124,23 @@ describe('DeviceQualificationFromComponent', () => { closeButton?.click(); - expect(closeSpy).toHaveBeenCalledWith(); + expect(closeSpy).toHaveBeenCalledWith({ + action: FormAction.Close, + index: 0, + device: { + manufacturer: '', + model: '', + mac_addr: '', + test_modules: { + udmi: { + enabled: false, + }, + connection: { + enabled: true, + }, + }, + }, + }); closeSpy.calls.reset(); }); @@ -304,6 +322,7 @@ describe('DeviceQualificationFromComponent', () => { component.data = { testModules: MOCK_TEST_MODULES, devices: [device], + index: 0, }; component.ngOnInit(); fixture.detectChanges(); @@ -341,6 +360,7 @@ describe('DeviceQualificationFromComponent', () => { }, }, }, + index: 0, }; component.ngOnInit(); @@ -360,7 +380,7 @@ describe('DeviceQualificationFromComponent', () => { }); }); - describe('with questioner', () => { + describe('with questionnaire', () => { it('should have steps', () => { expect( (component.deviceQualificationForm.get('steps') as FormArray).controls diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts index bdb8dc92d..f4efb34db 100644 --- a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts +++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts @@ -18,7 +18,9 @@ import { Component, ElementRef, Inject, + OnDestroy, OnInit, + ViewChild, } from '@angular/core'; import { AbstractControl, @@ -56,7 +58,8 @@ import { MatRadioButton, MatRadioGroup } from '@angular/material/radio'; import { ProfileValidators } from '../../../risk-assessment/profile-form/profile.validators'; import { DevicesStore } from '../../devices.store'; import { DynamicFormComponent } from '../../../../components/dynamic-form/dynamic-form.component'; -import { skip } from 'rxjs'; +import { skip, Subject, takeUntil, timer } from 'rxjs'; +import { FormAction, FormResponse } from '../../devices.component'; const MAC_ADDRESS_PATTERN = '^[\\s]*[a-fA-F0-9]{2}(?:[:][a-fA-F0-9]{2}){5}[\\s]*$'; @@ -66,6 +69,7 @@ interface DialogData { device?: Device; devices: Device[]; testModules: TestModule[]; + index: number; } @Component({ @@ -98,12 +102,16 @@ interface DialogData { }) export class DeviceQualificationFromComponent extends EscapableDialogComponent - implements OnInit, AfterViewInit + implements OnInit, AfterViewInit, OnDestroy { + @ViewChild('stepper') public stepper!: StepperComponent; testModules: TestModule[] = []; deviceQualificationForm: FormGroup = this.fb.group({}); device: Device | undefined; format: DeviceQuestionnaireSection[] = []; + selectedIndex: number = 0; + + private destroy$: Subject = new Subject(); get model() { return this.getStep(0).get('model') as AbstractControl; @@ -146,6 +154,13 @@ export class DeviceQualificationFromComponent this.devicesStore.questionnaireFormat$.pipe(skip(1)).subscribe(format => { this.createDeviceForm(format); this.format = format; + if (this.data.index) { + timer(0) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.selectedIndex = this.data.index; + }); + } }); this.devicesStore.getQuestionnaireFormat(); @@ -157,12 +172,21 @@ export class DeviceQualificationFromComponent this.element.nativeElement.offsetHeight + 'px'; } + ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.unsubscribe(); + } + submit(): void { this.device = this.createDeviceFromForm(this.getStep(0)); } closeForm(): void { - this.dialogRef.close(); + this.dialogRef.close({ + action: FormAction.Close, + device: this.createDeviceFromForm(this.getStep(0)), + index: this.stepper.selectedIndex, + } as FormResponse); } getStep(step: number) { @@ -229,7 +253,7 @@ export class DeviceQualificationFromComponent ); }); - // TODO dummy step + // summary step (this.deviceQualificationForm.get('steps') as FormArray).controls.push( this.fb.group({}) ); diff --git a/modules/ui/src/app/pages/devices/devices.component.html b/modules/ui/src/app/pages/devices/devices.component.html index aef3730c5..7ea597d42 100644 --- a/modules/ui/src/app/pages/devices/devices.component.html +++ b/modules/ui/src/app/pages/devices/devices.component.html @@ -22,7 +22,7 @@

Devices

{ mockDevicesStore = jasmine.createSpyObj('DevicesStore', [ 'setIsOpenAddDevice', 'selectDevice', - 'deleteDevice', 'setStatus', 'getTestModules', ]); @@ -163,6 +161,7 @@ describe('DevicesComponent', () => { title: 'Create Device', testModules: [], devices: [device, device, device], + index: 0, }, autoFocus: true, hasBackdrop: true, @@ -180,7 +179,7 @@ describe('DevicesComponent', () => { } as MatDialogRef); fixture.detectChanges(); - component.openDialog([device], MOCK_TEST_MODULES, device); + component.openDialog([device], MOCK_TEST_MODULES, device, true); expect(openSpy).toHaveBeenCalled(); expect(openSpy).toHaveBeenCalledWith(DeviceQualificationFromComponent, { @@ -190,6 +189,7 @@ describe('DevicesComponent', () => { title: 'Edit device', devices: [device], testModules: MOCK_TEST_MODULES, + index: 0, }, autoFocus: true, hasBackdrop: true, @@ -199,31 +199,6 @@ describe('DevicesComponent', () => { openSpy.calls.reset(); }); - - it('should open device dialog with delete-button focus element', () => { - const openSpy = spyOn(component.dialog, 'open').and.returnValue({ - afterClosed: () => of(true), - } as MatDialogRef); - fixture.detectChanges(); - - component.openDialog([device], MOCK_TEST_MODULES, device, true); - - expect(openSpy).toHaveBeenCalledWith(DeviceQualificationFromComponent, { - ariaLabel: 'Edit device', - data: { - device: device, - title: 'Edit device', - devices: [device], - testModules: MOCK_TEST_MODULES, - }, - autoFocus: '.delete-button', - hasBackdrop: true, - disableClose: true, - panelClass: 'device-form-dialog', - }); - - openSpy.calls.reset(); - }); }); it('should disable device if deviceInProgress is exist', () => { @@ -243,21 +218,7 @@ describe('DevicesComponent', () => { expect(mockDevicesStore.setIsOpenAddDevice).toHaveBeenCalled(); }); - it('should delete device if dialog closes with object, action delete and selected device', () => { - spyOn(component.dialog, 'open').and.returnValue({ - afterClosed: () => - of({ - device, - action: FormAction.Delete, - }), - } as MatDialogRef); - - component.openDialog([device], MOCK_TEST_MODULES, device); - - expect(mockDevicesStore.deleteDevice).toHaveBeenCalled(); - }); - - describe('delete device dialog', () => { + describe('close dialog', () => { beforeEach(() => { component.viewModel$ = of({ devices: [device, device, device], @@ -274,32 +235,20 @@ describe('DevicesComponent', () => { expect(item.length).toEqual(3); })); - it('should delete device when dialog return true', () => { - spyOn(component.dialog, 'open').and.returnValue({ - afterClosed: () => of(true), - } as MatDialogRef); - - component.openDeleteDialog([device], MOCK_TEST_MODULES, device); - - const args = mockDevicesStore.deleteDevice.calls.argsFor(0); - // @ts-expect-error config is in object - expect(args[0].device).toEqual(device); - expect(mockDevicesStore.deleteDevice).toHaveBeenCalled(); - }); - it('should open device dialog when dialog return null', () => { const openDeviceDialogSpy = spyOn(component, 'openDialog'); spyOn(component.dialog, 'open').and.returnValue({ afterClosed: () => of(null), } as MatDialogRef); - component.openDeleteDialog([device], MOCK_TEST_MODULES, device); + component.openCloseDialog([device], MOCK_TEST_MODULES, device); expect(openDeviceDialogSpy).toHaveBeenCalledWith( [device], MOCK_TEST_MODULES, device, - true + false, + 0 ); }); }); diff --git a/modules/ui/src/app/pages/devices/devices.component.ts b/modules/ui/src/app/pages/devices/devices.component.ts index e320663bf..a7243e148 100644 --- a/modules/ui/src/app/pages/devices/devices.component.ts +++ b/modules/ui/src/app/pages/devices/devices.component.ts @@ -22,21 +22,28 @@ import { } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Device, DeviceView, TestModule } from '../../model/device'; -import { - FormAction, - FormResponse, -} from './components/device-form/device-form.component'; import { Subject, takeUntil } from 'rxjs'; import { SimpleDialogComponent } from '../../components/simple-dialog/simple-dialog.component'; import { combineLatest } from 'rxjs/internal/observable/combineLatest'; import { FocusManagerService } from '../../services/focus-manager.service'; import { Routes } from '../../model/routes'; import { Router } from '@angular/router'; -import { timer } from 'rxjs/internal/observable/timer'; import { TestrunInitiateFormComponent } from '../testrun/components/testrun-initiate-form/testrun-initiate-form.component'; import { DevicesStore } from './devices.store'; import { DeviceQualificationFromComponent } from './components/device-qualification-from/device-qualification-from.component'; +export enum FormAction { + Delete = 'Delete', + Close = 'Close', + Save = 'Save', +} + +export interface FormResponse { + device?: Device; + action: FormAction; + index: number; +} + @Component({ selector: 'app-device-repository', templateUrl: './devices.component.html', @@ -113,17 +120,19 @@ export class DevicesComponent implements OnInit, OnDestroy { devices: Device[] = [], testModules: TestModule[], selectedDevice?: Device, - focusDeleteButton = false + isEditDevice = false, + index = 0 ): void { const dialogRef = this.dialog.open(DeviceQualificationFromComponent, { - ariaLabel: selectedDevice ? 'Edit device' : 'Create Device', + ariaLabel: isEditDevice ? 'Edit device' : 'Create Device', data: { device: selectedDevice || null, - title: selectedDevice ? 'Edit device' : 'Create Device', + title: isEditDevice ? 'Edit device' : 'Create Device', testModules: testModules, devices, + index, }, - autoFocus: focusDeleteButton ? '.delete-button' : true, + autoFocus: true, hasBackdrop: true, disableClose: true, panelClass: 'device-form-dialog', @@ -138,37 +147,30 @@ export class DevicesComponent implements OnInit, OnDestroy { this.devicesStore.setIsOpenAddDevice(false); return; } - if ( - response.action === FormAction.Save && - response.device && - !selectedDevice - ) { - timer(10) - .pipe(takeUntil(this.destroy$)) - .subscribe(() => { - this.focusManagerService.focusFirstElementInContainer(); - }); - } - if (response.action === FormAction.Delete && selectedDevice) { - this.devicesStore.selectDevice(selectedDevice); - this.openDeleteDialog(devices, testModules, selectedDevice); + if (response.action === FormAction.Close) { + this.openCloseDialog( + devices, + testModules, + response.device, + isEditDevice, + response.index + ); } }); } - openDeleteDialog( + openCloseDialog( devices: Device[], testModules: TestModule[], - device: Device + device?: Device, + isEditDevice = false, + index = 0 ) { const dialogRef = this.dialog.open(SimpleDialogComponent, { - ariaLabel: 'Delete device', + ariaLabel: 'Close the Device menu', data: { - title: 'Delete device?', - content: `You are about to delete ${ - device.manufacturer + ' ' + device.model - }. Are you sure?`, - device: device, + title: 'Close the Device menu?', + content: `Are you ok to close the Device menu?`, }, autoFocus: true, hasBackdrop: true, @@ -179,24 +181,16 @@ export class DevicesComponent implements OnInit, OnDestroy { dialogRef ?.afterClosed() .pipe(takeUntil(this.destroy$)) - .subscribe(deleteDevice => { - if (deleteDevice) { - this.devicesStore.deleteDevice({ - device, - onDelete: () => { - this.focusNextButton(); - this.devicesStore.selectDevice(null); - }, - }); - } else { - this.openDialog(devices, testModules, device, true); + .subscribe(close => { + if (!close) { + this.openDialog(devices, testModules, device, isEditDevice, index); this.devicesStore.selectDevice(null); } }); } private focusNextButton() { - // Try to focus next device item, if exitst + // Try to focus next device item, if exist const next = this.element.nativeElement.querySelector( '.device-item-selected + app-device-item button' );