From 2870d6d6797fb622f85c125fc84b3b5c1b0882f8 Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Mon, 4 Nov 2024 16:43:35 -0600 Subject: [PATCH] FIO-9308: Fixed the paths with nested forms by ensuring we are always dealing with the absolute paths with clearOnHide, conditions, filters, and validations. --- src/process/__tests__/process.test.ts | 148 +++++++++++++++++++++++++- src/process/clearHidden.ts | 10 +- src/process/conditions/index.ts | 8 +- src/process/filter/index.ts | 4 +- src/process/hideChildren.ts | 4 +- src/process/validation/index.ts | 9 +- src/utils/conditions.ts | 14 ++- src/utils/logic.ts | 8 +- 8 files changed, 183 insertions(+), 22 deletions(-) diff --git a/src/process/__tests__/process.test.ts b/src/process/__tests__/process.test.ts index 49b57db9..77d02182 100644 --- a/src/process/__tests__/process.test.ts +++ b/src/process/__tests__/process.test.ts @@ -14,6 +14,7 @@ import { skipValidForLogicallyHiddenComp, skipValidWithHiddenParentComp, } from './fixtures'; +import { get } from 'lodash'; /* describe('Process Tests', () => { @@ -975,6 +976,148 @@ describe('Process Tests', function () { assert.equal(context.scope.errors.length, 0); }); + it('Should allow data from a Conditionally shown nested form when another nested form is conditionally not shown.', async function () { + const form = { + components: [ + { + label: 'Radio', + values: [ + { + label: 'Show A', + value: 'a', + shortcut: '', + }, + { + label: 'Show B', + value: 'b', + shortcut: '', + }, + ], + key: 'radio', + type: 'radio', + input: true, + }, + { + label: 'Form', + conditional: { + show: true, + conjunction: 'all', + conditions: [ + { + component: 'radio', + operator: 'isEqual', + value: 'a', + }, + ], + }, + type: 'form', + key: 'form', + input: true, + components: [ + { + label: 'Form', + key: 'form', + type: 'form', + input: true, + components: [ + { + label: 'Text Field', + validate: { + required: true, + }, + key: 'textField', + type: 'textfield', + input: true, + }, + { + label: 'Text Field', + key: 'textField1', + type: 'textfield', + input: true, + }, + ], + }, + ], + }, + { + label: 'Form', + key: 'form1', + conditional: { + show: true, + conjunction: 'all', + conditions: [ + { + component: 'radio', + operator: 'isEqual', + value: 'b', + }, + ], + }, + type: 'form', + input: true, + components: [ + { + label: 'Form', + key: 'form', + type: 'form', + input: true, + components: [ + { + label: 'Text Field', + validate: { + required: true, + }, + key: 'textField', + type: 'textfield', + input: true, + }, + { + label: 'Text Field', + key: 'textField1', + type: 'textfield', + input: true, + }, + ], + }, + ], + }, + ], + }; + const submission = { + data: { + radio: 'b', + form1: { + data: { + form: { + data: { + textField: 'one 1', + textField1: 'two 2', + }, + }, + }, + }, + }, + }; + const errors: any = []; + const context = { + form, + submission, + data: submission.data, + components: form.components, + processors: ProcessTargets.submission, + scope: { errors }, + config: { + server: true, + }, + }; + 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'); + }); + it('should remove submission data not in a nested form definition', async function () { const form = { _id: {}, @@ -4398,7 +4541,10 @@ describe('Process Tests', function () { processSync(context); assert.deepEqual(context.data, data); context.scope.conditionals.forEach((cond: any) => { - assert.equal(cond.conditionallyHidden, cond.path === 'postalCode'); + assert.equal( + cond.conditionallyHidden, + cond.path === 'pmta.data.contacts.data.applicantOrganization.data.address.data.postalCode', + ); }); }); diff --git a/src/process/clearHidden.ts b/src/process/clearHidden.ts index ef2054e4..ce55e11d 100644 --- a/src/process/clearHidden.ts +++ b/src/process/clearHidden.ts @@ -6,6 +6,7 @@ import { ProcessorFnSync, ConditionsScope, } from 'types'; +import { getComponentAbsolutePath } from 'utils/formUtil'; type ClearHiddenScope = ProcessorScope & { clearHidden: { @@ -17,7 +18,8 @@ type ClearHiddenScope = ProcessorScope & { * This processor function checks components for the `hidden` property and unsets corresponding data */ export const clearHiddenProcess: ProcessorFnSync = (context) => { - const { component, data, path, value, scope } = context; + const { component, data, value, scope, path } = context; + const absolutePath = getComponentAbsolutePath(component) || path; // No need to unset the value if it's undefined if (value === undefined) { @@ -30,7 +32,7 @@ export const clearHiddenProcess: ProcessorFnSync = (context) = // 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; + return absolutePath === cond.path && cond.conditionallyHidden; }); const shouldClearValueWhenHidden = @@ -40,8 +42,8 @@ export const clearHiddenProcess: ProcessorFnSync = (context) = shouldClearValueWhenHidden && (isConditionallyHidden || component.hidden || component.ephemeralState?.conditionallyHidden) ) { - unset(data, path); - scope.clearHidden[path] = true; + unset(data, absolutePath); + scope.clearHidden[absolutePath] = true; } }; diff --git a/src/process/conditions/index.ts b/src/process/conditions/index.ts index ac24d75f..f469bde3 100644 --- a/src/process/conditions/index.ts +++ b/src/process/conditions/index.ts @@ -15,6 +15,7 @@ import { isSimpleConditional, isJSONConditional, } from 'utils/conditions'; +import { getComponentAbsolutePath } from 'utils/formUtil'; const hasCustomConditions = (context: ConditionsContext): boolean => { const { component } = context; @@ -83,7 +84,8 @@ export const isConditionallyHidden = (context: ConditionsContext): boolean => { export type ConditionallyHidden = (context: ConditionsContext) => boolean; export const conditionalProcess = (context: ConditionsContext, isHidden: ConditionallyHidden) => { - const { scope, path } = context; + const { scope, path, component } = context; + const absolutePath = getComponentAbsolutePath(component) || path; if (!hasConditions(context)) { return; } @@ -91,9 +93,9 @@ export const conditionalProcess = (context: ConditionsContext, isHidden: Conditi if (!scope.conditionals) { scope.conditionals = []; } - let conditionalComp = scope.conditionals.find((cond) => cond.path === path); + let conditionalComp = scope.conditionals.find((cond) => cond.path === absolutePath); if (!conditionalComp) { - conditionalComp = { path, conditionallyHidden: false }; + conditionalComp = { path: absolutePath, conditionallyHidden: false }; scope.conditionals.push(conditionalComp); } diff --git a/src/process/filter/index.ts b/src/process/filter/index.ts index 72186af1..d8dbab4c 100644 --- a/src/process/filter/index.ts +++ b/src/process/filter/index.ts @@ -4,9 +4,9 @@ import { Utils } from 'utils'; import { get, isObject } from 'lodash'; import { getComponentAbsolutePath } from 'utils/formUtil'; export const filterProcessSync: ProcessorFnSync = (context: FilterContext) => { - const { scope, component } = context; + const { scope, component, path } = context; const { value } = context; - const absolutePath = getComponentAbsolutePath(component); + const absolutePath = getComponentAbsolutePath(component) || path; if (!scope.filter) scope.filter = {}; if (value !== undefined) { const modelType = Utils.getModelType(component); diff --git a/src/process/hideChildren.ts b/src/process/hideChildren.ts index aa5e7114..5037e8c4 100644 --- a/src/process/hideChildren.ts +++ b/src/process/hideChildren.ts @@ -7,15 +7,17 @@ import { ProcessorFn, } from 'types'; import { registerEphemeralState } from 'utils'; +import { getComponentAbsolutePath } from 'utils/formUtil'; /** * This processor function checks components for the `hidden` property and, if children are present, sets them to hidden as well. */ export const hideChildrenProcessor: ProcessorFnSync = (context) => { const { component, path, parent, scope } = context; + const absolutePath = getComponentAbsolutePath(component) || path; // Check if there's a conditional set for the component and if it's marked as conditionally hidden const isConditionallyHidden = scope.conditionals?.find((cond) => { - return path === cond.path && cond.conditionallyHidden; + return absolutePath === cond.path && cond.conditionallyHidden; }); if (!scope.conditionals) { diff --git a/src/process/validation/index.ts b/src/process/validation/index.ts index daacee41..187e502b 100644 --- a/src/process/validation/index.ts +++ b/src/process/validation/index.ts @@ -15,7 +15,7 @@ import { evaluationRules, rules, serverRules } from './rules'; import find from 'lodash/find'; import get from 'lodash/get'; import pick from 'lodash/pick'; -import { getComponentAbsolutePath, getComponentPath } from 'utils/formUtil'; +import { getComponentAbsolutePath } from 'utils/formUtil'; import { getErrorMessage } from 'utils/error'; import { FieldError } from 'error'; import { @@ -107,11 +107,12 @@ export const _shouldSkipValidation = ( isConditionallyHidden: ConditionallyHidden, ) => { const { component, scope, path } = context; + const absolutePath = getComponentAbsolutePath(component) || path; if ( (scope as ConditionsScope)?.conditionals && (find((scope as ConditionsScope).conditionals, { - path: getComponentPath(component, path), + path: absolutePath, conditionallyHidden: true, }) || component.ephemeralState?.conditionallyHidden === true) @@ -169,8 +170,8 @@ export function shouldValidateServer(context: ValidationContext): boolean { } function handleError(error: FieldError | null, context: ValidationContext) { - const { scope, component } = context; - const absolutePath = getComponentAbsolutePath(component); + const { scope, component, path } = context; + const absolutePath = getComponentAbsolutePath(component) || path; if (error) { const cleanedError = cleanupValidationError(error); cleanedError.context.path = absolutePath; diff --git a/src/utils/conditions.ts b/src/utils/conditions.ts index d1d4cd78..b0b13bbe 100644 --- a/src/utils/conditions.ts +++ b/src/utils/conditions.ts @@ -1,6 +1,11 @@ import { ConditionsContext, JSONConditional, LegacyConditional, SimpleConditional } from 'types'; import { EvaluatorFn, evaluate, JSONLogicEvaluator } from 'modules/jsonlogic'; -import { flattenComponents, getComponent, getComponentActualValue } from './formUtil'; +import { + flattenComponents, + getComponent, + getComponentAbsolutePath, + getComponentActualValue, +} from './formUtil'; import { has, isObject, map, every, some, find, filter, isBoolean, split } from 'lodash'; import ConditionOperators from './operators'; @@ -17,10 +22,11 @@ export const isSimpleConditional = (conditional: any): conditional is SimpleCond }; export function conditionallyHidden(context: ConditionsContext) { - const { scope, path } = context; - if (scope.conditionals && path) { + const { scope, path, component } = context; + const absolutePath = getComponentAbsolutePath(component) || path; + if (scope.conditionals && absolutePath) { const hidden = find(scope.conditionals, (conditional) => { - return conditional.path === path; + return conditional.path === absolutePath; }); return hidden?.conditionallyHidden; } diff --git a/src/utils/logic.ts b/src/utils/logic.ts index b8a7360f..2d1d7a29 100644 --- a/src/utils/logic.ts +++ b/src/utils/logic.ts @@ -18,6 +18,7 @@ import { import { get, set, clone, isEqual, assign } from 'lodash'; import { evaluate, interpolate } from 'modules/jsonlogic'; import { registerEphemeralState } from './utils'; +import { getComponentAbsolutePath } from './formUtil'; export const hasLogic = (context: LogicContext): boolean => { const { component } = context; @@ -69,6 +70,7 @@ export function setActionBooleanProperty( action: LogicActionPropertyBoolean, ): boolean { const { component, scope, path } = context; + const absolutePath = getComponentAbsolutePath(component) || path; const property = action.property.value; const currentValue = get(component, property, false).toString(); const newValue = action.state.toString(); @@ -77,19 +79,19 @@ export function setActionBooleanProperty( // If this is "logic" forcing a component to set hidden property, then we will set the "conditionallyHidden" // flag which will trigger the clearOnHide functionality. - if (property === 'hidden' && path) { + if (property === 'hidden' && absolutePath) { if (!(scope as ConditionsScope).conditionals) { (scope as ConditionsScope).conditionals = []; } const conditionallyHidden = (scope as ConditionsScope).conditionals?.find((cond: any) => { - return cond.path === path; + return cond.path === absolutePath; }); if (conditionallyHidden) { conditionallyHidden.conditionallyHidden = !!component.hidden; registerEphemeralState(component, 'conditionallyHidden', !!component.hidden); } else { (scope as ConditionsScope).conditionals?.push({ - path, + path: absolutePath, conditionallyHidden: !!component.hidden, }); }