From dce7435b3c125b48de19e0402ba0ec61b046a643 Mon Sep 17 00:00:00 2001 From: sanjacornelius Date: Tue, 25 Jan 2022 14:11:00 -0800 Subject: [PATCH 1/6] Check for loops that reference the same variable and merge their validations --- src/ValidationsFactory.js | 48 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/ValidationsFactory.js b/src/ValidationsFactory.js index 89177046b..66f288f84 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, isEmpty, set } from 'lodash'; import { Parser } from 'expr-eval'; let globalObject = typeof window === 'undefined' @@ -110,11 +110,57 @@ class FormLoopValidations extends Validations { return; } set(validations, this.element.config.name, {}); + validations = this.checkForSiblings(validations); const loopField = get(validations, this.element.config.name); loopField['$each'] = {}; 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 => { + 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); + setTimeout(() => { + if (loopValidations.hasOwnProperty('$each')) { + _.merge(loopValidations['$each'], siblingValidations); + } + set(validations[this.element.config.name]['$each'], loopValidations); + }, 1000); + } + return validations; + } + + camelCase(name) { + return name.replace(/_\w/g, m => m.substr(1, 1).toUpperCase()); + } } /** From b5220c5ad4e8e473c372cc4e0113c0bb526b2103 Mon Sep 17 00:00:00 2001 From: sanjacornelius Date: Wed, 26 Jan 2022 09:10:47 -0800 Subject: [PATCH 2/6] Fix foreach is not a function console error --- src/ValidationsFactory.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ValidationsFactory.js b/src/ValidationsFactory.js index 66f288f84..aa9a969d6 100644 --- a/src/ValidationsFactory.js +++ b/src/ValidationsFactory.js @@ -131,6 +131,10 @@ class FormLoopValidations extends 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]; From b8c5a4679e17359dbef20ea0f493def38af2fe09 Mon Sep 17 00:00:00 2001 From: sanjacornelius Date: Wed, 26 Jan 2022 09:11:12 -0800 Subject: [PATCH 3/6] Add test to ensure validations work on multiple loops --- .../e2e/fixtures/multi_loop_validations.json | 1 + tests/e2e/specs/Loop.spec.js | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 tests/e2e/fixtures/multi_loop_validations.json 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..7d1c743a7 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')); + }); }); From 2f7000e2bd30107e7ae64937141bcdab5d655a07 Mon Sep 17 00:00:00 2001 From: sanjacornelius Date: Wed, 26 Jan 2022 09:23:23 -0800 Subject: [PATCH 4/6] Fix lint errors --- tests/e2e/specs/Loop.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e/specs/Loop.spec.js b/tests/e2e/specs/Loop.spec.js index 7d1c743a7..fae815b8c 100644 --- a/tests/e2e/specs/Loop.spec.js +++ b/tests/e2e/specs/Loop.spec.js @@ -109,14 +109,14 @@ describe('Loop control', () => { // Ensure the form cannot yet be submitted cy.get(':nth-child(4) > .form-group > .btn') - .click() - .then(() => expect(alert).to.equal(false)); + .click() + .then(() => expect(alert).to.equal(false)); - // Fill out the required missing field; ensure the form *can* be submitted + // 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')); - }); + }); }); From 0e573c808d8efcbf7ee865c125ebad6c5c471ccd Mon Sep 17 00:00:00 2001 From: sanjacornelius Date: Wed, 26 Jan 2022 10:07:00 -0800 Subject: [PATCH 5/6] Fix lint errors --- src/ValidationsFactory.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ValidationsFactory.js b/src/ValidationsFactory.js index aa9a969d6..5a4778a19 100644 --- a/src/ValidationsFactory.js +++ b/src/ValidationsFactory.js @@ -1,6 +1,6 @@ import { validators } from './mixins/ValidationRules'; import DataProvider from './DataProvider'; -import { get, isEmpty, set } from 'lodash'; +import { get, set, merge } from 'lodash'; import { Parser } from 'expr-eval'; let globalObject = typeof window === 'undefined' @@ -134,7 +134,7 @@ class FormLoopValidations extends Validations { if (!item.config.validation) { return; } - + item.config.validation.forEach(validation => { const rule = this.camelCase(validation.value.split(':')[0]); const validationFn = validators[rule]; @@ -142,7 +142,7 @@ class FormLoopValidations extends Validations { let ruleObj = {}; ruleObj[rule] = validationFn; obj[item.config.name] = ruleObj; - _.merge(siblingValidations, obj); + merge(siblingValidations, obj); }); }); }); @@ -154,7 +154,7 @@ class FormLoopValidations extends Validations { const loopValidations = get(validations, this.element.config.name); setTimeout(() => { if (loopValidations.hasOwnProperty('$each')) { - _.merge(loopValidations['$each'], siblingValidations); + merge(loopValidations['$each'], siblingValidations); } set(validations[this.element.config.name]['$each'], loopValidations); }, 1000); From d49cab89b56412de7d0cc4f62f8f5957664ce08e Mon Sep 17 00:00:00 2001 From: sanjacornelius Date: Fri, 28 Jan 2022 11:20:11 -0800 Subject: [PATCH 6/6] Clean up code & remove the settimeout function --- src/ValidationsFactory.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/ValidationsFactory.js b/src/ValidationsFactory.js index 5a4778a19..e3f1cc430 100644 --- a/src/ValidationsFactory.js +++ b/src/ValidationsFactory.js @@ -110,9 +110,9 @@ class FormLoopValidations extends Validations { return; } set(validations, this.element.config.name, {}); - validations = this.checkForSiblings(validations); 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']); } @@ -152,14 +152,11 @@ class FormLoopValidations extends Validations { if (Object.keys(siblingValidations).length != 0) { // Update the loop validations with its siblings. const loopValidations = get(validations, this.element.config.name); - setTimeout(() => { - if (loopValidations.hasOwnProperty('$each')) { - merge(loopValidations['$each'], siblingValidations); - } - set(validations[this.element.config.name]['$each'], loopValidations); - }, 1000); + if (loopValidations.hasOwnProperty('$each')) { + merge(loopValidations['$each'], siblingValidations); + } + set(validations[this.element.config.name]['$each'], loopValidations); } - return validations; } camelCase(name) {