diff --git a/src/process/__tests__/process.test.ts b/src/process/__tests__/process.test.ts index da0feae8..49b57db9 100644 --- a/src/process/__tests__/process.test.ts +++ b/src/process/__tests__/process.test.ts @@ -972,8 +972,6 @@ describe('Process Tests', function () { submission.data = context.data; context.processors = ProcessTargets.evaluator; processSync(context); - console.log(context.scope.errors); - assert.equal(context.scope.errors.length, 0); }); @@ -4033,13 +4031,377 @@ describe('Process Tests', function () { submission.data = context.data; context.processors = ProcessTargets.evaluator; processSync(context); - console.log(111, context.data, context.scope.conditionals); assert.deepEqual(context.data.textFieldShowOnTest1, 'test'); context.scope.conditionals.forEach((cond: any) => { assert.equal(cond.conditionallyHidden, cond.path !== 'textFieldShowOnTest1'); }); }); + it('Should not unset values of deeply nested form when some components in other forms have conditional components', function () { + const form = { + _id: '6718a657d064efa63512550e', + title: 'eSubmissionsExt', + name: 'eSubmissionsExt', + path: 'esubmissionsext', + type: 'form', + display: 'wizard', + tags: [], + deleted: null, + owner: '6718e9edf62d8a6fd8104211', + components: [ + { + title: 'eSubmissions', + breadcrumbClickable: true, + buttonSettings: { + previous: true, + cancel: true, + next: true, + }, + navigateOnEnter: false, + saveOnEnter: false, + scrollToTop: false, + collapsible: false, + key: 'eSubmissions', + type: 'panel', + label: 'Page 1', + components: [ + { + label: 'Text Field', + applyMaskOn: 'change', + tableView: true, + validateWhenHidden: false, + key: 'textField', + type: 'textfield', + input: true, + }, + { + label: 'PMTA', + tableView: true, + form: '6718a657d064efa6351254eb', + useOriginalRevision: false, + reference: false, + key: 'pmta', + conditional: { + show: true, + conjunction: 'all', + conditions: [ + { + component: 'textField', + operator: 'isEqual', + value: '5', + }, + ], + }, + type: 'form', + input: true, + components: [ + { + type: 'button', + label: 'Submit', + key: 'submit', + disableOnInvalid: true, + input: true, + tableView: false, + }, + { + title: 'Section I - Applicant Identification', + breadcrumbClickable: true, + buttonSettings: { + previous: true, + cancel: true, + next: true, + }, + navigateOnEnter: false, + saveOnEnter: false, + scrollToTop: false, + collapsible: false, + key: 'section1', + type: 'panel', + label: 'Page 6', + input: false, + tableView: false, + components: [ + { + label: 'Identification Information', + tableView: true, + form: '6718a657d064efa6351254e4', + useOriginalRevision: false, + reference: false, + clearOnHide: false, + key: 'contacts', + type: 'form', + input: true, + components: [ + { + title: 'Part A: Applicant Identification', + breadcrumbClickable: true, + buttonSettings: { + previous: true, + cancel: true, + next: true, + }, + navigateOnEnter: false, + saveOnEnter: false, + scrollToTop: false, + collapsible: false, + key: 'section1A', + type: 'panel', + label: 'Page 1', + components: [ + { + label: 'Applicant Organization', + tableView: true, + form: '6718a657d064efa6351254c8', + useOriginalRevision: false, + reference: false, + clearOnHide: false, + key: 'applicantOrganization', + type: 'form', + lazyLoad: true, + input: true, + components: [ + { + label: 'Address', + tableView: true, + form: '6718f5a4f62d8a6fd8110a41', + useOriginalRevision: false, + reference: false, + clearOnHide: false, + key: 'address', + type: 'form', + input: true, + components: [ + { + label: 'Country', + widget: 'choicesjs', + tableView: true, + data: { + values: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + ], + resource: '6718a657d064efa635125495', + }, + clearOnHide: false, + validateWhenHidden: false, + key: 'country', + type: 'select', + input: true, + }, + { + label: 'Zip Code', + displayMask: '99999-9999', + applyMaskOn: 'change', + tableView: true, + clearOnHide: false, + validateOn: 'blur', + validateWhenHidden: false, + key: 'zipCode', + conditional: { + show: true, + conjunction: 'all', + conditions: [ + { + component: 'country', + operator: 'isEqual', + value: 'a', + }, + ], + }, + type: 'textfield', + input: true, + }, + { + label: 'Postal Code', + applyMaskOn: 'change', + tableView: true, + clearOnHide: false, + validateWhenHidden: false, + key: 'postalCode', + conditional: { + show: true, + conjunction: 'all', + conditions: [ + { + component: 'country', + operator: 'isNotEqual', + value: 'a', + }, + ], + }, + type: 'textfield', + input: true, + }, + { + type: 'button', + label: 'Submit', + key: 'submit', + disableOnInvalid: true, + input: true, + tableView: false, + }, + ], + }, + { + type: 'button', + label: 'Submit', + key: 'submit', + disableOnInvalid: true, + input: true, + tableView: false, + }, + ], + }, + ], + input: false, + tableView: false, + }, + { + title: 'Part B: Authorized Representative or U.S. Agent Information', + breadcrumbClickable: true, + buttonSettings: { + previous: true, + cancel: true, + next: true, + }, + navigateOnEnter: false, + saveOnEnter: false, + scrollToTop: false, + collapsible: false, + key: 'section1B', + properties: { subsection: '1' }, + type: 'panel', + label: 'Page 1', + components: [ + { + label: 'Authorized Representative', + tableView: true, + form: '6718a657d064efa6351254d6', + useOriginalRevision: false, + reference: false, + clearOnHide: false, + key: 'authorizedRepresentative', + type: 'form', + lazyLoad: true, + input: true, + components: [ + { + label: 'Organization', + tableView: true, + form: '6718a657d064efa6351254b0', + useOriginalRevision: false, + reference: false, + key: 'organization', + type: 'form', + input: true, + clearOnHide: false, + components: [ + { + label: 'Organization Name', + applyMaskOn: 'change', + tableView: true, + clearOnHide: false, + validateWhenHidden: false, + key: 'organizationName', + type: 'textfield', + input: true, + }, + { + type: 'button', + label: 'Submit', + key: 'submit', + disableOnInvalid: true, + input: true, + tableView: false, + }, + ], + }, + { + type: 'button', + label: 'Submit', + key: 'submit', + disableOnInvalid: true, + input: true, + tableView: false, + }, + ], + }, + ], + input: false, + tableView: false, + }, + ], + }, + ], + }, + ], + }, + ], + input: false, + tableView: false, + }, + ], + created: '2024-10-23T07:31:35.235Z', + modified: '2024-10-23T13:14:44.389Z', + }; + + const data = { + textField: '5', + pmta: { + data: { + contacts: { + data: { + applicantOrganization: { + data: { + address: { + data: { country: 'a', zipCode: '555555555', postalCode: '' }, + metadata: { selectData: { country: { label: 'a' } } }, + }, + }, + metadata: {}, + }, + authorizedRepresentative: { + data: { + organization: { + data: { organizationName: '66666' }, + metadata: {}, + }, + }, + metadata: {}, + }, + }, + }, + }, + }, + }; + const submission = { + data: fastCloneDeep(data), + }; + + const errors: any = []; + const conditionals: any = []; + const context = { + form, + submission, + data: submission.data, + components: form.components, + processors: ProcessTargets.submission, + scope: { errors, conditionals }, + config: { + server: true, + }, + }; + + 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(cond.conditionallyHidden, cond.path === 'postalCode'); + }); + }); + it('Should not return errors for empty multiple values for url and dateTime', function () { const form = { _id: '671f7fbeaf87b0e2a26e4212', diff --git a/src/process/conditions/__tests__/conditions.test.ts b/src/process/conditions/__tests__/conditions.test.ts index 628543ae..33ac2d94 100644 --- a/src/process/conditions/__tests__/conditions.test.ts +++ b/src/process/conditions/__tests__/conditions.test.ts @@ -124,9 +124,9 @@ describe('Condition processor', function () { { component: 'selectBoxes', operator: 'isNotEqual', - value: '3' - } - ] + value: '3', + }, + ], }, type: 'textfield', input: true, diff --git a/src/process/validation/rules/__tests__/validateAvailableItems.test.ts b/src/process/validation/rules/__tests__/validateAvailableItems.test.ts index 63b6a904..9643ece2 100644 --- a/src/process/validation/rules/__tests__/validateAvailableItems.test.ts +++ b/src/process/validation/rules/__tests__/validateAvailableItems.test.ts @@ -476,11 +476,12 @@ describe('validateAvailableItems', function () { context.fetch = () => { return Promise.resolve({ ok: true, - json: () => Promise.resolve([ - { label: '1', value: '1' }, - { label: '2', value: '2' }, - { label: '3', value: '3' }, - ]), + json: () => + Promise.resolve([ + { label: '1', value: '1' }, + { label: '2', value: '2' }, + { label: '3', value: '3' }, + ]), }); }; const result = await validateAvailableItems(context); @@ -505,11 +506,12 @@ describe('validateAvailableItems', function () { context.fetch = () => { return Promise.resolve({ ok: true, - json: () => Promise.resolve([ - { label: '1', value: '1' }, - { label: '2', value: '2' }, - { label: '3', value: '3' }, - ]), + json: () => + Promise.resolve([ + { label: '1', value: '1' }, + { label: '2', value: '2' }, + { label: '3', value: '3' }, + ]), }); }; const result = await validateAvailableItems(context); diff --git a/src/process/validation/rules/validateAvailableItems.ts b/src/process/validation/rules/validateAvailableItems.ts index 7e37ffed..07abe967 100644 --- a/src/process/validation/rules/validateAvailableItems.ts +++ b/src/process/validation/rules/validateAvailableItems.ts @@ -249,8 +249,9 @@ export const validateAvailableItems: RuleFn = async (context: ValidationContext) ? null : error; } - return values.find((optionValue) => optionValue.value === value || optionValue === value) !== - undefined + return values.find( + (optionValue) => optionValue.value === value || optionValue === value, + ) !== undefined ? null : error; } diff --git a/src/sdk/Formio.ts b/src/sdk/Formio.ts index b211d577..aca7691c 100644 --- a/src/sdk/Formio.ts +++ b/src/sdk/Formio.ts @@ -2370,17 +2370,23 @@ export class Formio { * @param {HTMLElement} rootElement - The element after which the resource would be attached (useful when requiring resources from ShadowRoot). * @return {Promise} - A promise that will resolve when the plugin is ready to be used. */ - static requireLibrary(name: string, property: string, src: string | Array, polling: boolean = false, onload?: (ready: Promise) => void, rootElement?: HTMLElement ) { - + static requireLibrary( + name: string, + property: string, + src: string | Array, + polling: boolean = false, + onload?: (ready: Promise) => void, + rootElement?: HTMLElement, + ) { const resourceToDomOptions = { name, src, - formio:Formio, + formio: Formio, onload, - rootElement - } + rootElement, + }; - let hasResourceBeenAdded = false + let hasResourceBeenAdded = false; if (!Formio.libraries.hasOwnProperty(name)) { Formio.libraries[name] = {}; @@ -2399,9 +2405,8 @@ export class Formio { const plugin = get(window, property); if (plugin) { Formio.libraries[name].resolve(plugin); - } - else { - attachResourceToDom(resourceToDomOptions) + } else { + attachResourceToDom(resourceToDomOptions); hasResourceBeenAdded = true; // if no callback is provided, then check periodically for the script. @@ -2419,7 +2424,7 @@ export class Formio { const lib = Formio.libraries[name]; - if(rootElement && !hasResourceBeenAdded) { + if (rootElement && !hasResourceBeenAdded) { attachResourceToDom(resourceToDomOptions); } diff --git a/src/types/ResourceToDomOptions.ts b/src/types/ResourceToDomOptions.ts index 5ea60a33..b3969a46 100644 --- a/src/types/ResourceToDomOptions.ts +++ b/src/types/ResourceToDomOptions.ts @@ -1,9 +1,9 @@ -import { Formio } from "sdk"; +import { Formio } from 'sdk'; export type ResourceToDomOptions = { - name: string, - src: string | Array, - formio: typeof Formio, - onload?: (ready: Promise) => void, - rootElement?: HTMLElement + name: string; + src: string | Array; + formio: typeof Formio; + onload?: (ready: Promise) => void; + rootElement?: HTMLElement; }; diff --git a/src/utils/formUtil/eachComponent.ts b/src/utils/formUtil/eachComponent.ts index ee9a6b5b..9f9dc050 100644 --- a/src/utils/formUtil/eachComponent.ts +++ b/src/utils/formUtil/eachComponent.ts @@ -14,6 +14,8 @@ import { componentInfo, componentPath, componentFormPath } from './index'; * The current data path of the element. Example: data.user.firstName * @param {Object} parent * The parent object. + * @param {Boolean} noComponentChange + * Whether or not to add properties (e.g. path/parent) to the component object */ export function eachComponent( components: Component[], @@ -21,6 +23,7 @@ export function eachComponent( includeAll?: boolean, path: string = '', parent?: Component, + noComponentChange?: boolean, ) { if (!components) return; components.forEach((component: any) => { @@ -30,7 +33,7 @@ export function eachComponent( const info = componentInfo(component); let noRecurse = false; // Keep track of parent references. - if (parent) { + if (parent && !noComponentChange) { // Ensure we don't create infinite JSON structures. Object.defineProperty(component, 'parent', { enumerable: false, @@ -53,26 +56,44 @@ export function eachComponent( delete component.parent.rows; } - Object.defineProperty(component, 'path', { - enumerable: false, - writable: true, - value: componentPath(component, path), - }); + const compPath = componentPath(component, path); + + if (!noComponentChange) { + Object.defineProperty(component, 'path', { + enumerable: false, + writable: true, + value: compPath, + }); + } if (includeAll || component.tree || !info.layout) { - noRecurse = !!fn(component, component.path, components, parent); + noRecurse = !!fn(component, compPath, components, parent); } if (!noRecurse) { if (info.hasColumns) { component.columns.forEach((column: any) => - eachComponent(column.components, fn, includeAll, path, parent ? component : null), + eachComponent( + column.components, + fn, + includeAll, + path, + parent ? component : null, + noComponentChange, + ), ); } else if (info.hasRows) { component.rows.forEach((row: any) => { if (Array.isArray(row)) { row.forEach((column) => - eachComponent(column.components, fn, includeAll, path, parent ? component : null), + eachComponent( + column.components, + fn, + includeAll, + path, + parent ? component : null, + noComponentChange, + ), ); } }); @@ -81,8 +102,9 @@ export function eachComponent( component.components, fn, includeAll, - componentFormPath(component, path, component.path), + componentFormPath(component, path, compPath), parent ? component : null, + noComponentChange, ); } } diff --git a/src/utils/formUtil/eachComponentAsync.ts b/src/utils/formUtil/eachComponentAsync.ts index a2686397..deee2143 100644 --- a/src/utils/formUtil/eachComponentAsync.ts +++ b/src/utils/formUtil/eachComponentAsync.ts @@ -7,6 +7,7 @@ export async function eachComponentAsync( includeAll = false, path = '', parent?: any, + noComponentChange?: boolean, ) { if (!components) return; for (let i = 0; i < components.length; i++) { @@ -16,7 +17,7 @@ export async function eachComponentAsync( const component = components[i]; const info = componentInfo(component); // Keep track of parent references. - if (parent) { + if (parent && !noComponentChange) { // Ensure we don't create infinite JSON structures. Object.defineProperty(component, 'parent', { enumerable: false, @@ -38,13 +39,17 @@ export async function eachComponentAsync( delete component.parent.columns; delete component.parent.rows; } - Object.defineProperty(component, 'path', { - enumerable: false, - writable: true, - value: componentPath(component, path), - }); + const compPath = componentPath(component, path); + + if (!noComponentChange) { + Object.defineProperty(component, 'path', { + enumerable: false, + writable: true, + value: compPath, + }); + } if (includeAll || component.tree || !info.layout) { - if (await fn(component, component.path, components, parent)) { + if (await fn(component, compPath, components, parent)) { continue; } } @@ -56,6 +61,7 @@ export async function eachComponentAsync( includeAll, path, parent ? component : null, + noComponentChange, ); } } else if (info.hasRows) { @@ -69,6 +75,7 @@ export async function eachComponentAsync( includeAll, path, parent ? component : null, + noComponentChange, ); } } @@ -78,8 +85,9 @@ export async function eachComponentAsync( component.components, fn, includeAll, - componentFormPath(component, path, component.path), + componentFormPath(component, path, compPath), parent ? component : null, + noComponentChange, ); } } diff --git a/src/utils/formUtil/index.ts b/src/utils/formUtil/index.ts index a15ae3bc..318759d7 100644 --- a/src/utils/formUtil/index.ts +++ b/src/utils/formUtil/index.ts @@ -425,12 +425,15 @@ export function getComponent( eachComponent( components, (component: Component, path: any) => { - if (path === key || component.path === key || (component.input && component.key === key)) { + if (path === key || (component.input && component.key === key)) { result = component; return true; } }, includeAll, + undefined, + undefined, + true, ); return result; }