@plone/volto
Version:
Volto
366 lines (333 loc) • 9.81 kB
JSX
import map from 'lodash/map';
import keys from 'lodash/keys';
import intersection from 'lodash/intersection';
import isEmpty from 'lodash/isEmpty';
import { messages } from '@plone/volto/helpers/MessageLabels/MessageLabels';
import config from '@plone/volto/registry';
import { toast } from 'react-toastify';
import Toast from '@plone/volto/components/manage/Toast/Toast';
/**
* Will return the intl message if invalid
* @param {boolean} isValid
* @param {string} criterion
* @param {string | number} valueToCompare can compare '47' < 50
* @param {Function} intlFunc
*/
export const validationMessage = (
isValid,
criterion,
valueToCompare,
intlFunc,
) =>
!isValid
? intlFunc(messages[criterion], {
len: valueToCompare,
})
: null;
/**
* The string that comes might not be a valid JSON
* @param {string} requestItem
*/
export const tryParseJSON = (requestItem) => {
let resultObj = null;
try {
resultObj = JSON.parse(requestItem);
} catch (e) {
try {
resultObj = JSON.parse(requestItem.replace(/'/g, '"'));
} catch (e) {
try {
// Treats strings like: `'String "double quotes"'`
resultObj = JSON.parse(
requestItem.replace(/"/g, '\\"').replace(/'/g, '"'),
);
} catch (e) {
resultObj = null;
}
}
}
return resultObj;
};
/**
* If required fields are undefined, return list of errors
* @returns {Object[string]} - list of errors
*/
const validateRequiredFields = (
schema,
formData,
formatMessage,
touchedField,
) => {
const errors = {};
const fields = isEmpty(touchedField)
? schema.required
: intersection(schema.required, keys(touchedField));
map(fields, (requiredField) => {
const type = schema.properties[requiredField]?.type;
const widget = schema.properties[requiredField]?.widget;
let isEmpty = !formData[requiredField] && formData[requiredField] !== 0;
if (!isEmpty) {
if (type === 'array') {
isEmpty = formData[requiredField]
? formData[requiredField].length === 0
: true;
} else if (type === 'string' && widget === 'richtext') {
isEmpty = !(
formData[requiredField]?.data?.replace(/(<([^>]+)>)/g, '').length > 0
);
}
}
if (
schema.properties[requiredField] &&
schema.properties[requiredField].type !== 'boolean' &&
!schema.properties[requiredField].readonly &&
isEmpty
) {
errors[requiredField] = [];
errors[requiredField].push(formatMessage(messages.required));
}
});
return errors;
};
/**
* Return list of errors if field constraints are not respected
* (ex min, max, maxLength, email format, url format etc)
* each potential criterion has a validation process in widgetValidation
* !!ONLY fields with data will be tested (those undefined are ignored here)
* @returns {Object[string]} - list of errors
*/
const validateFieldsPerFieldset = (
schema,
formData,
formatMessage,
touchedField,
) => {
const errors = validateRequiredFields(
schema,
formData,
formatMessage,
touchedField,
);
function checkFieldErrors(fieldValidationCriteria, field, fieldData) {
return fieldValidationCriteria
.map((widgetCriterion) => {
const errorMessage =
fieldData === undefined || fieldData === null
? null
: widgetCriterion.method({
value: fieldData,
field,
formData,
formatMessage,
});
return errorMessage;
})
.filter((item) => !!item);
}
Object.entries(schema.properties).forEach(([fieldId, field]) => {
let fieldData = formData[fieldId];
field = { ...field, ...field.widgetOptions?.frontendOptions?.widgetProps };
// Validation per specific validator set (format property)
const hasSpecificValidator =
field.widgetOptions?.frontendOptions?.format || field.format;
let specificFieldErrors = [];
if (hasSpecificValidator) {
const specificValidationCriteria = config.getUtilities({
type: 'validator',
dependencies: { format: hasSpecificValidator },
});
specificFieldErrors = checkFieldErrors(
specificValidationCriteria,
field,
fieldData,
);
}
// Validation per field type
const fieldType = field.type || 'string'; // defaults to string
const fieldTypeValidationCriteria = config.getUtilities({
type: 'validator',
dependencies: { fieldType },
});
const fieldErrors = checkFieldErrors(
fieldTypeValidationCriteria,
field,
fieldData,
);
// Validation per field widget
const widgetName =
field.widgetOptions?.frontendOptions?.widget || field.widget || '';
let widgetErrors = [];
if (widgetName) {
const widgetNameValidationCriteria = config.getUtilities({
type: 'validator',
dependencies: { widget: widgetName },
});
widgetErrors = checkFieldErrors(
widgetNameValidationCriteria,
field,
fieldData,
);
}
// Validation per specific behavior and field name (for content types)
const behaviorName = field.behavior;
let perBehaviorFieldErrors = [];
if (behaviorName) {
const specificPerBehaviorFieldValidationCriteria = config.getUtilities({
type: 'validator',
dependencies: { behaviorName, fieldName: fieldId },
});
perBehaviorFieldErrors = checkFieldErrors(
specificPerBehaviorFieldValidationCriteria,
field,
fieldData,
);
}
// Validation per block type validator (for blocks)
const blockType = formData['@type'];
let blockTypeFieldErrors = [];
if (blockType) {
const blockTypeFieldValidationCriteria = config.getUtilities({
type: 'validator',
dependencies: { blockType, fieldName: fieldId },
});
blockTypeFieldErrors = checkFieldErrors(
blockTypeFieldValidationCriteria,
field,
fieldData,
);
}
const mergedErrors = [
...specificFieldErrors,
...fieldErrors,
...widgetErrors,
...perBehaviorFieldErrors,
...blockTypeFieldErrors,
];
if (mergedErrors.length > 0) {
errors[fieldId] = [
...(errors[fieldId] || []),
...specificFieldErrors,
...fieldErrors,
...widgetErrors,
...perBehaviorFieldErrors,
...blockTypeFieldErrors,
];
}
if (errors[fieldId]) {
errors[fieldId].title = field.title;
}
});
return errors;
};
/**
* Create the errors object from backend the same way it is done on Frontend validation
* @param {string} requestError form the server
* @returns {Object}
*/
const giveServerErrorsToCorrespondingFields = (requestError) => {
let errorsList = tryParseJSON(requestError);
const errors = {};
if (Array.isArray(errorsList) && errorsList.length > 0) {
errorsList.forEach((errorItem) => {
errors[errorItem.field] = errors[errorItem.field]
? errors[errorItem.field].push(errorItem.message)
: [errorItem.message];
});
}
return errors;
};
/**
* The first Fieldset (Tab) that has any errors
* will be selected
* @param {Object[]} errors
* @param {string} errors[].field
* @param {Object} schema
* @returns {number} activeIndex
*/
const getIndexOfFirstTabWithErrors = (errors, schema) => {
let activeIndex = 0;
schema.fieldsets.some((fieldSet, index) => {
let foundfield = fieldSet.fields.some((fieldId) => errors[fieldId]);
activeIndex = foundfield ? index : activeIndex;
return foundfield;
});
return activeIndex;
};
class FormValidation {
/**
* The first Fieldset (Tab) that has any errors
* will be selected
* @param {Object} errors
* @param {Object} schema
* @returns {number} activeIndex
*/
static showFirstTabWithErrors({
errors = {},
schema = { properties: {}, fieldsets: [], required: [] },
} = {}) {
return getIndexOfFirstTabWithErrors(errors, schema);
}
/**
* Create the errors object from backend the same way it is done on Frontend validation
* @param {string} requestError form the server
* @returns {Object}
*/
static giveServerErrorsToCorrespondingFields(requestError = '') {
return giveServerErrorsToCorrespondingFields(requestError);
}
/**
* Return validation result
* @param {Object} schema
* @param {Object} formData
* @param {function} formatMessage
* @returns {Object} errors
*/
static validateFieldsPerFieldset({
schema = { properties: {}, fieldsets: [], required: [] },
formData = {},
formatMessage = () => {},
touchedField = {},
} = {}) {
return validateFieldsPerFieldset(
schema,
formData,
formatMessage,
touchedField,
);
}
}
export default FormValidation;
/**
* Check if a file upload is within the maximum size limit.
* @param {File} file
* @param {Function} intlFunc
* @returns {Boolean}
*/
export const validateFileUploadSize = (file, intlFunc) => {
const isValid =
!config.settings.maxFileUploadSize ||
file.size <= config.settings.maxFileUploadSize;
if (!isValid) {
toast.error(
<Toast
error
title={intlFunc(messages.error)}
content={intlFunc(messages.fileTooLarge, {
limit: `${Math.floor(
config.settings.maxFileUploadSize / 1024 / 1024,
)}MB`,
})}
/>,
);
}
return isValid;
};
/**
* Extract invariant errors given an array of errors.
* @param {Array} erros
*/
export const extractInvariantErrors = (erros) => {
return erros
.filter((errorItem) => !('field' in errorItem))
.map((errorItem) => errorItem['message']);
};