From 8c811f179c4f02010f28084b6a3107e6fc5a14d2 Mon Sep 17 00:00:00 2001 From: brendanjbond Date: Wed, 16 Oct 2024 10:35:17 -0500 Subject: [PATCH 1/3] add intentionallyHidden ephemeral state; change clearOnHide behavior to only clear when conditionally hidden; add tests; minor linting --- package.json | 2 +- src/process/__tests__/process.test.ts | 814 ++++++++++-------- .../clearHidden/__tests__/clearHidden.test.ts | 60 ++ .../{clearHidden.ts => clearHidden/index.ts} | 2 +- src/process/hideChildren.ts | 6 +- src/process/validation/index.ts | 7 +- .../rules/__tests__/validateRequired.test.ts | 10 + .../validation/rules/validateRequired.ts | 2 +- src/types/BaseComponent.ts | 1 + 9 files changed, 542 insertions(+), 362 deletions(-) create mode 100644 src/process/clearHidden/__tests__/clearHidden.test.ts rename src/process/{clearHidden.ts => clearHidden/index.ts} (95%) diff --git a/package.json b/package.json index daea8b91..b1fc2cb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@formio/core", - "version": "2.1.0-dev.tt.13", + "version": "2.3.0-dev.160.cabaa43", "description": "The core Form.io renderering framework.", "main": "lib/index.js", "exports": { diff --git a/src/process/__tests__/process.test.ts b/src/process/__tests__/process.test.ts index 6a3e4779..c4291317 100644 --- a/src/process/__tests__/process.test.ts +++ b/src/process/__tests__/process.test.ts @@ -6,68 +6,6 @@ import { process, processSync, ProcessTargets } from '../index'; import { fastCloneDeep } from 'utils'; import { addressComponentWithOtherCondComponents, addressComponentWithOtherCondComponents2, clearOnHideWithCustomCondition, clearOnHideWithHiddenParent, forDataGridRequired, skipValidForConditionallyHiddenComp, skipValidForLogicallyHiddenComp, skipValidWithHiddenParentComp } from './fixtures' -/* -describe('Process Tests', () => { - it('Should perform the processes using the processReduced method.', async () => { - const reduced: ReducerScope = process({ - components: form1.components, - data: data1a.data, - scope: { - processes: {} - } - }); - const targets = processReduceTargets(reduced.processes); - expect(targets.length).to.equal(5); - expect(targets[0].target).to.equal('server'); - expect(Object.keys(targets[0].processes).length).to.equal(1); - expect(targets[0].processes.defaultValue.length).to.equal(6); - expect(targets[1].target).to.equal('custom'); - expect(Object.keys(targets[1].processes).length).to.equal(1); - expect(targets[1].processes.customDefaultValue.length).to.equal(1); - expect(targets[2].target).to.equal('server'); - expect(Object.keys(targets[2].processes).length).to.equal(1); - expect(targets[2].processes.fetch.length).to.equal(1); - expect(targets[3].target).to.equal('custom'); - expect(Object.keys(targets[3].processes).length).to.equal(1); - expect(targets[3].processes.calculate.length).to.equal(6); - expect(targets[4].target).to.equal('server'); - expect(Object.keys(targets[4].processes).length).to.equal(2); - expect(targets[4].processes.conditions.length).to.equal(1); - expect(targets[4].processes.validate.length).to.equal(28); - const scope = {errors: []}; - - // Reset all values that will be calculated. - reduced.data.subtotal = 0; - reduced.data.taxes = 0; - reduced.data.total = 0; - reduced.data.cart.forEach((item: any) => { - item.price = 0; - }); - for (let i = 0; i < targets.length; i++) { - await processReduced({ - components: form1.components, - data: reduced.data, - processes: targets[i].processes, - fetch: (url: string, options?: RequestInit | undefined): Promise => { - return Promise.resolve({ - json: () => { - return Promise.resolve(subs); - } - } as Response); - }, - scope - }); - } - expect(reduced.data.subtotal).to.equal(100); - expect(reduced.data.taxes).to.equal(8); - expect(reduced.data.total).to.equal(108); - expect(reduced.data.cart[0].price).to.equal(30); - expect(reduced.data.cart[1].price).to.equal(20); - expect(reduced.data.cart[2].price).to.equal(10); - }); -}); -*/ - describe('Process Tests', () => { it('Should submit data within a nested form.', async () => { const form = { @@ -832,7 +770,6 @@ describe('Process Tests', () => { 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', pathName: '/', onLine: true, - }, data: { number: 23, @@ -948,7 +885,6 @@ describe('Process Tests', () => { }, owner: '65ea3601c3792e416cabcb2a', access: [], - _vnote: '', state: 'submitted', form: '65ea368b705068f84a93c87a', @@ -1526,7 +1462,6 @@ describe('Process Tests', () => { }); }); - // TODO: test case naming it('Should not unset submission data of nested forms with identical keys', () => { const parentForm = { display: 'form', @@ -3511,7 +3446,7 @@ describe('Process Tests', () => { assert.equal(context.scope.errors.length, 0); }); -it('Should not unset values for conditionally visible fields with different formats of condtion based on selectboxes value', async () => { + it('Should not unset values for conditionally visible fields with different formats of condtion based on selectboxes value', async () => { const form = { _id: '66ffa92ac25689df8702f283', title: 'cond NEW', @@ -3657,7 +3592,7 @@ it('Should not unset values for conditionally visible fields with different form assert.deepEqual(context.data, data); }); - it('Should not return error for the form with conditionals based on the Day component', () => { + it('Should not return error for the form with conditionals based on the Day component', () => { const form = { _id: '66ffe59b598a729e707869bf', title: '9143 condition day', @@ -4009,29 +3944,6 @@ it('Should not unset values for conditionally visible fields with different form }); }); - it('Should not return fields from conditionally hidden containers, clearOnHide = true', async () => { - const { form, submission } = clearOnHideWithCustomCondition; - const context = { - form, - submission, - data: submission.data, - components: form.components, - processors: ProcessTargets.submission, - scope: {}, - config: { - server: true, - }, - }; - - processSync(context); - context.processors = ProcessTargets.evaluator; - processSync(context); - - expect(context.data).to.deep.equal({ - candidates:[{candidate:{data:{}}}], - submit: true - }); - }); it('Should skip child validation with conditional', async () => { const { form, submission } = skipValidForConditionallyHiddenComp; @@ -4093,82 +4005,6 @@ it('Should not unset values for conditionally visible fields with different form expect((context.scope as ValidationScope).errors).to.have.length(0); }); - it('Should not return fields from conditionally hidden containers, clearOnHide = false', async () => { - const { form, submission } = clearOnHideWithCustomCondition; - const containerComponent = getComponent(form.components, 'section6') as ContainerComponent; - containerComponent.clearOnHide = false; - const context = { - form, - submission, - data: submission.data, - components: form.components, - processors: ProcessTargets.submission, - scope: {}, - config: { - server: true, - }, - }; - - processSync(context); - context.processors = ProcessTargets.evaluator; - processSync(context); - - expect(context.data).to.deep.equal({ - candidates:[{candidate:{data:{section6:{}}}}], - submit: true - }); - }); - - it('Should not validate fields from hidden containers, clearOnHide = false', async () => { - const { form, submission } = clearOnHideWithHiddenParent; - const context = { - form, - submission, - data: submission.data, - components: form.components, - processors: ProcessTargets.submission, - scope: { errors: []}, - config: { - server: true, - }, - }; - - processSync(context); - context.processors = ProcessTargets.evaluator; - processSync(context); - - expect(context.data).to.deep.equal({ - candidates:[{candidate:{data:{section6:{}}}}], - submit: true - }); - expect(context.scope.errors.length).to.equal(0); - }); - - it('Should not return fields from hidden containers, clearOnHide = true', async () => { - const { form, submission } = clearOnHideWithHiddenParent; - const containerComponent = getComponent(form.components, 'section6') as ContainerComponent; - containerComponent.clearOnHide = true; - const context = { - form, - submission, - data: submission.data, - components: form.components, - processors: ProcessTargets.submission, - scope: {}, - config: { - server: true, - }, - }; - - processSync(context); - context.processors = ProcessTargets.evaluator; - processSync(context); - - expect(context.data).to.deep.equal({ - candidates:[{candidate:{data:{}}}], - submit: true - }); - }); it('Should validate when all child components are empty in required Data Grid', async () => { const { form, submission } = forDataGridRequired; @@ -4380,199 +4216,465 @@ it('Should not unset values for conditionally visible fields with different form }); }); - /* - it('Should not clearOnHide when set to false', async () => { - var components = [ - { - "input": true, - "tableView": true, - "inputType": "radio", - "label": "Selector", - "key": "selector", - "values": [ - { - "value": "one", - "label": "One" - }, - { - "value": "two", - "label": "Two" - } - ], - "defaultValue": "", - "protected": false, - "persistent": true, - "validate": { - "required": false, - "custom": "", - "customPrivate": false - }, - "type": "radio", - "conditional": { - "show": "", - "when": null, - "eq": "" - } - }, - { - "input": false, - "title": "Panel", - "theme": "default", - "components": [ - { - "input": true, - "tableView": true, - "inputType": "text", - "inputMask": "", - "label": "No Clear Field", - "key": "noClear", - "placeholder": "", - "prefix": "", - "suffix": "", - "multiple": false, - "defaultValue": "", - "protected": false, - "unique": false, - "persistent": true, - "clearOnHide": false, - "validate": { - "required": false, - "minLength": "", - "maxLength": "", - "pattern": "", - "custom": "", - "customPrivate": false - }, - "conditional": { - "show": null, - "when": null, - "eq": "" - }, - "type": "textfield" - } - ], - "type": "panel", - "key": "panel", - "conditional": { - "show": "true", - "when": "selector", - "eq": "two" - } - } - ]; + describe('clearOnHide', function () { + it('Should not include submission data from conditionally hidden containers when clearOnHide ("Omit Data When Conditionally Hidden" is true', async () => { + const { form, submission } = clearOnHideWithCustomCondition; + const context = { + form, + submission, + data: submission.data, + components: form.components, + processors: ProcessTargets.submission, + scope: {}, + config: { + server: true, + }, + }; - helper - .form('test', components) - .submission({ - selector: 'one', - noClear: 'testing' - }) - .execute(function(err) { - if (err) { - return done(err); - } + processSync(context); + context.processors = ProcessTargets.evaluator; + processSync(context); - var submission = helper.getLastSubmission(); - assert.deepEqual({selector: 'one', noClear: 'testing'}, submission.data); - done(); - }); - }); + expect(context.data).to.deep.equal({ + candidates:[{candidate:{data:{}}}], + submit: true + }); + }); - it('Should clearOnHide when set to true', async () => { - var components = [ - { - "input": true, - "tableView": true, - "inputType": "radio", - "label": "Selector", - "key": "selector", - "values": [ - { - "value": "one", - "label": "One" - }, - { - "value": "two", - "label": "Two" - } - ], - "defaultValue": "", - "protected": false, - "persistent": true, - "validate": { - "required": false, - "custom": "", - "customPrivate": false - }, - "type": "radio", - "conditional": { - "show": "", - "when": null, - "eq": "" - } - }, - { - "input": false, - "title": "Panel", - "theme": "default", - "components": [ - { - "input": true, - "tableView": true, - "inputType": "text", - "inputMask": "", - "label": "Clear Me", - "key": "clearMe", - "placeholder": "", - "prefix": "", - "suffix": "", - "multiple": false, - "defaultValue": "", - "protected": false, - "unique": false, - "persistent": true, - "clearOnHide": true, - "validate": { - "required": false, - "minLength": "", - "maxLength": "", - "pattern": "", - "custom": "", - "customPrivate": false - }, - "conditional": { - "show": null, - "when": null, - "eq": "" - }, - "type": "textfield" - } - ], - "type": "panel", - "key": "panel", - "conditional": { - "show": "true", - "when": "selector", - "eq": "two" - } - } - ]; + it('Should not return fields from conditionally hidden containers, clearOnHide = false', async () => { + const { form, submission } = clearOnHideWithCustomCondition; + const containerComponent = getComponent(form.components, 'section6') as ContainerComponent; + containerComponent.clearOnHide = false; + const context = { + form, + submission, + data: submission.data, + components: form.components, + processors: ProcessTargets.submission, + scope: {}, + config: { + server: true, + }, + }; + + processSync(context); + context.processors = ProcessTargets.evaluator; + processSync(context); + + expect(context.data).to.deep.equal({ + candidates:[{candidate:{data:{section6:{}}}}], + submit: true + }); + }); + + it('Should not validate fields from hidden containers, clearOnHide = false', async () => { + const { form, submission } = clearOnHideWithHiddenParent; + const context = { + form, + submission, + data: submission.data, + components: form.components, + processors: ProcessTargets.submission, + scope: { errors: []}, + config: { + server: true, + }, + }; + + processSync(context); + context.processors = ProcessTargets.evaluator; + processSync(context); + + expect(context.data).to.deep.equal({ + candidates:[{candidate:{data:{section6: {c: {}, d: []}}}}], + submit: true + }); + expect(context.scope.errors.length).to.equal(0); + }); + + it('Should include submission data from hidden containers even when clearOnHide ("Omit Data When Conditionally Hidden" is true', async () => { + const { form, submission } = clearOnHideWithHiddenParent; + const containerComponent = getComponent(form.components, 'section6') as ContainerComponent; + containerComponent.clearOnHide = true; + const context = { + form, + submission, + data: submission.data, + components: form.components, + processors: ProcessTargets.submission, + scope: {}, + config: { + server: true, + }, + }; + + processSync(context); + context.processors = ProcessTargets.evaluator; + processSync(context); + + expect(context.data).to.deep.equal({ + candidates:[{candidate:{data:{section6: {c: {}, d: []}}}}], + submit: true + }); + }); + + it('Should include submission data for simple fields that are intentionally hidden, even when clearOnHide ("Omit When Conditionally Hidden") is true', async function () { + const components = [ + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + hidden: true, + clearOnHide: true + }, + ]; + const submission = { + data: { + textField: 'test' + }, + }; + const context = { + form: { components }, + submission, + data: submission.data, + components, + processors: ProcessTargets.evaluator, + scope: {}, + }; + processSync(context); + expect(context.data).to.deep.equal({ textField: 'test' }); + }); + + it('Should include submission data for simple components that are intentionally hidden when clearOnHide ("Omit When Conditionally Hidden") is false', async function () { + const components = [ + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + hidden: true, + clearOnHide: false + }, + ]; + const submission = { + data: { + textField: 'test' + }, + }; + const context = { + form: { components }, + submission, + data: submission.data, + components, + processors: ProcessTargets.evaluator, + scope: {}, + }; + processSync(context); + expect(context.data).to.deep.equal({ textField: 'test' }); + }); + + it('Should include submission data for container components that are intentionally hidden, even when clearOnHide ("Omit When Conditionally Hidden") is true', async function () { + const components = [ + { + key: 'container', + type: 'container', + input: true, + hidden: true, + clearOnHide: true, + components: [ + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + clearOnHide: true + }, + ] + }, + ]; + const submission = { + data: { + container: { + textField: 'test' + } + }, + }; + const context = { + form: { components }, + submission, + data: submission.data, + components, + processors: ProcessTargets.evaluator, + scope: {}, + }; + processSync(context); + expect(context.data).to.deep.equal({ container: { textField: 'test' } }); + }); + + it('Should include submission data for container components that are intentionally hidden when clearOnHide ("Omit When Conditionally Hidden") is false', async function () { + const components = [ + { + key: 'container', + type: 'container', + input: true, + hidden: true, + clearOnHide: false, + components: [ + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + clearOnHide: true + }, + ] + }, + ]; + const submission = { + data: { + container: { + textField: 'test' + } + }, + }; + const context = { + form: { components }, + submission, + data: submission.data, + components, + processors: ProcessTargets.evaluator, + scope: {}, + }; + processSync(context); + expect(context.data).to.deep.equal({ container: { textField: 'test' } }); + }); + + it("Should not include submission data for simple fields that are conditionally hidden when clearOnHide ('Omit When Conditionally Hidden') is true", async function () { + const components = [ + { + type: 'checkbox', + key: 'selector', + label: 'Selector', + input: true, + }, + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + conditional: { + show: true, + when: 'selector', + eq: true + }, + clearOnHide: true + }, + ] + const submission = { + data: { + selector: false, + textField: 'test' + } + }; + const context = { + submission, + data: submission.data, + components, + processors: ProcessTargets.evaluator, + scope: {}, + }; + processSync(context); + expect(context.data).to.deep.equal({ selector: false }); + }); + + it("Should include submission data for simple fields that are conditionally hidden when clearOnHide ('Omit When Conditionally Hidden') is false", async function () { + const components = [ + { + type: 'checkbox', + key: 'selector', + label: 'Selector', + input: true, + }, + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + conditional: { + show: true, + when: 'selector', + eq: true + }, + clearOnHide: false + }, + ] + const submission = { + data: { + selector: false, + textField: 'test' + } + }; + const context = { + submission, + data: submission.data, + components, + processors: ProcessTargets.evaluator, + scope: {}, + }; + processSync(context); + expect(context.data).to.deep.equal({ selector: false, textField: 'test' }); + }); - helper - .form('test', components) - .submission({ - selector: 'one', - clearMe: 'Clear Me!!!!' - }) - .execute(function(err) { - if (err) { - return done(err); - } + it("Should not include submission data for container components that are conditionally hidden when clearOnHide ('Omit When Conditionally Hidden') is true", async function () { + const components = [ + { + type: 'checkbox', + key: 'selector', + label: 'Selector', + input: true, + }, + { + key: 'container', + type: 'container', + input: true, + conditional: { + show: true, + when: 'selector', + eq: true + }, + clearOnHide: true, + components: [ + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + }, + ] + }, + ] + const submission = { + data: { + selector: false, + container: { + textField: 'test' + } + } + }; + const context = { + submission, + data: submission.data, + components, + processors: ProcessTargets.evaluator, + scope: {}, + }; + processSync(context); + expect(context.data).to.deep.equal({ selector: false }); + }); - var submission = helper.getLastSubmission(); - assert.deepEqual({selector: 'one'}, submission.data); - done(); - }); - }); - */ + it("Should include submission data for container components that are conditionally hidden when clearOnHide ('Omit When Conditionally Hidden') is false (but not their children, assuming clearOnHide is true or omitted in the child)", async function () { + const components = [ + { + type: 'checkbox', + key: 'selector', + label: 'Selector', + input: true, + }, + { + key: 'container', + type: 'container', + input: true, + conditional: { + show: true, + when: 'selector', + eq: true + }, + clearOnHide: false, + components: [ + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + }, + ] + }, + ] + const submission = { + data: { + selector: false, + container: { + textField: 'test' + } + } + }; + const context = { + submission, + data: submission.data, + components, + processors: ProcessTargets.evaluator, + scope: {}, + }; + processSync(context); + expect(context.data).to.deep.equal({ selector: false, container: {} }); + }); + + it("Should include submission data for container components that are conditionally hidden when clearOnHide ('Omit When Conditionally Hidden') is false (and include their children when clearOnHide is false in the child)", async function () { + const components = [ + { + type: 'checkbox', + key: 'selector', + label: 'Selector', + input: true, + }, + { + key: 'container', + type: 'container', + input: true, + conditional: { + show: true, + when: 'selector', + eq: true + }, + clearOnHide: false, + components: [ + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + clearOnHide: false, + }, + ] + }, + ] + const submission = { + data: { + selector: false, + container: { + textField: 'test' + } + } + }; + const context = { + submission, + data: submission.data, + components, + processors: ProcessTargets.evaluator, + scope: {}, + }; + processSync(context); + expect(context.data).to.deep.equal({ selector: false, container: { textField: 'test' } }); + }); + }); }); diff --git a/src/process/clearHidden/__tests__/clearHidden.test.ts b/src/process/clearHidden/__tests__/clearHidden.test.ts new file mode 100644 index 00000000..85406fb5 --- /dev/null +++ b/src/process/clearHidden/__tests__/clearHidden.test.ts @@ -0,0 +1,60 @@ +import { expect } from "chai"; + +import { clearHiddenProcess } from "../index"; + +describe('clearHidden', function (){ + it('Shoud not clear conditionally hidden component data when clearOnHide is false', function () { + // Test case data + const context = { + component: { + type: 'textfield', + key: 'foo', + clearOnHide: false, + input: true + }, + data: { + foo: 'bar', + }, + value: 'foo', + row: {}, + scope: { + clearHidden: {}, + conditionals: [{ + path: 'foo', + conditionallyHidden: true, + }], + }, + path: 'foo' + }; + const _ = clearHiddenProcess(context); + expect(context.data).to.deep.equal({ foo: 'bar' }); + }); + + it('Should clear conditiionally hidden component data when clearOnHide is true', function () { + // Test case data + const context = { + component: { + type: 'textfield', + key: 'foo', + clearOnHide: true, + input: true + }, + data: { + foo: 'bar', + }, + value: 'foo', + row: {}, + scope: { + clearHidden: {}, + conditionals: [{ + path: 'foo', + conditionallyHidden: true, + }] + }, + path: 'foo', + }; + const _ = clearHiddenProcess(context); + expect(context.data).to.deep.equal({}); + }); + +}); diff --git a/src/process/clearHidden.ts b/src/process/clearHidden/index.ts similarity index 95% rename from src/process/clearHidden.ts rename to src/process/clearHidden/index.ts index 39c22b33..3b0c0886 100644 --- a/src/process/clearHidden.ts +++ b/src/process/clearHidden/index.ts @@ -35,7 +35,7 @@ export const clearHiddenProcess: ProcessorFnSync = (context) = const shouldClearValueWhenHidden = !component.hasOwnProperty('clearOnHide') || component.clearOnHide; - if (shouldClearValueWhenHidden && (isConditionallyHidden || component.hidden || component.ephemeralState?.conditionallyHidden)) { + if (shouldClearValueWhenHidden && (isConditionallyHidden || component.ephemeralState?.conditionallyHidden)) { unset(data, path); scope.clearHidden[path] = true; } diff --git a/src/process/hideChildren.ts b/src/process/hideChildren.ts index d1ad4dbe..9f04e8fd 100644 --- a/src/process/hideChildren.ts +++ b/src/process/hideChildren.ts @@ -22,9 +22,13 @@ export const hideChildrenProcessor: ProcessorFnSync = (context) scope.conditionals = []; } - if (isConditionallyHidden || component.hidden || parent?.ephemeralState?.conditionallyHidden) { + if (isConditionallyHidden || parent?.ephemeralState?.conditionallyHidden) { registerEphemeralState(component, 'conditionallyHidden', true); } + + if ((component.hasOwnProperty('hidden') && !!component.hidden) || parent?.ephemeralState?.intentionallyHidden) { + registerEphemeralState(component, 'intentionallyHidden', true); + } } export const hideChildrenProcessorAsync: ProcessorFn = async (context) => { diff --git a/src/process/validation/index.ts b/src/process/validation/index.ts index 7469479b..7b99a58f 100644 --- a/src/process/validation/index.ts +++ b/src/process/validation/index.ts @@ -60,7 +60,7 @@ export function isInputComponent(context: ValidationContext): boolean { } export function isValueHidden(context: ValidationContext): boolean { - const { component, config } = context; + const { component } = context; if (component.protected) { return false; } @@ -78,6 +78,9 @@ export function isForcedHidden(context: ValidationContext, isConditionallyHidden if (isConditionallyHidden(context as ConditionsContext)) { return true; } + if (component.ephemeralState?.intentionallyHidden) { + return true; + } if (component.hasOwnProperty('hidden')) { return !!component.hidden; } @@ -96,7 +99,7 @@ export const _shouldSkipValidation = (context: ValidationContext, isConditionall ) { return true; } - const { validateWhenHidden = false } = component || {}; + const { validateWhenHidden = false } = component; const rules = [ // Skip validation if component is readOnly // () => this.options.readOnly, diff --git a/src/process/validation/rules/__tests__/validateRequired.test.ts b/src/process/validation/rules/__tests__/validateRequired.test.ts index 0ae84619..955c43fe 100644 --- a/src/process/validation/rules/__tests__/validateRequired.test.ts +++ b/src/process/validation/rules/__tests__/validateRequired.test.ts @@ -78,6 +78,16 @@ it('Should not validate a hidden component that has the hidden property set to t expect(context.scope.errors.length).to.equal(0); }); +it('Should validate a hidden component that has the `validateWhenHidden` property set to true.', async () => { + const component = { ...hiddenRequiredField }; + component.validateWhenHidden = true; + const data = {}; + const context = generateProcessorContext(component, data) as ProcessorsContext; + context.processors = [validateProcessInfo]; + await processOne(context); + expect(context.scope.errors.length).to.equal(1); +}) + it('Validating a simple component that is required but conditionally hidden', async () => { const component = {...simpleTextField}; component.validate = { required: true }; diff --git a/src/process/validation/rules/validateRequired.ts b/src/process/validation/rules/validateRequired.ts index d467ef02..04876b84 100644 --- a/src/process/validation/rules/validateRequired.ts +++ b/src/process/validation/rules/validateRequired.ts @@ -56,7 +56,7 @@ const valueIsPresent = (value: any, considerFalseTruthy: boolean, isNestedDataty export const shouldValidate = (context: ValidationContext) => { const { component } = context; - if (component.validate?.required && !(component.hidden || component.ephemeralState?.conditionallyHidden)) { + if (component.validate?.required) { return true; } return false; diff --git a/src/types/BaseComponent.ts b/src/types/BaseComponent.ts index c723cf79..d399e7a4 100644 --- a/src/types/BaseComponent.ts +++ b/src/types/BaseComponent.ts @@ -25,6 +25,7 @@ export type BaseComponent = { hidden?: boolean; ephemeralState?: { conditionallyHidden?: boolean; + intentionallyHidden?: boolean; } clearOnHide?: boolean; refreshOn?: string; From a781e56c3f56dd8e307e9c463e2734c05e378f09 Mon Sep 17 00:00:00 2001 From: brendanjbond Date: Mon, 11 Nov 2024 10:41:12 +0100 Subject: [PATCH 2/3] more tests --- src/process/__tests__/process.test.ts | 46 ++++++++++++++++++- .../clearHidden/__tests__/clearHidden.test.ts | 26 ++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/process/__tests__/process.test.ts b/src/process/__tests__/process.test.ts index 7ce9b638..4e81b5cc 100644 --- a/src/process/__tests__/process.test.ts +++ b/src/process/__tests__/process.test.ts @@ -1603,7 +1603,6 @@ describe('Process Tests', function () { }); }); - // TODO: test case naming it('Should not unset submission data of nested forms with identical keys', function () { const parentForm = { display: 'form', @@ -3323,6 +3322,51 @@ describe('Process Tests', function () { }); }); + it('Should include submission data for intentionally hidden fields', async function () { + const form = { + display: 'form', + components: [ + { + type: 'textfield', + key: 'textField', + label: 'Text Field', + input: true, + }, + { + type: 'textarea', + key: 'textArea', + label: 'Text Area', + input: true, + hidden: true, + }, + ], + }; + + const submission = { + data: { + textField: 'not empty', + textArea: 'also not empty', + }, + }; + + const context = { + form, + submission, + data: submission.data, + components: form.components, + processors: ProcessTargets.evaluator, + scope: {}, + config: { + server: true, + }, + }; + processSync(context); + expect(context.data).to.deep.equal({ + textField: 'not empty', + textArea: 'also not empty', + }); + }); + it('Should not filter a simple datamap compoennt', async function () { const form = { display: 'form', diff --git a/src/process/clearHidden/__tests__/clearHidden.test.ts b/src/process/clearHidden/__tests__/clearHidden.test.ts index ddb42e1a..1209f76b 100644 --- a/src/process/clearHidden/__tests__/clearHidden.test.ts +++ b/src/process/clearHidden/__tests__/clearHidden.test.ts @@ -32,7 +32,7 @@ describe('clearHidden', function () { expect(context.data).to.deep.equal({ foo: 'bar' }); }); - it('Should clear conditiionally hidden component data when clearOnHide is true', function () { + it('Should clear conditionally hidden component data when clearOnHide is true', function () { // Test case data const context = { component: { @@ -60,4 +60,28 @@ describe('clearHidden', function () { clearHiddenProcess(context); expect(context.data).to.deep.equal({}); }); + + it('Should not clear component data when the component is intentionally hidden', function () { + // Test case data + const context = { + component: { + type: 'textfield', + key: 'foo', + clearOnHide: true, + input: true, + hidden: true, + }, + data: { + foo: 'bar', + }, + value: 'foo', + row: {}, + scope: { + clearHidden: {}, + }, + path: 'foo', + }; + clearHiddenProcess(context); + expect(context.data).to.deep.equal({ foo: 'bar' }); + }); }); From 4ae654a0f2d699d5aac111e5d3ba0ce230100506 Mon Sep 17 00:00:00 2001 From: brendanjbond Date: Wed, 11 Dec 2024 16:59:41 -0600 Subject: [PATCH 3/3] fix merge conflict problem --- src/process/clearHidden/index.ts | 2 +- src/process/hideChildren.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/process/clearHidden/index.ts b/src/process/clearHidden/index.ts index bb92dd6a..91e7f8e3 100644 --- a/src/process/clearHidden/index.ts +++ b/src/process/clearHidden/index.ts @@ -38,7 +38,7 @@ export const clearHiddenProcess: ProcessorFnSync = (context) = if ( shouldClearValueWhenHidden && - (isConditionallyHidden || component.hidden || component.scope?.conditionallyHidden) + (isConditionallyHidden || component.scope?.conditionallyHidden) ) { unset(data, path); scope.clearHidden[path] = true; diff --git a/src/process/hideChildren.ts b/src/process/hideChildren.ts index b83b6aa4..fe71fc56 100644 --- a/src/process/hideChildren.ts +++ b/src/process/hideChildren.ts @@ -22,7 +22,7 @@ export const hideChildrenProcessor: ProcessorFnSync = (context) scope.conditionals = []; } - if (isConditionallyHidden || component.hidden || parent?.scope?.conditionallyHidden) { + if (isConditionallyHidden || parent?.scope?.conditionallyHidden) { setComponentScope(component, 'conditionallyHidden', true); }