From: pujashahu Date: Fri, 24 Jul 2026 08:29:01 +0000 (+0530) Subject: mgr/dashboard : If user is selected auto-fetch option while creating subsystems then... X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=cbc41b07d8340d8b6497c6f494398484d3f8dd4d;p=ceph.git mgr/dashboard : If user is selected auto-fetch option while creating subsystems then subnet-mask is mandatory param Fixes: https://tracker.ceph.com/issues/78641 Signed-off-by: pujashahu --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-1/nvmeof-subsystem-step-1.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-1/nvmeof-subsystem-step-1.component.html index 1496c01d9633..0dc795631500 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-1/nvmeof-subsystem-step-1.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-1/nvmeof-subsystem-step-1.component.html @@ -120,7 +120,7 @@ @@ -138,7 +138,7 @@ [invalid]="subnetMaskRef.isInvalid" /> - + This field is required. diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-1/nvmeof-subsystem-step-1.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-1/nvmeof-subsystem-step-1.component.spec.ts index 237a76cb3d12..56d887983b84 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-1/nvmeof-subsystem-step-1.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-1/nvmeof-subsystem-step-1.component.spec.ts @@ -71,5 +71,13 @@ describe('NvmeofSubsystemsStepOneComponent', () => { formHelper.setValue('nqn', 'nqn:2001-07.com.ceph:'); formHelper.expectError('nqn', 'nqnPattern'); }); + + it('should require subnet mask when auto-fetch is selected and validated', () => { + formHelper.setValue('listenerMode', component.LISTENER_MODE.AUTO_FETCH); + formHelper.setValue('subnetMask', ''); + form.get('subnetMask')?.updateValueAndValidity(); + + expect(form.get('subnetMask')?.hasError('required')).toBeTruthy(); + }); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.spec.ts index ffe9576c6bb3..7405093f55e0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component, ViewChild } from '@angular/core'; import { By } from '@angular/platform-browser'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; import { SharedModule } from '../../shared.module'; import { TearsheetStepComponent } from '../tearsheet-step/tearsheet-step.component'; import { TearsheetComponent, TearsheetOverflowScroll } from './tearsheet.component'; @@ -47,6 +48,51 @@ class MockHostComponent { tearsheet!: TearsheetComponent; } +@Component({ + selector: 'cd-mock-form-step', + template: '', + standalone: false +}) +class MockFormStepComponent { + formGroup = new FormGroup({ + parent: new FormGroup({ + child: new FormControl('', Validators.required) + }) + }); +} + +@Component({ + template: ` + + + + + +
Step 2
+
+
+ `, + standalone: false +}) +class MockFormHostComponent { + steps = [ + { label: 'Step 1', complete: false }, + { label: 'Step 2', complete: false } + ]; + title = 'Form Host'; + description = 'Form Host Description'; + + @ViewChild(TearsheetComponent) + tearsheet!: TearsheetComponent; + + @ViewChild(MockFormStepComponent) + formStep!: MockFormStepComponent; +} + describe('TearsheetComponent', () => { let hostFixture: ComponentFixture; let hostComponent: MockHostComponent; @@ -54,7 +100,13 @@ describe('TearsheetComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [TearsheetComponent, TearsheetStepComponent, MockHostComponent], + declarations: [ + TearsheetComponent, + TearsheetStepComponent, + MockHostComponent, + MockFormStepComponent, + MockFormHostComponent + ], imports: [SharedModule], providers: [ { @@ -180,6 +232,25 @@ describe('TearsheetComponent', () => { }); }); + describe('nested form validation on next', () => { + it('should mark nested controls dirty and touched on next', () => { + const formHostFixture = TestBed.createComponent(MockFormHostComponent); + formHostFixture.detectChanges(); + const formHost = formHostFixture.componentInstance; + + const childControl = formHost.formStep.formGroup.get('parent.child'); + expect(childControl).toBeTruthy(); + expect(childControl?.dirty).toBe(false); + expect(childControl?.touched).toBe(false); + + formHost.tearsheet.onNext(); + + expect(childControl?.dirty).toBe(true); + expect(childControl?.touched).toBe(true); + expect(childControl?.hasError('required')).toBe(true); + }); + }); + describe('[stepValid] seeding and live sync', () => { it('should seed step as invalid immediately when stepValid starts false', () => { hostComponent.step1Valid = false; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts index 9dc6d1b1c887..2bd40d44a42f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts @@ -16,7 +16,7 @@ import { TemplateRef, ViewEncapsulation } from '@angular/core'; -import { FormBuilder } from '@angular/forms'; +import { AbstractControl, FormArray, FormBuilder, FormGroup } from '@angular/forms'; import { Step } from 'carbon-components-angular'; import { TearsheetStepComponent } from '../tearsheet-step/tearsheet-step.component'; import { ModalCdsService } from '../../services/modal-cds.service'; @@ -211,13 +211,12 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy, OnC onNext() { this.validateStep.emit({ step: this.currentStep }); - const wrapper = this.stepContents?.toArray()?.[this.currentStep]; - const legacyForm = wrapper?.resolvedFormGroup; - if (legacyForm) { - legacyForm.markAllAsTouched(); - legacyForm.updateValueAndValidity({ emitEvent: true }); - this._updateStepInvalid(this.currentStep, legacyForm.invalid); + const currentForm = wrapper?.resolvedFormGroup; + currentForm?.markAllAsTouched(); + this.markControlsDirtyAndValidate(currentForm); + if (currentForm) { + this._updateStepInvalid(this.currentStep, currentForm.invalid); } const canAdvance = wrapper ? wrapper.canProceed : true; @@ -243,7 +242,7 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy, OnC const form = wrapper.resolvedFormGroup; if (!form) return; form.markAllAsTouched(); - form.updateValueAndValidity({ emitEvent: true }); + this.markControlsDirtyAndValidate(form); this._updateStepInvalid(index, form.invalid); }); @@ -257,6 +256,17 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy, OnC this.submitRequested.emit(mergedPayloads); } + private markControlsDirtyAndValidate(control: AbstractControl | null) { + if (!control) { + return; + } + if (control instanceof FormGroup || control instanceof FormArray) { + Object.values(control.controls).forEach((child) => this.markControlsDirtyAndValidate(child)); + } + control.markAsDirty({ onlySelf: true }); + control.updateValueAndValidity({ onlySelf: true, emitEvent: true }); + } + closeFullTearsheet() { this.cdsModalService.show(ConfirmationModalComponent, { titleText: $localize`Are you sure you want to cancel ?`,