design-comuni-plone-theme
Version:
Volto Theme for Italia design guidelines
463 lines (441 loc) • 14.1 kB
JavaScript
/**
* Customizzazione:
* - gestione campi required di tipo blocks
* - gestione motivo dello stato di servizio che è required solo se il servizio non è fruibile
* - gestione campi tipo dataGridField
* - gestione timeline_tempi_scadenze nel servizio che ha solo un campo richiesto su 5
* - aggiunta la possibilita' di avere validazioni pluggable per ogni campo con widget data_grid,
* aggiungi i tuoi dgfield e le funzioni di validazione sotto CUSTOM_DGFIELD_VALIDATION.
* Rispettare la convenzione con l'id del campo proveniente dallo schema del CT, ie:
* export const CUSTOM_DGFIELD_VALIDATION = {
timeline_tempi_scadenze: {
isValid: (value, itemObj, intlFunc) => {
const isValid = value.every((val, i) => val.title);
return !isValid
? intlFunc(
dgfieldValidationMessages.timeline_tempi_scadenze_validation_error,
)
: null;
},
},
};
*/
import { map, uniq, keys, intersection, isEmpty } from 'lodash';
import { messages } from '@plone/volto/helpers/MessageLabels/MessageLabels';
import { toast } from 'react-toastify';
import Toast from '@plone/volto/components/manage/Toast/Toast';
import {
serviceFormValidationHelper,
eventFormValidationHelper,
getRealEmptyField,
getSpecificDataGridFieldValidation,
realWidgetType,
CUSTOM_DGFIELD_VALIDATION,
} from 'design-comuni-plone-theme/helpers';
import config from '@plone/volto/registry';
/**
* Will return the intl message if invalid
* @param {boolean} isValid
* @param {string} criterion
* @param {string | number} valueToCompare can compare '47' < 50
* @param {Function} intlFunc
*/
const validationMessage = (isValid, criterion, valueToCompare, intlFunc) =>
!isValid
? intlFunc(messages[criterion], {
len: valueToCompare,
})
: null;
/**
* Returns if based on the criterion the value is lower or equal
* @param {string | number} value can compare '47' < 50
* @param {string | number} valueToCompare can compare '47' < 50
* @param {string} maxCriterion
* @param {Function} intlFunc
*/
const isMaxPropertyValid = (value, valueToCompare, maxCriterion, intlFunc) => {
const isValid = valueToCompare !== undefined ? value <= valueToCompare : true;
return validationMessage(isValid, maxCriterion, valueToCompare, intlFunc);
};
/**
* Returns if based on the criterion the value is higher or equal
* @param {string | number} value can compare '47' < 50
* @param {string | number} valueToCompare can compare '47' < 50
* @param {string} minCriterion
* @param {Function} intlFunc
*/
const isMinPropertyValid = (value, valueToCompare, minCriterion, intlFunc) => {
const isValid = valueToCompare !== undefined ? value >= valueToCompare : true;
return validationMessage(isValid, minCriterion, valueToCompare, intlFunc);
};
const widgetValidation = {
email: {
isValidEmail: (emailValue, emailObj, intlFunc) => {
// Email Regex taken from from WHATWG living standard:
// https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type=email)
const emailRegex =
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
const isValid = emailRegex.test(emailValue);
return !isValid ? intlFunc(messages.isValidEmail) : null;
},
minLength: (emailValue, emailObj, intlFunc) =>
isMinPropertyValid(
emailValue.length,
emailObj.minLength,
'minLength',
intlFunc,
),
maxLength: (emailValue, emailObj, intlFunc) =>
isMaxPropertyValid(
emailValue.length,
emailObj.maxLength,
'maxLength',
intlFunc,
),
},
url: {
isValidURL: (urlValue, urlObj, intlFunc) => {
var urlRegex = new RegExp(
'^' +
'(https?:\\/\\/)?' + // optional protocol
'(' +
'([a-z\\d]+(-[a-z\\d]+)*\\.)+[a-z]{2,}|' + // validate domain name
'(\\d{1,3}\\.){3}\\d{1,3}|' + // OR ip (v4) address
'localhost' + // OR localhost
')' +
'(\\:\\d+)?' + // optional port
'(\\/[-a-z\\d%_.~+@:]*)*' + // path: zero or più caratteri, include ":" per SharePoint
// '(\\?[;&a-z\\d%_.~+\\/=-]*)?' + // validate query string
'(\\?.*)?' + // validate query string (relaxed)
'(\\#[-a-z\\d\\/_]*)?' + // validate fragment locator
'$',
'i',
);
const isValid = urlRegex.test(urlValue);
return !isValid ? intlFunc(messages.isValidURL) : null;
},
minLength: (urlValue, urlObj, intlFunc) =>
isMinPropertyValid(
urlValue.length,
urlObj.minLength,
'minLength',
intlFunc,
),
maxLength: (urlValue, urlObj, intlFunc) =>
isMaxPropertyValid(
urlValue.length,
urlObj.maxLength,
'maxLength',
intlFunc,
),
},
password: {
minLength: (passwordValue, passwordObj, intlFunc) =>
isMinPropertyValid(
passwordValue.length,
passwordObj.minLength,
'minLength',
intlFunc,
),
maxLength: (passwordValue, passwordObj, intlFunc) =>
isMaxPropertyValid(
passwordValue.length,
passwordObj.maxLength,
'maxLength',
intlFunc,
),
},
string: {
minLength: (value, itemObj, intlFunc) =>
isMinPropertyValid(
value.length,
itemObj.minLength,
'minLength',
intlFunc,
),
maxLength: (value, itemObj, intlFunc) =>
isMaxPropertyValid(
value.length,
itemObj.maxLength,
'maxLength',
intlFunc,
),
},
number: {
isNumber: (value, itemObj, intlFunc) => {
const floatRegex = /^[+-]?\d+(\.\d+)?$/;
const isValid = !isNaN(value) && floatRegex.test(value);
return !isValid ? intlFunc(messages.isNumber) : null;
},
minimum: (value, itemObj, intlFunc) =>
isMinPropertyValid(value, itemObj.minimum, 'minimum', intlFunc),
maximum: (value, itemObj, intlFunc) =>
isMaxPropertyValid(value, itemObj.maximum, 'maximum', intlFunc),
},
integer: {
isInteger: (value, itemObj, intlFunc) => {
const intRegex = /^-?[0-9]+$/;
const isValid = !isNaN(value) && intRegex.test(value);
return !isValid ? intlFunc(messages.isInteger) : null;
},
minimum: (value, itemObj, intlFunc) =>
isMinPropertyValid(value, itemObj.minimum, 'minimum', intlFunc),
maximum: (value, itemObj, intlFunc) =>
isMaxPropertyValid(value, itemObj.maximum, 'maximum', intlFunc),
},
...CUSTOM_DGFIELD_VALIDATION,
};
/**
* The string that comes my 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) {
resultObj = null;
}
}
return resultObj;
};
/**
* Returns errors if obj has unique Items
* @param {Object} field
* @param {*} fieldData
* @returns {Object[string]} - list of errors
*/
const hasUniqueItems = (field, fieldData, formatMessage) => {
const errors = [];
if (
field.uniqueItems &&
fieldData &&
uniq(fieldData).length !== fieldData.length
) {
errors.push(formatMessage(messages.uniqueItems));
}
return errors;
};
/**
* 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)
? // Custom: Serve creare una copia dei required
// per poterlo modificare tre righe più giù
schema.required.slice()
: intersection(schema.required, keys(touchedField));
serviceFormValidationHelper(schema, formData, touchedField, fields);
eventFormValidationHelper(
schema,
formData,
touchedField,
fields,
errors,
formatMessage,
);
const uniqueFields = uniq(fields);
map(uniqueFields, (requiredField) => {
const type = schema.properties[requiredField]?.type;
const widget = schema.properties[requiredField]?.widget;
let isEmptyField = getRealEmptyField(
formData,
touchedField,
requiredField,
type,
widget,
);
if (!isEmptyField) {
if (type === 'array') {
// Custom: Supporto alla validazione dei campi DataGridField
if (widget === 'data_grid') {
const dgfFields = schema.properties[requiredField]?.items;
const dgfRequiredFields = dgfFields?.required;
const dgfData = formData[requiredField];
isEmptyField =
dgfRequiredFields.filter((dgfRequiredField) =>
dgfData.some((dgfField) => !!!dgfField[dgfRequiredField]),
)?.length > 0;
} else {
isEmptyField = formData[requiredField]
? formData[requiredField].length === 0
: true;
}
}
}
if (
schema.properties[requiredField] &&
schema.properties[requiredField].type !== 'boolean' &&
!schema.properties[requiredField].readonly &&
isEmptyField
) {
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,
);
map(schema.properties, (field, fieldId) => {
const fieldWidgetType = realWidgetType(field, fieldId);
const widgetValidationCriteria = widgetValidation[fieldWidgetType]
? Object.keys(widgetValidation[fieldWidgetType])
: getSpecificDataGridFieldValidation(fieldId);
let fieldData = formData[fieldId];
// test each criterion ex maximum, isEmail, isUrl, maxLength etc
const fieldErrors = widgetValidationCriteria
.map((widgetCriterion) => {
const errorMessage =
fieldData === undefined || fieldData === null
? null
: widgetValidation[fieldWidgetType][widgetCriterion](
fieldData,
field,
formatMessage,
);
return errorMessage;
})
.filter((item) => !!item);
const uniqueErrors = hasUniqueItems(field, fieldData, formatMessage);
const mergedErrors = [...fieldErrors, ...uniqueErrors];
if (mergedErrors.length > 0) {
errors[fieldId] = [
...(errors[fieldId] || []),
...fieldErrors,
...uniqueErrors,
];
}
});
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;
};