Skip to content

Commit

Permalink
Merge branch 'master' into fio-8316-invalid-data-nested-form
Browse files Browse the repository at this point in the history
  • Loading branch information
John Teague committed Jun 12, 2024
2 parents a613a53 + 73c7c1e commit a7bf63c
Show file tree
Hide file tree
Showing 24 changed files with 268 additions and 110 deletions.
7 changes: 6 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 nyc --reporter=lcov --reporter=text --reporter=text-summary mocha -r ts-node/register -r tsconfig-paths/register -r mock-local-storage -r jsdom-global/register -t 0 'src/**/__tests__/*.test.ts'",
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
4 changes: 2 additions & 2 deletions src/process/__tests__/process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ describe('Process Tests', () => {
type: "address",
providerOptions: {
params: {
key: "AIzaSyBNL2e4MnmyPj9zN7SVAe428nCSLP1X144",
key: "",
region: "",
autocompleteOptions: {
},
Expand Down Expand Up @@ -721,7 +721,7 @@ describe('Process Tests', () => {
type: "address",
providerOptions: {
params: {
key: "AIzaSyBNL2e4MnmyPj9zN7SVAe428nCSLP1X144",
key: "",
region: "",
autocompleteOptions: {
},
Expand Down
34 changes: 34 additions & 0 deletions src/process/normalize/__tests__/normalize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,37 @@ it('Should normalize a radio component value with a number', () => {
normalizeProcessSync(context);
expect(context.data).to.deep.equal({radio: 0});
});

it('Should normalize a number component value with a string value', () => {
const numberComp = {
type: 'number',
key: 'number',
input: true,
label: 'Number'
};
const data = {
number: '000123'
};
const context = generateProcessorContext(numberComp, data);
normalizeProcessSync(context);
expect(context.data).to.deep.equal({number: 123});
});

it('Should normalize a number component value with a multiple values allowed', () => {
const numberComp = {
type: 'number',
key: 'number',
input: true,
label: 'Number',
multiple: true
};
const data = {
number: [
'000.0123',
'123'
]
};
const context = generateProcessorContext(numberComp, data);
normalizeProcessSync(context);
expect(context.data).to.deep.equal({number: [0.0123, 123]});
});
27 changes: 26 additions & 1 deletion src/process/normalize/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
DefaultValueScope,
ProcessorInfo,
ProcessorContext,
TimeComponent
TimeComponent,
NumberComponent
} from "types";

type NormalizeScope = DefaultValueScope & {
Expand All @@ -42,6 +43,7 @@ const isSelectBoxesComponent = (component: any): component is SelectBoxesCompone
const isTagsComponent = (component: any): component is TagsComponent => component.type === "tags";
const isTextFieldComponent = (component: any): component is TextFieldComponent => component.type === "textfield";
const isTimeComponent = (component: any): component is TimeComponent => component.type === "time";
const isNumberComponent = (component: any): component is NumberComponent => component.type === "number";

const normalizeAddressComponentValue = (component: AddressComponent, value: any) => {
if (!component.multiple && Boolean(component.enableManualMode) && value && !value.mode) {
Expand Down Expand Up @@ -252,6 +254,10 @@ const normalizeTextFieldComponentValue = (
value: any,
path: string
) => {
// If the component has truncate multiple spaces enabled, then normalize the value to remove extra spaces.
if (component.truncateMultipleSpaces && typeof value === 'string') {
value = value.trim().replace(/\s{2,}/g, ' ');
}
if (component.allowMultipleMasks && component.inputMasks && component.inputMasks.length > 0) {
if (Array.isArray(value)) {
return value.map((val) => normalizeMaskValue(component, defaultValues, val, path));
Expand All @@ -273,6 +279,22 @@ const normalizeTimeComponentValue = (component: TimeComponent, value: string) =>
return value;
};

const normalizeSingleNumberComponentValue = (component: NumberComponent, value: any) => {
if (!isNaN(parseFloat(value)) && isFinite(value)) {
return +value;
}

return value;
}

const normalizeNumberComponentValue = (component: NumberComponent, value: any) => {
if (component.multiple && Array.isArray(value)) {
return value.map((singleValue) => normalizeSingleNumberComponentValue(component, singleValue));
}

return normalizeSingleNumberComponentValue(component, value);
};

export const normalizeProcess: ProcessorFn<NormalizeScope> = async (context) => {
return normalizeProcessSync(context);
}
Expand Down Expand Up @@ -317,6 +339,9 @@ export const normalizeProcessSync: ProcessorFnSync<NormalizeScope> = (context) =
} else if (isTimeComponent(component)) {
set(data, path, normalizeTimeComponentValue(component, value));
scope.normalize[path].normalized = true;
} else if (isNumberComponent(component)) {
set(data, path, normalizeNumberComponentValue(component, value));
scope.normalize[path].normalized = true;
}

// Next perform component-type-agnostic transformations (i.e. super())
Expand Down
40 changes: 40 additions & 0 deletions src/process/validation/__tests__/multiple.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { expect } from 'chai';
import { validationRules } from '..';
import { rules, serverRules } from '../rules';

const allRules = [...rules, ...serverRules];

const component = {
type: 'textfield',
key: 'multiple_textfield',
label: 'Multiple Textfield',
input: true,
multiple: true,
validate: {
required: true,
maxLength: 10,
minLength: 5,
pattern: '^[0-9]+$',
}
};

const context = {
component,
value: [],
path: 'multiple_textfield',
data: {multiple_textfield: []},
row: {multiple_textfield: []},
scope: {errors: []},
};

it('Validating required rule will work for multiple values component with no rows', async () => {
const fullValueRules = allRules.filter((rule) => rule.fullValue);
const rulesToValidate = validationRules(context, fullValueRules, undefined);
expect(rulesToValidate).to.not.have.length(0);
});

it('Validati olther rules will skip for multiple values component with no rows', async () => {
const otherRules = allRules.filter((rule) => !rule.fullValue);
const rulesToValidate = validationRules(context, otherRules, undefined);
expect(rulesToValidate).to.have.length(0);
});
3 changes: 0 additions & 3 deletions src/process/validation/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
Expand Down
15 changes: 12 additions & 3 deletions src/process/validation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ export function validationRules(
}
const validationRules: ValidationRuleInfo[] = [];
return rules.reduce((acc, rule: ValidationRuleInfo) => {
if (context.component.multiple &&
Array.isArray(context.value) &&
context.value?.length === 0 &&
!rule.fullValue
) {
return acc;
}

if (rule.shouldProcess && rule.shouldProcess(context)) {
acc.push(rule);
}
Expand Down Expand Up @@ -165,7 +173,8 @@ function handleError(error: FieldError | null, context: ValidationContext) {
}

export const validateProcess: ValidationProcessorFn = async (context) => {
const { component, data, row, path, instance, scope, rules, skipValidation, value } = context;
const { component, data, row, path, instance, scope, rules, skipValidation } = context;
let { value } = context;
if (!scope.validated) scope.validated = [];
if (!scope.errors) scope.errors = [];
if (!rules || !rules.length) {
Expand Down Expand Up @@ -216,7 +225,7 @@ export const validateProcess: ValidationProcessorFn = async (context) => {
return;
}
if (component.truncateMultipleSpaces && value && typeof value === 'string') {
set(data, path, value.trim().replace(/\s{2,}/g, ' '));
value = value.trim().replace(/\s{2,}/g, ' ');
}
for (const rule of rulesToExecute) {
try {
Expand Down Expand Up @@ -279,7 +288,7 @@ export const validateProcessSync: ValidationProcessorFnSync = (context) => {
return;
}
if (component.truncateMultipleSpaces && value && typeof value === 'string') {
set(data, path, value.trim().replace(/\s{2,}/g, ' '));
value = value.trim().replace(/\s{2,}/g, ' ');
}
for (const rule of rulesToExecute) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
36 changes: 2 additions & 34 deletions src/process/validation/rules/__tests__/validateDate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,44 +73,12 @@ it('Validating a textField calendar picker component with no data will return nu
expect(result).to.equal(null);
});

it('Validating a textField calendar picker component with an invalid date string value will return a FieldError', async () => {
it('Textfield calendar picker component date values should not be validated and return null', async () => {
const component = calendarTextField;
const data = {
component: 'hello, world!',
};
const context = generateProcessorContext(component, data);
const result = await validateDate(context);
expect(result).to.be.instanceOf(FieldError);
expect(result?.errorKeyOrMessage).to.equal('invalidDate');
});

it('Validating a textField calendar picker component with an valid date string value will return null', async () => {
const component = calendarTextField;
const data = {
component: '2023-03-09T12:00:00-06:00',
};
const context = generateProcessorContext(component, data);
const result = await validateDate(context);
expect(result).to.equal(null);
});

it('Validating a textField calendar picker component with an invalid Date object will return a FieldError', async () => {
const component = calendarTextField;
const data = {
component: new Date('Hello, world!'),
};
const context = generateProcessorContext(component, data);
const result = await validateDate(context);
expect(result).to.be.instanceOf(FieldError);
expect(result?.errorKeyOrMessage).to.equal('invalidDate');
});

it('Validating a textField calendar picker component with a valid Date object will return null', async () => {
const component = calendarTextField;
const data = {
component: new Date(),
};
const context = generateProcessorContext(component, data);
const result = await validateDate(context);
expect(result).to.equal(null);
expect(result).to.be.equal(null);
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('validateMultiple', () => {

it('should return false for textArea component with as !== json', () => {
const component: TextAreaComponent = {
type: 'textArea',
type: 'textarea',
as: 'text',
input: true,
key: 'textArea',
Expand Down
2 changes: 0 additions & 2 deletions src/process/validation/rules/databaseRules.ts
Original file line number Diff line number Diff line change
@@ -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
];
Loading

0 comments on commit a7bf63c

Please sign in to comment.