]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard : If user is selected auto-fetch option while creating subsystems then... 70515/head
authorpujashahu <pshahu@redhat.com>
Fri, 24 Jul 2026 08:29:01 +0000 (13:59 +0530)
committerpujashahu <pshahu@redhat.com>
Mon, 27 Jul 2026 08:31:06 +0000 (14:01 +0530)
Fixes: https://tracker.ceph.com/issues/78641
Signed-off-by: pujashahu <pshahu@redhat.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-1/nvmeof-subsystem-step-1.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-1/nvmeof-subsystem-step-1.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts

index 1496c01d96331ccb3834fa9ac5ef6e842a8cf2bd..0dc7956315007543cd12d5bb2fdd6854c26eec22 100644 (file)
           <cds-text-label
             i18n
             [invalid]="subnetMaskRef.isInvalid"
-            [invalidText]="subnetMaskInvalidText"
+            [invalidText]="requiredFieldInvalidText"
             helperText="Listeners from this subnet-masks will be use."
             i18n-helperText
           >
               [invalid]="subnetMaskRef.isInvalid"
             />
           </cds-text-label>
-          <ng-template #subnetMaskInvalidText>
+          <ng-template #requiredFieldInvalidText>
             <span i18n>This field is required.</span>
           </ng-template>
         </div>
index 237a76cb3d126013a8ff5c29621bf71b4cad99a8..56d887983b84066ea955ac3e6a68111e3d5798af 100644 (file)
@@ -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();
+    });
   });
 });
index ffe9576c6bb307d23b201406b47f55519181ab47..7405093f55e03d7c441a1dea60f3034609212fea 100644 (file)
@@ -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: `
+    <cd-tearsheet
+      [steps]="steps"
+      [title]="title"
+      [description]="description"
+    >
+      <cd-tearsheet-step>
+        <cd-mock-form-step #tearsheetStep></cd-mock-form-step>
+      </cd-tearsheet-step>
+      <cd-tearsheet-step>
+        <div>Step 2</div>
+      </cd-tearsheet-step>
+    </cd-tearsheet>
+  `,
+  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<MockHostComponent>;
   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;
index 9dc6d1b1c887ba9c3a57e47c18b89bc6acb4cdc7..2bd40d44a42f1c39dd0be091c17c483c95667910 100644 (file)
@@ -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 ?`,