AAE-49688 fix for hidden fields in groups holding up validations (#12135)

This commit is contained in:
Alex Molodyh
2026-08-06 22:49:17 -07:00
committed by GitHub
parent 4cbe97c12c
commit 33cf47342d
3 changed files with 231 additions and 2 deletions
@@ -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;
}
@@ -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',
@@ -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,