diff --git a/src/ValidationsFactory.js b/src/ValidationsFactory.js index 89177046b..e3f1cc430 100644 --- a/src/ValidationsFactory.js +++ b/src/ValidationsFactory.js @@ -1,6 +1,6 @@ import { validators } from './mixins/ValidationRules'; import DataProvider from './DataProvider'; -import { get, set } from 'lodash'; +import { get, set, merge } from 'lodash'; import { Parser } from 'expr-eval'; let globalObject = typeof window === 'undefined' @@ -112,9 +112,56 @@ class FormLoopValidations extends Validations { set(validations, this.element.config.name, {}); const loopField = get(validations, this.element.config.name); loopField['$each'] = {}; + this.checkForSiblings(validations); const firstRow = (get(this.data, this.element.config.name) || [{}])[0]; await ValidationsFactory(this.element.items, { screen: this.screen, data: {_parent: this.data, ...firstRow } }).addValidations(loopField['$each']); } + checkForSiblings(validations) { + const siblings = []; + const siblingValidations = []; + // Find loops that reference the same variable + this.screen.config.forEach(page => { + page.items.filter(item => { + if (item.component === 'FormLoop' && item.config.name === this.element.config.name) { + siblings.push(item); + } + }); + + // Get siblings validations + if (siblings) { + siblings.forEach(sibling => { + sibling.items.filter(item => { + if (!item.config.validation) { + return; + } + + item.config.validation.forEach(validation => { + const rule = this.camelCase(validation.value.split(':')[0]); + const validationFn = validators[rule]; + const obj = {}; + let ruleObj = {}; + ruleObj[rule] = validationFn; + obj[item.config.name] = ruleObj; + merge(siblingValidations, obj); + }); + }); + }); + } + }); + + if (Object.keys(siblingValidations).length != 0) { + // Update the loop validations with its siblings. + const loopValidations = get(validations, this.element.config.name); + if (loopValidations.hasOwnProperty('$each')) { + merge(loopValidations['$each'], siblingValidations); + } + set(validations[this.element.config.name]['$each'], loopValidations); + } + } + + camelCase(name) { + return name.replace(/_\w/g, m => m.substr(1, 1).toUpperCase()); + } } /** diff --git a/tests/e2e/fixtures/multi_loop_validations.json b/tests/e2e/fixtures/multi_loop_validations.json new file mode 100644 index 000000000..cf980586f --- /dev/null +++ b/tests/e2e/fixtures/multi_loop_validations.json @@ -0,0 +1 @@ +{"type":"screen_package","version":"2","screens":[{"id":16,"screen_category_id":"1","title":"Loop Validations","description":"test","type":"FORM","config":[{"name":"Loop Validations","items":[{"label":"Rich Text","config":{"icon":"fas fa-pencil-ruler","label":null,"content":"

Multiple Loops<\/p>","interactive":true,"renderVarHtml":false},"component":"FormHtmlViewer","inspector":[{"type":"FormTextArea","field":"content","config":{"rows":5,"label":"Content","value":null,"helper":"The HTML text to display"}},{"type":"FormCheckbox","field":"renderVarHtml","config":{"label":"Render HTML from a Variable","value":null,"helper":null}},{"type":"FormInput","field":"conditionalHide","config":{"label":"Visibility Rule","helper":"This control is hidden until this expression is true"}},{"type":"FormInput","field":"customFormatter","config":{"label":"Custom Format String","helper":"Use the Mask Pattern format
Date ##\/##\/####
SSN ###-##-####
Phone (###) ###-####","validation":null}},{"type":"FormInput","field":"customCssSelector","config":{"label":"CSS Selector Name","helper":"Use this in your custom css rules","validation":"regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]"}}],"editor-control":"FormHtmlEditor","editor-component":"FormHtmlEditor"},{"items":[{"label":"Line Input","config":{"icon":"far fa-square","name":"form_input_1","type":"text","label":"New Input","helper":null,"readonly":false,"dataFormat":"string","validation":[{"value":"required","helper":"Checks if the length of the String representation of the value is >","content":"Required"}],"placeholder":null},"component":"FormInput","inspector":[{"type":"FormInput","field":"name","config":{"name":"Variable Name","label":"Variable Name","helper":"A variable name is a symbolic name to reference information.","validation":"regex:\/^([a-zA-Z]([a-zA-Z0-9_]?)+\\.?)+(? Date ##\/##\/####
SSN ###-##-####
Phone (###) ###-####","validation":null}},{"type":"FormInput","field":"customCssSelector","config":{"label":"CSS Selector Name","helper":"Use this in your custom css rules","validation":"regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]"}}],"editor-control":"FormInput","editor-component":"FormInput"}],"label":"Loop","config":{"icon":"fas fa-redo","name":"accounts","label":null,"settings":{"add":false,"type":"existing","times":"3","varname":"accounts"},"conditionalHide":null},"component":"FormLoop","container":true,"inspector":[{"type":"LoopInspector","field":"settings","config":{"label":null,"helper":null}},{"type":"FormInput","field":"conditionalHide","config":{"label":"Visibility Rule","helper":"This control is hidden until this expression is true"}},{"type":"FormInput","field":"customFormatter","config":{"label":"Custom Format String","helper":"Use the Mask Pattern format
Date ##\/##\/####
SSN ###-##-####
Phone (###) ###-####","validation":null}},{"type":"FormInput","field":"customCssSelector","config":{"label":"CSS Selector Name","helper":"Use this in your custom css rules","validation":"regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]"}}],"editor-control":"Loop","editor-component":"Loop"},{"items":[{"label":"Line Input","config":{"icon":"far fa-square","name":"form_input_2","type":"text","label":"New Input","helper":null,"readonly":false,"dataFormat":"string","validation":[{"value":"required","helper":"Checks if the length of the String representation of the value is >","content":"Required"}],"placeholder":null},"component":"FormInput","inspector":[{"type":"FormInput","field":"name","config":{"name":"Variable Name","label":"Variable Name","helper":"A variable name is a symbolic name to reference information.","validation":"regex:\/^([a-zA-Z]([a-zA-Z0-9_]?)+\\.?)+(? Date ##\/##\/####
SSN ###-##-####
Phone (###) ###-####","validation":null}},{"type":"FormInput","field":"customCssSelector","config":{"label":"CSS Selector Name","helper":"Use this in your custom css rules","validation":"regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]"}}],"editor-control":"FormInput","editor-component":"FormInput"}],"label":"Loop","config":{"icon":"fas fa-redo","name":"accounts","label":null,"settings":{"add":false,"type":"existing","times":"3","varname":"accounts"}},"component":"FormLoop","container":true,"inspector":[{"type":"LoopInspector","field":"settings","config":{"label":null,"helper":null}},{"type":"FormInput","field":"conditionalHide","config":{"label":"Visibility Rule","helper":"This control is hidden until this expression is true"}},{"type":"FormInput","field":"customFormatter","config":{"label":"Custom Format String","helper":"Use the Mask Pattern format
Date ##\/##\/####
SSN ###-##-####
Phone (###) ###-####","validation":null}},{"type":"FormInput","field":"customCssSelector","config":{"label":"CSS Selector Name","helper":"Use this in your custom css rules","validation":"regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]"}}],"editor-control":"Loop","editor-component":"Loop"},{"label":"Submit Button","config":{"icon":"fas fa-share-square","name":null,"event":"submit","label":"New Submit","variant":"primary","fieldValue":null,"defaultSubmit":true},"component":"FormButton","inspector":[{"type":"FormInput","field":"label","config":{"label":"Label","helper":"The label describes the button's text"}},{"type":"FormInput","field":"name","config":{"name":"Variable Name","label":"Variable Name","helper":"A variable name is a symbolic name to reference information."}},{"type":"FormMultiselect","field":"event","config":{"label":"Type","helper":"Choose whether the button should submit the form","options":[{"value":"submit","content":"Submit Button"},{"value":"script","content":"Regular Button"}]}},{"type":"FormInput","field":"fieldValue","config":{"label":"Value","helper":"The value being submitted"}},{"type":"FormMultiselect","field":"variant","config":{"label":"Button Variant Style","helper":"The variant determines the appearance of the button","options":[{"value":"primary","content":"Primary"},{"value":"secondary","content":"Secondary"},{"value":"success","content":"Success"},{"value":"danger","content":"Danger"},{"value":"warning","content":"Warning"},{"value":"info","content":"Info"},{"value":"light","content":"Light"},{"value":"dark","content":"Dark"},{"value":"link","content":"Link"}]}},{"type":"FormInput","field":"conditionalHide","config":{"label":"Visibility Rule","helper":"This control is hidden until this expression is true"}},{"type":"FormInput","field":"customFormatter","config":{"label":"Custom Format String","helper":"Use the Mask Pattern format
Date ##\/##\/####
SSN ###-##-####
Phone (###) ###-####","validation":null}},{"type":"FormInput","field":"customCssSelector","config":{"label":"CSS Selector Name","helper":"Use this in your custom css rules","validation":"regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]"}}],"editor-control":"FormSubmit","editor-component":"FormButton"}]}],"computed":[],"custom_css":null,"created_at":"2022-01-19T16:57:54+00:00","updated_at":"2022-01-26T16:34:30+00:00","status":"ACTIVE","key":null,"watchers":[],"categories":[{"id":1,"name":"Uncategorized","status":"ACTIVE","is_system":0,"created_at":"2021-12-22T18:38:53+00:00","updated_at":"2021-12-22T18:38:53+00:00","pivot":{"assignable_id":16,"category_id":1,"category_type":"ProcessMaker\\Models\\ScreenCategory"}}]}],"screen_categories":[],"scripts":[]} \ No newline at end of file diff --git a/tests/e2e/specs/Loop.spec.js b/tests/e2e/specs/Loop.spec.js index af106c996..fae815b8c 100644 --- a/tests/e2e/specs/Loop.spec.js +++ b/tests/e2e/specs/Loop.spec.js @@ -91,4 +91,32 @@ describe('Loop control', () => { expect(str).to.equal('Preview Form was Submitted'); }); }); + + it('Runs validations on loops referencing same variable ', () => { + cy.visit('/'); + cy.server(); + let alert = false; + cy.on('window:alert', msg => alert = msg); + cy.loadFromJson('multi_loop_validations.json', 0); + + cy.setPreviewDataInput('{"accounts": [{"name": "foobar"}]}'); + + cy.get('[data-cy=mode-preview]').click(); + + // Add data to input field in last loop + cy.get('[data-cy=screen-field-form_input_2]').type('bar'); + cy.wait(1000); + + // Ensure the form cannot yet be submitted + cy.get(':nth-child(4) > .form-group > .btn') + .click() + .then(() => expect(alert).to.equal(false)); + + // Fill out the required missing field; ensure the form *can* be submitted + cy.get('[data-cy=screen-field-form_input_1]').type('text'); + + cy.get(':nth-child(4) > .form-group > .btn') + .click() + .then(() => expect(alert).to.equal('Preview Form was Submitted')); + }); });