diff --git a/Changelog.md b/Changelog.md index 302b81fd..e8ae3304 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,4 +1,4 @@ -## [Unreleased: 2.1.0-rc.1] +## [Unreleased: 2.2.0-rc.1] ### Changed - FIO-8177: fix unsetting empty array values - FIO-8185: Fixing issues with EditGrid and DataGrid clearOnHide with Conditionally visible elements @@ -19,6 +19,11 @@ - FIO-8128: adds includeAll flag to eachComponentData and eachComponentDataAsync - FIO-7507: publish-dev-tag-to-npm - FIO-8264: update validate required + - FIO-8336 fix validation on multiple values + - FIO-8037: added number component normalization + - FIO-8288: do not validate dates in textfield components with calendar widgets + - FIO-8254 fixed available values validation error for Select component + - FIO-8281: fixed sync validation error for select component with url data src ## 2.0.0-rc.24 diff --git a/package.json b/package.json index d902b6c3..d32781e2 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "./process": "./lib/process/index.js", "./types": "./lib/types/index.js", "./experimental": "./lib/experimental/index.js", - "./dist/formio.core.min.js": "./dist/formio.core.min.js" + "./dist/formio.core.min.js": "./dist/formio.core.min.js", + "./error": "./lib/error/index.js" }, "scripts": { "test": "TEST=1 mocha -r ts-node/register -r tsconfig-paths/register -r mock-local-storage -r jsdom-global/register -b -t 0 'src/**/__tests__/*.test.ts'", diff --git a/src/index.ts b/src/index.ts index 233ebd6c..145533cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,8 @@ export * from './modules'; export * from './utils'; export * from './process/validation'; +export * from './process/validation/rules'; export * from './process'; export * from './sdk'; export * from './types'; +export * from './error'; diff --git a/src/process/__tests__/process.test.ts b/src/process/__tests__/process.test.ts index 18083e96..a6f05eea 100644 --- a/src/process/__tests__/process.test.ts +++ b/src/process/__tests__/process.test.ts @@ -421,7 +421,7 @@ describe('Process Tests', () => { type: "address", providerOptions: { params: { - key: "AIzaSyBNL2e4MnmyPj9zN7SVAe428nCSLP1X144", + key: "", region: "", autocompleteOptions: { }, @@ -721,7 +721,7 @@ describe('Process Tests', () => { type: "address", providerOptions: { params: { - key: "AIzaSyBNL2e4MnmyPj9zN7SVAe428nCSLP1X144", + key: "", region: "", autocompleteOptions: { }, diff --git a/src/process/validation/i18n/en.ts b/src/process/validation/i18n/en.ts index 3362078d..d1de6ca3 100644 --- a/src/process/validation/i18n/en.ts +++ b/src/process/validation/i18n/en.ts @@ -31,9 +31,6 @@ export const EN_ERRORS = { invalidValueProperty: 'Invalid Value Property', mask: '{{field}} does not match the mask.', valueIsNotAvailable: '{{ field }} is an invalid value.', - captchaTokenValidation: 'ReCAPTCHA: Token validation error', - captchaTokenNotSpecified: 'ReCAPTCHA: Token is not specified in submission', - captchaFailure: 'ReCaptcha: Response token not found', time: '{{field}} is not a valid time.', invalidDate: '{{field}} is not a valid date', number: '{{field}} is not a valid number.' diff --git a/src/process/validation/rules/__tests__/validateAvailableItems.test.ts b/src/process/validation/rules/__tests__/validateAvailableItems.test.ts index f35f2641..42787ce0 100644 --- a/src/process/validation/rules/__tests__/validateAvailableItems.test.ts +++ b/src/process/validation/rules/__tests__/validateAvailableItems.test.ts @@ -9,7 +9,7 @@ import { simpleSelectOptions, } from './fixtures/components'; import { generateProcessorContext } from './fixtures/util'; -import { validateAvailableItems } from '../validateAvailableItems'; +import { validateAvailableItems, validateAvailableItemsSync } from '../validateAvailableItems'; it('Validating a component without the available items validation parameter will return null', async () => { const component = simpleTextField; @@ -103,6 +103,28 @@ it('Validating a simple static values select component with the available items expect(result).to.equal(null); }); +it('Validating a simple static values select component with the available items validation parameter will return null if the selected item is valid and dataSrc is not specified', async () => { + const component: SelectComponent = { + ...simpleSelectOptions, + dataSrc: undefined, + data: { + values: [ + { label: 'foo', value: 'foo' }, + { label: 'bar', value: 'bar' }, + { label: 'baz', value: 'baz' }, + { label: 'baz', value: 'baz' }, + ], + }, + validate: { onlyAvailableItems: true }, + }; + const data = { + component: 'foo', + }; + const context = generateProcessorContext(component, data); + const result = await validateAvailableItems(context); + expect(result).to.equal(null); +}); + it('Validating a simple URL select component without the available items validation parameter will return null', async () => { const component: SelectComponent = { ...simpleSelectOptions, @@ -120,6 +142,43 @@ it('Validating a simple URL select component without the available items validat expect(result).to.equal(null); }); +it('Validating a simple URL select component synchronously will return null', async () => { + const component: SelectComponent = { + ...simpleSelectOptions, + dataSrc: 'url', + data: { + url: 'http://localhost:8080/numbers', + headers: [], + }, + validate: { onlyAvailableItems: true }, + }; + const data = { + component: 'foo', + }; + const context = generateProcessorContext(component, data); + const result = validateAvailableItemsSync(context); + expect(result).to.equal(null); +}); + +it('Validating a multiple URL select component synchronously will return null', async () => { + const component: SelectComponent = { + ...simpleSelectOptions, + dataSrc: 'url', + data: { + url: 'http://localhost:8080/numbers', + headers: [], + }, + multiple: true, + validate: { onlyAvailableItems: true }, + }; + const data = { + component: ['foo'], + }; + const context = generateProcessorContext(component, data); + const result = validateAvailableItemsSync(context); + expect(result).to.equal(null); +}); + it('Validating a simple JSON select component (string JSON) without the available items validation parameter will return null', async () => { const component: SelectComponent = { ...simpleSelectOptions, diff --git a/src/process/validation/rules/databaseRules.ts b/src/process/validation/rules/databaseRules.ts index 54e21f2e..667dd25c 100644 --- a/src/process/validation/rules/databaseRules.ts +++ b/src/process/validation/rules/databaseRules.ts @@ -1,11 +1,9 @@ import { ValidationRuleInfo } from "types"; import { validateUniqueInfo } from "./validateUnique"; -import { validateCaptchaInfo } from "./validateCaptcha"; import { validateResourceSelectValueInfo } from "./validateResourceSelectValue"; // These are the validations that require a database connection. export const databaseRules: ValidationRuleInfo[] = [ validateUniqueInfo, - validateCaptchaInfo, validateResourceSelectValueInfo ]; diff --git a/src/process/validation/rules/validateAvailableItems.ts b/src/process/validation/rules/validateAvailableItems.ts index 3b146c86..4c7feb2c 100644 --- a/src/process/validation/rules/validateAvailableItems.ts +++ b/src/process/validation/rules/validateAvailableItems.ts @@ -1,4 +1,4 @@ -import { isEmpty } from 'lodash'; +import { isEmpty, isUndefined} from 'lodash'; import { FieldError, ProcessorError } from 'error'; import { Evaluator } from 'utils'; import { RadioComponent, SelectComponent, RuleFn, RuleFnSync, ValidationContext } from 'types'; @@ -37,6 +37,9 @@ function mapStaticValues(values: { label: string; value: string }[]) { } async function getAvailableSelectValues(component: SelectComponent, context: ValidationContext) { + if (isUndefined(component.dataSrc) && component.data.hasOwnProperty('values')) { + component.dataSrc = 'values'; + }; switch (component.dataSrc) { case 'values': if (Array.isArray(component.data.values)) { @@ -107,6 +110,9 @@ async function getAvailableSelectValues(component: SelectComponent, context: Val } function getAvailableSelectValuesSync(component: SelectComponent, context: ValidationContext) { + if (isUndefined(component.dataSrc) && component.data.hasOwnProperty('values')) { + component.dataSrc = 'values'; + }; switch (component.dataSrc) { case 'values': if (Array.isArray(component.data?.values)) { @@ -156,6 +162,8 @@ function getAvailableSelectValuesSync(component: SelectComponent, context: Valid 'validate:validateAvailableItems' ); } + case 'url': + return null; default: throw new ProcessorError( `Failed to validate available values in select component '${component.key}': data source ${component.dataSrc} is not valid}`, diff --git a/src/process/validation/rules/validateCaptcha.ts b/src/process/validation/rules/validateCaptcha.ts deleted file mode 100644 index b9f39cea..00000000 --- a/src/process/validation/rules/validateCaptcha.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { FieldError } from '../../../error/FieldError'; -import { RuleFn, ValidationContext } from '../../../types/index'; -import { ProcessorError } from 'error'; -import { ProcessorInfo } from 'types/process/ProcessorInfo'; - -export const shouldValidate = (context: ValidationContext) => { - const { component } = context; - if (component.type === 'recaptcha') { - return true; - } - return false; -}; - -export const validateCaptcha: RuleFn = async (context: ValidationContext) => { - const { value, config, component } = context; - if (!shouldValidate(context)) { - return null; - } - - if (!config || !config.database) { - throw new ProcessorError("Can't test for recaptcha success without a database config object", context, 'validate:validateCaptcha'); - } - try { - if (!value || !value.token) { - return new FieldError('captchaTokenNotSpecified', context, 'catpcha'); - } - if (!value.success) { - return new FieldError('captchaTokenValidation', context, 'captcha'); - } - const captchaResult: boolean = await config.database?.validateCaptcha(value.token); - return (captchaResult === true) ? null : new FieldError('captchaFailure', context, 'captcha'); - } - catch (err: any) { - throw new ProcessorError(err.message || err, context, 'validate:validateCaptcha'); - } -}; - -export const validateCaptchaInfo: ProcessorInfo = { - name: 'validateCaptcha', - process: validateCaptcha, - shouldProcess: shouldValidate, -}; diff --git a/src/process/validation/rules/validateRequired.ts b/src/process/validation/rules/validateRequired.ts index bf83a247..5294f481 100644 --- a/src/process/validation/rules/validateRequired.ts +++ b/src/process/validation/rules/validateRequired.ts @@ -8,7 +8,11 @@ import { DayComponent } from 'types'; import { isEmptyObject } from '../util'; +<<<<<<< HEAD import { isComponentNestedDataType } from 'utils/formUtil' +======= +import { isComponentNestedDataType } from 'utils/formUtil'; +>>>>>>> 73c7c1ec076f88b1d3ef940f41422f04a3aaaafa import { ProcessorInfo } from 'types/process/ProcessorInfo'; const isAddressComponent = (component: any): component is AddressComponent => { @@ -29,11 +33,7 @@ const isComponentThatCannotHaveFalseValue = (component: any): boolean => { return component.type === 'checkbox' || component.type === 'selectboxes' } -const valueIsPresent = ( - value: any, - considerFalseTruthy: boolean, - isNestedDataType?: boolean -): boolean => { +const valueIsPresent = (value: any, considerFalseTruthy: boolean, isNestedDatatype?: boolean): boolean => { // Evaluate for 3 out of 6 falsy values ("", null, undefined), don't check for 0 // and only check for false under certain conditions if (value === null || value === undefined || value === "" || (!considerFalseTruthy && value === false)) { @@ -48,8 +48,8 @@ const valueIsPresent = ( return false; } // Recursively evaluate - else if (typeof value === 'object' && !isNestedDataType) { - return Object.values(value).some((val) => valueIsPresent(val, considerFalseTruthy, isNestedDataType)); + else if (typeof value === 'object' && !isNestedDatatype) { + return Object.values(value).some((val) => valueIsPresent(val, considerFalseTruthy, isNestedDatatype)); } return true; } diff --git a/src/sdk/Formio.ts b/src/sdk/Formio.ts index 82858c03..e7f90e35 100644 --- a/src/sdk/Formio.ts +++ b/src/sdk/Formio.ts @@ -1566,13 +1566,13 @@ export class Formio { if (!response.ok) { if (response.status === 440) { Formio.setToken(null, opts); - Formio.events.emit('formio.sessionExpired', response.body); + Formio.events.emit('formio.sessionExpired', response.body || response); } else if (response.status === 401) { - Formio.events.emit('formio.unauthorized', response.body); + Formio.events.emit('formio.unauthorized', response.body || response); } else if (response.status === 416) { - Formio.events.emit('formio.rangeIsNotSatisfiable', response.body); + Formio.events.emit('formio.rangeIsNotSatisfiable', response.body || response); } else if (response.status === 504) { return Promise.reject(new Error('Network request failed')); diff --git a/src/sdk/__tests__/Formio.test.ts b/src/sdk/__tests__/Formio.test.ts index 8b01cc2f..e49612ac 100644 --- a/src/sdk/__tests__/Formio.test.ts +++ b/src/sdk/__tests__/Formio.test.ts @@ -2173,4 +2173,60 @@ describe('Formio.js Tests', () => { assert.ok(plugin.wrapStaticRequestPromise.calledOnce, 'wrapStaticRequestPromise should be called once'); }); }); + describe('Formio.request', () => { + it('should emit a formio.sessionExpired event when the response status is 440 and the response object should exist', (done) => { + let eventFired = false + let responseNotUndefined = false + setTimeout(()=>{ + assert(eventFired, 'formio.sessionExpired event was not called'); + assert(responseNotUndefined, 'a response was not passed into the event'); + fetchMock.restore() + done() + },200) + Formio.events.on('formio.sessionExpired', (response: any) => { + eventFired = true + if (response){ + responseNotUndefined = true + } + }) + fetchMock.mock('http://localhost:8080/test', 440); + Formio.request('http://localhost:8080/test'); + }); + it('should emit a formio.unauthorized event when the response status is 401', (done) => { + let eventFired = false + let responseNotUndefined = false + setTimeout(()=>{ + assert(eventFired, 'formio.unauthorized event was not called'); + assert(responseNotUndefined, 'a response was not passed into the event'); + fetchMock.restore() + done() + },200); + Formio.events.on('formio.unauthorized', (response: any) => { + eventFired = true; + if (response){ + responseNotUndefined = true; + } + }) + fetchMock.mock('http://localhost:8080/test', 401); + Formio.request('http://localhost:8080/test'); + }); + it('should emit a formio.rangeIsNotSatisfiable event when the response status is 416', (done) => { + let eventFired = false; + let responseNotUndefined = false; + setTimeout(()=>{ + assert(eventFired, 'formio.rangeIsNotSatisfiable event was not called'); + assert(responseNotUndefined, 'a response was not passed into the event'); + fetchMock.restore() + done() + },200); + Formio.events.on('formio.rangeIsNotSatisfiable', (response) => { + eventFired = true; + if (response) { + responseNotUndefined = true; + } + }) + fetchMock.mock('http://localhost:8080/test', 416); + Formio.request('http://localhost:8080/test'); + }); + }); }); diff --git a/src/types/Component.ts b/src/types/Component.ts index 25aa84e7..5fec266f 100644 --- a/src/types/Component.ts +++ b/src/types/Component.ts @@ -371,7 +371,7 @@ type StaticValuesSelectData = { data: { values: { label: string; value: string }[]; }; - dataSrc: 'values'; + dataSrc?: undefined | 'values'; }; type JsonValuesSelectData = { diff --git a/src/types/project/settings/ProjectSettings.ts b/src/types/project/settings/ProjectSettings.ts index ee8d839d..92348efc 100644 --- a/src/types/project/settings/ProjectSettings.ts +++ b/src/types/project/settings/ProjectSettings.ts @@ -9,7 +9,7 @@ import { ProjectFileStorageConfig, ProjectGoogleDriveConfig, ProjectKickboxConfig, - ProjectReCaptchaConfig, + ProjectCaptchaConfig, ProjectSQLConnectorConfig, } from './integrations'; @@ -39,7 +39,8 @@ export type ProjectSettings = { // Integrations email?: ProjectEmailConfig; - recaptcha?: ProjectReCaptchaConfig; + captcha?: ProjectCaptchaConfig; + recaptcha?: ProjectCaptchaConfig; esign?: ProjectESignConfig; google?: ProjectGoogleDriveConfig; kickbox?: ProjectKickboxConfig; diff --git a/src/types/project/settings/integrations/reCaptcha.ts b/src/types/project/settings/integrations/captcha.ts similarity index 54% rename from src/types/project/settings/integrations/reCaptcha.ts rename to src/types/project/settings/integrations/captcha.ts index 0594423a..dd0ed2f0 100644 --- a/src/types/project/settings/integrations/reCaptcha.ts +++ b/src/types/project/settings/integrations/captcha.ts @@ -1,4 +1,4 @@ -export type ProjectReCaptchaConfig = { +export type ProjectCaptchaConfig = { siteKey: string; secretKey: string; }; diff --git a/src/types/project/settings/integrations/index.ts b/src/types/project/settings/integrations/index.ts index 781cf5bd..fb02f5e1 100644 --- a/src/types/project/settings/integrations/index.ts +++ b/src/types/project/settings/integrations/index.ts @@ -2,4 +2,4 @@ export * from './dataConnections'; export * from './email'; export * from './eSign'; export * from './fileStorage'; -export * from './reCaptcha'; +export * from './captcha';