From fdd53c0118fab57f6c14f5a8244a759542a98fc4 Mon Sep 17 00:00:00 2001 From: Robert Hasselle <123402053+rhasselle-oddball@users.noreply.github.com> Date: Mon, 4 Nov 2024 12:22:53 -0600 Subject: [PATCH] add useAllFormData ui:option for ui:validations (#32791) * add useAllFormData ui:option for ui:validations --- .../src/js/components/SchemaForm.jsx | 8 +- .../src/js/containers/FormPage.jsx | 12 +- .../array-builder/ArrayBuilderItemPage.jsx | 2 + src/platform/forms-system/src/js/types.js | 1 + .../forms-system/src/js/validation.js | 17 ++- .../test/js/validation.unit.spec.js | 103 ++++++++++++++++++ 6 files changed, 137 insertions(+), 6 deletions(-) diff --git a/src/platform/forms-system/src/js/components/SchemaForm.jsx b/src/platform/forms-system/src/js/components/SchemaForm.jsx index 753116df79db..4d37ac2756ad 100644 --- a/src/platform/forms-system/src/js/components/SchemaForm.jsx +++ b/src/platform/forms-system/src/js/components/SchemaForm.jsx @@ -165,7 +165,7 @@ class SchemaForm extends React.Component { } validate(formData, errors) { - const { schema, uiSchema, appStateData } = this.props; + const { schema, uiSchema, appStateData, getFormData } = this.props; if (uiSchema) { uiSchemaValidate( errors, @@ -175,6 +175,7 @@ class SchemaForm extends React.Component { '', null, appStateData, + getFormData, ); } return errors; @@ -227,7 +228,6 @@ class SchemaForm extends React.Component { } SchemaForm.propTypes = { - idSchema: PropTypes.object.isRequired, name: PropTypes.string.isRequired, schema: PropTypes.object.isRequired, title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, @@ -239,8 +239,10 @@ SchemaForm.propTypes = { editModeOnReviewPage: PropTypes.bool, formContext: PropTypes.shape({}), formOptions: PropTypes.shape({}), + getFormData: PropTypes.func, hideTitle: PropTypes.bool, - pagePerItemIndex: PropTypes.number, + idSchema: PropTypes.object, + pagePerItemIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), reviewMode: PropTypes.bool, safeRenderCompletion: PropTypes.bool, onChange: PropTypes.func, diff --git a/src/platform/forms-system/src/js/containers/FormPage.jsx b/src/platform/forms-system/src/js/containers/FormPage.jsx index 7a1480c3c8ae..8db4f1d08090 100644 --- a/src/platform/forms-system/src/js/containers/FormPage.jsx +++ b/src/platform/forms-system/src/js/containers/FormPage.jsx @@ -177,7 +177,15 @@ class FormPage extends React.Component { } }; - formData = () => { + /** + * @param {Object} [options] + * @param {boolean} [options.all] If true, return the entire form data regardless of context + */ + formData = ({ all } = {}) => { + if (all) { + return this.props.form.data; + } + const { pageConfig } = this.props.route; // If it's a CustomPage, return the entire form data if (pageConfig.CustomPage && !pageConfig.customPageUsesPagePerItemData) { @@ -340,6 +348,7 @@ class FormPage extends React.Component { uploadFile={this.props.uploadFile} schema={schema} uiSchema={uiSchema} + getFormData={this.formData} goBack={this.goBack} goForward={this.onSubmit} goToPath={this.goToPath} @@ -374,6 +383,7 @@ class FormPage extends React.Component { uiSchema={uiSchema} pagePerItemIndex={params ? params.index : undefined} formContext={formContext} + getFormData={this.formData} trackingPrefix={this.props.form.trackingPrefix} uploadFile={this.props.uploadFile} onChange={this.onChange} diff --git a/src/platform/forms-system/src/js/patterns/array-builder/ArrayBuilderItemPage.jsx b/src/platform/forms-system/src/js/patterns/array-builder/ArrayBuilderItemPage.jsx index 2f9051c692d9..92961c07cda4 100644 --- a/src/platform/forms-system/src/js/patterns/array-builder/ArrayBuilderItemPage.jsx +++ b/src/platform/forms-system/src/js/patterns/array-builder/ArrayBuilderItemPage.jsx @@ -74,6 +74,7 @@ export default function ArrayBuilderItemPage({ uiSchema={uiSchema} pagePerItemIndex={props.pagePerItemIndex} formContext={props.formContext} + getFormData={props.getFormData} trackingPrefix={props.trackingPrefix} onChange={onChange} onSubmit={onSubmit} @@ -142,6 +143,7 @@ export default function ArrayBuilderItemPage({ PageContentBeforeButtons: PropTypes.node, data: PropTypes.object, formContext: PropTypes.object, + getFormData: PropTypes.func, goBack: PropTypes.func, goToPath: PropTypes.func, onChange: PropTypes.func, diff --git a/src/platform/forms-system/src/js/types.js b/src/platform/forms-system/src/js/types.js index 595837571639..59a6195ce96d 100644 --- a/src/platform/forms-system/src/js/types.js +++ b/src/platform/forms-system/src/js/types.js @@ -335,6 +335,7 @@ * @property {boolean} [useVaCards] For arrays on a single page. If true, will use the `VaCard` component to wrap each item in the array. Has a white background with border instead of gray background. * @property {boolean} [reflectInputError] Whether or not to add usa-input--error as class if error message is outside of component. * @property {string} [reviewItemHeaderLevel] Optional level for the item-header on Review page - for arrays. Defaults to '5' for a
header-tag. + * @property {boolean} [useAllFormData] `formData` will return all form data instead of just the current item in an array. Applicable to `ui:validations`. TODO other fields. * @property {boolean} [useDlWrap] On the review page, moves \ tag to immediately surrounding the \ field instead of using a \. \ fields should be wrapped in \ fields, so this fixes that a11y issue. Formats fields horizontally. * @property {'single' | 'multiple'} [useFormsPattern] Used if you want to define the formHeading and formDescription for the web component field, which can include JSX, so it can be read out by screen readers. Accepts 'single' for a single field on the page where the error will show on the entire block, or 'multiple' for multiple fields on the page where the error will show only on the field. * @property {boolean} [useHeaderStyling] Enables developer to implement and use alternate style classes for auto generated html elements such as in ObjectField or ArrayField diff --git a/src/platform/forms-system/src/js/validation.js b/src/platform/forms-system/src/js/validation.js index 8e986add267e..4f6235f4f3d2 100644 --- a/src/platform/forms-system/src/js/validation.js +++ b/src/platform/forms-system/src/js/validation.js @@ -133,6 +133,8 @@ export function transformErrors(errors, uiSchema) { * @param {Object} formData The (flattened) data for the entire form * @param {string} [path] The path to the current field relative to the root of the page. * @param {number} [currentIndex] Used to select the correct field data to validate against + * @param {Object} [appStateData] Data from the app state + * @param {({ all }) => any} [getFormData] Function to get the form data. Useful if you need all form data */ export function uiSchemaValidate( @@ -143,6 +145,7 @@ export function uiSchemaValidate( path = '', currentIndex = null, appStateData, + getFormData, ) { if (uiSchema && schema) { const currentData = path !== '' ? get(path, formData) : formData; @@ -170,6 +173,7 @@ export function uiSchemaValidate( newPath, index, appStateData, + getFormData, ); }); } else if (!uiSchema.items) { @@ -195,11 +199,15 @@ export function uiSchemaValidate( nextPath, currentIndex, appStateData, + getFormData, ); }); } const validations = uiSchema['ui:validations']; + const useAllFormData = uiSchema?.['ui:options']?.useAllFormData; + const data = + useAllFormData && getFormData ? getFormData({ all: true }) : formData; if (validations && currentData !== undefined) { validations.forEach(validation => { const pathErrors = path ? get(path, errors) : errors; @@ -207,7 +215,7 @@ export function uiSchemaValidate( validation( pathErrors, currentData, - formData, + data, schema, uiSchema['ui:errorMessages'], currentIndex, @@ -217,7 +225,7 @@ export function uiSchemaValidate( validation.validator( pathErrors, currentData, - formData, + data, schema, uiSchema['ui:errorMessages'], validation.options, @@ -264,6 +272,7 @@ export function isValidForm(form, pageList, isTesting = false) { ); const v = new Validator(); + let getFormData; return validPages.reduce( ({ isValid, errors }, page) => { @@ -309,6 +318,9 @@ export function isValidForm(form, pageList, isTesting = false) { const result = v.validate(formData, schema); + // mimics FormPage formData() function + getFormData = ({ all }) => (all ? form.data : formData); + if (result.valid) { const customErrors = {}; uiSchemaValidate( @@ -319,6 +331,7 @@ export function isValidForm(form, pageList, isTesting = false) { '', null, appStateData, + getFormData, ); return { diff --git a/src/platform/forms-system/test/js/validation.unit.spec.js b/src/platform/forms-system/test/js/validation.unit.spec.js index a94b2d5fc776..cd64ff4c86ea 100644 --- a/src/platform/forms-system/test/js/validation.unit.spec.js +++ b/src/platform/forms-system/test/js/validation.unit.spec.js @@ -218,6 +218,109 @@ describe('Schemaform validations', () => { ), ).to.be.true; }); + + it('can use global formData for validation on fields in array', () => { + const errors = {}; + const validatorLocal = sinon.spy(); + const validatorGlobal = sinon.spy(); + const schemaArray = { + type: 'array', + items: [ + { + properties: { + field1: { + type: 'string', + }, + field2: { + type: 'string', + }, + }, + }, + ], + }; + const schema = { + type: 'object', + properties: { + otherItem: { + type: 'string', + }, + arrayItems: schemaArray, + }, + }; + const uiSchemaArray = { + items: { + field1: { + 'ui:title': 'Field 1', + 'ui:validations': [validatorLocal], + }, + field2: { + 'ui:title': 'Field 2', + 'ui:validations': [validatorGlobal], + 'ui:options': { + useAllFormData: true, + }, + }, + }, + }; + const uiSchema = { + otherItem: { + 'ui:title': 'Other item', + }, + arrayItems: uiSchemaArray, + }; + const arrayFormData = [ + { + field1: 'foo', + field2: 'bar', + }, + ]; + + const globalFormData = { + otherItem: 'test', + arrayItems: arrayFormData, + }; + + const getFormData = ({ all }) => (all ? globalFormData : arrayFormData); + + uiSchemaValidate( + errors, + uiSchemaArray, + schemaArray, + arrayFormData, + undefined, + undefined, + undefined, + getFormData, + ); + + let v = validatorLocal.args[0]; + expect(v[1]).to.equal('foo'); + expect(v[2]).to.equal(arrayFormData); + + [v] = validatorGlobal.args; + expect(v[1]).to.equal('bar'); + expect(v[2]).to.equal(globalFormData); + + uiSchemaValidate( + {}, + uiSchema, + schema, + globalFormData, + undefined, + undefined, + undefined, + getFormData, + ); + + [v] = validatorLocal.args; + expect(v[1]).to.equal('foo'); + expect(v[2]).to.equal(arrayFormData); + + [v] = validatorGlobal.args; + expect(v[1]).to.equal('bar'); + expect(v[2]).to.equal(globalFormData); + }); + it('should skip validation when array is undefined', () => { const errors = {}; const validator = sinon.spy();