From 694ea4e63b843cf2aab16f3069b4bc92108a8456 Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Tue, 1 Jul 2025 13:51:07 -0500 Subject: [PATCH 1/2] FIO-10337: Refactored the core data processors to significantly improve performance (#270) * FIO-10337: Refactored the core data processors to significantly improve performance. * Update postProcess function signature. * Make sure to not autoset an array value if the component is required. * Make sure to add set paths to the filter. * Make sure that deliberate filtered paths are not included. --- src/process/__tests__/process.test.ts | 390 +++++++++++------- src/process/calculation/index.ts | 4 + src/process/clearHidden/index.ts | 41 +- src/process/conditions/index.ts | 2 +- src/process/defaultValue/index.ts | 43 +- src/process/fetch/index.ts | 7 +- src/process/filter/__tests__/filter.test.ts | 71 ++-- src/process/filter/index.ts | 110 +++-- src/process/index.ts | 1 - src/process/normalize/index.ts | 2 +- src/process/process.ts | 76 +++- src/process/processOne.ts | 58 +-- src/process/validation/index.ts | 21 + src/types/formUtil.ts | 8 + src/types/process/ProcessorFn.ts | 6 + src/types/process/ProcessorInfo.ts | 3 +- .../process/calculation/CalculationScope.ts | 4 +- .../process/defaultValue/DefaultValueScope.ts | 4 +- src/types/process/fetch/FetchScope.ts | 4 +- src/types/process/filter/FilterScope.ts | 11 +- src/types/process/logic/LogicScope.ts | 4 +- src/utils/formUtil/eachComponentData.ts | 36 +- src/utils/formUtil/eachComponentDataAsync.ts | 37 +- src/utils/formUtil/index.ts | 79 +++- src/utils/logic.ts | 6 +- 25 files changed, 662 insertions(+), 366 deletions(-) diff --git a/src/process/__tests__/process.test.ts b/src/process/__tests__/process.test.ts index 6b942313..dc774726 100644 --- a/src/process/__tests__/process.test.ts +++ b/src/process/__tests__/process.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import assert from 'node:assert'; import type { ContainerComponent, ValidationScope } from 'types'; import { getComponent } from 'utils/formUtil'; -import { process, processSync, ProcessTargets } from '../index'; +import { process, processSync, Processors } from '../index'; import { fastCloneDeep } from 'utils'; import { addressComponentWithOtherCondComponents, @@ -903,7 +903,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors }, config: { server: true, @@ -911,8 +911,6 @@ describe('Process Tests', function () { }; processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); assert.equal(context.scope.errors.length, 0); }); @@ -1044,7 +1042,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors }, config: { server: true, @@ -1052,8 +1050,6 @@ describe('Process Tests', function () { }; processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); assert.equal(_.get(context.submission.data, 'form1.data.form.data.textField'), 'one 1'); assert.equal(_.get(context.submission.data, 'form1.data.form.data.textField1'), 'two 2'); }); @@ -1114,7 +1110,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, @@ -1127,6 +1123,171 @@ describe('Process Tests', function () { expect(context.data.child.data).to.not.have.property('invalid'); }); + it('Should produce errors for a multivalue textfield with a required value', async function () { + const submission = { data: {} }; + const form = { + components: [ + { + input: true, + tableView: true, + inputType: 'text', + inputMask: '', + label: 'Text Field', + key: 'textField', + placeholder: '', + prefix: '', + suffix: '', + multiple: true, + defaultValue: '', + protected: false, + unique: false, + persistent: true, + validate: { + required: true, + minLength: '', + maxLength: '', + pattern: '', + custom: '', + customPrivate: false, + }, + conditional: { + show: null, + when: null, + eq: '', + }, + type: 'textfield', + }, + ], + }; + const context = { + form, + submission, + data: submission.data, + components: form.components, + processors: Processors, + scope: {}, + config: { + server: true, + }, + }; + processSync(context); + assert.equal((context.scope as any).errors.length, 2); + assert.equal((context.scope as any).errors[0].ruleName, 'array'); + assert.equal((context.scope as any).errors[1].ruleName, 'required'); + }); + + it('Sets a value based on advanced conditions', async function () { + const form = { + components: [ + { + properties: {}, + tags: [], + labelPosition: 'top', + hideLabel: false, + type: 'textfield', + conditional: { + eq: '', + when: null, + show: '', + }, + validate: { + customPrivate: false, + custom: '', + pattern: '', + maxLength: '', + minLength: '', + required: false, + }, + clearOnHide: true, + hidden: false, + persistent: true, + unique: false, + protected: false, + defaultValue: '', + multiple: false, + suffix: '', + prefix: '', + placeholder: '', + key: 'test', + label: 'Test', + inputMask: '', + inputType: 'text', + tableView: true, + input: true, + }, + { + properties: {}, + tags: [], + labelPosition: 'top', + hideLabel: false, + type: 'textfield', + conditional: { + eq: '', + when: null, + show: '', + }, + validate: { + customPrivate: false, + custom: '', + pattern: '', + maxLength: '', + minLength: '', + required: false, + }, + clearOnHide: true, + hidden: false, + persistent: true, + unique: false, + protected: false, + defaultValue: '', + multiple: false, + suffix: '', + prefix: '', + placeholder: '', + key: 'changeme', + label: 'Change me', + inputMask: '', + inputType: 'text', + tableView: true, + input: true, + logic: [ + { + name: 'Test 1', + trigger: { + javascript: "result = data.test === '1';", + type: 'javascript', + }, + actions: [ + { + name: 'Set Value', + type: 'value', + value: "value = 'Foo'", + }, + ], + }, + ], + }, + ], + }; + const submission = { + data: { test: '1' }, + }; + const context: any = { + form, + submission, + data: submission.data, + components: form.components, + processors: Processors, + scope: {}, + config: { + server: true, + }, + }; + processSync(context); + assert.equal(context.data.test, '1'); + assert.equal(context.data.changeme, 'Foo'); + }); + it('Should process nested form data correctly', async function () { const submission = { data: { @@ -1201,7 +1362,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, config: { server: true, @@ -1385,7 +1546,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors }, config: { server: true, @@ -1393,8 +1554,6 @@ describe('Process Tests', function () { }; processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); assert.equal(context.scope.errors.length, 0); assert.equal(context.data.showA, true); assert.equal(context.data.showB, true); @@ -1581,7 +1740,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors }, config: { server: true, @@ -1589,8 +1748,6 @@ describe('Process Tests', function () { }; processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); assert.equal(context.scope.errors.length, 0); assert.equal(context.data.showA, false); assert.equal(context.data.showB, true); @@ -1721,7 +1878,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: parentForm.components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, @@ -1729,8 +1886,6 @@ describe('Process Tests', function () { }; processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); assert.equal(context.data.showA, false); assert(!context.data.hasOwnProperty('childA'), 'The childA form should not be present.'); assert.deepEqual(context.data.childB.data, { @@ -1776,7 +1931,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, @@ -1861,7 +2016,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: { errors }, config: { server: true, @@ -1952,7 +2107,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: { errors }, config: { server: true, @@ -2042,7 +2197,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: { errors }, config: { server: true, @@ -2132,7 +2287,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: { errors }, config: { server: true, @@ -2236,7 +2391,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: { errors }, config: { server: true, @@ -2341,7 +2496,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: { errors }, config: { server: true, @@ -2445,7 +2600,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: { errors }, config: { server: true, @@ -2549,7 +2704,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: { errors }, config: { server: true, @@ -2601,7 +2756,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, config: { server: true, @@ -2672,7 +2827,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, config: { server: true, @@ -2766,7 +2921,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, config: { server: true, @@ -2867,7 +3022,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, config: { server: true, @@ -2974,7 +3129,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, config: { server: true, @@ -3095,7 +3250,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, config: { server: true, @@ -3224,7 +3379,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, config: { server: true, @@ -3310,7 +3465,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors: [] }, config: { server: true }, }; @@ -3385,7 +3540,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, config: { server: true, @@ -3436,7 +3591,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, config: { server: true, @@ -3485,7 +3640,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, config: { server: true, @@ -3583,7 +3738,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors }, config: { server: true, @@ -3591,8 +3746,6 @@ describe('Process Tests', function () { }; processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); assert.equal(context.scope.errors.length, 0); assert.deepEqual(context.data.form, { _id: '66c455fc0f00757fd4b0e79b', data: {} }); }); @@ -3605,7 +3758,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors }, config: { server: true, @@ -3613,8 +3766,6 @@ describe('Process Tests', function () { }; processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); expect(context.submission.data.number).to.be.equal(1); expect(context.submission.data.textField).to.be.equal('some text'); }); @@ -3627,7 +3778,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors }, config: { server: true, @@ -3635,8 +3786,6 @@ describe('Process Tests', function () { }; processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); expect(context.submission.data.number1).to.be.equal(1); expect(context.submission.data.number2).to.be.equal(2); }); @@ -3700,7 +3849,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors }, config: { server: true, @@ -3708,8 +3857,6 @@ describe('Process Tests', function () { }; processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); assert.equal(context.scope.errors.length, 0); }); @@ -3845,7 +3992,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors }, config: { server: true, @@ -3853,8 +4000,6 @@ describe('Process Tests', function () { }; processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); (context.scope as any).conditionals.forEach((v: any) => assert.equal(v.conditionallyHidden, false), ); @@ -3947,7 +4092,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors, conditionals }, config: { server: true, @@ -3956,8 +4101,6 @@ describe('Process Tests', function () { processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); expect(context.scope.conditionals).to.have.length(1); expect(context.scope.conditionals?.[0].conditionallyHidden).to.equal(false); assert.equal(context.scope.errors.length, 0); @@ -4097,7 +4240,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors, conditionals }, config: { server: true, @@ -4106,8 +4249,6 @@ describe('Process Tests', function () { processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); assert.deepEqual(context.data, { textField: '', textAreaFieldSet: 'test' }); context.scope.conditionals.forEach((cond: any) => { expect(cond.conditionallyHidden).to.be.equal(true); @@ -4226,7 +4367,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors, conditionals }, config: { server: true, @@ -4235,8 +4376,6 @@ describe('Process Tests', function () { processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); assert.deepEqual(context.data.textFieldShowOnTest1, 'test'); context.scope.conditionals.forEach((cond: any) => { assert.equal(cond.conditionallyHidden, cond.path !== 'textFieldShowOnTest1'); @@ -4591,7 +4730,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors, conditionals }, config: { server: true, @@ -4600,8 +4739,6 @@ describe('Process Tests', function () { processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); assert.deepEqual(context.data, data); context.scope.conditionals.forEach((cond: any) => { assert.equal( @@ -4660,14 +4797,12 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, }, }; - processSync(context); - context.processors = ProcessTargets.evaluator; const scope = processSync(context); expect((scope as ValidationScope).errors).to.have.length(2); }); @@ -4728,14 +4863,12 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, }, }; - processSync(context); - context.processors = ProcessTargets.evaluator; const scope = processSync(context); expect((scope as ValidationScope).errors).to.have.length(3); }); @@ -4873,7 +5006,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, @@ -4881,8 +5014,6 @@ describe('Process Tests', function () { }; processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); // Note that we DID omit textField3, which has clearOnHide enabled by default assert.deepEqual(context.data, { checkbox: true, @@ -5043,7 +5174,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, @@ -5051,8 +5182,6 @@ describe('Process Tests', function () { }; processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); // Note that we DID omit textField3, which has clearOnHide enabled by default assert.deepEqual(context.data, { checkbox: false, @@ -5137,7 +5266,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors, conditionals }, config: { server: true, @@ -5146,8 +5275,6 @@ describe('Process Tests', function () { processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); assert.deepEqual(context.scope.errors.length, 0); }); @@ -5347,7 +5474,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, fetch: (): Promise => { return Promise.resolve({ @@ -5363,8 +5490,6 @@ describe('Process Tests', function () { }; await process(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - await process(context); assert.deepEqual(context.data, submission.data); }); @@ -5419,15 +5544,13 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, }, }; processSync(context); - context.processors = ProcessTargets.evaluator; - processSync(context); expect(context.data).to.deep.equal({ dataGrid: [{ form: { data: { textField: 'test' } } }], }); @@ -5492,15 +5615,13 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, }, }; processSync(context); - context.processors = ProcessTargets.evaluator; - processSync(context); assert.equal(context.data.radio, 'yes'); }); @@ -5524,15 +5645,13 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, }, }; processSync(context); - context.processors = ProcessTargets.evaluator; - processSync(context); expect((context.scope as ValidationScope).errors).to.have.length(1); }); }); @@ -5544,15 +5663,13 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, }, }; - processSync(context); - context.processors = ProcessTargets.evaluator; processSync(context); expect((context.scope as ValidationScope).errors).to.have.length(0); }); @@ -5564,15 +5681,13 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, }, }; - await process(context); - context.processors = ProcessTargets.evaluator; await process(context); expect((context.scope as ValidationScope).errors).to.have.length(0); }); @@ -5584,15 +5699,13 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, }, }; - processSync(context as any); - context.processors = ProcessTargets.evaluator; processSync(context as any); expect((context.scope as ValidationScope).errors).to.have.length(0); }); @@ -5604,7 +5717,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, @@ -5612,9 +5725,6 @@ describe('Process Tests', function () { }; processSync(context); - context.processors = ProcessTargets.evaluator; - processSync(context); - expect((context.scope as ValidationScope).errors).to.have.length(1); }); @@ -5685,7 +5795,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors }, config: { server: true, @@ -5693,8 +5803,6 @@ describe('Process Tests', function () { }; processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); assert.equal(context.scope.errors.length, 1); assert.equal(context.scope.errors[0].ruleName, 'required'); }); @@ -5720,15 +5828,13 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, }, }; processSync(context); - context.processors = ProcessTargets.evaluator; - processSync(context); expect(context.data).to.deep.equal({ editGrid: [] }); }); @@ -5756,15 +5862,13 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, }, }; processSync(context); - context.processors = ProcessTargets.evaluator; - processSync(context); console.log(JSON.stringify(context.data, null, 2)); expect((context.scope as ValidationScope).errors).to.have.length(0); @@ -5793,15 +5897,13 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, }, }; processSync(context); - context.processors = ProcessTargets.evaluator; - processSync(context); expect((context.scope as ValidationScope).errors).to.have.length(1); }); }); @@ -5810,12 +5912,13 @@ describe('Process Tests', function () { describe('clearOnHide', function () { it('Should not include submission data from conditionally hidden containers when clearOnHide ("Omit Data When Conditionally Hidden" is true', async function () { const { form, submission } = clearOnHideWithCustomCondition; + const clonedSubmission = JSON.parse(JSON.stringify(submission)); const context = { form, - submission, - data: submission.data, + submission: clonedSubmission, + data: clonedSubmission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, @@ -5823,9 +5926,6 @@ describe('Process Tests', function () { }; processSync(context); - context.processors = ProcessTargets.evaluator; - processSync(context); - expect(context.data).to.deep.equal({ candidates: [{ candidate: { data: {} } }], submit: true, @@ -5834,14 +5934,15 @@ describe('Process Tests', function () { it('Should not return fields from conditionally hidden containers, clearOnHide = false', async function () { const { form, submission } = clearOnHideWithCustomCondition; + const clonedSubmission = JSON.parse(JSON.stringify(submission)); const containerComponent = getComponent(form.components, 'section6') as ContainerComponent; containerComponent.clearOnHide = false; const context = { form, - submission, - data: submission.data, + submission: clonedSubmission, + data: clonedSubmission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, @@ -5849,9 +5950,6 @@ describe('Process Tests', function () { }; processSync(context); - context.processors = ProcessTargets.evaluator; - processSync(context); - expect(context.data).to.deep.equal({ candidates: [{ candidate: { data: { section6: {} } } }], submit: true, @@ -5865,7 +5963,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: { errors: [] }, config: { server: true, @@ -5873,9 +5971,6 @@ describe('Process Tests', function () { }; processSync(context); - context.processors = ProcessTargets.evaluator; - processSync(context); - expect(context.data).to.deep.equal({ candidates: [{ candidate: { data: { section6: { c: {}, d: [] } } } }], submit: true, @@ -5892,7 +5987,7 @@ describe('Process Tests', function () { submission, data: submission.data, components: form.components, - processors: ProcessTargets.submission, + processors: Processors, scope: {}, config: { server: true, @@ -5900,9 +5995,6 @@ describe('Process Tests', function () { }; processSync(context); - context.processors = ProcessTargets.evaluator; - processSync(context); - expect(context.data).to.deep.equal({ candidates: [{ candidate: { data: { section6: { c: {}, d: [] } } } }], submit: true, @@ -5931,7 +6023,7 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, }; processSync(context); @@ -5960,7 +6052,7 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, }; processSync(context); @@ -5999,7 +6091,7 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, }; processSync(context); @@ -6038,7 +6130,7 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, }; processSync(context); @@ -6077,7 +6169,7 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, }; processSync(context); @@ -6116,7 +6208,7 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, }; processSync(context); @@ -6164,7 +6256,7 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, }; processSync(context); @@ -6212,7 +6304,7 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, }; processSync(context); @@ -6261,7 +6353,7 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {}, }; processSync(context); @@ -6303,7 +6395,7 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.evaluator, + processors: Processors, scope: {} as { errors: Record[] }, }; processSync(context); @@ -6322,13 +6414,11 @@ describe('Process Tests', function () { submission, data: submission.data, components, - processors: ProcessTargets.submission, + processors: Processors, scope: {} as { errors: Record[] }, }; processSync(context); submission.data = context.data; - context.processors = ProcessTargets.evaluator; - processSync(context); expect(context.scope.errors.length).to.equal(0); }); }); diff --git a/src/process/calculation/index.ts b/src/process/calculation/index.ts index bf679b97..2b6c44ef 100644 --- a/src/process/calculation/index.ts +++ b/src/process/calculation/index.ts @@ -50,6 +50,10 @@ export const calculateProcessSync: ProcessorFnSync = ( value: newValue, }); set(data, path, newValue); + if (!scope.filter) scope.filter = {}; + if (!scope.filter.hasOwnProperty(path)) { + scope.filter[path] = true; + } } return; }; diff --git a/src/process/clearHidden/index.ts b/src/process/clearHidden/index.ts index 1d025301..98326c42 100644 --- a/src/process/clearHidden/index.ts +++ b/src/process/clearHidden/index.ts @@ -1,14 +1,15 @@ import { unset } from 'lodash'; import { - ProcessorScope, ProcessorContext, ProcessorInfo, ProcessorFnSync, ConditionsScope, ProcessorFn, + FilterScope, } from 'types'; +import { getModelType, setComponentScope } from 'utils/formUtil'; -type ClearHiddenScope = ProcessorScope & { +export type ClearHiddenScope = FilterScope & { clearHidden: { [path: string]: boolean; }; @@ -18,21 +19,32 @@ type ClearHiddenScope = ProcessorScope & { * This processor function checks components for the `hidden` property and unsets corresponding data */ export const clearHiddenProcessSync: ProcessorFnSync = (context) => { - const { component, data, value, scope, path } = context; + const { component, data, value, scope, path, parent } = context; - // No need to unset the value if it's undefined - if (value === undefined) { - return; + // Check if there's a conditional set for the component and if it's marked as conditionally hidden + const isConditionallyHidden = + parent?.scope?.conditionallyHidden || + (scope as ConditionsScope).conditionals?.find((cond) => { + return path === cond.path && cond.conditionallyHidden; + }); + + if (isConditionallyHidden) { + setComponentScope(component, 'conditionallyHidden', true); } - if (!scope.clearHidden) { - scope.clearHidden = {}; + if ( + parent?.scope?.intentionallyHidden || + (component.hasOwnProperty('hidden') && !!component.hidden) + ) { + setComponentScope(component, 'intentionallyHidden', true); } - // Check if there's a conditional set for the component and if it's marked as conditionally hidden - const isConditionallyHidden = (scope as ConditionsScope).conditionals?.find((cond) => { - return path === cond.path && cond.conditionallyHidden; - }); + const compModel = getModelType(component); + + // No need to unset the value if it's undefined or is a non-data component. + if (value === undefined || !component.type || compModel === 'none' || compModel === 'content') { + return; + } const shouldClearValueWhenHidden = !component.hasOwnProperty('clearOnHide') || component.clearOnHide; @@ -42,7 +54,12 @@ export const clearHiddenProcessSync: ProcessorFnSync = (contex (isConditionallyHidden || component.scope?.conditionallyHidden) ) { unset(data, path); + if (!scope.clearHidden) scope.clearHidden = {}; scope.clearHidden[path] = true; + + // Make sure the filter does not include the value back. + if (!scope.filter) scope.filter = {}; + scope.filter[path] = false; } }; diff --git a/src/process/conditions/index.ts b/src/process/conditions/index.ts index f393e35a..55f6ec56 100644 --- a/src/process/conditions/index.ts +++ b/src/process/conditions/index.ts @@ -162,5 +162,5 @@ export const conditionProcessInfo: ProcessorInfo = { name: 'conditions', process: conditionProcess, processSync: conditionProcessSync, - shouldProcess: hasSimpleConditions, + shouldProcess: hasConditions, }; diff --git a/src/process/defaultValue/index.ts b/src/process/defaultValue/index.ts index fcd2ed09..4d63d22c 100644 --- a/src/process/defaultValue/index.ts +++ b/src/process/defaultValue/index.ts @@ -7,7 +7,7 @@ import { } from 'types'; import { set, has } from 'lodash'; import { evaluate } from 'utils'; -import { getComponentKey } from 'utils/formUtil'; +import { getComponentKey, getModelType } from 'utils/formUtil'; export const hasCustomDefaultValue = (context: DefaultValueContext): boolean => { const { component } = context; @@ -35,10 +35,29 @@ export const customDefaultValueProcess: ProcessorFn = async ( return customDefaultValueProcessSync(context); }; +function setDefaultValue(context: DefaultValueContext, defaultValue: any) { + const { component, data, scope, path } = context; + if (defaultValue === null || defaultValue === undefined) { + return; + } + if (!scope.defaultValues) scope.defaultValues = []; + scope.defaultValues.push({ + path, + value: defaultValue, + }); + set(data, path, defaultValue); + + // If this component is not already included in the filter and is not a number, then include it from the default. + if (!scope.filter) scope.filter = {}; + if (!scope.filter.hasOwnProperty(path) && getModelType(component) !== 'number') { + scope.filter[path] = true; + } +} + export const customDefaultValueProcessSync: ProcessorFnSync = ( context: DefaultValueContext, ) => { - const { component, row, data, scope, path } = context; + const { component, row, scope } = context; if (!hasCustomDefaultValue(context)) { return; } @@ -58,14 +77,8 @@ export const customDefaultValueProcessSync: ProcessorFnSync = ( if (component.multiple && !Array.isArray(defaultValue)) { defaultValue = defaultValue ? [defaultValue] : []; } - scope.defaultValues.push({ - path, - value: defaultValue, - }); - } - if (defaultValue !== null && defaultValue !== undefined) { - set(data, path, defaultValue); } + setDefaultValue(context, defaultValue); }; export const serverDefaultValueProcess: ProcessorFn = async ( @@ -77,7 +90,7 @@ export const serverDefaultValueProcess: ProcessorFn = async ( export const serverDefaultValueProcessSync: ProcessorFnSync = ( context: DefaultValueContext, ) => { - const { component, row, data, scope, path } = context; + const { component, row, scope } = context; if (!hasServerDefaultValue(context)) { return; } @@ -86,19 +99,13 @@ export const serverDefaultValueProcessSync: ProcessorFnSync = ( return; } let defaultValue = null; - if (component.defaultValue !== undefined && component.defaultValue !== null) { + if (component.defaultValue) { defaultValue = component.defaultValue; if (component.multiple && !Array.isArray(defaultValue)) { defaultValue = defaultValue ? [defaultValue] : []; } - scope.defaultValues.push({ - path, - value: defaultValue, - }); - } - if (defaultValue !== null && defaultValue !== undefined) { - set(data, path, defaultValue); } + setDefaultValue(context, defaultValue); }; export const defaultValueProcess: ProcessorFn = async ( diff --git a/src/process/fetch/index.ts b/src/process/fetch/index.ts index 16d3e90a..cb9dde1d 100644 --- a/src/process/fetch/index.ts +++ b/src/process/fetch/index.ts @@ -5,7 +5,6 @@ import { FetchScope, FetchFn, DataSourceComponent, - FilterContext, } from 'types'; import { get, set } from 'lodash'; import { evaluate, interpolate } from 'utils'; @@ -102,8 +101,10 @@ export const fetchProcess: ProcessorFn = async (context: FetchContex ); // Make sure the value does not get filtered for now... - if (!(scope as FilterContext).filter) (scope as FilterContext).filter = {}; - (scope as FilterContext).filter[path] = true; + if (!scope.filter) scope.filter = {}; + if (!scope.filter.hasOwnProperty(path)) { + scope.filter[path] = true; + } scope.fetched[path] = get(row, key); } catch (err: any) { console.log(err.message); diff --git a/src/process/filter/__tests__/filter.test.ts b/src/process/filter/__tests__/filter.test.ts index b20de5d7..3923ddbc 100644 --- a/src/process/filter/__tests__/filter.test.ts +++ b/src/process/filter/__tests__/filter.test.ts @@ -1,9 +1,14 @@ import { expect } from 'chai'; -import { filterProcessSync } from '../'; +import { filterProcessSync, filterPostProcess } from '../'; import { generateProcessorContext } from '../../__tests__/fixtures/util'; import { FilterScope, ProcessorContext } from 'types'; +const filterProcess = async (context: ProcessorContext) => { + filterProcessSync(context); + filterPostProcess(context); +}; + describe('Filter processor', function () { it('Should not filter empty array value for dataGrid component', async function () { const dataGridComp = { @@ -24,9 +29,9 @@ describe('Filter processor', function () { dataGrid: [], }; const context: any = generateProcessorContext(dataGridComp, data); - filterProcessSync(context); + filterProcess(context); expect(context.scope.filter).to.deep.equal({ - dataGrid: { compModelType: 'nestedArray', include: true, value: [] }, + dataGrid: true, }); }); @@ -48,9 +53,9 @@ describe('Filter processor', function () { editGrid: [], }; const context: any = generateProcessorContext(editGridComp, data); - filterProcessSync(context); + filterProcess(context); expect(context.scope.filter).to.deep.equal({ - editGrid: { compModelType: 'nestedArray', include: true, value: [] }, + editGrid: true, }); }); @@ -73,9 +78,9 @@ describe('Filter processor', function () { dataTable: [], }; const context: any = generateProcessorContext(dataTableComp, data); - filterProcessSync(context); + filterProcess(context); expect(context.scope.filter).to.deep.equal({ - dataTable: { compModelType: 'nestedArray', include: true, value: [] }, + dataTable: true, }); }); @@ -128,32 +133,28 @@ describe('Filter processor', function () { ], }; const context: any = generateProcessorContext(tagpadComp, data); - filterProcessSync(context); - expect(context.scope.filter).to.deep.equal({ - tagpad: { - compModelType: 'nestedDataArray', - include: true, - value: [ - { - coordinate: { - x: 83, - y: 61, - width: 280, - height: 133, - }, - data: {}, + filterProcess(context); + expect(context.scope.filtered).to.deep.equal({ + tagpad: [ + { + coordinate: { + x: 83, + y: 61, + width: 280, + height: 133, }, - { - coordinate: { - x: 194, - y: 93, - width: 280, - height: 133, - }, - data: {}, + data: {}, + }, + { + coordinate: { + x: 194, + y: 93, + width: 280, + height: 133, }, - ], - }, + data: {}, + }, + ], }); }); @@ -184,11 +185,11 @@ describe('Filter processor', function () { }; const context: ProcessorContext = generateProcessorContext(dataMapComp, data); - filterProcessSync(context); - expect(context.scope.filter).to.deep.equal({ + filterProcess(context); + expect(context.scope.filtered).to.deep.equal({ dataMap: { - compModelType: 'map', - include: true, + foo: 'bar', + baz: 'biz', }, }); }); diff --git a/src/process/filter/index.ts b/src/process/filter/index.ts index 9c9f56d1..f317997c 100644 --- a/src/process/filter/index.ts +++ b/src/process/filter/index.ts @@ -1,49 +1,20 @@ -import { FilterContext, FilterScope, ProcessorFn, ProcessorFnSync, ProcessorInfo } from 'types'; -import { set } from 'lodash'; -import { get, isObject } from 'lodash'; +import { + FilterContext, + FilterScope, + ProcessorFn, + ProcessorFnSync, + ProcessorInfo, + ProcessorPostFn, + ProcessorPostFnSync, +} from 'types'; +import { has, set } from 'lodash'; +import { get } from 'lodash'; import { getModelType } from 'utils/formUtil'; export const filterProcessSync: ProcessorFnSync = (context: FilterContext) => { - const { scope, component, path } = context; - const { value } = context; + const { scope, path, value } = context; if (!scope.filter) scope.filter = {}; if (value !== undefined) { - const modelType = getModelType(component); - switch (modelType) { - case 'dataObject': - scope.filter[path] = { - compModelType: modelType, - include: true, - value: { data: {} }, - }; - break; - case 'nestedArray': - scope.filter[path] = { - compModelType: modelType, - include: true, - value: [], - }; - break; - case 'nestedDataArray': - scope.filter[path] = { - compModelType: modelType, - include: true, - value: Array.isArray(value) ? value.map((v) => ({ ...v, data: {} })) : [], - }; - break; - case 'object': - scope.filter[path] = { - compModelType: modelType, - include: true, - value: component.type === 'address' ? false : {}, - }; - break; - default: - scope.filter[path] = { - compModelType: modelType, - include: true, - }; - break; - } + scope.filter[path] = true; } }; @@ -51,23 +22,47 @@ export const filterProcess: ProcessorFn = async (context: FilterCon return filterProcessSync(context); }; -export const filterPostProcess: ProcessorFnSync = (context: FilterContext) => { - const { scope, submission } = context; - const filtered = {}; - for (const path in scope.filter) { - if (scope.filter[path].include) { - let value = get(submission?.data, path); - if (scope.filter[path].value) { - if (isObject(value) && scope.filter[path].value?.data) { - value = { ...value, ...scope.filter[path].value }; - } else { - value = scope.filter[path].value; - } - } - set(filtered, path, value); +export const filterPostProcessSync: ProcessorPostFnSync = ( + context: FilterContext, +): boolean | undefined => { + const { scope, path, data, component, value } = context; + if (!scope.filter) scope.filter = {}; + if (value === undefined || !scope.filter[path]) { + return; + } + scope.filtered = scope.filtered || {}; + const modelType = getModelType(component); + if ( + component.type === 'address' || + (modelType !== 'dataObject' && + modelType !== 'nestedArray' && + modelType !== 'nestedDataArray' && + modelType !== 'object') + ) { + set(scope.filtered, path, value); + } else { + if (modelType === 'dataObject') { + set(data, `${path}.data`, get(scope.filtered, `${path}.data`, value?.data || {})); + set(scope.filtered, path, get(data, path)); + } else if (modelType === 'nestedDataArray') { + const filtered: any = get(scope.filtered, path, []); + set( + scope.filtered, + path, + value.map((item: any, index: number) => { + return { ...item, data: filtered[index]?.data || {} }; + }), + ); + } else if (!has(scope.filtered, path)) { + set(scope.filtered, path, value); + } else { + set(data, path, get(scope.filtered, path, value)); } } - context.data = filtered; +}; + +export const filterPostProcess: ProcessorPostFn = async (context: FilterContext) => { + return filterPostProcessSync(context); }; export const filterProcessInfo: ProcessorInfo = { @@ -75,5 +70,6 @@ export const filterProcessInfo: ProcessorInfo = { process: filterProcess, processSync: filterProcessSync, postProcess: filterPostProcess, + postProcessSync: filterPostProcessSync, shouldProcess: () => true, }; diff --git a/src/process/index.ts b/src/process/index.ts index 423b0417..afb77245 100644 --- a/src/process/index.ts +++ b/src/process/index.ts @@ -11,4 +11,3 @@ export * from './process'; export * from './normalize'; export * from './dereference'; export * from './clearHidden'; -export * from './hideChildren'; diff --git a/src/process/normalize/index.ts b/src/process/normalize/index.ts index 332deb6a..b74f4840 100644 --- a/src/process/normalize/index.ts +++ b/src/process/normalize/index.ts @@ -407,7 +407,7 @@ export const normalizeProcessSync: ProcessorFnSync = (context) = } // Next perform component-type-agnostic transformations (i.e. super()) - if (component.multiple && !Array.isArray(value)) { + if (component.multiple && !component.validate?.required && !Array.isArray(value)) { set(data, path, value ? [value] : []); scope.normalize[path].normalized = true; } diff --git a/src/process/process.ts b/src/process/process.ts index 89b5029a..02c57d30 100644 --- a/src/process/process.ts +++ b/src/process/process.ts @@ -1,6 +1,6 @@ -import { ProcessContext, ProcessTarget, ProcessorInfo, ProcessorScope } from 'types'; +import { FilterScope, ProcessContext, ProcessTarget, ProcessorInfo, ProcessorScope } from 'types'; import { eachComponentData, eachComponentDataAsync } from 'utils/formUtil'; -import { processOne, processOneSync } from './processOne'; +import { postProcessOne, postProcessOneSync, processOne, processOneSync } from './processOne'; import { defaultValueProcessInfo, serverDefaultValueProcessInfo, @@ -15,6 +15,7 @@ import { simpleConditionProcessInfo, } from './conditions'; import { + postValidateProcessInfo, validateCustomProcessInfo, validateProcessInfo, validateServerProcessInfo, @@ -23,13 +24,11 @@ import { filterProcessInfo } from './filter'; import { normalizeProcessInfo } from './normalize'; import { dereferenceProcessInfo } from './dereference'; import { clearHiddenProcessInfo } from './clearHidden'; -import { hideChildrenProcessorInfo } from './hideChildren'; export async function process( context: ProcessContext, ): Promise { - const { instances, components, data, scope, flat, processors, local, parent, parentPaths } = - context; + const { instances, components, data, scope, flat, local, parent, parentPaths } = context; await eachComponentDataAsync( components, data, @@ -60,19 +59,32 @@ export async function process( local, parent, parentPaths, + false, + async (component, compData, row, path, components, index, parent, paths) => { + await postProcessOne({ + ...context, + data: compData, + component, + components, + path, + paths, + row, + index, + instance: instances + ? instances[component.modelType === 'none' && paths?.fullPath ? paths.fullPath : path] + : undefined, + parent, + }); + }, ); - for (let i = 0; i < processors?.length; i++) { - const processor = processors[i]; - if (processor.postProcess) { - processor.postProcess(context); - } + if ((scope as FilterScope).filtered) { + context.data = (scope as FilterScope).filtered || {}; } return scope; } export function processSync(context: ProcessContext): ProcessScope { - const { instances, components, data, scope, flat, processors, local, parent, parentPaths } = - context; + const { instances, components, data, scope, flat, local, parent, parentPaths } = context; eachComponentData( components, data, @@ -103,12 +115,26 @@ export function processSync(context: ProcessContext) local, parent, parentPaths, + false, + (component, compData, row, path, components, index, parent, paths) => { + postProcessOneSync({ + ...context, + data: compData, + component, + components, + path, + paths, + row, + index, + instance: instances + ? instances[component.modelType === 'none' && paths?.fullPath ? paths.fullPath : path] + : undefined, + parent, + }); + }, ); - for (let i = 0; i < processors?.length; i++) { - const processor = processors[i]; - if (processor.postProcess) { - processor.postProcess(context); - } + if ((scope as FilterScope).filtered) { + context.data = (scope as FilterScope).filtered || {}; } return scope; } @@ -130,9 +156,22 @@ export const ProcessorMap: Record> = { validate: validateProcessInfo, validateCustom: validateCustomProcessInfo, validateServer: validateServerProcessInfo, - hideChildren: hideChildrenProcessorInfo, }; +export const Processors: ProcessorInfo[] = [ + filterProcessInfo, + defaultValueProcessInfo, + normalizeProcessInfo, + dereferenceProcessInfo, + fetchProcessInfo, + calculateProcessInfo, + conditionProcessInfo, + logicProcessInfo, + clearHiddenProcessInfo, + postValidateProcessInfo, +]; + +// Deprecated: Use Processors instead export const ProcessTargets: ProcessTarget = { submission: [ filterProcessInfo, @@ -148,7 +187,6 @@ export const ProcessTargets: ProcessTarget = { calculateProcessInfo, logicProcessInfo, conditionProcessInfo, - hideChildrenProcessorInfo, clearHiddenProcessInfo, validateProcessInfo, ], diff --git a/src/process/processOne.ts b/src/process/processOne.ts index c919fdb9..195fefb8 100644 --- a/src/process/processOne.ts +++ b/src/process/processOne.ts @@ -2,9 +2,8 @@ import { get, set } from 'lodash'; import { ProcessorsContext, ProcessorType } from 'types'; import { getModelType } from 'utils/formUtil'; -export async function processOne(context: ProcessorsContext) { - const { processors, component, paths, local, path } = context; - // Create a getter for `value` that is always derived from the current data object +export function contextValue(context: ProcessorsContext) { + const { component, paths, local, path } = context; if (typeof context.value === 'undefined') { const dataPath = local ? paths?.localDataPath || path : paths?.dataPath || path; Object.defineProperty(context, 'value', { @@ -26,7 +25,11 @@ export async function processOne(context: ProcessorsContext(context: ProcessorsContext) { + const { processors } = context; + contextValue(context); context.processor = ProcessorType.Custom; for (const processor of processors) { if (processor?.process) { @@ -36,31 +39,8 @@ export async function processOne(context: ProcessorsContext(context: ProcessorsContext) { - const { processors, component, paths, local, path } = context; - // Create a getter for `value` that is always derived from the current data object - if (typeof context.value === 'undefined') { - const dataPath = local ? paths?.localDataPath || path : paths?.dataPath || path; - Object.defineProperty(context, 'value', { - enumerable: true, - get() { - const modelType = getModelType(component); - if (!component.type || modelType === 'none' || modelType === 'content') { - return undefined; - } - return get(context.data, dataPath); - }, - set(newValue: any) { - const modelType = getModelType(component); - if (!component.type || modelType === 'none' || modelType === 'content') { - // Do not set the value if the model type is 'none' or 'content' - return; - } - set(context.data, dataPath, newValue); - }, - }); - } - - // Process the components. + const { processors } = context; + contextValue(context); context.processor = ProcessorType.Custom; for (const processor of processors) { if (processor?.processSync) { @@ -68,3 +48,25 @@ export function processOneSync(context: ProcessorsContext(context: ProcessorsContext) { + const { processors } = context; + contextValue(context); + context.processor = ProcessorType.Custom; + for (const processor of processors) { + if (processor?.postProcess) { + await processor.postProcess(context); + } + } +} + +export function postProcessOneSync(context: ProcessorsContext) { + const { processors } = context; + contextValue(context); + context.processor = ProcessorType.Custom; + for (const processor of processors) { + if (processor?.postProcessSync) { + processor.postProcessSync(context); + } + } +} diff --git a/src/process/validation/index.ts b/src/process/validation/index.ts index c4adcfcc..b5f163ef 100644 --- a/src/process/validation/index.ts +++ b/src/process/validation/index.ts @@ -9,6 +9,8 @@ import { ValidationScope, SkipValidationFn, ConditionsContext, + ProcessorPostFn, + ProcessorPostFnSync, } from 'types'; import { evaluationRules, rules, serverRules } from './rules'; import find from 'lodash/find'; @@ -376,6 +378,18 @@ export const validateAllProcessSync: ProcessorFnSync = ( return validateProcessSync(context); }; +export const validatePostProcess: ProcessorPostFn = async ( + context: ValidationContext, +): Promise => { + await validateAllProcess(context); +}; + +export const validatePostProcessSync: ProcessorPostFnSync = ( + context: ValidationContext, +): void => { + validateAllProcessSync(context); +}; + export const validateCustomProcessInfo: ProcessorInfo = { name: 'validateCustom', process: validateCustomProcess, @@ -397,4 +411,11 @@ export const validateProcessInfo: ProcessorInfo = { shouldProcess: shouldValidateAll, }; +export const postValidateProcessInfo: ProcessorInfo = { + name: 'validate', + postProcess: validatePostProcess, + postProcessSync: validatePostProcessSync, + shouldProcess: shouldValidateAll, +}; + export * from './util'; diff --git a/src/types/formUtil.ts b/src/types/formUtil.ts index faf74cee..6ea39936 100644 --- a/src/types/formUtil.ts +++ b/src/types/formUtil.ts @@ -120,6 +120,12 @@ export enum ComponentPath { * The "localDataPath" to the TextField component from the perspective of a configuration within the Form, would be "dataGrid[1].textField" */ localDataPath = 'localDataPath', + + /** + * The contextual "row" path for the component. + */ + localContextualRowPath = 'localContextualRowPath', + contextualRowPath = 'contextualRowPath', } /** @@ -133,6 +139,8 @@ export type ComponentPaths = { dataPath?: string; localDataPath?: string; dataIndex?: number; + contextualRowPath?: string; + localContextualRowPath?: string; }; export type EachComponentDataAsyncCallback = ( diff --git a/src/types/process/ProcessorFn.ts b/src/types/process/ProcessorFn.ts index 8bf02c23..c657a4f1 100644 --- a/src/types/process/ProcessorFn.ts +++ b/src/types/process/ProcessorFn.ts @@ -3,3 +3,9 @@ export type ProcessorFn = ( context: ProcessorContext, ) => Promise; export type ProcessorFnSync = (context: ProcessorContext) => void; +export type ProcessorPostFn = ( + context: ProcessorContext, +) => Promise; +export type ProcessorPostFnSync = ( + context: ProcessorContext, +) => void; diff --git a/src/types/process/ProcessorInfo.ts b/src/types/process/ProcessorInfo.ts index 74ae34aa..65c3ad1c 100644 --- a/src/types/process/ProcessorInfo.ts +++ b/src/types/process/ProcessorInfo.ts @@ -4,6 +4,7 @@ export type ProcessorInfo = { fullValue?: boolean; process?: (context: ProcessorContext) => Promise; processSync?: (context: ProcessorContext) => ProcessorReturnType; - postProcess?: (context: ProcessorContext) => void; + postProcess?: (context: ProcessorContext) => Promise; + postProcessSync?: (context: ProcessorContext) => void; shouldProcess: ProcessCheckFn; }; diff --git a/src/types/process/calculation/CalculationScope.ts b/src/types/process/calculation/CalculationScope.ts index b9b41c52..1e5f4af9 100644 --- a/src/types/process/calculation/CalculationScope.ts +++ b/src/types/process/calculation/CalculationScope.ts @@ -1,7 +1,7 @@ -import { ProcessorScope } from '..'; +import { FilterScope } from '..'; export type CalculationScope = { calculated?: Array<{ path: string; value: any; }>; -} & ProcessorScope; +} & FilterScope; diff --git a/src/types/process/defaultValue/DefaultValueScope.ts b/src/types/process/defaultValue/DefaultValueScope.ts index 74766c74..b3e0f949 100644 --- a/src/types/process/defaultValue/DefaultValueScope.ts +++ b/src/types/process/defaultValue/DefaultValueScope.ts @@ -1,8 +1,8 @@ -import { ProcessorScope } from '..'; +import { FilterScope } from '..'; export type DefaultValueScope = { defaultValue?: any; defaultValues?: Array<{ path: string; value: any; }>; -} & ProcessorScope; +} & FilterScope; diff --git a/src/types/process/fetch/FetchScope.ts b/src/types/process/fetch/FetchScope.ts index d88346fc..4bc5ec7d 100644 --- a/src/types/process/fetch/FetchScope.ts +++ b/src/types/process/fetch/FetchScope.ts @@ -1,4 +1,4 @@ -import { ProcessorScope } from '..'; +import { FilterScope } from '..'; export type FetchScope = { fetched?: Record; -} & ProcessorScope; +} & FilterScope; diff --git a/src/types/process/filter/FilterScope.ts b/src/types/process/filter/FilterScope.ts index 26fd3c75..5af2a390 100644 --- a/src/types/process/filter/FilterScope.ts +++ b/src/types/process/filter/FilterScope.ts @@ -1,11 +1,6 @@ +import { DataObject } from 'types/DataObject'; import { ProcessorScope } from '..'; export type FilterScope = { - filter: Record< - string, - { - compModelType: string; - include: boolean; - value?: any; - } - >; + filtered?: DataObject; + filter?: Record; } & ProcessorScope; diff --git a/src/types/process/logic/LogicScope.ts b/src/types/process/logic/LogicScope.ts index 2b3e9b0a..b5cc0f6d 100644 --- a/src/types/process/logic/LogicScope.ts +++ b/src/types/process/logic/LogicScope.ts @@ -1,2 +1,2 @@ -import { ProcessorScope } from '..'; -export type LogicScope = {} & ProcessorScope; +import { FilterScope } from '..'; +export type LogicScope = {} & FilterScope; diff --git a/src/utils/formUtil/eachComponentData.ts b/src/utils/formUtil/eachComponentData.ts index 02b3d690..e2786bea 100644 --- a/src/utils/formUtil/eachComponentData.ts +++ b/src/utils/formUtil/eachComponentData.ts @@ -37,6 +37,7 @@ export const eachComponentData = ( parent?: Component, parentPaths?: ComponentPaths, noScopeReset?: boolean, + afterFn?: EachComponentDataCallback, ) => { if (!components) { return; @@ -45,6 +46,20 @@ export const eachComponentData = ( components, (component, compPath, componentComponents, compParent, compPaths) => { const row = getContextualRowData(component, data, compPaths, local); + const callAfterFn = () => { + if (afterFn) { + return afterFn( + component, + data, + row, + (component.modelType === 'none' ? compPaths?.fullPath : compPaths?.dataPath) || '', + componentComponents, + compPaths?.dataIndex, + compParent, + compPaths, + ); + } + }; if ( fn( component, @@ -57,6 +72,7 @@ export const eachComponentData = ( compPaths, ) === true ) { + callAfterFn(); if (!noScopeReset) { resetComponentScope(component); } @@ -73,6 +89,9 @@ export const eachComponentData = ( ) { if (Array.isArray(value) && value.length) { for (let i = 0; i < value.length; i++) { + if (!value[i]) { + continue; + } if (compPaths) { compPaths.dataIndex = i; } @@ -85,8 +104,12 @@ export const eachComponentData = ( component, compPaths, noScopeReset, + afterFn, ); } + if (compPaths) { + compPaths.dataIndex = undefined; + } } else if (includeAll || isUndefined(value)) { eachComponentData( component.components, @@ -97,14 +120,18 @@ export const eachComponentData = ( component, compPaths, noScopeReset, + afterFn, ); } + callAfterFn(); if (!noScopeReset) { resetComponentScope(component); } return true; } else { - if (!includeAll && !shouldProcessComponent(component, row, value)) { + const shouldProcess = shouldProcessComponent(component, value, compPaths); + if (!includeAll && !shouldProcess) { + callAfterFn(); if (!noScopeReset) { resetComponentScope(component); } @@ -119,8 +146,10 @@ export const eachComponentData = ( component, compPaths, noScopeReset, + afterFn, ); } + callAfterFn(); if (!noScopeReset) { resetComponentScope(component); } @@ -138,6 +167,7 @@ export const eachComponentData = ( component, compPaths, noScopeReset, + afterFn, ), ); } else if (info.hasRows) { @@ -153,6 +183,7 @@ export const eachComponentData = ( component, compPaths, noScopeReset, + afterFn, ), ); } @@ -167,13 +198,16 @@ export const eachComponentData = ( component, compPaths, noScopeReset, + afterFn, ); } + callAfterFn(); if (!noScopeReset) { resetComponentScope(component); } return true; } + callAfterFn(); if (!noScopeReset) { resetComponentScope(component); } diff --git a/src/utils/formUtil/eachComponentDataAsync.ts b/src/utils/formUtil/eachComponentDataAsync.ts index 35bfd62e..12b19aa4 100644 --- a/src/utils/formUtil/eachComponentDataAsync.ts +++ b/src/utils/formUtil/eachComponentDataAsync.ts @@ -29,6 +29,7 @@ export const eachComponentDataAsync = async ( parent?: Component, parentPaths?: ComponentPaths, noScopeReset?: boolean, + afterFn?: EachComponentDataAsyncCallback, ) => { if (!components) { return; @@ -43,6 +44,20 @@ export const eachComponentDataAsync = async ( compPaths: ComponentPaths | undefined, ) => { const row = getContextualRowData(component, data, compPaths, local); + const callAfterFn = async () => { + if (afterFn) { + await afterFn( + component, + data, + row, + (component.modelType === 'none' ? compPaths?.fullPath : compPaths?.dataPath) || '', + componentComponents, + compPaths?.dataIndex, + compParent, + compPaths, + ); + } + }; if ( (await fn( component, @@ -52,8 +67,10 @@ export const eachComponentDataAsync = async ( componentComponents, compPaths?.dataIndex, compParent, + compPaths, )) === true ) { + await callAfterFn(); if (!noScopeReset) { resetComponentScope(component); } @@ -67,6 +84,9 @@ export const eachComponentDataAsync = async ( ) { if (Array.isArray(value) && value.length) { for (let i = 0; i < value.length; i++) { + if (!value[i]) { + continue; + } if (compPaths) { compPaths.dataIndex = i; } @@ -79,8 +99,12 @@ export const eachComponentDataAsync = async ( component, compPaths, noScopeReset, + afterFn, ); } + if (compPaths) { + compPaths.dataIndex = undefined; + } } else if (includeAll) { await eachComponentDataAsync( component.components, @@ -91,14 +115,18 @@ export const eachComponentDataAsync = async ( component, compPaths, noScopeReset, + afterFn, ); } + await callAfterFn(); if (!noScopeReset) { resetComponentScope(component); } return true; } else { - if (!includeAll && !shouldProcessComponent(component, row, value)) { + const shouldProcess = shouldProcessComponent(component, value, compPaths); + if (!includeAll && !shouldProcess) { + await callAfterFn(); if (!noScopeReset) { resetComponentScope(component); } @@ -113,8 +141,10 @@ export const eachComponentDataAsync = async ( component, compPaths, noScopeReset, + afterFn, ); } + await callAfterFn(); if (!noScopeReset) { resetComponentScope(component); } @@ -133,6 +163,7 @@ export const eachComponentDataAsync = async ( component, compPaths, noScopeReset, + afterFn, ); } } else if (info.hasRows) { @@ -149,6 +180,7 @@ export const eachComponentDataAsync = async ( component, compPaths, noScopeReset, + afterFn, ); } } @@ -163,13 +195,16 @@ export const eachComponentDataAsync = async ( component, compPaths, noScopeReset, + afterFn, ); } + await callAfterFn(); if (!noScopeReset) { resetComponentScope(component); } return true; } + await callAfterFn(); if (!noScopeReset) { resetComponentScope(component); } diff --git a/src/utils/formUtil/index.ts b/src/utils/formUtil/index.ts index 9229b2bb..0c1b4f0a 100644 --- a/src/utils/formUtil/index.ts +++ b/src/utils/formUtil/index.ts @@ -267,23 +267,33 @@ export function componentPath( const relative = type === ComponentPath.localPath || type === ComponentPath.fullLocalPath || - type === ComponentPath.localDataPath; + type === ComponentPath.localDataPath || + type === ComponentPath.localContextualRowPath; // Full paths include all layout component ids in the path. const fullPath = type === ComponentPath.fullPath || type === ComponentPath.fullLocalPath; // See if this is a data path. - const dataPath = type === ComponentPath.dataPath || type === ComponentPath.localDataPath; + const dataPath = + type === ComponentPath.dataPath || + type === ComponentPath.localDataPath || + type === ComponentPath.contextualRowPath || + type === ComponentPath.localContextualRowPath; // Determine if this component should include its key. - const includeKey = fullPath || (!!component.type && compModel !== 'none'); + const includeKey = + fullPath || (!!component.type && compModel !== 'none' && compModel !== 'content'); // The key is provided if the component can have data or if we are fetching the full path. const key = includeKey ? getComponentKey(component) : ''; + // If this is a contextual row path. + const contextual = + type === ComponentPath.contextualRowPath || type === ComponentPath.localContextualRowPath; + if (!parent) { // Return the key if there is no parent. - return key; + return contextual ? '' : key; } // Get the parent model type. @@ -291,11 +301,16 @@ export function componentPath( // If there is a parent, then we only return the key if the parent is a nested form and it is a relative path. if (relative && parentModel === 'dataObject') { - return key; + return contextual ? '' : key; } // Return the parent path. - let parentPath = parentPaths?.hasOwnProperty(type) ? parentPaths[type] || '' : ''; + const parentType = contextual + ? relative + ? ComponentPath.localDataPath + : ComponentPath.dataPath + : type; + let parentPath = parentPaths?.hasOwnProperty(parentType) ? parentPaths[parentType] || '' : ''; // For data paths (where we wish to get the path to the data), we need to ensure we append the parent // paths to the end of the path so that any component within this component properly references their data. @@ -308,6 +323,11 @@ export function componentPath( } } + // If this is a contextual row path, then return here. + if (contextual) { + return parentPath; + } + // Return the parent path with its relative component path (if applicable). return parentPath ? (key ? `${parentPath}.${key}` : parentPath) : key; } @@ -332,6 +352,18 @@ export function getComponentPaths( dataPath: componentPath(component, parent, parentPaths, ComponentPath.dataPath), localDataPath: componentPath(component, parent, parentPaths, ComponentPath.localDataPath), dataIndex: parentPaths?.dataIndex, + contextualRowPath: componentPath( + component, + parent, + parentPaths, + ComponentPath.contextualRowPath, + ), + localContextualRowPath: componentPath( + component, + parent, + parentPaths, + ComponentPath.localContextualRowPath, + ), }; } @@ -545,25 +577,23 @@ export function getComponentKey(component: Component) { return component.key; } -export function getContextualRowPath( - component: Component, - paths?: ComponentPaths, - local?: boolean, -): string { - if (!paths) { - return ''; - } - const dataPath = local ? paths.localDataPath : paths.dataPath; - return dataPath?.replace(new RegExp(`.?${escapeRegExp(getComponentKey(component))}$`), '') || ''; -} - export function getContextualRowData( component: Component, data: any, paths?: ComponentPaths, local?: boolean, ): any { - const rowPath = getContextualRowPath(component, paths, local); + if ( + paths?.hasOwnProperty('localContextualRowPath') && + paths?.hasOwnProperty('contextualRowPath') + ) { + const rowPath = local ? paths?.localContextualRowPath : paths?.contextualRowPath; + return rowPath ? get(data, rowPath, null) : data; + } + // Fallback to the less performant regex method. + const dataPath = local ? paths?.localDataPath : paths?.dataPath; + const rowPath = + dataPath?.replace(new RegExp(`.?${escapeRegExp(getComponentKey(component))}$`), '') || ''; return rowPath ? get(data, rowPath, null) : data; } @@ -576,13 +606,20 @@ export function getComponentLocalData(paths: ComponentPaths, data: any, local?: return parentPath ? get(data, parentPath, null) : data; } -export function shouldProcessComponent(comp: Component, row: any, value: any): boolean { +export function shouldProcessComponent( + comp: Component, + value: any, + paths?: ComponentPaths, +): boolean { if (getModelType(comp) === 'dataObject') { const noReferenceAttached = value?._id ? isEmpty(value.data) && !has(value, 'form') : false; const shouldBeCleared = (!comp.hasOwnProperty('clearOnHide') || comp.clearOnHide) && (comp.hidden || comp.scope?.conditionallyHidden); - const shouldSkipProcessingNestedFormData = noReferenceAttached || shouldBeCleared; + // Also skip processing if the value is empty and the form is in an array component. + const emptyInDataGrid = !value && paths?.dataIndex !== undefined; + const shouldSkipProcessingNestedFormData = + noReferenceAttached || (shouldBeCleared && !comp.validateWhenHidden) || emptyInDataGrid; if (shouldSkipProcessingNestedFormData) { return false; } diff --git a/src/utils/logic.ts b/src/utils/logic.ts index bdf7444c..c5bc12f0 100644 --- a/src/utils/logic.ts +++ b/src/utils/logic.ts @@ -130,7 +130,7 @@ export function setActionProperty(context: LogicContext, action: LogicActionProp } export function setValueProperty(context: LogicContext, action: LogicActionValue) { - const { component, data, path } = context; + const { component, data, path, scope } = context; const oldValue = get(data, path); const newValue = evaluate(action.value, context, 'value', false, (evalContext: any) => { evalContext.value = clone(oldValue); @@ -140,6 +140,10 @@ export function setValueProperty(context: LogicContext, action: LogicActionValue !(component.clearOnHide && conditionallyHidden(context as ProcessorContext)) ) { set(data, path, newValue); + if (!scope.filter) scope.filter = {}; + if (!scope.filter.hasOwnProperty(path)) { + scope.filter[path] = true; + } return true; } return false; From fcd92a8a29ee133971547363d810a60749af9782 Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Thu, 3 Jul 2025 12:51:35 -0500 Subject: [PATCH 2/2] FIO-10337: Fixed the normalize processor to handle null and undefined values in a much better way. (#275) * FIO-10337: Refactored the core data processors to significantly improve performance. * Update postProcess function signature. * Make sure to not autoset an array value if the component is required. * Make sure to add set paths to the filter. * Make sure that deliberate filtered paths are not included. * FIO-10337: Refactored the normalize processor to handle null and undefined components better. --- src/process/__tests__/process.test.ts | 81 +++++++++++++++++++++++++++ src/process/calculation/index.ts | 2 +- src/process/defaultValue/index.ts | 2 +- src/process/fetch/index.ts | 2 +- src/process/normalize/index.ts | 48 ++++++++-------- src/utils/logic.ts | 2 +- 6 files changed, 107 insertions(+), 30 deletions(-) diff --git a/src/process/__tests__/process.test.ts b/src/process/__tests__/process.test.ts index dc774726..4ca51fbe 100644 --- a/src/process/__tests__/process.test.ts +++ b/src/process/__tests__/process.test.ts @@ -6421,5 +6421,86 @@ describe('Process Tests', function () { submission.data = context.data; expect(context.scope.errors.length).to.equal(0); }); + + it('Should not add undefined values for components.', async function () { + const form = { + components: [ + { + input: true, + tableView: true, + inputType: 'text', + inputMask: '', + label: 'fname', + key: 'fname', + placeholder: '', + prefix: '', + suffix: '', + multiple: false, + defaultValue: '', + protected: false, + unique: false, + persistent: true, + validate: { + required: true, + minLength: '', + maxLength: '', + pattern: '', + custom: '', + customPrivate: false, + }, + conditional: { + show: '', + when: null, + eq: '', + }, + type: 'textfield', + }, + { + input: true, + tableView: true, + inputType: 'text', + inputMask: '', + label: 'lname', + key: 'lname', + placeholder: '', + prefix: '', + suffix: '', + multiple: false, + defaultValue: '', + protected: false, + unique: false, + persistent: true, + validate: { + required: true, + minLength: '', + maxLength: '', + pattern: '', + custom: '', + customPrivate: false, + }, + conditional: { + show: '', + when: null, + eq: '', + }, + type: 'textfield', + }, + ], + }; + const submission = { + data: {}, + }; + const context = { + form, + submission, + data: submission.data, + components: form.components, + processors: Processors, + scope: {}, + }; + processSync(context); + assert(!context.data.hasOwnProperty('fname')); + assert(!context.data.hasOwnProperty('lname')); + }); }); }); diff --git a/src/process/calculation/index.ts b/src/process/calculation/index.ts index 2b6c44ef..985b594f 100644 --- a/src/process/calculation/index.ts +++ b/src/process/calculation/index.ts @@ -51,7 +51,7 @@ export const calculateProcessSync: ProcessorFnSync = ( }); set(data, path, newValue); if (!scope.filter) scope.filter = {}; - if (!scope.filter.hasOwnProperty(path)) { + if (!(scope as any).clearHidden?.hasOwnProperty(path)) { scope.filter[path] = true; } } diff --git a/src/process/defaultValue/index.ts b/src/process/defaultValue/index.ts index 4d63d22c..ffa81874 100644 --- a/src/process/defaultValue/index.ts +++ b/src/process/defaultValue/index.ts @@ -49,7 +49,7 @@ function setDefaultValue(context: DefaultValueContext, defaultValue: any) { // If this component is not already included in the filter and is not a number, then include it from the default. if (!scope.filter) scope.filter = {}; - if (!scope.filter.hasOwnProperty(path) && getModelType(component) !== 'number') { + if (!(scope as any).clearHidden?.hasOwnProperty(path) && getModelType(component) !== 'number') { scope.filter[path] = true; } } diff --git a/src/process/fetch/index.ts b/src/process/fetch/index.ts index cb9dde1d..ecf8daeb 100644 --- a/src/process/fetch/index.ts +++ b/src/process/fetch/index.ts @@ -102,7 +102,7 @@ export const fetchProcess: ProcessorFn = async (context: FetchContex // Make sure the value does not get filtered for now... if (!scope.filter) scope.filter = {}; - if (!scope.filter.hasOwnProperty(path)) { + if (!(scope as any).clearHidden?.hasOwnProperty(path)) { scope.filter[path] = true; } scope.fetched[path] = get(row, key); diff --git a/src/process/normalize/index.ts b/src/process/normalize/index.ts index b74f4840..b2e08481 100644 --- a/src/process/normalize/index.ts +++ b/src/process/normalize/index.ts @@ -19,7 +19,7 @@ import { NumberComponent, } from 'types'; -type NormalizeScope = DefaultValueScope & { +export type NormalizeScope = DefaultValueScope & { normalize?: { [path: string]: any; }; @@ -371,45 +371,41 @@ export const normalizeProcessSync: ProcessorFnSync = (context) = type: component.type, normalized: false, }; - // First check for component-type-specific transformations + let newValue = value; if (isAddressComponent(component)) { - set(data, path, normalizeAddressComponentValue(component, value)); - scope.normalize[path].normalized = true; + newValue = normalizeAddressComponentValue(component, value); } else if (isDayComponent(component)) { - set(data, path, normalizeDayComponentValue(component, form, value)); - scope.normalize[path].normalized = true; + newValue = normalizeDayComponentValue(component, form, value); } else if (isEmailComponent(component)) { - if (value && typeof value === 'string') { - set(data, path, value.toLowerCase()); - scope.normalize[path].normalized = true; - } + newValue = value && isString(value) ? value.trim().toLowerCase() : value; } else if (isRadioComponent(component)) { - set(data, path, normalizeRadioComponentValue(value, component.dataType)); - scope.normalize[path].normalized = true; + newValue = normalizeRadioComponentValue(value, component.dataType); } else if (isSelectComponent(component)) { - set(data, path, normalizeSelectComponentValue(component, value)); - scope.normalize[path].normalized = true; + newValue = normalizeSelectComponentValue(component, value); } else if (isSelectBoxesComponent(component)) { - set(data, path, normalizeSelectBoxesComponentValue(value)); - scope.normalize[path].normalized = true; + newValue = normalizeSelectBoxesComponentValue(value); } else if (isTagsComponent(component)) { - set(data, path, normalizeTagsComponentValue(component, value)); - scope.normalize[path].normalized = true; + newValue = normalizeTagsComponentValue(component, value); } else if (isTextFieldComponent(component)) { - set(data, path, normalizeTextFieldComponentValue(component, defaultValues, value, path)); - scope.normalize[path].normalized = true; + newValue = normalizeTextFieldComponentValue(component, defaultValues, value, path); } else if (isTimeComponent(component)) { - set(data, path, normalizeTimeComponentValue(component, value)); - scope.normalize[path].normalized = true; + newValue = normalizeTimeComponentValue(component, value); } else if (isNumberComponent(component)) { - set(data, path, normalizeNumberComponentValue(component, value)); - scope.normalize[path].normalized = true; + newValue = normalizeNumberComponentValue(component, value); } - // Next perform component-type-agnostic transformations (i.e. super()) if (component.multiple && !component.validate?.required && !Array.isArray(value)) { - set(data, path, value ? [value] : []); + newValue = value ? [value] : []; + } + + if (newValue === undefined || newValue === null) { + scope.filter = scope.filter || {}; + scope.filter[path] = false; + } else if (value !== newValue && !(scope as any).clearHidden?.hasOwnProperty(path)) { + set(data, path, newValue); scope.normalize[path].normalized = true; + scope.filter = scope.filter || {}; + scope.filter[path] = true; } }; diff --git a/src/utils/logic.ts b/src/utils/logic.ts index c5bc12f0..30e855d2 100644 --- a/src/utils/logic.ts +++ b/src/utils/logic.ts @@ -141,7 +141,7 @@ export function setValueProperty(context: LogicContext, action: LogicActionValue ) { set(data, path, newValue); if (!scope.filter) scope.filter = {}; - if (!scope.filter.hasOwnProperty(path)) { + if (!(scope as any).clearHidden?.hasOwnProperty(path)) { scope.filter[path] = true; } return true;