From 20c4b0fbb3f164efc00a89c99ccfad75a0537022 Mon Sep 17 00:00:00 2001 From: sanjacornelius Date: Thu, 27 Jan 2022 08:54:11 -0800 Subject: [PATCH 1/4] Check for sibling loops that reference the same variable --- src/ValidationsFactory.js | 51 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/ValidationsFactory.js b/src/ValidationsFactory.js index 6902b11f1..ad0f85c61 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' @@ -110,11 +110,60 @@ 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 => { + 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); + 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 d0ea154a1e2520ee73fcbdc4a32a10139a04c239 Mon Sep 17 00:00:00 2001 From: sanjacornelius Date: Thu, 27 Jan 2022 08:55:04 -0800 Subject: [PATCH 2/4] Test validations 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..3bb2f5649 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 e588088e245566a271c17ec3443de92ca4e74436 Mon Sep 17 00:00:00 2001 From: sanjacornelius Date: Fri, 28 Jan 2022 11:23:11 -0800 Subject: [PATCH 3/4] clean code & remove settimeout function --- src/ValidationsFactory.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/ValidationsFactory.js b/src/ValidationsFactory.js index ad0f85c61..c436624b6 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) { return name.replace(/_\w/g, m => m.substr(1, 1).toUpperCase()); From dfa8b23d18da96fb08cea3ec05b64822216c54b6 Mon Sep 17 00:00:00 2001 From: sanjacornelius Date: Fri, 25 Feb 2022 09:54:26 -0800 Subject: [PATCH 4/4] Fix lint errors --- tests/e2e/specs/Loop.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/specs/Loop.spec.js b/tests/e2e/specs/Loop.spec.js index bf21f2870..ee26cc19e 100644 --- a/tests/e2e/specs/Loop.spec.js +++ b/tests/e2e/specs/Loop.spec.js @@ -115,7 +115,7 @@ describe('Loop control', () => { // 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') + cy.get(':nth-child(4) > .form-group > .btn'); });