From 33cf47342d04be1d29334b753ff168aadbd1f4e1 Mon Sep 17 00:00:00 2001 From: Alex Molodyh <140214274+amolodyh-hyland@users.noreply.github.com> Date: Thu, 6 Aug 2026 22:49:17 -0700 Subject: [PATCH] AAE-49688 fix for hidden fields in groups holding up validations (#12135) --- .../widgets/core/form-field.model.ts | 22 +- .../widgets/core/form.model.spec.ts | 192 ++++++++++++++++++ .../components/widgets/core/form.model.ts | 19 +- 3 files changed, 231 insertions(+), 2 deletions(-) diff --git a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts index 0603a7c272..6241cf8f84 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts @@ -483,10 +483,30 @@ export class FormFieldModel extends FormWidgetModel { return; } - this.rows.push(this.createRow(fields, form, this.rows.length)); + const row = this.createRow(fields, form, this.rows.length); + this.copyParentVisibilityValidationState(this.columns, row.columns); + this.rows.push(row); this.form.onRepeatableSectionRowCountChanged(this); } + private copyParentVisibilityValidationState(sourceColumns: ContainerColumnModel[], targetColumns: ContainerColumnModel[]): void { + const sourceFields = sourceColumns.flatMap((column) => column.fields); + + for (const targetColumn of targetColumns) { + for (const targetField of targetColumn.fields) { + const sourceField = sourceFields.find((field) => field.json.id === targetField.json.id); + if (!sourceField) { + continue; + } + + targetField.checkParentVisibilityForValidation = sourceField.checkParentVisibilityForValidation; + if (targetField.type === FormFieldTypes.SECTION) { + this.copyParentVisibilityValidationState(sourceField.columns, targetField.columns); + } + } + } + } + private shouldAddRow(): boolean { return !this.params.maxNumberOfRows || this.rows.length < this.params.maxNumberOfRows; } diff --git a/lib/core/src/lib/form/components/widgets/core/form.model.spec.ts b/lib/core/src/lib/form/components/widgets/core/form.model.spec.ts index b4686ddea1..f2a834bb54 100644 --- a/lib/core/src/lib/form/components/widgets/core/form.model.spec.ts +++ b/lib/core/src/lib/form/components/widgets/core/form.model.spec.ts @@ -820,6 +820,50 @@ describe('FormModel', () => { describe('FormModel - isFieldOrParentHidden', () => { let form: FormModel; + const repeatableSectionFormJson = (checkParentVisibilityForValidation: boolean) => ({ + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'repeatableSection1', + type: FormFieldTypes.REPEATABLE_SECTION, + numberOfColumns: 1, + params: { initialNumberOfRows: 2 }, + fields: { + 1: [{ id: 'field1', type: FormFieldTypes.TEXT, required: true, checkParentVisibilityForValidation }] + } + } + ] + }); + + const sectionInRepeatableSectionFormJson = (checkParentVisibilityForValidation: boolean) => ({ + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'repeatableSection1', + type: FormFieldTypes.REPEATABLE_SECTION, + numberOfColumns: 1, + params: { initialNumberOfRows: 2 }, + fields: { + 1: [ + { + id: 'section1', + type: FormFieldTypes.SECTION, + numberOfColumns: 1, + fields: { + 1: [{ id: 'field1', type: FormFieldTypes.TEXT, required: true, checkParentVisibilityForValidation }] + } + } + ] + } + } + ] + }); + + const getRepeatableSectionField = (repeatableSection: ContainerModel, rowIndex: number): FormFieldModel => + repeatableSection.field.rows[rowIndex].columns[0].fields[0]; + beforeEach(() => { form = new FormModel(); }); @@ -998,6 +1042,154 @@ describe('FormModel', () => { expect(testForm.isFieldOrParentHidden(field)).toBe(true); }); + it('should return true for field in the first row of a hidden repeatable section when opt-in is enabled', () => { + const testForm = new FormModel(repeatableSectionFormJson(true)); + testForm.enableParentVisibilityCheck = true; + const repeatableSection = testForm.fields[0] as ContainerModel; + repeatableSection.field.isVisible = false; + const field = getRepeatableSectionField(repeatableSection, 0); + field.isVisible = true; + expect(testForm.isFieldOrParentHidden(field)).toBe(true); + }); + + it('should return true for field in a later row of a hidden repeatable section when opt-in is enabled', () => { + const testForm = new FormModel(repeatableSectionFormJson(true)); + testForm.enableParentVisibilityCheck = true; + const repeatableSection = testForm.fields[0] as ContainerModel; + repeatableSection.field.isVisible = false; + const field = getRepeatableSectionField(repeatableSection, 1); + field.isVisible = true; + expect(testForm.isFieldOrParentHidden(field)).toBe(true); + }); + + it('should return true for field in a section nested in a hidden repeatable section when opt-in is enabled', () => { + const testForm = new FormModel(sectionInRepeatableSectionFormJson(true)); + testForm.enableParentVisibilityCheck = true; + const repeatableSection = testForm.fields[0] as ContainerModel; + repeatableSection.field.isVisible = false; + const nestedSection = getRepeatableSectionField(repeatableSection, 1); + nestedSection.isVisible = true; + const field = nestedSection.columns[0].fields[0]; + field.isVisible = true; + expect(testForm.isFieldOrParentHidden(field)).toBe(true); + }); + + it('should return false for field in a later row of a visible repeatable section when opt-in is enabled', () => { + const testForm = new FormModel(repeatableSectionFormJson(true)); + testForm.enableParentVisibilityCheck = true; + const repeatableSection = testForm.fields[0] as ContainerModel; + repeatableSection.field.isVisible = true; + const field = getRepeatableSectionField(repeatableSection, 1); + field.isVisible = true; + expect(testForm.isFieldOrParentHidden(field)).toBe(false); + }); + + it('should return false for field in hidden repeatable section when opt-in is disabled - backward compatible', () => { + const testForm = new FormModel(repeatableSectionFormJson(false)); + const repeatableSection = testForm.fields[0] as ContainerModel; + repeatableSection.field.isVisible = false; + const field = getRepeatableSectionField(repeatableSection, 1); + field.isVisible = true; + expect(testForm.isFieldOrParentHidden(field)).toBe(false); + }); + + it('should enable the parent visibility check on a repeatable section field added after model opt-in', () => { + const testForm = new FormModel(repeatableSectionFormJson(false)); + testForm.enableParentVisibilityCheck = true; + testForm.getFormFields().forEach((field) => (field.checkParentVisibilityForValidation = true)); + const repeatableSection = testForm.fields[0] as ContainerModel; + + repeatableSection.field.addRow(repeatableSection.json.fields, repeatableSection.form); + + const field = getRepeatableSectionField(repeatableSection, 2); + expect(field.checkParentVisibilityForValidation).toBe(true); + }); + + it('should preserve selective parent visibility checks when adding a repeatable section row', () => { + const testForm = new FormModel({ + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'repeatableSection1', + type: FormFieldTypes.REPEATABLE_SECTION, + numberOfColumns: 1, + params: { initialNumberOfRows: 1 }, + fields: { + 1: [ + { id: 'optedInField', type: FormFieldTypes.TEXT }, + { id: 'optedOutField', type: FormFieldTypes.TEXT } + ] + } + }, + { id: 'unrelatedField', type: FormFieldTypes.TEXT } + ] + }); + testForm.enableParentVisibilityCheck = true; + const repeatableSection = testForm.fields[0] as ContainerModel; + const sourceFields = repeatableSection.field.rows[0].columns[0].fields; + sourceFields[0].checkParentVisibilityForValidation = true; + + repeatableSection.field.addRow(repeatableSection.json.fields, repeatableSection.form); + + const addedFields = repeatableSection.field.rows[1].columns[0].fields; + expect(addedFields[0].checkParentVisibilityForValidation).toBe(true); + expect(addedFields[1].checkParentVisibilityForValidation).toBe(false); + expect(testForm.getFieldById('unrelatedField').checkParentVisibilityForValidation).toBe(false); + }); + + it('should copy nested field parent visibility checks to a new repeatable section row', () => { + const testForm = new FormModel(sectionInRepeatableSectionFormJson(false)); + testForm.enableParentVisibilityCheck = true; + const repeatableSection = testForm.fields[0] as ContainerModel; + const sourceSection = getRepeatableSectionField(repeatableSection, 0); + sourceSection.columns[0].fields[0].checkParentVisibilityForValidation = true; + + repeatableSection.field.addRow(repeatableSection.json.fields, repeatableSection.form); + + const addedSection = getRepeatableSectionField(repeatableSection, 2); + expect(addedSection.checkParentVisibilityForValidation).toBe(false); + expect(addedSection.columns[0].fields[0].checkParentVisibilityForValidation).toBe(true); + }); + + it('should copy template parent visibility checks when adding the first repeatable section row', () => { + const formJson = repeatableSectionFormJson(false); + formJson.fields[0].params.initialNumberOfRows = 0; + const testForm = new FormModel(formJson); + testForm.enableParentVisibilityCheck = true; + const repeatableSection = testForm.fields[0] as ContainerModel; + repeatableSection.field.columns[0].fields[0].checkParentVisibilityForValidation = true; + + repeatableSection.field.addRow(repeatableSection.json.fields, repeatableSection.form); + + const field = getRepeatableSectionField(repeatableSection, 0); + expect(field.checkParentVisibilityForValidation).toBe(true); + }); + + it('should exclude a required field added to a hidden repeatable section from validation after model opt-in', () => { + const testForm = new FormModel(repeatableSectionFormJson(false)); + testForm.enableParentVisibilityCheck = true; + testForm.getFormFields().forEach((field) => (field.checkParentVisibilityForValidation = true)); + const repeatableSection = testForm.fields[0] as ContainerModel; + repeatableSection.field.addRow(repeatableSection.json.fields, repeatableSection.form); + repeatableSection.field.isVisible = false; + + testForm.validateForm(); + + expect(testForm.isValid).toBe(true); + }); + + it('should preserve validation for a required field added to a hidden repeatable section without model opt-in', () => { + const testForm = new FormModel(repeatableSectionFormJson(false)); + const repeatableSection = testForm.fields[0] as ContainerModel; + repeatableSection.field.addRow(repeatableSection.json.fields, repeatableSection.form); + repeatableSection.field.isVisible = false; + + testForm.validateForm(); + + expect(testForm.isValid).toBe(false); + }); + it('should return false for visible field with visible parents - ContainerModel', () => { const formJson = { id: 'test-form', diff --git a/lib/core/src/lib/form/components/widgets/core/form.model.ts b/lib/core/src/lib/form/components/widgets/core/form.model.ts index 3cacf92904..2fea8ce8dd 100644 --- a/lib/core/src/lib/form/components/widgets/core/form.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/form.model.ts @@ -613,13 +613,30 @@ export class FormModel implements ProcessFormModel { private getColumnsFromElement(element: ContainerModel | FormFieldModel): ContainerColumnModel[] | null { if (element instanceof ContainerModel) { - return element.field?.columns || null; + return FormFieldTypes.isRepeatableSectionType(element.field?.type) + ? this.getColumnsFromAllRows(element.field) + : element.field?.columns || null; } else if (element instanceof FormFieldModel && element.type === FormFieldTypes.SECTION) { return element.columns || null; } return null; } + private getColumnsFromAllRows(field: FormFieldModel): ContainerColumnModel[] | null { + if (!field.rows?.length) { + return field.columns || null; + } + + const columns: ContainerColumnModel[] = []; + for (const row of field.rows) { + if (row?.columns?.length) { + columns.push(...row.columns); + } + } + + return columns.length > 0 ? columns : null; + } + private searchFieldsInColumns( columns: ContainerColumnModel[], parentElement: ContainerModel | FormFieldModel,