From bbaf317abf7512fb7c5359f66aa7eaa9dff95657 Mon Sep 17 00:00:00 2001 From: brendanbond Date: Wed, 27 Aug 2025 14:08:54 +0000 Subject: [PATCH] $'syncing commit from monorepo. PR: 416, Title: FIO-10539: fixed server validation when instance.path is used and fixed front-end validation for nested data components with layout children' --- src/components/Components.js | 4 +- src/utils/formUtils.js | 13 ++- ...idationForContainerWithNestedLayoutComp.js | 90 +++++++++++++++++++ test/unit/Webform.unit.js | 16 +++- 4 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 test/forms/testCustomValidationForContainerWithNestedLayoutComp.js diff --git a/src/components/Components.js b/src/components/Components.js index 49317f49e7..254b6eec74 100644 --- a/src/components/Components.js +++ b/src/components/Components.js @@ -2,6 +2,7 @@ import Component from './_classes/component/Component'; import EditFormUtils from './_classes/component/editForm/utils'; import BaseEditForm from './_classes/component/Component.form'; import _ from 'lodash'; +import { isDataComponent } from '../utils'; export default class Components { static _editFormUtils = EditFormUtils; @@ -80,7 +81,8 @@ export default class Components { else { comp = new Component(component, options, data); } - if (comp.path) { + + if (comp.path && isDataComponent(comp.component)) { comp.componentsMap[comp.path] = comp; } // Reset the componentMatches on the root element if any new component is created. diff --git a/src/utils/formUtils.js b/src/utils/formUtils.js index 16cedb052b..7ddebbe7bf 100644 --- a/src/utils/formUtils.js +++ b/src/utils/formUtils.js @@ -47,6 +47,16 @@ const { getItemTemplateKeys } = Utils; +/** + * Checks if the component is expected to have data in submission object. + * @param {import('@formio/core').Component} component - The component to check. + * @returns {boolean} - TRUE if the component is a data component. + */ +function isDataComponent(component) { + const modelType = getModelType(component); + return modelType !== 'none' && modelType !== 'content'; +} + export { flattenComponents, guid, @@ -92,5 +102,6 @@ export { isComponentDataEmpty, isSelectResourceWithObjectValue, compareSelectResourceWithObjectTypeValues, - getItemTemplateKeys + getItemTemplateKeys, + isDataComponent }; diff --git a/test/forms/testCustomValidationForContainerWithNestedLayoutComp.js b/test/forms/testCustomValidationForContainerWithNestedLayoutComp.js new file mode 100644 index 0000000000..6ff707adc8 --- /dev/null +++ b/test/forms/testCustomValidationForContainerWithNestedLayoutComp.js @@ -0,0 +1,90 @@ +export default { + _id: '68ac2d19ca88b30e3544e46c', + title: 'Path example', + name: 'pathExample', + path: 'pathexample', + type: 'form', + display: 'form', + owner: '68ac2d7fca88b30e3544e83e', + components: [ + { + label: 'Your contact details', + tableView: false, + validate: { + required: true, + custom: + 'if (_.isEmpty(_.get(data,`${instance.path}.phone`)) && \n_.isEmpty(_.get(data,`${instance.path}.email`))) {\n valid=`One of either phone or email must be provided.`;\n}', + }, + key: 'contact', + type: 'container', + input: true, + components: [ + { + label: 'HTML', + attrs: [ + { + attr: '', + value: '', + }, + ], + content: 'utuut', + refreshOnChange: false, + key: 'htmloooo', + type: 'htmlelement', + input: false, + tableView: false, + }, + { + label: 'Panel', + collapsible: false, + key: 'panel', + type: 'panel', + input: false, + tableView: false, + components: [ + { + label: 'Number', + applyMaskOn: 'change', + mask: false, + tableView: false, + delimiter: false, + requireDecimal: false, + inputFormat: 'plain', + truncateMultipleSpaces: false, + validateWhenHidden: false, + key: 'number', + type: 'number', + input: true, + }, + ], + }, + { + label: 'Phone', + applyMaskOn: 'change', + tableView: true, + key: 'phone', + type: 'textfield', + input: true, + }, + { + label: 'Email', + applyMaskOn: 'change', + tableView: true, + key: 'email', + type: 'email', + input: true, + }, + ], + }, + { + label: 'Submit', + tableView: false, + key: 'submit', + type: 'button', + input: true, + saveOnEnter: false, + }, + ], + machineName: 'izutenexavvxnws:pathExample', + project: '67211a9aa929e4e6ebc2bf77', +}; diff --git a/test/unit/Webform.unit.js b/test/unit/Webform.unit.js index dc1214a3a5..a2effd9e02 100644 --- a/test/unit/Webform.unit.js +++ b/test/unit/Webform.unit.js @@ -97,6 +97,7 @@ import formWithMergeComponentSchemaAndCustomLogic from '../forms/formWithMergeCo import formWithServerValidation from '../forms/formWithServerValidation.js'; import formWithNumbers from '../forms/formWithNumbers.js'; import { wait } from '../util.js'; +import testCustomValidationForContainerWithNestedLayoutComp from '../forms/testCustomValidationForContainerWithNestedLayoutComp.js'; const SpySanitize = sinon.spy(FormioUtils, 'sanitize'); @@ -108,7 +109,20 @@ if (_.has(Formio, 'Components.setComponents')) { describe('Webform tests', function() { this.retries(3); - it('Should show form validation errors if we insert invalid underscore data into a number component', async () => { + it('Should show validation errors on submit for container with layout components inside', (done) => { + Formio.createForm(document.createElement('div'), testCustomValidationForContainerWithNestedLayoutComp).then(form => { + assert.equal(!!form.visibleErrors.length, false); + form.submit(); + setTimeout(() => { + assert.equal(form.errors.length, 1); + assert.equal(form.errors[0].message, 'One of either phone or email must be provided.'); + assert.equal(!!form.visibleErrors.length, true); + done(); + }, 400); + }).catch((err) => done(err)); + }); + + it('Should show form validation errors if we insert invalid underscore data into a number component', async () => { const form = await Formio.createForm(document.createElement('div'), formWithNumbers); const numberDatagrid = form.getComponent('dataGrid.number'); const number = form.getComponent('number');