From 0f1a78f969ce83c6ae1732214dacc2d36cea4cf1 Mon Sep 17 00:00:00 2001 From: mikekotikov Date: Fri, 15 Nov 2024 15:24:08 +0100 Subject: [PATCH 1/2] FIO-9354: Fix custom translation not applied to error message --- src/utils/i18n.js | 4 ++++ test/forms/translationErrorMessages.js | 25 +++++++++++++++++++++++++ test/unit/Webform.unit.js | 24 ++++++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 test/forms/translationErrorMessages.js diff --git a/src/utils/i18n.js b/src/utils/i18n.js index b1711d910c..c92b2ce950 100644 --- a/src/utils/i18n.js +++ b/src/utils/i18n.js @@ -83,6 +83,10 @@ export class I18n { t(text, ...args) { if (this.currentLanguage[text]) { + const customTranslationFieldName = args[0]?.field; + if (customTranslationFieldName && this.currentLanguage[customTranslationFieldName]) { + args[0].field = this.currentLanguage[customTranslationFieldName] + } return Evaluator.interpolateString(this.currentLanguage[text], ...args); } return Evaluator.interpolateString(text, ...args); diff --git a/test/forms/translationErrorMessages.js b/test/forms/translationErrorMessages.js new file mode 100644 index 0000000000..f4a78afd5e --- /dev/null +++ b/test/forms/translationErrorMessages.js @@ -0,0 +1,25 @@ +export default { + name: "textrandom", + path: "textrandom", + type: "form", + display: "form", + components: [ + { + label: "My textField", + applyMaskOn: "change", + tableView: true, + validate: { + minLength: 5, + minWords: 2 + }, + validateWhenHidden: false, + key: "textField", + type: "textfield", + input: true + }, + ], + created: "2024-11-14T15:52:30.402Z", + modified: "2024-11-15T07:03:41.301Z", + machineName: "glvmkehegcvqksg:text", +} + diff --git a/test/unit/Webform.unit.js b/test/unit/Webform.unit.js index 28cb768d2f..145d157693 100644 --- a/test/unit/Webform.unit.js +++ b/test/unit/Webform.unit.js @@ -86,6 +86,7 @@ import webformWithNestedWizard from '../forms/webformWIthNestedWizard'; import formWithUniqueValidation from '../forms/formWithUniqueValidation.js'; import formWithConditionalEmail from '../forms/formWithConditionalEmail.js'; import formsWithSimpleConditionals from '../forms/formsWithSimpleConditionals.js'; +import translationErrorMessages from '../forms/translationErrorMessages.js'; const SpySanitize = sinon.spy(FormioUtils, 'sanitize'); if (_.has(Formio, 'Components.setComponents')) { @@ -928,6 +929,29 @@ describe('Webform tests', function() { }).catch(done); }); + it('Should translate field name in error messages', (done) => { + const element = document.createElement('div'); + const form = new Webform(element, { + language: 'en', + i18n: { + en: { + 'My textField': 'My Value' + }, + } + }); + form.setForm(translationErrorMessages).then(() => { + const textField = form.getComponent('textField'); + textField.setValue('123'); + textField.onChange() + setTimeout(() => { + assert.equal(form.errors.length, 2); + assert.equal(form.errors[0].message, 'My Value must have at least 5 characters.'); + assert.equal(form.errors[1].message, 'My Value must have at least 2 words.'); + done(); + }, 300); + }).catch(done); + }); + it('Should translate value in franch if _userInput option is provided and value does not present in reserved translation names', done => { const formElement = document.createElement('div'); const selectLabel = 'Select test label'; From 5777d3d227b89faf271ef05be567a0dda0ddf6b0 Mon Sep 17 00:00:00 2001 From: Brendan Bond Date: Mon, 18 Nov 2024 10:17:28 -0600 Subject: [PATCH 2/2] FIO-9329: validateWhenHidden respects both conditionally hidden and intentionally hidden (#5906) * validateWhenHidden respects all kinds of visibility * add clearOnHide: false to fix test --- package.json | 2 +- .../_classes/component/Component.js | 15 +- test/unit/validateWhenHidden.unit.js | 532 ++++++++++++++++++ test/util.js | 3 + yarn.lock | 15 +- 5 files changed, 554 insertions(+), 13 deletions(-) create mode 100644 test/unit/validateWhenHidden.unit.js create mode 100644 test/util.js diff --git a/package.json b/package.json index dd333b3f62..a6656d7916 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "dependencies": { "@formio/bootstrap": "3.0.0-dev.98.17ba6ea", "@formio/choices.js": "^10.2.1", - "@formio/core": "v2.1.0-dev.174.9a3c6ec", + "@formio/core": "2.1.0-dev.191.8c609ab", "@formio/text-mask-addons": "^3.8.0-formio.3", "@formio/vanilla-text-mask": "^5.1.1-formio.1", "abortcontroller-polyfill": "^1.7.5", diff --git a/src/components/_classes/component/Component.js b/src/components/_classes/component/Component.js index 62d9305828..c25b280a2c 100644 --- a/src/components/_classes/component/Component.js +++ b/src/components/_classes/component/Component.js @@ -3706,12 +3706,6 @@ export default class Component extends Element { } shouldSkipValidation(data, row, flags = {}) { - const { validateWhenHidden = false } = this.component || {}; - const forceValidOnHidden = (!this.visible || !this.checkCondition(row, data)) && !validateWhenHidden; - if (forceValidOnHidden) { - // If this component is forced valid when it is hidden, then we also need to reset the errors for this component. - this._errors = []; - } const rules = [ // Do not validate if the flags say not too. () => flags.noValidate, @@ -3722,7 +3716,14 @@ export default class Component extends Element { // Check to see if we are editing and if so, check component persistence. () => this.isValueHidden(), // Force valid if component is hidden. - () => forceValidOnHidden + () => { + if (!this.component.validateWhenHidden && (!this.visible || !this.checkCondition(row, data))) { + // If this component is forced valid when it is hidden, then we also need to reset the errors for this component. + this._errors = []; + return true; + } + return false; + } ]; return rules.some(pred => pred()); diff --git a/test/unit/validateWhenHidden.unit.js b/test/unit/validateWhenHidden.unit.js new file mode 100644 index 0000000000..4634b28d6b --- /dev/null +++ b/test/unit/validateWhenHidden.unit.js @@ -0,0 +1,532 @@ +import Harness from "../harness"; +import assert from "power-assert"; +import { Formio } from "../../src/Formio"; +import { wait } from "../util"; + +describe("Validate When Hidden behavior", function () { + describe("Simple components", function () { + it("Should not validate intentionally hidden components that do not include the `validateWhenHidden` parameter", async () => { + const formWithIntentionallyHiddenField = { + components: [ + { + type: "textfield", + key: "foo", + label: "Foo", + hidden: true, + validate: { + required: true, + } + }, + ], + }; + const form = await Formio.createForm( + document.createElement("div"), + formWithIntentionallyHiddenField, + ); + const errors = form.validate(); + assert.equal(errors.length, 0); + }); + + it("Should not validate conditionally hidden components that do not include the `validateWhenHidden` parameter", async () => { + const formWithConditionallyHiddenField = { + components: [ + { + type: "checkbox", + key: "checkbox", + label: "Checkbox", + input: true, + }, + { + type: "textfield", + key: "foo", + label: "Foo", + conditional: { + json: { + var: "data.checkbox", + }, + }, + validate: { + required: true + } + }, + ], + }; + const form = await Formio.createForm( + document.createElement("div"), + formWithConditionallyHiddenField, + ); + const errors = form.validate(); + assert.equal(errors.length, 0); + }); + + it("Should validate intentionally hidden components that include the `validateWhenHidden` parameter", async () => { + const formWithIntentionallyHiddenField = { + components: [ + { + type: "textfield", + key: "foo", + label: "Foo", + hidden: true, + validateWhenHidden: true, + validate: { + required: true, + }, + }, + ], + }; + const form = await Formio.createForm( + document.createElement("div"), + formWithIntentionallyHiddenField, + ); + const errors = form.validate(); + assert.equal(errors.length, 1); + }); + + it("Should validate conditionally hidden components that include the `validateWhenHidden` parameter", async () => { + const formWithConditionallyHiddenField = { + components: [ + { + type: "checkbox", + key: "checkbox", + label: "Checkbox", + input: true, + }, + { + type: "textfield", + key: "foo", + label: "Foo", + conditional: { + json: { + var: "data.checkbox", + }, + }, + validateWhenHidden: true, + validate: { + required: true, + }, + }, + ], + }; + const form = await Formio.createForm( + document.createElement("div"), + formWithConditionallyHiddenField, + ); + const errors = form.validate(); + assert.equal(errors.length, 1); + }); + }); + + describe("Layout components", function () { + it("Should not validate intentionally hidden components that are inside of a panel component", async function () { + const formWithIntentionallyHiddenField = { + components: [ + { + type: "panel", + key: "panel", + components: [ + { + type: "textfield", + key: "foo", + label: "Foo", + hidden: true, + validate: { + required: true, + } + }, + ], + }, + ], + }; + const form = await Formio.createForm( + document.createElement("div"), + formWithIntentionallyHiddenField, + ); + const errors = form.validate(); + assert.equal(errors.length, 0); + }); + + it("Should validate intentionally hidden components that include the `validateWhenHidden` parameter that are inside of a panel component", async function () { + const formWithIntentionallyHiddenField = { + components: [ + { + type: "panel", + key: "panel", + components: [ + { + type: "textfield", + key: "foo", + label: "Foo", + hidden: true, + validateWhenHidden: true, + validate: { + required: true + } + }, + ], + }, + ], + }; + const form = await Formio.createForm( + document.createElement("div"), + formWithIntentionallyHiddenField, + ); + const errors = form.validate(); + assert.equal(errors.length, 1); + }); + + it("Should not validate conditionally hidden components that are inside of a panel component", async function () { + const formWithConditionallyHiddenField = { + components: [ + { + type: "checkbox", + key: "checkbox", + label: "Checkbox", + input: true, + }, + { + type: "panel", + key: "panel", + components: [ + { + type: "textfield", + key: "foo", + label: "Foo", + conditional: { + json: { + var: "data.checkbox", + }, + }, + validate: { + required: true + } + }, + ], + }, + ], + }; + const form = await Formio.createForm( + document.createElement("div"), + formWithConditionallyHiddenField, + ); + const textField = form.getComponent('foo'); + assert.equal(textField.visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 0); + }); + + it("Should validate conditionally hidden components that include the `validateWhenHidden` parameter that are inside of a panel component", async function () { + const formWithConditionallyHiddenField = { + components: [ + { + type: "checkbox", + key: "checkbox", + label: "Checkbox", + input: true, + }, + { + type: "panel", + key: "panel", + components: [ + { + type: "textfield", + key: "foo", + label: "Foo", + conditional: { + json: { + var: "data.checkbox", + }, + }, + validateWhenHidden: true, + validate: { + required: true + } + }, + ], + }, + ], + }; + const form = await Formio.createForm( + document.createElement("div"), + formWithConditionallyHiddenField, + ); + const textField = form.getComponent('foo'); + assert.equal(textField.visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 1); + }); + + it('Should not validate components that are children of an intentionally hidden panel component', async function () { + const formWithIntentionallyHiddenPanel = { + components: [ + { + type: 'panel', + key: 'panel', + hidden: true, + components: [ + { + type: 'textfield', + key: 'foo', + label: 'Foo', + validate: { + required: true + } + } + ] + } + ] + }; + const form = await Formio.createForm( + document.createElement('div'), + formWithIntentionallyHiddenPanel + ); + assert.equal(form.getComponent('foo').visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 0); + }); + + it('Should validate components that are children of an intentionally hidden panel component if those components have the `validateWhenHidden` property', async function () { + const formWithIntentionallyHiddenPanel = { + components: [ + { + type: 'panel', + key: 'panel', + hidden: true, + components: [ + { + type: 'textfield', + key: 'foo', + label: 'Foo', + validateWhenHidden: true, + validate: { + required: true + } + } + ] + } + ] + }; + const form = await Formio.createForm( + document.createElement('div'), + formWithIntentionallyHiddenPanel + ); + assert.equal(form.getComponent('foo').visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 1); + }); + + it('Should not validate components that are children of a conditionally hidden panel component', async function () { + const formWithConditionallyHiddenPanel = { + components: [ + { + type: 'checkbox', + key: 'checkbox', + label: 'Checkbox', + input: true + }, + { + type: 'panel', + key: 'panel', + conditional: { + json: { + var: 'data.checkbox' + } + }, + components: [ + { + type: 'textfield', + key: 'foo', + label: 'Foo', + validate: { + required: true + } + } + ] + } + ] + }; + const form = await Formio.createForm( + document.createElement('div'), + formWithConditionallyHiddenPanel + ); + assert.equal(form.getComponent('foo').visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 0); + }); + + it('Should validate components that are children of a conditionally hidden panel component if those components include the `validateWhenHidden` parameter', async function () { + const formWithConditionallyHiddenPanel = { + components: [ + { + type: 'checkbox', + key: 'checkbox', + label: 'Checkbox', + input: true + }, + { + type: 'panel', + key: 'panel', + conditional: { + json: { + var: 'data.checkbox' + } + }, + components: [ + { + type: 'textfield', + key: 'foo', + label: 'Foo', + validateWhenHidden: true, + validate: { + required: true + } + } + ] + } + ] + }; + const form = await Formio.createForm( + document.createElement('div'), + formWithConditionallyHiddenPanel + ); + assert.equal(form.getComponent('foo').visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 1); + }); + }); + + describe('Container components', function () { + it('Should not validate components that are children of an intentionally hidden container component', async function () { + const formWithIntentionallyHiddenContainer = { + components: [ + { + type: 'container', + key: 'container', + hidden: true, + components: [ + { + type: 'textfield', + key: 'foo', + label: 'Foo', + validate: { + required: true + } + } + ] + } + ] + }; + const form = await Formio.createForm( + document.createElement('div'), + formWithIntentionallyHiddenContainer + ); + assert.equal(form.getComponent('foo').visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 0); + }); + + it('Should validate components that are children of an intentionally hidden container component if those components have the `validateWhenHidden` property', async function () { + const formWithIntentionallyHiddenContainer = { + components: [ + { + type: 'container', + key: 'container', + hidden: true, + clearOnHide: false, + components: [ + { + type: 'textfield', + key: 'foo', + label: 'Foo', + validateWhenHidden: true, + validate: { + required: true + } + } + ] + } + ] + }; + const form = await Formio.createForm( + document.createElement('div'), + formWithIntentionallyHiddenContainer + ); + assert.equal(form.getComponent('foo').visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 1); + }); + + it('Should not validate components that are children of a conditionally hidden container component', async function () { + const formWithConditionallyHiddenContainer = { + components: [ + { + type: 'checkbox', + key: 'checkbox', + label: 'Checkbox', + input: true + }, + { + type: 'container', + key: 'container', + conditional: { + json: { + var: 'data.checkbox' + } + }, + components: [ + { + type: 'textfield', + key: 'foo', + label: 'Foo', + validate: { + required: true + } + } + ] + } + ] + }; + const form = await Formio.createForm(document.createElement('div'), formWithConditionallyHiddenContainer); + assert.equal(form.getComponent('foo').visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 0); + }); + + it('Should validate components that are children of a conditionally hidden container component if those components include the `validateWhenHidden` parameter (NOTE THAT CLEAR ON HIDE MUST BE FALSE)', async function () { + const formWithConditionallyHiddenContainer = { + components: [ + { + type: 'checkbox', + key: 'checkbox', + label: 'Checkbox', + input: true + }, + { + type: 'container', + key: 'container', + clearOnHide: false, + conditional: { + json: { + var: 'data.checkbox' + } + }, + components: [ + { + type: 'textfield', + key: 'foo', + label: 'Foo', + validateWhenHidden: true, + validate: { + required: true + } + } + ] + } + ] + }; + const form = await Formio.createForm(document.createElement('div'), formWithConditionallyHiddenContainer); + assert.equal(form.getComponent('foo').visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 1); + }); + }) +}); diff --git a/test/util.js b/test/util.js new file mode 100644 index 0000000000..e10b7c5305 --- /dev/null +++ b/test/util.js @@ -0,0 +1,3 @@ +export function wait(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2ab2a0c002..a600f82a4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -381,15 +381,15 @@ fuse.js "^6.6.2" redux "^4.2.0" -"@formio/core@v2.1.0-dev.174.9a3c6ec": - version "2.1.0-dev.174.9a3c6ec" - resolved "https://registry.yarnpkg.com/@formio/core/-/core-2.1.0-dev.174.9a3c6ec.tgz#f223b5ce4f374a9f4e922dada0af7c029320e035" - integrity sha512-QQK04dP0xBFa3vuhiOi+TUP8Zwqlg38qxzHgDmBwSlRO5XqQIObPJpSSnv2VA8H7fBWWiV2g7AErHBxugJW7Rw== +"@formio/core@2.1.0-dev.191.8c609ab": + version "2.1.0-dev.191.8c609ab" + resolved "https://registry.yarnpkg.com/@formio/core/-/core-2.1.0-dev.191.8c609ab.tgz#2e442888c60786268ca16edc7cd26c38cbd4b773" + integrity sha512-lVj8hqddIwUJiWI6/yWF0NFl/f8QPS3ek4l6h0U1SFMPmeEdWQtcBTMLKi02gHx09kDgXhYocJIbBVVpYyqFnw== dependencies: browser-cookies "^1.2.0" core-js "^3.38.0" dayjs "^1.11.12" - dompurify "^3.1.6" + dompurify "^3.1.7" eventemitter3 "^5.0.0" fast-json-patch "^3.1.1" fetch-ponyfill "^7.1.0" @@ -2498,6 +2498,11 @@ dompurify@^3.1.6: resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.6.tgz#43c714a94c6a7b8801850f82e756685300a027e2" integrity sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ== +dompurify@^3.1.7: + version "3.2.0" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.0.tgz#53c414317c51503183696fcdef6dd3f916c607ed" + integrity sha512-AMdOzK44oFWqHEi0wpOqix/fUNY707OmoeFDnbi3Q5I8uOpy21ufUA5cDJPr0bosxrflOVD/H2DMSvuGKJGfmQ== + downloadjs@^1.4.7: version "1.4.7" resolved "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz#f69f96f940e0d0553dac291139865a3cd0101e3c"