From d02a64b3ac5649b378442aa2ba587b9d1604656b Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 10 Jul 2024 16:47:51 +0200 Subject: [PATCH 01/51] Add foundations for extensible validation in forms --- Makefile | 4 +- packages/registry/src/index.ts | 35 +- packages/registry/src/registry.test.tsx | 52 ++- packages/volto/src/config/index.js | 3 + packages/volto/src/config/validation.ts | 83 +++++ .../helpers/FormValidation/FormValidation.jsx | 215 +++--------- .../FormValidation/FormValidation.test.js | 309 +++++++++++++++++- .../src/helpers/FormValidation/validators.ts | 128 ++++++++ packages/volto/test-setup-config.jsx | 5 + 9 files changed, 644 insertions(+), 190 deletions(-) create mode 100644 packages/volto/src/config/validation.ts create mode 100644 packages/volto/src/helpers/FormValidation/validators.ts diff --git a/Makefile b/Makefile index 1164cab39c..6273195bbf 100644 --- a/Makefile +++ b/Makefile @@ -137,10 +137,10 @@ docs-test: docs-clean docs-linkcheckbroken docs-vale ## Clean docs build, then cypress-install: ## Install Cypress for acceptance tests $(NODEBIN)/cypress install -packages/registry/dist: packages/registry/src +packages/registry/dist: $(shell find packages/registry/src -type f) pnpm build:registry -packages/components/dist: packages/components/src +packages/components/dist: $(shell find packages/components/src -type f) pnpm build:components .PHONY: build-deps diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index 932f84bc01..f94a8b24fe 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -149,10 +149,43 @@ class Config { } } + getComponents( + options: { name: string; dependencies?: string[] | string } | string, + ): Array { + if (typeof options === 'object') { + const { name, dependencies = '' } = options; + let depsString: string = ''; + if (dependencies && Array.isArray(dependencies)) { + depsString = dependencies.join('+'); + } else if (typeof dependencies === 'string') { + depsString = dependencies; + } + const componentName = `${name}${depsString ? `|${depsString}` : ''}`; + const componentsKeys = Object.keys(this._data.components).filter((key) => + key.startsWith(componentName), + ); + const components = componentsKeys.map( + (key) => this._data.components[key], + ); + + return components; + } else { + // Shortcut notation, accepting a lonely string as argument + const componentName = options; + const componentsKeys = Object.keys(this._data.components).filter((key) => + key.startsWith(componentName), + ); + const components = componentsKeys.map( + (key) => this._data.components[key], + ); + return components; + } + } + registerComponent(options: { name: string; dependencies?: string[] | string; - component: React.ComponentType; + component: (args: any) => any; }) { const { name, component, dependencies = '' } = options; let depsString: string = ''; diff --git a/packages/registry/src/registry.test.tsx b/packages/registry/src/registry.test.tsx index 4763b93946..20af23c27d 100644 --- a/packages/registry/src/registry.test.tsx +++ b/packages/registry/src/registry.test.tsx @@ -1,21 +1,22 @@ import config from './index'; -import { describe, expect, it, afterEach } from 'vitest'; +import { describe, expect, it, afterEach, beforeEach } from 'vitest'; -config.set('components', { - Toolbar: { component: 'this is the Toolbar component' }, - 'Toolbar.Types': { component: 'this is the Types component' }, - 'Teaser|News Item': { component: 'This is the News Item Teaser component' }, +beforeEach(() => { + config.set('components', { + Toolbar: { component: 'this is the Toolbar component' }, + 'Toolbar.Types': { component: 'this is the Types component' }, + 'Teaser|News Item': { component: 'This is the News Item Teaser component' }, + }); + config.set('slots', {}); }); -config.set('slots', {}); - describe('Component registry', () => { - it('get components', () => { + it('get a component', () => { expect(config.getComponent('Toolbar').component).toEqual( 'this is the Toolbar component', ); }); - it('get components with context', () => { + it('get a component with context', () => { expect( config.getComponent({ name: 'Teaser', dependencies: 'News Item' }) .component, @@ -111,6 +112,39 @@ describe('Component registry', () => { }).component, ).toEqual('this is a Bar component'); }); + it('getComponents - get a collection of component registration', () => { + expect(config.getComponents('Toolbar').length).toEqual(2); + expect(config.getComponents('Toolbar')[0].component).toEqual( + 'this is the Toolbar component', + ); + expect(config.getComponents('Toolbar')[1].component).toEqual( + 'this is the Types component', + ); + }); + it('getComponents - get a collection of component registration and deps', () => { + config.registerComponent({ + name: 'Toolbar', + component: 'this is a StringFieldWidget component', + dependencies: ['News Item', 'StringFieldWidget'], + }); + config.registerComponent({ + name: 'Toolbar', + component: 'this is a AnotherWidget component', + dependencies: ['News Item', 'AnotherWidget'], + }); + expect( + config.getComponents({ name: 'Toolbar', dependencies: ['News Item'] }) + .length, + ).toEqual(2); + expect( + config.getComponents({ name: 'Toolbar', dependencies: ['News Item'] })[0] + .component, + ).toEqual('this is a StringFieldWidget component'); + expect( + config.getComponents({ name: 'Toolbar', dependencies: ['News Item'] })[1] + .component, + ).toEqual('this is a AnotherWidget component'); + }); }); describe('Slots registry', () => { diff --git a/packages/volto/src/config/index.js b/packages/volto/src/config/index.js index 9c7b625fcc..5a0cf59a13 100644 --- a/packages/volto/src/config/index.js +++ b/packages/volto/src/config/index.js @@ -32,6 +32,7 @@ import applyAddonConfiguration, { addonsInfo } from 'load-volto-addons'; import ConfigRegistry from '@plone/volto/registry'; import { getSiteAsyncPropExtender } from '@plone/volto/helpers'; +import { registerValidators } from './validation'; const host = process.env.HOST || 'localhost'; const port = process.env.PORT || '3000'; @@ -239,4 +240,6 @@ ConfigRegistry.addonReducers = config.addonReducers; ConfigRegistry.components = config.components; ConfigRegistry.slots = config.slots; +registerValidators(ConfigRegistry); + applyAddonConfiguration(ConfigRegistry); diff --git a/packages/volto/src/config/validation.ts b/packages/volto/src/config/validation.ts new file mode 100644 index 0000000000..3309aa8213 --- /dev/null +++ b/packages/volto/src/config/validation.ts @@ -0,0 +1,83 @@ +import { ConfigType } from '@plone/registry'; + +import { + minLengthValidator, + maxLengthValidator, + urlValidator, + emailValidator, + isNumber, + maximumValidator, + minimumValidator, + isInteger, + hasUniqueItems, +} from '@plone/volto/helpers/FormValidation/validators'; + +const registerValidators = (config: ConfigType) => { + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['default', 'minLength'], + component: minLengthValidator, + }); + + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['default', 'maxLength'], + component: maxLengthValidator, + }); + + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['email', 'isValidEmail'], + component: emailValidator, + }); + + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['url', 'isValidURL'], + component: urlValidator, + }); + + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['number', 'isNumber'], + component: isNumber, + }); + + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['number', 'minimum'], + component: minimumValidator, + }); + + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['number', 'maximum'], + component: maximumValidator, + }); + + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['integer', 'isNumber'], + component: isInteger, + }); + + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['integer', 'minimum'], + component: minimumValidator, + }); + + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['integer', 'maximum'], + component: maximumValidator, + }); + + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['array', 'uniqueItems'], + component: hasUniqueItems, + }); +}; + +export { registerValidators }; diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.jsx b/packages/volto/src/helpers/FormValidation/FormValidation.jsx index 95c470fa77..ae478e6402 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.jsx +++ b/packages/volto/src/helpers/FormValidation/FormValidation.jsx @@ -1,4 +1,4 @@ -import { map, uniq, keys, intersection, isEmpty } from 'lodash'; +import { map, keys, intersection, isEmpty } from 'lodash'; import { messages } from '../MessageLabels/MessageLabels'; import config from '@plone/volto/registry'; import { toast } from 'react-toastify'; @@ -11,7 +11,12 @@ import Toast from '@plone/volto/components/manage/Toast/Toast'; * @param {string | number} valueToCompare can compare '47' < 50 * @param {Function} intlFunc */ -const validationMessage = (isValid, criterion, valueToCompare, intlFunc) => +export const validationMessage = ( + isValid, + criterion, + valueToCompare, + intlFunc, +) => !isValid ? intlFunc(messages[criterion], { len: valueToCompare, @@ -19,142 +24,7 @@ const validationMessage = (isValid, criterion, valueToCompare, intlFunc) => : 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?:\\/\\/)?' + // validate protocol - '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // validate domain name - '((\\d{1,3}\\.){3}\\d{1,3}))|' + // validate OR ip (v4) address - '(localhost)' + // validate OR localhost address - '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // validate port and path - '(\\?[;&a-z\\d%_.~+=-]*)?' + // validate query string - '(\\#[-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), - }, -}; - -/** - * The string that comes my not be a valid JSON + * The string that comes might not be a valid JSON * @param {string} requestItem */ export const tryParseJSON = (requestItem) => { @@ -171,24 +41,6 @@ export const tryParseJSON = (requestItem) => { 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 @@ -252,35 +104,58 @@ const validateFieldsPerFieldset = ( touchedField, ); - map(schema.properties, (field, fieldId) => { - const fieldWidgetType = field.widget || field.type; - const widgetValidationCriteria = widgetValidation[fieldWidgetType] - ? Object.keys(widgetValidation[fieldWidgetType]) - : []; - let fieldData = formData[fieldId]; - // test each criterion ex maximum, isEmail, isUrl, maxLength etc - const fieldErrors = widgetValidationCriteria + function checkFieldErrors(fieldValidationCriteria, field, fieldData) { + return fieldValidationCriteria .map((widgetCriterion) => { const errorMessage = fieldData === undefined || fieldData === null ? null - : widgetValidation[fieldWidgetType][widgetCriterion]( - fieldData, + : widgetCriterion.component({ field, + value: fieldData, formatMessage, - ); + }); return errorMessage; }) .filter((item) => !!item); + } + + Object.entries(schema.properties).forEach(([fieldId, field]) => { + let fieldData = formData[fieldId]; + + // Default validation for all fields (required, minLength, maxLength) + const defaultFieldValidationCriteria = config.getComponents({ + name: 'fieldValidator', + dependencies: ['default'], + }); + + const defaultFieldErrors = checkFieldErrors( + defaultFieldValidationCriteria, + field, + fieldData, + ); + + // Validation per field type or field widget + const fieldWidgetType = field.widget || field.type || 'string'; + // test each criterion eg. maximum, isEmail, isUrl, etc + const fieldValidationCriteria = config.getComponents({ + name: 'fieldValidator', + dependencies: [fieldWidgetType], + }); + + const fieldErrors = checkFieldErrors( + fieldValidationCriteria, + field, + fieldData, + ); - const uniqueErrors = hasUniqueItems(field, fieldData, formatMessage); - const mergedErrors = [...fieldErrors, ...uniqueErrors]; + const mergedErrors = [...defaultFieldErrors, ...fieldErrors]; if (mergedErrors.length > 0) { errors[fieldId] = [ ...(errors[fieldId] || []), + ...defaultFieldErrors, ...fieldErrors, - ...uniqueErrors, ]; } }); diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.test.js b/packages/volto/src/helpers/FormValidation/FormValidation.test.js index 9f97c6849a..cfda30886b 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.test.js +++ b/packages/volto/src/helpers/FormValidation/FormValidation.test.js @@ -54,7 +54,7 @@ describe('FormValidation', () => { expect(FormValidation.validateFieldsPerFieldset()).toEqual({}); }); - it('validates missing required', () => { + it('required - validates missing', () => { expect( FormValidation.validateFieldsPerFieldset({ schema, @@ -66,7 +66,7 @@ describe('FormValidation', () => { }); }); - it('do not treat 0 as missing required value', () => { + it('required - do not treat 0 as missing required value', () => { let newSchema = { ...schema, properties: { @@ -98,7 +98,7 @@ describe('FormValidation', () => { ).toEqual({}); }); - it('validates incorrect email', () => { + it('email - validates incorrect', () => { expect( FormValidation.validateFieldsPerFieldset({ schema, @@ -110,7 +110,7 @@ describe('FormValidation', () => { }); }); - it('validates correct email', () => { + it('email - validates', () => { formData.email = 'test@domain.name'; expect( FormValidation.validateFieldsPerFieldset({ @@ -120,7 +120,8 @@ describe('FormValidation', () => { }), ).toEqual({}); }); - it('validates incorrect url', () => { + + it('url - validates incorrect url', () => { formData.url = 'foo'; expect( FormValidation.validateFieldsPerFieldset({ @@ -130,7 +131,8 @@ describe('FormValidation', () => { }), ).toEqual({ url: [messages.isValidURL.defaultMessage] }); }); - it('validates url', () => { + + it('url - validates', () => { formData.url = 'https://plone.org/'; expect( FormValidation.validateFieldsPerFieldset({ @@ -140,7 +142,8 @@ describe('FormValidation', () => { }), ).toEqual({}); }); - it('validates url with ip', () => { + + it('url - validates url with ip', () => { formData.url = 'http://127.0.0.1:8080/Plone'; expect( FormValidation.validateFieldsPerFieldset({ @@ -150,7 +153,8 @@ describe('FormValidation', () => { }), ).toEqual({}); }); - it('validates url with localhost', () => { + + it('url - validates url with localhost', () => { formData.url = 'http://localhost:8080/Plone'; expect( FormValidation.validateFieldsPerFieldset({ @@ -160,5 +164,294 @@ describe('FormValidation', () => { }), ).toEqual({}); }); + + it('default - min lenght', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'password', + description: '', + minLength: '8', + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 'asd', + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.minLength.defaultMessage], + }); + }); + + it('default - max lenght', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'password', + description: '', + maxLength: '8', + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 'asdasdasdasdasd', + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.maxLength.defaultMessage], + }); + }); + + it('number - isNumber', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Number field', + type: 'number', + description: '', + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: '1', + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.isNumber.defaultMessage], + }); + }); + + it('number - minimum', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Number field', + type: 'number', + description: '', + minimum: 8, + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 1, + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.minimum.defaultMessage], + }); + }); + + it('number - maximum', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Number field', + type: 'number', + description: '', + maximum: 8, + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 10, + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.maximum.defaultMessage], + }); + }); + + it('integer - isInteger', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Integer field', + type: 'integer', + description: '', + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 1.5, + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.isInteger.defaultMessage], + }); + }); + + it('integer - minimum', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Integer field', + type: 'integer', + description: '', + minimum: 8, + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 1, + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.minimum.defaultMessage], + }); + }); + + it('integer - maximum', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Integer field', + type: 'number', + description: '', + maximum: 8, + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 10, + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.maximum.defaultMessage], + }); + }); + + it('password - min lenght', () => { + let newSchema = { + ...schema, + properties: { + ...schema.properties, + password: { + title: 'password', + type: 'password', + description: '', + minLength: '8', + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { username: 'test username', password: 'asd' }, + formatMessage, + }), + ).toEqual({ + password: [messages.minLength.defaultMessage], + }); + }); + + it('password - max lenght', () => { + let newSchema = { + ...schema, + properties: { + ...schema.properties, + password: { + title: 'password', + type: 'password', + description: '', + maxLength: '8', + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { username: 'test username', password: 'asdasdasdasdasd' }, + formatMessage, + }), + ).toEqual({ + password: [messages.maxLength.defaultMessage], + }); + }); + + it('array - uniqueItems', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Array field', + type: 'array', + description: '', + uniqueItems: true, + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: [1, 1], + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.uniqueItems.defaultMessage], + }); + }); }); }); diff --git a/packages/volto/src/helpers/FormValidation/validators.ts b/packages/volto/src/helpers/FormValidation/validators.ts new file mode 100644 index 0000000000..2316160f49 --- /dev/null +++ b/packages/volto/src/helpers/FormValidation/validators.ts @@ -0,0 +1,128 @@ +import { validationMessage } from './FormValidation'; +import { messages } from '@plone/volto/helpers/MessageLabels/MessageLabels'; + +type MinMaxValidator = { + value: string | number; + fieldSpec: string | number; + criterion: string; + formatMessage: Function; +}; + +type Validator = { + value: string; + field: { + minLength: number; + maxLength: number; + minimum: number; + maximum: number; + uniqueItems: Boolean; + }; + formatMessage: Function; +}; + +export const isMaxPropertyValid = ({ + value, + fieldSpec, + criterion, + formatMessage, +}: MinMaxValidator) => { + const isValid = fieldSpec !== undefined ? value <= fieldSpec : true; + return validationMessage(isValid, criterion, fieldSpec, formatMessage); +}; + +export const isMinPropertyValid = ({ + value, + fieldSpec, + criterion, + formatMessage, +}: MinMaxValidator) => { + const isValid = fieldSpec !== undefined ? value >= fieldSpec : true; + return validationMessage(isValid, criterion, fieldSpec, formatMessage); +}; + +export const minLengthValidator = ({ + value, + field, + formatMessage, +}: Validator) => + isMinPropertyValid({ + value: value.length, + fieldSpec: field.minLength, + criterion: 'minLength', + formatMessage, + }); + +export const maxLengthValidator = ({ + value, + field, + formatMessage, +}: Validator) => + isMaxPropertyValid({ + value: value.length, + fieldSpec: field.maxLength, + criterion: 'maxLength', + formatMessage, + }); + +export const urlValidator = ({ value, formatMessage }: Validator) => { + const urlRegex = new RegExp( + '^(https?:\\/\\/)?' + // validate protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // validate domain name + '((\\d{1,3}\\.){3}\\d{1,3}))|' + // validate OR ip (v4) address + '(localhost)' + // validate OR localhost address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // validate port and path + '(\\?[;&a-z\\d%_.~+=-]*)?' + // validate query string + '(\\#[-a-z\\d_]*)?$', // validate fragment locator + 'i', + ); + const isValid = urlRegex.test(value); + return !isValid ? formatMessage(messages.isValidURL) : null; +}; + +export const emailValidator = ({ value, formatMessage }: Validator): string => { + // 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(value); + return !isValid ? formatMessage(messages.isValidEmail) : null; +}; + +export const isNumber = ({ value, formatMessage }: Validator) => { + const floatRegex = /^[+-]?\d+(\.\d+)?$/; + const isValid = + typeof value === 'number' && !isNaN(value) && floatRegex.test(value); + return !isValid ? formatMessage(messages.isNumber) : null; +}; + +export const minimumValidator = ({ value, field, formatMessage }: Validator) => + isMinPropertyValid({ + value, + fieldSpec: field.minimum, + criterion: 'minimum', + formatMessage, + }); + +export const maximumValidator = ({ value, field, formatMessage }: Validator) => + isMaxPropertyValid({ + value, + fieldSpec: field.maximum, + criterion: 'maximum', + formatMessage, + }); + +export const isInteger = ({ value, formatMessage }: Validator) => { + const intRegex = /^-?[0-9]+$/; + const isValid = + typeof value === 'number' && !isNaN(value) && intRegex.test(value); + return !isValid ? formatMessage(messages.isInteger) : null; +}; + +export const hasUniqueItems = ({ value, field, formatMessage }: Validator) => { + const isValid = + field.uniqueItems && + value && + // unique items + [...new Set(value)].length === value.length; + return !isValid ? formatMessage(messages.uniqueItems) : null; +}; diff --git a/packages/volto/test-setup-config.jsx b/packages/volto/test-setup-config.jsx index 3f9d1dc8d8..33ef175b17 100644 --- a/packages/volto/test-setup-config.jsx +++ b/packages/volto/test-setup-config.jsx @@ -23,6 +23,7 @@ import { } from '@plone/volto/config/ControlPanels'; import ListingBlockSchema from '@plone/volto/components/manage/Blocks/Listing/schema'; +import { registerValidators } from '@plone/volto/config/validation'; config.set('settings', { apiPath: 'http://localhost:8080/Plone', @@ -153,9 +154,13 @@ config.set('components', { component: (props) => Image component mock, }, }); + +registerValidators(config); + config.set('experimental', { addBlockButton: { enabled: false, }, }); + config.set('slots', {}); From d4a6d707ec7e1d94f39d5b71e4ace6ba7eb12403 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 10 Jul 2024 16:57:59 +0200 Subject: [PATCH 02/51] Fix tests --- .github/actions/node_env_setup/action.yml | 2 +- .github/workflows/unit.yml | 23 ++----------------- .../FormValidation/FormValidation.test.js | 2 ++ 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/.github/actions/node_env_setup/action.yml b/.github/actions/node_env_setup/action.yml index b058a41457..78e3dd5d1d 100644 --- a/.github/actions/node_env_setup/action.yml +++ b/.github/actions/node_env_setup/action.yml @@ -36,7 +36,7 @@ runs: - name: Install Volto dependencies shell: bash - run: pnpm i + run: make install - name: Install Cypress if not in cache if: steps.cache-cypress-binary.outputs.cache-hit != 'true' diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index cfccae4362..b479cc7fb1 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -16,30 +16,11 @@ jobs: steps: - uses: actions/checkout@v4 - # node setup - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + - name: Set up Node.js environment + uses: ./.github/actions/node_env_setup with: node-version: ${{ matrix.node-version }} - - name: Enable corepack - run: corepack enable - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - uses: actions/cache@v4 - name: Setup pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - run: pnpm i - # Locales in place are needed for the tests to pass - run: pnpm --filter @plone/volto i18n diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.test.js b/packages/volto/src/helpers/FormValidation/FormValidation.test.js index cfda30886b..d211cddff8 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.test.js +++ b/packages/volto/src/helpers/FormValidation/FormValidation.test.js @@ -454,4 +454,6 @@ describe('FormValidation', () => { }); }); }); + + describe('validateFieldsPerFieldset', () => {}); }); From bc7b779fd1eda46d41cc18a9d3136f174413af00 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 10 Jul 2024 17:00:27 +0200 Subject: [PATCH 03/51] fix build-deps precedence --- Makefile | 3 ++- packages/volto/Makefile | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 6273195bbf..99c2b746e1 100644 --- a/Makefile +++ b/Makefile @@ -69,10 +69,11 @@ clean: ## Clean development environment find ./packages -name node_modules -exec rm -rf {} \; .PHONY: install -install: build-deps ## Set up development environment +install: ## Set up development environment # Setup ESlint for VSCode node packages/scripts/vscodesettings.js pnpm i + make build-deps ##### Documentation diff --git a/packages/volto/Makefile b/packages/volto/Makefile index 51236a9395..21b3b8487a 100644 --- a/packages/volto/Makefile +++ b/packages/volto/Makefile @@ -57,9 +57,11 @@ clean: ## Clean development environment rm -rf node_modules .PHONY: install -install: build-deps ## Set up development environment +install: ## Set up development environment # Setup ESlint for VSCode node packages/scripts/vscodesettings.js + pnpm i + make build-deps ##### Build From ad3a0d6a3c40e0329e1ec907752bd424c758a994 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 10 Jul 2024 17:07:29 +0200 Subject: [PATCH 04/51] Fix more --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 99c2b746e1..568b297d80 100644 --- a/Makefile +++ b/Makefile @@ -71,8 +71,8 @@ clean: ## Clean development environment .PHONY: install install: ## Set up development environment # Setup ESlint for VSCode - node packages/scripts/vscodesettings.js pnpm i + node packages/scripts/vscodesettings.js make build-deps ##### Documentation From 1fa71ed752dfe869647b3586c36d058a32ff2770 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 10 Jul 2024 17:50:31 +0200 Subject: [PATCH 05/51] Fix generator tests --- .github/workflows/acceptance.yml | 38 ++------------------------------ 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index af5967f394..0d53b79548 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -497,45 +497,11 @@ jobs: steps: - uses: actions/checkout@v4 - # node setup - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + - name: Set up Node.js environment + uses: ./.github/actions/node_env_setup with: node-version: ${{ matrix.node-version }} - - name: Enable corepack - run: corepack enable - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - uses: actions/cache@v4 - name: Setup pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Cache Cypress Binary - id: cache-cypress-binary - uses: actions/cache@v4 - with: - path: ~/.cache/Cypress - key: binary-${{ matrix.node-version }}-${{ hashFiles('pnpm-lock.yaml') }} - - - run: pnpm i - - - name: Build dependencies - run: pnpm build:deps - - - name: Install Cypress if not in cache - if: steps.cache-cypress-binary.outputs.cache-hit != 'true' - working-directory: packages/volto - run: make cypress-install - # Generator own tests - name: Generator tests run: pnpm test From f6bba96ffb6a4e261abcffb1c38769c2c8609af0 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 10 Jul 2024 18:09:24 +0200 Subject: [PATCH 06/51] Fix react-share package --- packages/volto/package.json | 2 +- pnpm-lock.yaml | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/volto/package.json b/packages/volto/package.json index 1d6732e1d7..d1771fe13b 100644 --- a/packages/volto/package.json +++ b/packages/volto/package.json @@ -240,7 +240,7 @@ "react-router-hash-link": "2.4.3", "react-select": "4.3.1", "react-select-async-paginate": "0.5.3", - "react-share": "2.3.1", + "react-share": "5.1.0", "react-side-effect": "2.1.2", "react-simple-code-editor": "0.7.1", "react-sortable-hoc": "2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d3fbd3f95..49ff49b5c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1872,8 +1872,8 @@ importers: specifier: 0.5.3 version: 0.5.3(react-dom@18.2.0(react@18.2.0))(react-select@4.3.1(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0) react-share: - specifier: 2.3.1 - version: 2.3.1(react@18.2.0) + specifier: 5.1.0 + version: 5.1.0(react@18.2.0) react-side-effect: specifier: 2.1.2 version: 2.1.2(react@18.2.0) @@ -9504,6 +9504,9 @@ packages: classnames@2.2.6: resolution: {integrity: sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + clean-css@5.3.3: resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} engines: {node: '>= 10.0'} @@ -16442,6 +16445,11 @@ packages: peerDependencies: react: ^0.13.0 || ^0.14.0 || ^15.0.0 || ^16.0.0-0 + react-share@5.1.0: + resolution: {integrity: sha512-OvyfMtj/0UzH1wi90OdHhZVJ6WUC/+IeWvBwppeZozwIGyAjQgyR0QXlHOrxVHVECqnGvcpBaFTXVrqouTieaw==} + peerDependencies: + react: ^17 || ^18 + react-side-effect@2.1.2: resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==} peerDependencies: @@ -30404,6 +30412,8 @@ snapshots: classnames@2.2.6: {} + classnames@2.5.1: {} + clean-css@5.3.3: dependencies: source-map: 0.6.1 @@ -40224,6 +40234,14 @@ snapshots: transitivePeerDependencies: - supports-color + react-share@5.1.0(react@18.2.0): + dependencies: + classnames: 2.5.1 + jsonp: 0.2.1 + react: 18.2.0 + transitivePeerDependencies: + - supports-color + react-side-effect@2.1.2(react@18.2.0): dependencies: react: 18.2.0 From 3a4b00070fb69b0cd8993fc21274d17a5951c96d Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 10 Jul 2024 18:23:04 +0200 Subject: [PATCH 07/51] fix test for social sharing --- .../__snapshots__/SocialSharing.test.jsx.snap | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/packages/volto/src/components/theme/SocialSharing/__snapshots__/SocialSharing.test.jsx.snap b/packages/volto/src/components/theme/SocialSharing/__snapshots__/SocialSharing.test.jsx.snap index 5d9e15d1b6..a9a7caf006 100644 --- a/packages/volto/src/components/theme/SocialSharing/__snapshots__/SocialSharing.test.jsx.snap +++ b/packages/volto/src/components/theme/SocialSharing/__snapshots__/SocialSharing.test.jsx.snap @@ -10,13 +10,20 @@ exports[`SocialSharing renders a social sharing component 1`] = ` } >
-
-
-
+ -
+

From 709ed59ba245cb6ffc938d16896697d201ac357d Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 10 Jul 2024 18:23:34 +0200 Subject: [PATCH 08/51] Add custom validation field property --- .../helpers/FormValidation/FormValidation.jsx | 24 ++++++- .../FormValidation/FormValidation.test.js | 66 ++++++++++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.jsx b/packages/volto/src/helpers/FormValidation/FormValidation.jsx index ae478e6402..356c12a152 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.jsx +++ b/packages/volto/src/helpers/FormValidation/FormValidation.jsx @@ -149,13 +149,35 @@ const validateFieldsPerFieldset = ( fieldData, ); - const mergedErrors = [...defaultFieldErrors, ...fieldErrors]; + // Validation per field type or field widget + const hasSpecificValidator = field.validator; + let specificFieldErrors = []; + // test each criterion eg. maximum, isEmail, isUrl, etc + if (hasSpecificValidator) { + const specificFieldValidationCriteria = config.getComponents({ + name: 'fieldValidator', + dependencies: [hasSpecificValidator], + }); + + specificFieldErrors = checkFieldErrors( + specificFieldValidationCriteria, + field, + fieldData, + ); + } + + const mergedErrors = [ + ...defaultFieldErrors, + ...fieldErrors, + ...specificFieldErrors, + ]; if (mergedErrors.length > 0) { errors[fieldId] = [ ...(errors[fieldId] || []), ...defaultFieldErrors, ...fieldErrors, + ...specificFieldErrors, ]; } }); diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.test.js b/packages/volto/src/helpers/FormValidation/FormValidation.test.js index d211cddff8..df2a9c4e31 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.test.js +++ b/packages/volto/src/helpers/FormValidation/FormValidation.test.js @@ -1,5 +1,7 @@ import FormValidation from './FormValidation'; import { messages } from '../MessageLabels/MessageLabels'; +import config from '@plone/volto/registry'; +import { urlValidator } from './validators'; const schema = { properties: { @@ -453,7 +455,69 @@ describe('FormValidation', () => { customField: [messages.uniqueItems.defaultMessage], }); }); + + it('default - specific validator set - Errors', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Default field', + description: '', + validator: 'isURL', + }, + }, + required: [], + }; + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['isURL'], + component: urlValidator, + }); + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 'foo', + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.isValidURL.defaultMessage], + }); + }); + + it('default - specific validator set - Succeeds', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Default field', + description: '', + validator: 'isURL', + }, + }, + required: [], + }; + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['isURL'], + component: urlValidator, + }); + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 'https://plone.org/', + }, + formatMessage, + }), + ).toEqual({}); + }); }); - describe('validateFieldsPerFieldset', () => {}); + // describe('validateBlockDataFields', () => { + + // }); }); From 26c82f9ff57570afa8817fc2af3ac9d409db73cf Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Fri, 12 Jul 2024 10:30:24 +0200 Subject: [PATCH 09/51] Fix uniqueValidator --- .../FormValidation/FormValidation.test.js | 35 +++++++++++++++++++ .../src/helpers/FormValidation/validators.ts | 3 ++ 2 files changed, 38 insertions(+) diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.test.js b/packages/volto/src/helpers/FormValidation/FormValidation.test.js index df2a9c4e31..466aa29363 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.test.js +++ b/packages/volto/src/helpers/FormValidation/FormValidation.test.js @@ -456,6 +456,41 @@ describe('FormValidation', () => { }); }); + it('array - uniqueItems false', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Array field', + type: 'array', + description: '', + uniqueItems: false, + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: [1, 1], + }, + formatMessage, + }), + ).toEqual({}); + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: [1], + }, + formatMessage, + }), + ).toEqual({}); + }); + it('default - specific validator set - Errors', () => { let newSchema = { properties: { diff --git a/packages/volto/src/helpers/FormValidation/validators.ts b/packages/volto/src/helpers/FormValidation/validators.ts index 2316160f49..9c24be3220 100644 --- a/packages/volto/src/helpers/FormValidation/validators.ts +++ b/packages/volto/src/helpers/FormValidation/validators.ts @@ -119,6 +119,9 @@ export const isInteger = ({ value, formatMessage }: Validator) => { }; export const hasUniqueItems = ({ value, field, formatMessage }: Validator) => { + if (!field.uniqueItems) { + return null; + } const isValid = field.uniqueItems && value && From 3378ade06cc139a75f5ed20fc4e1515b5a234054 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Fri, 12 Jul 2024 15:37:23 +0200 Subject: [PATCH 10/51] Add documentation --- docs/source/configuration/index.md | 1 + docs/source/configuration/validation.md | 72 +++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 docs/source/configuration/validation.md diff --git a/docs/source/configuration/index.md b/docs/source/configuration/index.md index 3375c41f9c..5f06beb069 100644 --- a/docs/source/configuration/index.md +++ b/docs/source/configuration/index.md @@ -27,4 +27,5 @@ environmentvariables expanders locking slots +validation ``` diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md new file mode 100644 index 0000000000..f37cc9a631 --- /dev/null +++ b/docs/source/configuration/validation.md @@ -0,0 +1,72 @@ +--- +myst: + html_meta: + "description": "Client side form field validation" + "property=og:description": "Client side form field validation" + "property=og:title": "Form fields validation" + "keywords": "Volto, Plone, frontend, React, configuration, form, fields, validation" +--- + +# Client side form field validation + +Volto provides a mechanism for providing form field validation in an extensible way. +This extensibility is based on the Volto component registry. + +## Registering a validator + +You can register a validator using the component registry API from your add-on configuration. +All validators are registered under the name `fieldValidator`. +The validators are registered using the `dependencies` array of the `registerComponent` API to differentiate the kind of validator to be registered. + +### `default` validators + +These validators are registered and applied to all fields. + +```ts +config.registerComponent({ + name: 'fieldValidator', + dependencies: ['default', 'minLength'], + component: minLengthValidator, +}); +``` + +They should be registered for the `name` `fieldValidator` and the dependency `default`. +It should have a second dependency item, to identify it. +In the case of the example, this other dependency is `minLenght`. +It can be any string. + +### Per field `widget` or `type` validators + +These validators are applied depending on the specified `widget` or the `type` of the field. +If both `widget` and `type` are specified, then only validators of the widget are applied. + +```ts +config.registerComponent({ + name: 'fieldValidator', + dependencies: ['integer', 'maximum'], + component: maximumValidator, +}); +``` + +### Specific validator using the `validator` key in the field + +A final type of validator are applied to the field if the `validator` key is present in the JSON schema definition of the form field. + +```ts +config.registerComponent({ + name: 'fieldValidator', + dependencies: ['isURL'], + component: urlValidator, +}); +``` + +It does not need to be tied to any field `type` or `widget` definition. +It runs in addition to all the above, so it complements the normal validators, if any apply. + +## Volto's default validators + +Volto provide a set of validators by default, you can find them in this module: `packages/volto/src/config/validators.ts` + +### How to override them + +You can override them in your add-on as any other component defined in the registry, by redefining them using the same `dependencies`, and providing your own. From 0101fca8bfc61876ec63d191bcc4fd1fce6478e4 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Fri, 12 Jul 2024 15:37:55 +0200 Subject: [PATCH 11/51] Changelog --- packages/volto/news/6161.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/volto/news/6161.feature diff --git a/packages/volto/news/6161.feature b/packages/volto/news/6161.feature new file mode 100644 index 0000000000..721ce5e616 --- /dev/null +++ b/packages/volto/news/6161.feature @@ -0,0 +1 @@ +Add foundations for extensible validation in forms @sneridagh From 7f2cfaa73e4408f598c523ae61aa366474fe54cd Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Fri, 12 Jul 2024 15:45:49 +0200 Subject: [PATCH 12/51] Changelog --- packages/registry/news/6161.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/registry/news/6161.feature diff --git a/packages/registry/news/6161.feature b/packages/registry/news/6161.feature new file mode 100644 index 0000000000..75641a0fa7 --- /dev/null +++ b/packages/registry/news/6161.feature @@ -0,0 +1 @@ +Add `getComponents` that match a partial set of dependencies, given a name. @sneridagh From 7963197d4d6c19f4fb323494b9a4a72c05120121 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Fri, 12 Jul 2024 16:35:48 +0200 Subject: [PATCH 13/51] Checkpoint --- .../src/components/Blocks/TestBlock/Data.tsx | 12 ++++- .../src/components/Blocks/TestBlock/schema.ts | 2 +- packages/types/src/blocks/index.d.ts | 1 + .../manage/Blocks/Block/BlocksForm.jsx | 2 + .../volto/src/components/manage/Form/Form.jsx | 48 +++++++++++++++++++ .../src/components/manage/Form/InlineForm.jsx | 5 +- packages/volto/src/helpers/Blocks/schema.ts | 0 7 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 packages/volto/src/helpers/Blocks/schema.ts diff --git a/packages/coresandbox/src/components/Blocks/TestBlock/Data.tsx b/packages/coresandbox/src/components/Blocks/TestBlock/Data.tsx index 104ea631ee..0a89035bdd 100644 --- a/packages/coresandbox/src/components/Blocks/TestBlock/Data.tsx +++ b/packages/coresandbox/src/components/Blocks/TestBlock/Data.tsx @@ -3,8 +3,15 @@ import { BlockDataForm } from '@plone/volto/components/manage/Form'; import type { BlockEditProps } from '@plone/types'; const TestBlockData = (props: BlockEditProps) => { - const { block, blocksConfig, contentType, data, navRoot, onChangeBlock } = - props; + const { + block, + blocksConfig, + contentType, + data, + navRoot, + onChangeBlock, + errors, + } = props; const intl = useIntl(); const schema = blocksConfig[data['@type']].blockSchema({ intl, props }); @@ -24,6 +31,7 @@ const TestBlockData = (props: BlockEditProps) => { blocksConfig={blocksConfig} navRoot={navRoot} contentType={contentType} + errors={errors} /> ); }; diff --git a/packages/coresandbox/src/components/Blocks/TestBlock/schema.ts b/packages/coresandbox/src/components/Blocks/TestBlock/schema.ts index 8846418c98..d7cfbef7b2 100644 --- a/packages/coresandbox/src/components/Blocks/TestBlock/schema.ts +++ b/packages/coresandbox/src/components/Blocks/TestBlock/schema.ts @@ -154,7 +154,7 @@ export const SliderSchema: BlockConfigBase['blockSchema'] = ({ intl }) => ({ widget: 'richtext', }, }, - required: [], + required: ['fieldAfterObjectList'], }); export const multipleFieldsetsSchema: BlockConfigBase['blockSchema'] = ({ diff --git a/packages/types/src/blocks/index.d.ts b/packages/types/src/blocks/index.d.ts index 28043b0918..5c40dff0d6 100644 --- a/packages/types/src/blocks/index.d.ts +++ b/packages/types/src/blocks/index.d.ts @@ -116,4 +116,5 @@ export interface BlockEditProps { history: History; location: Location; token: string; + errors: Record; } diff --git a/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx b/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx index a5fb57e217..02588f19e0 100644 --- a/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx +++ b/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx @@ -58,6 +58,7 @@ const BlocksForm = (props) => { history, location, token, + errors, } = props; const [isClient, setIsClient] = useState(false); @@ -354,6 +355,7 @@ const BlocksForm = (props) => { history, location, token, + errors, }; return editBlockWrapper( dragProps, diff --git a/packages/volto/src/components/manage/Form/Form.jsx b/packages/volto/src/components/manage/Form/Form.jsx index 1ebdca28f7..9ef1c205c6 100644 --- a/packages/volto/src/components/manage/Form/Form.jsx +++ b/packages/volto/src/components/manage/Form/Form.jsx @@ -551,6 +551,53 @@ class Form extends Component { ); // Changes the focus to the metadata tab in the sidebar if error this.props.setSidebarTab(0); + } else if (keys(errors).length === 0) { + const blocks = this.state.formData[getBlocksFieldname(formData)]; + const blocksLayout = + this.state.formData[getBlocksLayoutFieldname(formData)]; + let blocksErrors = {}; + const defaultSchema = { + properties: {}, + fieldsets: [], + required: [], + }; + blocksLayout.items.forEach((block) => { + let blockSchema = + config.blocks.blocksConfig[blocks[block]['@type']].blockSchema || + defaultSchema; + if (typeof blockSchema === 'function') { + blockSchema = blockSchema({ + intl: this.props.intl, + formData: blocks[block], + }); + } + const blockErrors = FormValidation.validateFieldsPerFieldset({ + schema: blockSchema, + formData: blocks[block], + formatMessage: this.props.intl.formatMessage, + }); + blocksErrors = { + ...blocksErrors, + ...blockErrors, + }; + }); + this.setState( + { + errors: blocksErrors, + }, + () => { + Object.keys(errors).forEach((err) => + toast.error( + , + ), + ); + }, + ); + this.props.setSidebarTab(1); } else { // Get only the values that have been modified (Edit forms), send all in case that // it's an add form @@ -730,6 +777,7 @@ class Form extends Component { history={this.props.history} location={this.props.location} token={this.props.token} + errors={this.state.errors} /> {this.state.isClient && this.state.sidebarMetadataIsAvailable && diff --git a/packages/volto/src/components/manage/Form/InlineForm.jsx b/packages/volto/src/components/manage/Form/InlineForm.jsx index 64bfb21946..32728d417f 100644 --- a/packages/volto/src/components/manage/Form/InlineForm.jsx +++ b/packages/volto/src/components/manage/Form/InlineForm.jsx @@ -109,7 +109,8 @@ const InlineForm = (props) => { ); } } - + console.log(errors); + console.log(keys(errors).length); return (
{title && ( @@ -142,7 +143,6 @@ const InlineForm = (props) => { content={error.message} /> )} -
{map(defaultFieldset.fields, (field, index) => ( @@ -166,7 +166,6 @@ const InlineForm = (props) => { )}
- {other.map((fieldset, index) => (
diff --git a/packages/volto/src/helpers/Blocks/schema.ts b/packages/volto/src/helpers/Blocks/schema.ts new file mode 100644 index 0000000000..e69de29bb2 From b1355453e7f66ccac379ee3932cb87722f0e927f Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Fri, 12 Jul 2024 18:21:06 +0200 Subject: [PATCH 14/51] Validation for blocks too :) --- .../src/components/Blocks/TestBlock/schema.ts | 12 ++++- .../manage/Blocks/Block/BlocksForm.jsx | 1 + .../manage/Blocks/Block/Order/Item.jsx | 8 ++- .../manage/Blocks/Block/Order/Order.jsx | 2 + .../volto/src/components/manage/Form/Form.jsx | 51 ++++++++++++------- .../src/components/manage/Form/InlineForm.jsx | 7 ++- .../helpers/MessageLabels/MessageLabels.js | 4 ++ .../themes/pastanaga/extras/sidebar.less | 4 ++ 8 files changed, 64 insertions(+), 25 deletions(-) diff --git a/packages/coresandbox/src/components/Blocks/TestBlock/schema.ts b/packages/coresandbox/src/components/Blocks/TestBlock/schema.ts index d7cfbef7b2..756f93966d 100644 --- a/packages/coresandbox/src/components/Blocks/TestBlock/schema.ts +++ b/packages/coresandbox/src/components/Blocks/TestBlock/schema.ts @@ -154,7 +154,7 @@ export const SliderSchema: BlockConfigBase['blockSchema'] = ({ intl }) => ({ widget: 'richtext', }, }, - required: ['fieldAfterObjectList'], + required: [], }); export const multipleFieldsetsSchema: BlockConfigBase['blockSchema'] = ({ @@ -182,6 +182,11 @@ export const multipleFieldsetsSchema: BlockConfigBase['blockSchema'] = ({ title: 'fourth', fields: ['href', 'firstWithDefault', 'style'], }, + { + id: 'fifth', + title: 'fifth', + fields: ['fieldRequiredInFieldset'], + }, ], properties: { slides: { @@ -232,6 +237,9 @@ export const multipleFieldsetsSchema: BlockConfigBase['blockSchema'] = ({ title: 'HTML', widget: 'richtext', }, + fieldRequiredInFieldset: { + title: 'Field required in fieldset', + }, }, - required: [], + required: ['fieldRequiredInFieldset'], }); diff --git a/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx b/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx index 02588f19e0..40e15faff8 100644 --- a/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx +++ b/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx @@ -282,6 +282,7 @@ const BlocksForm = (props) => { onDeleteBlock={onDeleteBlock} onSelectBlock={onSelectBlock} removable + errors={errors} />
, document.getElementById('sidebar-order'), diff --git a/packages/volto/src/components/manage/Blocks/Block/Order/Item.jsx b/packages/volto/src/components/manage/Blocks/Block/Order/Item.jsx index db73f4fbf7..68e20edcc6 100644 --- a/packages/volto/src/components/manage/Blocks/Block/Order/Item.jsx +++ b/packages/volto/src/components/manage/Blocks/Block/Order/Item.jsx @@ -2,7 +2,7 @@ import React, { forwardRef } from 'react'; import classNames from 'classnames'; import { useDispatch, useSelector } from 'react-redux'; import { includes } from 'lodash'; - +import cx from 'classnames'; import { Icon } from '@plone/volto/components'; import { setUIState } from '@plone/volto/actions'; import config from '@plone/volto/registry'; @@ -28,6 +28,7 @@ export const Item = forwardRef( style, value, wrapperRef, + errors, ...props }, ref, @@ -37,6 +38,7 @@ export const Item = forwardRef( const multiSelected = useSelector((state) => state.form.ui.multiSelected); const gridSelected = useSelector((state) => state.form.ui.gridSelected); const dispatch = useDispatch(); + return (
  • - + 0 })} + > {config.blocks.blocksConfig[data?.['@type']]?.icon && ( handleRemove(id) : undefined} onSelectBlock={onSelectBlock} + errors={errors?.[id] || {}} /> ))} {createPortal( diff --git a/packages/volto/src/components/manage/Form/Form.jsx b/packages/volto/src/components/manage/Form/Form.jsx index 9ef1c205c6..37e62d7a1e 100644 --- a/packages/volto/src/components/manage/Form/Form.jsx +++ b/packages/volto/src/components/manage/Form/Form.jsx @@ -576,28 +576,45 @@ class Form extends Component { formData: blocks[block], formatMessage: this.props.intl.formatMessage, }); - blocksErrors = { - ...blocksErrors, - ...blockErrors, - }; + if (keys(blockErrors).length > 0) { + blocksErrors = { + ...blocksErrors, + [block]: { ...blockErrors }, + }; + } }); - this.setState( - { - errors: blocksErrors, - }, - () => { - Object.keys(errors).forEach((err) => + + if (keys(blocksErrors).length > 0) { + this.setState( + { + errors: blocksErrors, + }, + () => { + const errorField = Object.entries( + Object.entries(blocksErrors)[0][1], + )[0][0]; + const errorMessage = Object.entries( + Object.entries(blocksErrors)[0][1], + )[0][1]; toast.error( , - ), - ); - }, - ); - this.props.setSidebarTab(1); + ); + }, + ); + this.props.setSidebarTab(1); + this.props.setUIState({ + selected: Object.keys(blocksErrors)[0], + multiSelected: [], + hovered: null, + }); + } } else { // Get only the values that have been modified (Edit forms), send all in case that // it's an add form diff --git a/packages/volto/src/components/manage/Form/InlineForm.jsx b/packages/volto/src/components/manage/Form/InlineForm.jsx index 32728d417f..f3998f434e 100644 --- a/packages/volto/src/components/manage/Form/InlineForm.jsx +++ b/packages/volto/src/components/manage/Form/InlineForm.jsx @@ -109,8 +109,7 @@ const InlineForm = (props) => { ); } } - console.log(errors); - console.log(keys(errors).length); + return (
    {title && ( @@ -157,7 +156,7 @@ const InlineForm = (props) => { onChangeField(id, value, itemInfo); }} key={field} - error={errors[field]} + error={errors?.[block]?.[field] || {}} block={block} /> ))} @@ -198,7 +197,7 @@ const InlineForm = (props) => { onChangeField(id, value); }} key={field} - error={errors[field]} + error={errors?.[block]?.[field] || {}} block={block} /> ))} diff --git a/packages/volto/src/helpers/MessageLabels/MessageLabels.js b/packages/volto/src/helpers/MessageLabels/MessageLabels.js index a6483fe4c9..a287694282 100644 --- a/packages/volto/src/helpers/MessageLabels/MessageLabels.js +++ b/packages/volto/src/helpers/MessageLabels/MessageLabels.js @@ -375,4 +375,8 @@ export const messages = defineMessages({ id: 'fileTooLarge', defaultMessage: 'This website does not accept files larger than {limit}', }, + blocksFieldsErrorTitle: { + id: 'blocksFieldsErrorTitle', + defaultMessage: 'Error in the block field {errorField}.', + }, }); diff --git a/packages/volto/theme/themes/pastanaga/extras/sidebar.less b/packages/volto/theme/themes/pastanaga/extras/sidebar.less index 633cc070b2..5931049d89 100644 --- a/packages/volto/theme/themes/pastanaga/extras/sidebar.less +++ b/packages/volto/theme/themes/pastanaga/extras/sidebar.less @@ -518,6 +518,10 @@ padding-left: 0.5rem; text-overflow: ellipsis; white-space: nowrap; + + &.errored { + color: @red; + } } &.disable-interaction { From 3d6ef22e9f1b54e3a77c9760d95d9115e9d79869 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Mon, 15 Jul 2024 11:05:02 +0200 Subject: [PATCH 15/51] locales --- packages/volto/locales/ca/LC_MESSAGES/volto.po | 5 +++++ packages/volto/locales/de/LC_MESSAGES/volto.po | 5 +++++ packages/volto/locales/en/LC_MESSAGES/volto.po | 5 +++++ packages/volto/locales/es/LC_MESSAGES/volto.po | 5 +++++ packages/volto/locales/eu/LC_MESSAGES/volto.po | 5 +++++ packages/volto/locales/fi/LC_MESSAGES/volto.po | 5 +++++ packages/volto/locales/fr/LC_MESSAGES/volto.po | 5 +++++ packages/volto/locales/hi/LC_MESSAGES/volto.po | 5 +++++ packages/volto/locales/it/LC_MESSAGES/volto.po | 5 +++++ packages/volto/locales/ja/LC_MESSAGES/volto.po | 5 +++++ packages/volto/locales/nl/LC_MESSAGES/volto.po | 5 +++++ packages/volto/locales/pt/LC_MESSAGES/volto.po | 5 +++++ packages/volto/locales/pt_BR/LC_MESSAGES/volto.po | 5 +++++ packages/volto/locales/ro/LC_MESSAGES/volto.po | 5 +++++ packages/volto/locales/volto.pot | 7 ++++++- packages/volto/locales/zh_CN/LC_MESSAGES/volto.po | 5 +++++ 16 files changed, 81 insertions(+), 1 deletion(-) diff --git a/packages/volto/locales/ca/LC_MESSAGES/volto.po b/packages/volto/locales/ca/LC_MESSAGES/volto.po index c208633667..0dafc61cc4 100644 --- a/packages/volto/locales/ca/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ca/LC_MESSAGES/volto.po @@ -4395,6 +4395,11 @@ msgstr "" msgid "availableViews" msgstr "" +#. Default: "Error in the block field {errorField}." +#: helpers/MessageLabels/MessageLabels +msgid "blocksFieldsErrorTitle" +msgstr "" + #. Default: "Forgot your password?" #: components/theme/Login/Login #: components/theme/PasswordReset/RequestPasswordReset diff --git a/packages/volto/locales/de/LC_MESSAGES/volto.po b/packages/volto/locales/de/LC_MESSAGES/volto.po index 431873a8ce..176780d0bb 100644 --- a/packages/volto/locales/de/LC_MESSAGES/volto.po +++ b/packages/volto/locales/de/LC_MESSAGES/volto.po @@ -4394,6 +4394,11 @@ msgstr "Geben Sie einen Nutzernamen ein, beispielsweise etwas wie "m.muster". Be msgid "availableViews" msgstr "Verfügbare Ansichten" +#. Default: "Error in the block field {errorField}." +#: helpers/MessageLabels/MessageLabels +msgid "blocksFieldsErrorTitle" +msgstr "" + #. Default: "Forgot your password?" #: components/theme/Login/Login #: components/theme/PasswordReset/RequestPasswordReset diff --git a/packages/volto/locales/en/LC_MESSAGES/volto.po b/packages/volto/locales/en/LC_MESSAGES/volto.po index 6446ae0d63..039a106e1c 100644 --- a/packages/volto/locales/en/LC_MESSAGES/volto.po +++ b/packages/volto/locales/en/LC_MESSAGES/volto.po @@ -4389,6 +4389,11 @@ msgstr "" msgid "availableViews" msgstr "" +#. Default: "Error in the block field {errorField}." +#: helpers/MessageLabels/MessageLabels +msgid "blocksFieldsErrorTitle" +msgstr "" + #. Default: "Forgot your password?" #: components/theme/Login/Login #: components/theme/PasswordReset/RequestPasswordReset diff --git a/packages/volto/locales/es/LC_MESSAGES/volto.po b/packages/volto/locales/es/LC_MESSAGES/volto.po index cdeb3c44d1..30abaa2758 100644 --- a/packages/volto/locales/es/LC_MESSAGES/volto.po +++ b/packages/volto/locales/es/LC_MESSAGES/volto.po @@ -4396,6 +4396,11 @@ msgstr "Introduzca el nombre de usuario que desee utilizar. Generalmente algo co msgid "availableViews" msgstr "Vistas disponibles" +#. Default: "Error in the block field {errorField}." +#: helpers/MessageLabels/MessageLabels +msgid "blocksFieldsErrorTitle" +msgstr "" + #. Default: "Forgot your password?" #: components/theme/Login/Login #: components/theme/PasswordReset/RequestPasswordReset diff --git a/packages/volto/locales/eu/LC_MESSAGES/volto.po b/packages/volto/locales/eu/LC_MESSAGES/volto.po index bbac9957bb..99dd24c13b 100644 --- a/packages/volto/locales/eu/LC_MESSAGES/volto.po +++ b/packages/volto/locales/eu/LC_MESSAGES/volto.po @@ -4396,6 +4396,11 @@ msgstr "Idatzi zure erabiltzaile izena, "jgarmendia" moduko zerbait. Ez da onart msgid "availableViews" msgstr "Bistak" +#. Default: "Error in the block field {errorField}." +#: helpers/MessageLabels/MessageLabels +msgid "blocksFieldsErrorTitle" +msgstr "" + #. Default: "Forgot your password?" #: components/theme/Login/Login #: components/theme/PasswordReset/RequestPasswordReset diff --git a/packages/volto/locales/fi/LC_MESSAGES/volto.po b/packages/volto/locales/fi/LC_MESSAGES/volto.po index 659146eaff..a3a6332edc 100644 --- a/packages/volto/locales/fi/LC_MESSAGES/volto.po +++ b/packages/volto/locales/fi/LC_MESSAGES/volto.po @@ -4394,6 +4394,11 @@ msgstr "Lisää käyttäjätunnus, esim. mameikal. Älä käytä erikoismerkkej msgid "availableViews" msgstr "Saatavilla olevat näkymät" +#. Default: "Error in the block field {errorField}." +#: helpers/MessageLabels/MessageLabels +msgid "blocksFieldsErrorTitle" +msgstr "" + #. Default: "Forgot your password?" #: components/theme/Login/Login #: components/theme/PasswordReset/RequestPasswordReset diff --git a/packages/volto/locales/fr/LC_MESSAGES/volto.po b/packages/volto/locales/fr/LC_MESSAGES/volto.po index 4b98a6ebd5..2a565e900b 100644 --- a/packages/volto/locales/fr/LC_MESSAGES/volto.po +++ b/packages/volto/locales/fr/LC_MESSAGES/volto.po @@ -4396,6 +4396,11 @@ msgstr "Saisissez un nom d'utilisateur, haituellement quelque chose comme "jdupo msgid "availableViews" msgstr "Vues disponibles" +#. Default: "Error in the block field {errorField}." +#: helpers/MessageLabels/MessageLabels +msgid "blocksFieldsErrorTitle" +msgstr "" + #. Default: "Forgot your password?" #: components/theme/Login/Login #: components/theme/PasswordReset/RequestPasswordReset diff --git a/packages/volto/locales/hi/LC_MESSAGES/volto.po b/packages/volto/locales/hi/LC_MESSAGES/volto.po index 7fd750d60e..12958cdd44 100644 --- a/packages/volto/locales/hi/LC_MESSAGES/volto.po +++ b/packages/volto/locales/hi/LC_MESSAGES/volto.po @@ -4389,6 +4389,11 @@ msgstr "एक उपयोगकर्ता नाम दर्ज करे msgid "availableViews" msgstr "उपलब्ध दृश्य" +#. Default: "Error in the block field {errorField}." +#: helpers/MessageLabels/MessageLabels +msgid "blocksFieldsErrorTitle" +msgstr "" + #. Default: "Forgot your password?" #: components/theme/Login/Login #: components/theme/PasswordReset/RequestPasswordReset diff --git a/packages/volto/locales/it/LC_MESSAGES/volto.po b/packages/volto/locales/it/LC_MESSAGES/volto.po index 55d33ae7ee..bcbef931ab 100644 --- a/packages/volto/locales/it/LC_MESSAGES/volto.po +++ b/packages/volto/locales/it/LC_MESSAGES/volto.po @@ -4389,6 +4389,11 @@ msgstr "Inserisci uno username, ad esempio 'jsmith'. Non sono consentiti spazi o msgid "availableViews" msgstr "Viste disponibili" +#. Default: "Error in the block field {errorField}." +#: helpers/MessageLabels/MessageLabels +msgid "blocksFieldsErrorTitle" +msgstr "" + #. Default: "Forgot your password?" #: components/theme/Login/Login #: components/theme/PasswordReset/RequestPasswordReset diff --git a/packages/volto/locales/ja/LC_MESSAGES/volto.po b/packages/volto/locales/ja/LC_MESSAGES/volto.po index 812edef96d..3ef85d883c 100644 --- a/packages/volto/locales/ja/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ja/LC_MESSAGES/volto.po @@ -4394,6 +4394,11 @@ msgstr "" msgid "availableViews" msgstr "" +#. Default: "Error in the block field {errorField}." +#: helpers/MessageLabels/MessageLabels +msgid "blocksFieldsErrorTitle" +msgstr "" + #. Default: "Forgot your password?" #: components/theme/Login/Login #: components/theme/PasswordReset/RequestPasswordReset diff --git a/packages/volto/locales/nl/LC_MESSAGES/volto.po b/packages/volto/locales/nl/LC_MESSAGES/volto.po index ac3e3f7898..15c2c40b37 100644 --- a/packages/volto/locales/nl/LC_MESSAGES/volto.po +++ b/packages/volto/locales/nl/LC_MESSAGES/volto.po @@ -4393,6 +4393,11 @@ msgstr "" msgid "availableViews" msgstr "" +#. Default: "Error in the block field {errorField}." +#: helpers/MessageLabels/MessageLabels +msgid "blocksFieldsErrorTitle" +msgstr "" + #. Default: "Forgot your password?" #: components/theme/Login/Login #: components/theme/PasswordReset/RequestPasswordReset diff --git a/packages/volto/locales/pt/LC_MESSAGES/volto.po b/packages/volto/locales/pt/LC_MESSAGES/volto.po index 610eb70a88..525fa7b0cd 100644 --- a/packages/volto/locales/pt/LC_MESSAGES/volto.po +++ b/packages/volto/locales/pt/LC_MESSAGES/volto.po @@ -4394,6 +4394,11 @@ msgstr "" msgid "availableViews" msgstr "" +#. Default: "Error in the block field {errorField}." +#: helpers/MessageLabels/MessageLabels +msgid "blocksFieldsErrorTitle" +msgstr "" + #. Default: "Forgot your password?" #: components/theme/Login/Login #: components/theme/PasswordReset/RequestPasswordReset diff --git a/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po b/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po index 2ef7345fc9..1968f15682 100644 --- a/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po +++ b/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po @@ -4395,6 +4395,11 @@ msgstr "Informe o nome do usuário que você deseja, geralmente algo como 'jsilv msgid "availableViews" msgstr "Visões disponíveis" +#. Default: "Error in the block field {errorField}." +#: helpers/MessageLabels/MessageLabels +msgid "blocksFieldsErrorTitle" +msgstr "" + #. Default: "Forgot your password?" #: components/theme/Login/Login #: components/theme/PasswordReset/RequestPasswordReset diff --git a/packages/volto/locales/ro/LC_MESSAGES/volto.po b/packages/volto/locales/ro/LC_MESSAGES/volto.po index 153a1c3701..c51227ef17 100644 --- a/packages/volto/locales/ro/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ro/LC_MESSAGES/volto.po @@ -4389,6 +4389,11 @@ msgstr "" msgid "availableViews" msgstr "" +#. Default: "Error in the block field {errorField}." +#: helpers/MessageLabels/MessageLabels +msgid "blocksFieldsErrorTitle" +msgstr "" + #. Default: "Forgot your password?" #: components/theme/Login/Login #: components/theme/PasswordReset/RequestPasswordReset diff --git a/packages/volto/locales/volto.pot b/packages/volto/locales/volto.pot index 3dd390b780..3cc1448a21 100644 --- a/packages/volto/locales/volto.pot +++ b/packages/volto/locales/volto.pot @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: Plone\n" -"POT-Creation-Date: 2024-07-09T06:25:05.312Z\n" +"POT-Creation-Date: 2024-07-12T16:21:15.231Z\n" "Last-Translator: Plone i18n \n" "Language-Team: Plone i18n \n" "Content-Type: text/plain; charset=utf-8\n" @@ -4391,6 +4391,11 @@ msgstr "" msgid "availableViews" msgstr "" +#. Default: "Error in the block field {errorField}." +#: helpers/MessageLabels/MessageLabels +msgid "blocksFieldsErrorTitle" +msgstr "" + #. Default: "Forgot your password?" #: components/theme/Login/Login #: components/theme/PasswordReset/RequestPasswordReset diff --git a/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po b/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po index 58f05932bc..d657f7233d 100644 --- a/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po +++ b/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po @@ -4395,6 +4395,11 @@ msgstr "" msgid "availableViews" msgstr "" +#. Default: "Error in the block field {errorField}." +#: helpers/MessageLabels/MessageLabels +msgid "blocksFieldsErrorTitle" +msgstr "" + #. Default: "Forgot your password?" #: components/theme/Login/Login #: components/theme/PasswordReset/RequestPasswordReset From 80d7462afeaf4903d33c8490fdde32c1d7c973fc Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Mon, 15 Jul 2024 11:05:12 +0200 Subject: [PATCH 16/51] Refactor validation in Form --- .../volto/src/components/manage/Form/Form.jsx | 99 +++++++++---------- 1 file changed, 49 insertions(+), 50 deletions(-) diff --git a/packages/volto/src/components/manage/Form/Form.jsx b/packages/volto/src/components/manage/Form/Form.jsx index 37e62d7a1e..151a2b542b 100644 --- a/packages/volto/src/components/manage/Form/Form.jsx +++ b/packages/volto/src/components/manage/Form/Form.jsx @@ -12,6 +12,7 @@ import { FormValidation, getBlocksFieldname, getBlocksLayoutFieldname, + hasBlocksData, messages, } from '@plone/volto/helpers'; import aheadSVG from '@plone/volto/icons/ahead.svg'; @@ -527,35 +528,13 @@ class Form extends Component { }) : {}; - if (keys(errors).length > 0) { - const activeIndex = FormValidation.showFirstTabWithErrors({ - errors, - schema: this.props.schema, - }); - this.setState( - { - errors, - activeIndex, - }, - () => { - Object.keys(errors).forEach((err) => - toast.error( - , - ), - ); - }, - ); - // Changes the focus to the metadata tab in the sidebar if error - this.props.setSidebarTab(0); - } else if (keys(errors).length === 0) { + let blocksErrors = {}; + + if (hasBlocksData(formData)) { + // Validate blocks const blocks = this.state.formData[getBlocksFieldname(formData)]; const blocksLayout = this.state.formData[getBlocksLayoutFieldname(formData)]; - let blocksErrors = {}; const defaultSchema = { properties: {}, fieldsets: [], @@ -583,30 +562,50 @@ class Form extends Component { }; } }); + } - if (keys(blocksErrors).length > 0) { - this.setState( - { - errors: blocksErrors, - }, - () => { - const errorField = Object.entries( - Object.entries(blocksErrors)[0][1], - )[0][0]; - const errorMessage = Object.entries( - Object.entries(blocksErrors)[0][1], - )[0][1]; - toast.error( - , - ); - }, + if (keys(errors).length > 0 || keys(blocksErrors).length > 0) { + const activeIndex = FormValidation.showFirstTabWithErrors({ + errors, + schema: this.props.schema, + }); + + this.setState({ + errors: { + ...errors, + ...(!isEmpty(blocksErrors) && { blocks: blocksErrors }), + }, + activeIndex, + }); + + if (keys(errors).length > 0) { + // Changes the focus to the metadata tab in the sidebar if error + Object.keys(errors).forEach((err) => + toast.error( + , + ), + ); + this.props.setSidebarTab(0); + } else if (keys(blocksErrors).length > 0) { + const errorField = Object.entries( + Object.entries(blocksErrors)[0][1], + )[0][0]; + const errorMessage = Object.entries( + Object.entries(blocksErrors)[0][1], + )[0][1]; + toast.error( + , ); this.props.setSidebarTab(1); this.props.setUIState({ @@ -794,7 +793,7 @@ class Form extends Component { history={this.props.history} location={this.props.location} token={this.props.token} - errors={this.state.errors} + errors={this.state.errors.blocks} /> {this.state.isClient && this.state.sidebarMetadataIsAvailable && From 9ad0eea4ce06756fe1f24ee762ad3f8455913728 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Mon, 15 Jul 2024 11:21:02 +0200 Subject: [PATCH 17/51] Changelog --- packages/types/news/6161.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/types/news/6161.bugfix diff --git a/packages/types/news/6161.bugfix b/packages/types/news/6161.bugfix new file mode 100644 index 0000000000..3e8c99230d --- /dev/null +++ b/packages/types/news/6161.bugfix @@ -0,0 +1 @@ +Add `errors` shape to the `BlockEditProps` @sneridagh From 6234d484606557681c5a3e81ab8d7af4be2316fc Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Mon, 15 Jul 2024 17:44:02 +0200 Subject: [PATCH 18/51] Separate type-widget validators. Add behavior-fieldId validator types. --- docs/source/configuration/validation.md | 121 ++++++++++++- .../volto/locales/ca/LC_MESSAGES/volto.po | 10 ++ .../volto/locales/de/LC_MESSAGES/volto.po | 10 ++ .../volto/locales/en/LC_MESSAGES/volto.po | 10 ++ .../volto/locales/es/LC_MESSAGES/volto.po | 10 ++ .../volto/locales/eu/LC_MESSAGES/volto.po | 10 ++ .../volto/locales/fi/LC_MESSAGES/volto.po | 10 ++ .../volto/locales/fr/LC_MESSAGES/volto.po | 10 ++ .../volto/locales/hi/LC_MESSAGES/volto.po | 10 ++ .../volto/locales/it/LC_MESSAGES/volto.po | 10 ++ .../volto/locales/ja/LC_MESSAGES/volto.po | 10 ++ .../volto/locales/nl/LC_MESSAGES/volto.po | 10 ++ .../volto/locales/pt/LC_MESSAGES/volto.po | 10 ++ .../volto/locales/pt_BR/LC_MESSAGES/volto.po | 10 ++ .../volto/locales/ro/LC_MESSAGES/volto.po | 10 ++ packages/volto/locales/volto.pot | 12 +- .../volto/locales/zh_CN/LC_MESSAGES/volto.po | 10 ++ packages/volto/src/config/validation.ts | 14 ++ packages/volto/src/helpers/Blocks/schema.ts | 0 .../helpers/FormValidation/FormValidation.jsx | 57 +++++- .../FormValidation/FormValidation.test.js | 167 ++++++++++++++++++ .../src/helpers/FormValidation/validators.ts | 31 +++- .../helpers/MessageLabels/MessageLabels.js | 8 + 23 files changed, 539 insertions(+), 21 deletions(-) delete mode 100644 packages/volto/src/helpers/Blocks/schema.ts diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md index f37cc9a631..754359a7c8 100644 --- a/docs/source/configuration/validation.md +++ b/docs/source/configuration/validation.md @@ -30,15 +30,14 @@ config.registerComponent({ }); ``` -They should be registered for the `name` `fieldValidator` and the dependency `default`. -It should have a second dependency item, to identify it. +It takes two `dependencies`. +The first element should be the `default` identifier, and the second you can set it up to identify the validator. In the case of the example, this other dependency is `minLenght`. It can be any string. -### Per field `widget` or `type` validators +### Per field `type` validators -These validators are applied depending on the specified `widget` or the `type` of the field. -If both `widget` and `type` are specified, then only validators of the widget are applied. +These validators are applied depending on the specified `type` of the field. ```ts config.registerComponent({ @@ -48,6 +47,70 @@ config.registerComponent({ }); ``` +It takes two `dependencies` since we can potentially have several validators for the same `type`. +The first element should be the `type`, and the second you can set it up to identify the validator. +You should specify the `type` in the JSON schema of the block (in a content type, this is included in the default serialization of the field). +The next example is for the use case of a block JSON schema: + +```ts +let blockSchema = { + properties: { + ...schema.properties, + customField: { + title: 'My custom field', + description: '', + type: 'integer' + }, + }, + required: [], +}; +``` + +### Per field `widget` validators + +These validators are applied depending on the specified `widget` of the field. + +```ts +config.registerComponent({ + name: 'fieldValidator', + dependencies: ['myCustomURLWidget', 'maximum'], + component: maximumValidator, +}); +``` + +It takes two `dependencies` since we can potentially have several validators for the same `widget`. +The first element should be the `widget`, and the second you can set it up to identify the validator. +You should specify the `widget` in the JSON schema of the block (or as an additional data in the content type definition). +The next example is for the use case of a block JSON schema: + +```ts +let blockSchema = { + properties: { + ...schema.properties, + customField: { + title: 'My custom field', + description: '', + widget: 'myCustomURLWidget', + }, + }, + required: [], +}; +``` + +### Per behavior and field name validator + +These validators are applied depending on the behavior (usually coming from a content type definition) in combination of the name of the field. + +```ts +config.registerComponent({ + name: 'fieldValidator', + dependencies: ['plone.eventbasic', 'start'], + component: urlValidator, +}); +``` +The first dependency should be the name of the behavior, and second the name (`id`) of the field. +This type of validator only applies to content type validators. + ### Specific validator using the `validator` key in the field A final type of validator are applied to the field if the `validator` key is present in the JSON schema definition of the form field. @@ -60,6 +123,23 @@ config.registerComponent({ }); ``` +The dependencies takes one single name, in this case, the name of the validator. +You should specify the validator in the JSON schema of the block (or as an additional data in the content type definition). + +```ts +let blockSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Default field', + description: '', + validator: 'isURL', + }, + }, + required: [], +}; +``` + It does not need to be tied to any field `type` or `widget` definition. It runs in addition to all the above, so it complements the normal validators, if any apply. @@ -70,3 +150,34 @@ Volto provide a set of validators by default, you can find them in this module: ### How to override them You can override them in your add-on as any other component defined in the registry, by redefining them using the same `dependencies`, and providing your own. + +## Signature of a validator + +A validator has the following signature: + +```ts +type Validator = { + // The field value + value: string; + // The field schema definition object + field: Record; + // The form data + formData?: any; + // The intl formatMessage function + formatMessage: Function; +}; +``` + +This is an example of a `isNumber` validator: + +```ts +export const isNumber = ({ value, formatMessage }: Validator) => { + const floatRegex = /^[+-]?\d+(\.\d+)?$/; + const isValid = + typeof value === 'number' && !isNaN(value) && floatRegex.test(value); + return !isValid ? formatMessage(messages.isNumber) : null; +}; +``` + +Using the `formData` you can perform validation checks using other field data as source. +This is interesting in the case that two fields are related, like `start` and `end` dates. diff --git a/packages/volto/locales/ca/LC_MESSAGES/volto.po b/packages/volto/locales/ca/LC_MESSAGES/volto.po index 0dafc61cc4..1ef04b6b5d 100644 --- a/packages/volto/locales/ca/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ca/LC_MESSAGES/volto.po @@ -1268,6 +1268,11 @@ msgstr "" msgid "End Date" msgstr "Data de finalització" +#. Default: "End event date happens before than the start event date" +#: helpers/MessageLabels/MessageLabels +msgid "End event date happens before than the start event date" +msgstr "" + #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -3437,6 +3442,11 @@ msgstr "Dividir" msgid "Start Date" msgstr "Data d'inici" +#. Default: "Start event date happens later than the end event date" +#: helpers/MessageLabels/MessageLabels +msgid "Start event date happens later than the end event date" +msgstr "" + #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" diff --git a/packages/volto/locales/de/LC_MESSAGES/volto.po b/packages/volto/locales/de/LC_MESSAGES/volto.po index 176780d0bb..6157a2e0c1 100644 --- a/packages/volto/locales/de/LC_MESSAGES/volto.po +++ b/packages/volto/locales/de/LC_MESSAGES/volto.po @@ -1267,6 +1267,11 @@ msgstr "Aktiviert?" msgid "End Date" msgstr "Enddatum" +#. Default: "End event date happens before than the start event date" +#: helpers/MessageLabels/MessageLabels +msgid "End event date happens before than the start event date" +msgstr "" + #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -3436,6 +3441,11 @@ msgstr "Aufsplitten" msgid "Start Date" msgstr "Anfangsdatum" +#. Default: "Start event date happens later than the end event date" +#: helpers/MessageLabels/MessageLabels +msgid "Start event date happens later than the end event date" +msgstr "" + #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" diff --git a/packages/volto/locales/en/LC_MESSAGES/volto.po b/packages/volto/locales/en/LC_MESSAGES/volto.po index 039a106e1c..447c2509be 100644 --- a/packages/volto/locales/en/LC_MESSAGES/volto.po +++ b/packages/volto/locales/en/LC_MESSAGES/volto.po @@ -1262,6 +1262,11 @@ msgstr "" msgid "End Date" msgstr "" +#. Default: "End event date happens before than the start event date" +#: helpers/MessageLabels/MessageLabels +msgid "End event date happens before than the start event date" +msgstr "" + #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -3431,6 +3436,11 @@ msgstr "" msgid "Start Date" msgstr "" +#. Default: "Start event date happens later than the end event date" +#: helpers/MessageLabels/MessageLabels +msgid "Start event date happens later than the end event date" +msgstr "" + #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" diff --git a/packages/volto/locales/es/LC_MESSAGES/volto.po b/packages/volto/locales/es/LC_MESSAGES/volto.po index 30abaa2758..1b28b346b9 100644 --- a/packages/volto/locales/es/LC_MESSAGES/volto.po +++ b/packages/volto/locales/es/LC_MESSAGES/volto.po @@ -1269,6 +1269,11 @@ msgstr "¿Activado?" msgid "End Date" msgstr "Fecha final" +#. Default: "End event date happens before than the start event date" +#: helpers/MessageLabels/MessageLabels +msgid "End event date happens before than the start event date" +msgstr "" + #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -3438,6 +3443,11 @@ msgstr "División" msgid "Start Date" msgstr "Fecha de inicio" +#. Default: "Start event date happens later than the end event date" +#: helpers/MessageLabels/MessageLabels +msgid "Start event date happens later than the end event date" +msgstr "" + #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" diff --git a/packages/volto/locales/eu/LC_MESSAGES/volto.po b/packages/volto/locales/eu/LC_MESSAGES/volto.po index 99dd24c13b..e6d1d7cc65 100644 --- a/packages/volto/locales/eu/LC_MESSAGES/volto.po +++ b/packages/volto/locales/eu/LC_MESSAGES/volto.po @@ -1269,6 +1269,11 @@ msgstr "Aktibatuta?" msgid "End Date" msgstr "Bukaera data" +#. Default: "End event date happens before than the start event date" +#: helpers/MessageLabels/MessageLabels +msgid "End event date happens before than the start event date" +msgstr "" + #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -3438,6 +3443,11 @@ msgstr "Banatu" msgid "Start Date" msgstr "Hasiera-data" +#. Default: "Start event date happens later than the end event date" +#: helpers/MessageLabels/MessageLabels +msgid "Start event date happens later than the end event date" +msgstr "" + #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" diff --git a/packages/volto/locales/fi/LC_MESSAGES/volto.po b/packages/volto/locales/fi/LC_MESSAGES/volto.po index a3a6332edc..b47a21d524 100644 --- a/packages/volto/locales/fi/LC_MESSAGES/volto.po +++ b/packages/volto/locales/fi/LC_MESSAGES/volto.po @@ -1267,6 +1267,11 @@ msgstr "Aktivoitu?" msgid "End Date" msgstr "Päättymispäivä" +#. Default: "End event date happens before than the start event date" +#: helpers/MessageLabels/MessageLabels +msgid "End event date happens before than the start event date" +msgstr "" + #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -3436,6 +3441,11 @@ msgstr "Halkaise" msgid "Start Date" msgstr "Aloituspäivä" +#. Default: "Start event date happens later than the end event date" +#: helpers/MessageLabels/MessageLabels +msgid "Start event date happens later than the end event date" +msgstr "" + #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" diff --git a/packages/volto/locales/fr/LC_MESSAGES/volto.po b/packages/volto/locales/fr/LC_MESSAGES/volto.po index 2a565e900b..59686d2042 100644 --- a/packages/volto/locales/fr/LC_MESSAGES/volto.po +++ b/packages/volto/locales/fr/LC_MESSAGES/volto.po @@ -1269,6 +1269,11 @@ msgstr "Activé ?" msgid "End Date" msgstr "Date de fin" +#. Default: "End event date happens before than the start event date" +#: helpers/MessageLabels/MessageLabels +msgid "End event date happens before than the start event date" +msgstr "" + #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -3438,6 +3443,11 @@ msgstr "Divisé" msgid "Start Date" msgstr "Date de début" +#. Default: "Start event date happens later than the end event date" +#: helpers/MessageLabels/MessageLabels +msgid "Start event date happens later than the end event date" +msgstr "" + #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" diff --git a/packages/volto/locales/hi/LC_MESSAGES/volto.po b/packages/volto/locales/hi/LC_MESSAGES/volto.po index 12958cdd44..bad6113aab 100644 --- a/packages/volto/locales/hi/LC_MESSAGES/volto.po +++ b/packages/volto/locales/hi/LC_MESSAGES/volto.po @@ -1262,6 +1262,11 @@ msgstr "सक्षम?" msgid "End Date" msgstr "अंतिम तिथि" +#. Default: "End event date happens before than the start event date" +#: helpers/MessageLabels/MessageLabels +msgid "End event date happens before than the start event date" +msgstr "" + #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -3431,6 +3436,11 @@ msgstr "विभाजित करें" msgid "Start Date" msgstr "प्रारंभ तिथि" +#. Default: "Start event date happens later than the end event date" +#: helpers/MessageLabels/MessageLabels +msgid "Start event date happens later than the end event date" +msgstr "" + #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" diff --git a/packages/volto/locales/it/LC_MESSAGES/volto.po b/packages/volto/locales/it/LC_MESSAGES/volto.po index bcbef931ab..511d8a3797 100644 --- a/packages/volto/locales/it/LC_MESSAGES/volto.po +++ b/packages/volto/locales/it/LC_MESSAGES/volto.po @@ -1262,6 +1262,11 @@ msgstr "Abilitato?" msgid "End Date" msgstr "Data di fine" +#. Default: "End event date happens before than the start event date" +#: helpers/MessageLabels/MessageLabels +msgid "End event date happens before than the start event date" +msgstr "" + #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -3431,6 +3436,11 @@ msgstr "Dividi" msgid "Start Date" msgstr "Data di inizio" +#. Default: "Start event date happens later than the end event date" +#: helpers/MessageLabels/MessageLabels +msgid "Start event date happens later than the end event date" +msgstr "" + #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" diff --git a/packages/volto/locales/ja/LC_MESSAGES/volto.po b/packages/volto/locales/ja/LC_MESSAGES/volto.po index 3ef85d883c..3143057326 100644 --- a/packages/volto/locales/ja/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ja/LC_MESSAGES/volto.po @@ -1267,6 +1267,11 @@ msgstr "" msgid "End Date" msgstr "終了日付" +#. Default: "End event date happens before than the start event date" +#: helpers/MessageLabels/MessageLabels +msgid "End event date happens before than the start event date" +msgstr "" + #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -3436,6 +3441,11 @@ msgstr "分割" msgid "Start Date" msgstr "開始日付" +#. Default: "Start event date happens later than the end event date" +#: helpers/MessageLabels/MessageLabels +msgid "Start event date happens later than the end event date" +msgstr "" + #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" diff --git a/packages/volto/locales/nl/LC_MESSAGES/volto.po b/packages/volto/locales/nl/LC_MESSAGES/volto.po index 15c2c40b37..a69cb7f9b6 100644 --- a/packages/volto/locales/nl/LC_MESSAGES/volto.po +++ b/packages/volto/locales/nl/LC_MESSAGES/volto.po @@ -1266,6 +1266,11 @@ msgstr "" msgid "End Date" msgstr "" +#. Default: "End event date happens before than the start event date" +#: helpers/MessageLabels/MessageLabels +msgid "End event date happens before than the start event date" +msgstr "" + #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -3435,6 +3440,11 @@ msgstr "" msgid "Start Date" msgstr "" +#. Default: "Start event date happens later than the end event date" +#: helpers/MessageLabels/MessageLabels +msgid "Start event date happens later than the end event date" +msgstr "" + #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" diff --git a/packages/volto/locales/pt/LC_MESSAGES/volto.po b/packages/volto/locales/pt/LC_MESSAGES/volto.po index 525fa7b0cd..d559c41160 100644 --- a/packages/volto/locales/pt/LC_MESSAGES/volto.po +++ b/packages/volto/locales/pt/LC_MESSAGES/volto.po @@ -1267,6 +1267,11 @@ msgstr "" msgid "End Date" msgstr "" +#. Default: "End event date happens before than the start event date" +#: helpers/MessageLabels/MessageLabels +msgid "End event date happens before than the start event date" +msgstr "" + #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -3436,6 +3441,11 @@ msgstr "Dividir" msgid "Start Date" msgstr "" +#. Default: "Start event date happens later than the end event date" +#: helpers/MessageLabels/MessageLabels +msgid "Start event date happens later than the end event date" +msgstr "" + #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" diff --git a/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po b/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po index 1968f15682..7ddb7ec3a3 100644 --- a/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po +++ b/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po @@ -1268,6 +1268,11 @@ msgstr "Ativada?" msgid "End Date" msgstr "Data Final" +#. Default: "End event date happens before than the start event date" +#: helpers/MessageLabels/MessageLabels +msgid "End event date happens before than the start event date" +msgstr "" + #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -3437,6 +3442,11 @@ msgstr "Dividir" msgid "Start Date" msgstr "Data de Início" +#. Default: "Start event date happens later than the end event date" +#: helpers/MessageLabels/MessageLabels +msgid "Start event date happens later than the end event date" +msgstr "" + #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" diff --git a/packages/volto/locales/ro/LC_MESSAGES/volto.po b/packages/volto/locales/ro/LC_MESSAGES/volto.po index c51227ef17..d73b6498d4 100644 --- a/packages/volto/locales/ro/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ro/LC_MESSAGES/volto.po @@ -1262,6 +1262,11 @@ msgstr "" msgid "End Date" msgstr "Data de încheiere" +#. Default: "End event date happens before than the start event date" +#: helpers/MessageLabels/MessageLabels +msgid "End event date happens before than the start event date" +msgstr "" + #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -3431,6 +3436,11 @@ msgstr "Împărțire" msgid "Start Date" msgstr "Data de început" +#. Default: "Start event date happens later than the end event date" +#: helpers/MessageLabels/MessageLabels +msgid "Start event date happens later than the end event date" +msgstr "" + #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" diff --git a/packages/volto/locales/volto.pot b/packages/volto/locales/volto.pot index 3cc1448a21..f3d97a00f4 100644 --- a/packages/volto/locales/volto.pot +++ b/packages/volto/locales/volto.pot @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: Plone\n" -"POT-Creation-Date: 2024-07-12T16:21:15.231Z\n" +"POT-Creation-Date: 2024-07-15T15:43:39.835Z\n" "Last-Translator: Plone i18n \n" "Language-Team: Plone i18n \n" "Content-Type: text/plain; charset=utf-8\n" @@ -1264,6 +1264,11 @@ msgstr "" msgid "End Date" msgstr "" +#. Default: "End event date happens before than the start event date" +#: helpers/MessageLabels/MessageLabels +msgid "End event date happens before than the start event date" +msgstr "" + #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -3433,6 +3438,11 @@ msgstr "" msgid "Start Date" msgstr "" +#. Default: "Start event date happens later than the end event date" +#: helpers/MessageLabels/MessageLabels +msgid "Start event date happens later than the end event date" +msgstr "" + #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" diff --git a/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po b/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po index d657f7233d..3ea60cec70 100644 --- a/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po +++ b/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po @@ -1268,6 +1268,11 @@ msgstr "启用?" msgid "End Date" msgstr "结束日期" +#. Default: "End event date happens before than the start event date" +#: helpers/MessageLabels/MessageLabels +msgid "End event date happens before than the start event date" +msgstr "" + #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -3437,6 +3442,11 @@ msgstr "" msgid "Start Date" msgstr "开始日期" +#. Default: "Start event date happens later than the end event date" +#: helpers/MessageLabels/MessageLabels +msgid "Start event date happens later than the end event date" +msgstr "" + #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" diff --git a/packages/volto/src/config/validation.ts b/packages/volto/src/config/validation.ts index 3309aa8213..0fc7d8aad7 100644 --- a/packages/volto/src/config/validation.ts +++ b/packages/volto/src/config/validation.ts @@ -10,6 +10,8 @@ import { minimumValidator, isInteger, hasUniqueItems, + startEventDateRangeValidator, + endEventDateRangeValidator, } from '@plone/volto/helpers/FormValidation/validators'; const registerValidators = (config: ConfigType) => { @@ -78,6 +80,18 @@ const registerValidators = (config: ConfigType) => { dependencies: ['array', 'uniqueItems'], component: hasUniqueItems, }); + + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['plone.eventbasic', 'start'], + component: startEventDateRangeValidator, + }); + + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['plone.eventbasic', 'end'], + component: endEventDateRangeValidator, + }); }; export { registerValidators }; diff --git a/packages/volto/src/helpers/Blocks/schema.ts b/packages/volto/src/helpers/Blocks/schema.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.jsx b/packages/volto/src/helpers/FormValidation/FormValidation.jsx index 356c12a152..593d5ed6bb 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.jsx +++ b/packages/volto/src/helpers/FormValidation/FormValidation.jsx @@ -111,8 +111,9 @@ const validateFieldsPerFieldset = ( fieldData === undefined || fieldData === null ? null : widgetCriterion.component({ - field, value: fieldData, + field, + formData, formatMessage, }); return errorMessage; @@ -135,22 +136,58 @@ const validateFieldsPerFieldset = ( fieldData, ); - // Validation per field type or field widget - const fieldWidgetType = field.widget || field.type || 'string'; + // Validation per field type + const fieldType = field.type || 'string'; // test each criterion eg. maximum, isEmail, isUrl, etc - const fieldValidationCriteria = config.getComponents({ + const fieldTypeValidationCriteria = config.getComponents({ name: 'fieldValidator', - dependencies: [fieldWidgetType], + dependencies: [fieldType], }); const fieldErrors = checkFieldErrors( - fieldValidationCriteria, + fieldTypeValidationCriteria, field, fieldData, ); - // Validation per field type or field widget - const hasSpecificValidator = field.validator; + // Validation per field widget + const fieldWidget = + field.widgetOptions?.frontendOptions?.widget || field.widget || ''; + + let widgetErrors = []; + if (fieldWidget) { + const fieldWidgetValidationCriteria = config.getComponents({ + name: 'fieldValidator', + dependencies: [fieldWidget], + }); + + widgetErrors = checkFieldErrors( + fieldWidgetValidationCriteria, + field, + fieldData, + ); + } + + // Validation per specific behavior and fieldId + const perBehaviorSpecificValidator = field.behavior; + let perBehaviorFieldErrors = []; + // test each criterion eg. maximum, isEmail, isUrl, etc + if (perBehaviorSpecificValidator) { + const specificFieldValidationCriteria = config.getComponents({ + name: 'fieldValidator', + dependencies: [perBehaviorSpecificValidator, fieldId], + }); + + perBehaviorFieldErrors = checkFieldErrors( + specificFieldValidationCriteria, + field, + fieldData, + ); + } + + // Validation per specific validator + const hasSpecificValidator = + field.widgetOptions?.frontendOptions?.validator || field.validator; let specificFieldErrors = []; // test each criterion eg. maximum, isEmail, isUrl, etc if (hasSpecificValidator) { @@ -169,6 +206,8 @@ const validateFieldsPerFieldset = ( const mergedErrors = [ ...defaultFieldErrors, ...fieldErrors, + ...widgetErrors, + ...perBehaviorFieldErrors, ...specificFieldErrors, ]; @@ -177,6 +216,8 @@ const validateFieldsPerFieldset = ( ...(errors[fieldId] || []), ...defaultFieldErrors, ...fieldErrors, + ...widgetErrors, + ...perBehaviorFieldErrors, ...specificFieldErrors, ]; } diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.test.js b/packages/volto/src/helpers/FormValidation/FormValidation.test.js index 466aa29363..8e19a87c81 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.test.js +++ b/packages/volto/src/helpers/FormValidation/FormValidation.test.js @@ -167,6 +167,109 @@ describe('FormValidation', () => { ).toEqual({}); }); + it('default - widget validator from block - Fails', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Default field', + description: '', + widget: 'isURL', + }, + }, + required: [], + }; + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['isURL'], + component: urlValidator, + }); + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 'asd', + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.isValidURL.defaultMessage], + }); + }); + + it('default - type and widget validator from block - Fails', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Default field', + description: '', + type: 'customfieldtype', + widget: 'isURL', + }, + }, + required: [], + }; + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['customfieldtype', 'willFail'], + component: () => 'Fails', + }); + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['isURL'], + component: urlValidator, + }); + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 'asd', + }, + formatMessage, + }), + ).toEqual({ + customField: ['Fails', messages.isValidURL.defaultMessage], + }); + }); + + it('default - widget validator from content type set - Fails', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Default field', + description: '', + widgetOptions: { + frontendOptions: { + widget: 'isURL', + }, + }, + }, + }, + required: [], + }; + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['isURL'], + component: urlValidator, + }); + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 'asd', + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.isValidURL.defaultMessage], + }); + }); + it('default - min lenght', () => { let newSchema = { properties: { @@ -550,6 +653,70 @@ describe('FormValidation', () => { }), ).toEqual({}); }); + + it('default - specific validator from content type set - Succeeds', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Default field', + description: '', + widgetOptions: { + frontendOptions: { + validator: 'isURL', + }, + }, + }, + }, + required: [], + }; + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['isURL'], + component: urlValidator, + }); + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 'https://plone.org/', + }, + formatMessage, + }), + ).toEqual({}); + }); + + it('default - per behavior specific - Fails', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + behavior: 'plone.event', + title: 'Default field', + description: '', + }, + }, + required: [], + }; + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['plone.event', 'customField'], + component: urlValidator, + }); + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 'asd', + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.isValidURL.defaultMessage], + }); + }); }); // describe('validateBlockDataFields', () => { diff --git a/packages/volto/src/helpers/FormValidation/validators.ts b/packages/volto/src/helpers/FormValidation/validators.ts index 9c24be3220..f6e4ed3057 100644 --- a/packages/volto/src/helpers/FormValidation/validators.ts +++ b/packages/volto/src/helpers/FormValidation/validators.ts @@ -10,13 +10,8 @@ type MinMaxValidator = { type Validator = { value: string; - field: { - minLength: number; - maxLength: number; - minimum: number; - maximum: number; - uniqueItems: Boolean; - }; + field: Record; + formData?: any; formatMessage: Function; }; @@ -129,3 +124,25 @@ export const hasUniqueItems = ({ value, field, formatMessage }: Validator) => { [...new Set(value)].length === value.length; return !isValid ? formatMessage(messages.uniqueItems) : null; }; + +export const startEventDateRangeValidator = ({ + value, + field, + formData, + formatMessage, +}: Validator) => { + const isValid = + value && formData.end && new Date(value) < new Date(formData.end); + return !isValid ? formatMessage(messages.startEventRange) : null; +}; + +export const endEventDateRangeValidator = ({ + value, + field, + formData, + formatMessage, +}: Validator) => { + const isValid = + value && formData.start && new Date(value) < new Date(formData.start); + return !isValid ? formatMessage(messages.endEventRange) : null; +}; diff --git a/packages/volto/src/helpers/MessageLabels/MessageLabels.js b/packages/volto/src/helpers/MessageLabels/MessageLabels.js index a287694282..bcf614ce15 100644 --- a/packages/volto/src/helpers/MessageLabels/MessageLabels.js +++ b/packages/volto/src/helpers/MessageLabels/MessageLabels.js @@ -379,4 +379,12 @@ export const messages = defineMessages({ id: 'blocksFieldsErrorTitle', defaultMessage: 'Error in the block field {errorField}.', }, + startEventRange: { + id: 'Start event date happens later than the end event date', + defaultMessage: 'Start event date happens later than the end event date', + }, + endEventRange: { + id: 'End event date happens before than the start event date', + defaultMessage: 'End event date happens before than the start event date', + }, }); From 30db8cdc63bc796c47af9fc9a0d9baf28b2f7a69 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Mon, 15 Jul 2024 17:54:57 +0200 Subject: [PATCH 19/51] Block-fieldId validators --- docs/source/configuration/validation.md | 14 +++++++++ .../helpers/FormValidation/FormValidation.jsx | 19 ++++++++++++ .../FormValidation/FormValidation.test.js | 31 +++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md index 754359a7c8..bf6b7c54a5 100644 --- a/docs/source/configuration/validation.md +++ b/docs/source/configuration/validation.md @@ -111,6 +111,20 @@ config.registerComponent({ The first dependency should be the name of the behavior, and second the name (`id`) of the field. This type of validator only applies to content type validators. +### Per block type and field name validator + +These validators are applied depending on the block type in combination of the name of the field in the block settings JSON schema. + +```ts +config.registerComponent({ + name: 'fieldValidator', + dependencies: ['slider', 'url'], + component: urlValidator, +}); +``` +The first dependency should be the `id` of the block, and second the `id` of the field. +This type of validator only applies to blocks. + ### Specific validator using the `validator` key in the field A final type of validator are applied to the field if the `validator` key is present in the JSON schema definition of the form field. diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.jsx b/packages/volto/src/helpers/FormValidation/FormValidation.jsx index 593d5ed6bb..3039f83114 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.jsx +++ b/packages/volto/src/helpers/FormValidation/FormValidation.jsx @@ -203,12 +203,30 @@ const validateFieldsPerFieldset = ( ); } + // Validation per block type validator + const hasBlockType = formData['@type']; + let blockTypeFieldErrors = []; + // test each criterion eg. maximum, isEmail, isUrl, etc + if (hasBlockType) { + const blockTypeFieldValidationCriteria = config.getComponents({ + name: 'fieldValidator', + dependencies: [hasBlockType, fieldId], + }); + + blockTypeFieldErrors = checkFieldErrors( + blockTypeFieldValidationCriteria, + field, + fieldData, + ); + } + const mergedErrors = [ ...defaultFieldErrors, ...fieldErrors, ...widgetErrors, ...perBehaviorFieldErrors, ...specificFieldErrors, + ...blockTypeFieldErrors, ]; if (mergedErrors.length > 0) { @@ -219,6 +237,7 @@ const validateFieldsPerFieldset = ( ...widgetErrors, ...perBehaviorFieldErrors, ...specificFieldErrors, + ...blockTypeFieldErrors, ]; } }); diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.test.js b/packages/volto/src/helpers/FormValidation/FormValidation.test.js index 8e19a87c81..057d6e0399 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.test.js +++ b/packages/volto/src/helpers/FormValidation/FormValidation.test.js @@ -717,6 +717,37 @@ describe('FormValidation', () => { customField: [messages.isValidURL.defaultMessage], }); }); + + it('block - per block type and fieldID specific - Fails', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Default field', + description: '', + }, + }, + required: [], + }; + config.registerComponent({ + name: 'fieldValidator', + dependencies: ['slider', 'customField'], + component: urlValidator, + }); + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + '@type': 'slider', + username: 'test username', + customField: 'asd', + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.isValidURL.defaultMessage], + }); + }); }); // describe('validateBlockDataFields', () => { From bef6a77c8fda51d3eb7d561b63ebf789e52e5ff9 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Tue, 16 Jul 2024 09:24:13 +0200 Subject: [PATCH 20/51] Changelog --- docs/source/upgrade-guide/index.md | 7 +++++++ packages/volto/news/6161.breaking | 5 +++++ packages/volto/news/6161.feature | 1 - 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 packages/volto/news/6161.breaking delete mode 100644 packages/volto/news/6161.feature diff --git a/docs/source/upgrade-guide/index.md b/docs/source/upgrade-guide/index.md index e71c1357b4..170156d438 100644 --- a/docs/source/upgrade-guide/index.md +++ b/docs/source/upgrade-guide/index.md @@ -376,6 +376,13 @@ It is unlikely that your code uses it, unless you heavily customized the Jest te This was not used by the core since some time ago, and nowadays is more suitable for being an add-on and not being included in core. If you still use it, bring it back as your main add-on dependency, bring back the `SocialSharing` component from Volto 17 as a custom component in your add-on code. +### Refactor of `FormValidation` module + +The `packages/volto/src/helpers/FormValidation/FormValidation.jsx` module has been heavily refactored. +Some helper functions have been moved to `packages/volto/src/helpers/FormValidation/validators.ts`, however, none of that functions were exported in the first place, so no imports will be broken. +In case that you've shadowed the `packages/volto/src/helpers/FormValidation/FormValidation.jsx` module, you should revisit it and update it with the latest refactor. +If you added more validators manually in that shadow, please refer to the documentation to add the validators in the new way: {doc}`../configuration/validation`. + (volto-upgrade-guide-17.x.x)= ## Upgrading to Volto 17.x.x diff --git a/packages/volto/news/6161.breaking b/packages/volto/news/6161.breaking new file mode 100644 index 0000000000..17a66bab4e --- /dev/null +++ b/packages/volto/news/6161.breaking @@ -0,0 +1,5 @@ +Add foundations for extensible validation in forms @sneridagh + +Breaking: +`packages/volto/src/helpers/FormValidation/FormValidation.jsx` has been heavily refactored. +If you have shadowed this component in your project/add-on, please review and update your shadow. diff --git a/packages/volto/news/6161.feature b/packages/volto/news/6161.feature deleted file mode 100644 index 721ce5e616..0000000000 --- a/packages/volto/news/6161.feature +++ /dev/null @@ -1 +0,0 @@ -Add foundations for extensible validation in forms @sneridagh From 41da3fb7745988b279a74dd1f993a85b11039b2e Mon Sep 17 00:00:00 2001 From: David Ichim Date: Tue, 16 Jul 2024 10:24:33 +0300 Subject: [PATCH 21/51] Typos correction for validation.md --- docs/source/configuration/validation.md | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md index bf6b7c54a5..ca5caac8ee 100644 --- a/docs/source/configuration/validation.md +++ b/docs/source/configuration/validation.md @@ -9,7 +9,7 @@ myst: # Client side form field validation -Volto provides a mechanism for providing form field validation in an extensible way. +Volto provides a mechanism for delivering form field validation in an extensible way. This extensibility is based on the Volto component registry. ## Registering a validator @@ -32,7 +32,7 @@ config.registerComponent({ It takes two `dependencies`. The first element should be the `default` identifier, and the second you can set it up to identify the validator. -In the case of the example, this other dependency is `minLenght`. +In the case of the example, this other dependency is `minLength`. It can be any string. ### Per field `type` validators @@ -80,7 +80,7 @@ config.registerComponent({ It takes two `dependencies` since we can potentially have several validators for the same `widget`. The first element should be the `widget`, and the second you can set it up to identify the validator. -You should specify the `widget` in the JSON schema of the block (or as an additional data in the content type definition). +You should specify the `widget` in the JSON schema of the block (or as additional data in the content type definition). The next example is for the use case of a block JSON schema: ```ts @@ -99,7 +99,7 @@ let blockSchema = { ### Per behavior and field name validator -These validators are applied depending on the behavior (usually coming from a content type definition) in combination of the name of the field. +These validators are applied depending on the behavior (usually coming from a content type definition) in combination with the name of the field. ```ts config.registerComponent({ @@ -108,12 +108,12 @@ config.registerComponent({ component: urlValidator, }); ``` -The first dependency should be the name of the behavior, and second the name (`id`) of the field. -This type of validator only applies to content type validators. +The first dependency should be the name of the behavior, and the second the name (`id`) of the field. +This type of validator only applies to content-type validators. ### Per block type and field name validator -These validators are applied depending on the block type in combination of the name of the field in the block settings JSON schema. +These validators are applied depending on the block type in combination with the name of the field in the block settings JSON schema. ```ts config.registerComponent({ @@ -122,12 +122,12 @@ config.registerComponent({ component: urlValidator, }); ``` -The first dependency should be the `id` of the block, and second the `id` of the field. +The first dependency should be the `id` of the block, and the second the `id` of the field. This type of validator only applies to blocks. ### Specific validator using the `validator` key in the field -A final type of validator are applied to the field if the `validator` key is present in the JSON schema definition of the form field. +A final type of validator is applied to the field if the `validator` key is present in the JSON schema definition of the form field. ```ts config.registerComponent({ @@ -137,8 +137,8 @@ config.registerComponent({ }); ``` -The dependencies takes one single name, in this case, the name of the validator. -You should specify the validator in the JSON schema of the block (or as an additional data in the content type definition). +The dependencies take one single name, in this case, the name of the validator. +You should specify the validator in the JSON schema of the block (or as additional data in the content type definition). ```ts let blockSchema = { @@ -155,11 +155,11 @@ let blockSchema = { ``` It does not need to be tied to any field `type` or `widget` definition. -It runs in addition to all the above, so it complements the normal validators, if any apply. +It runs in addition to all the above, so it complements the normal validators if any apply. ## Volto's default validators -Volto provide a set of validators by default, you can find them in this module: `packages/volto/src/config/validators.ts` +Volto provides a set of validators by default, you can find them in this module: `packages/volto/src/config/validators.ts` ### How to override them @@ -182,7 +182,7 @@ type Validator = { }; ``` -This is an example of a `isNumber` validator: +This is an example of an `isNumber` validator: ```ts export const isNumber = ({ value, formatMessage }: Validator) => { From 2a32edc8329eab9da37cd67a51d7f1e47e947829 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Tue, 16 Jul 2024 10:11:57 +0200 Subject: [PATCH 22/51] Complete default explanation --- docs/source/configuration/validation.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md index ca5caac8ee..8ffd779cb3 100644 --- a/docs/source/configuration/validation.md +++ b/docs/source/configuration/validation.md @@ -30,8 +30,8 @@ config.registerComponent({ }); ``` -It takes two `dependencies`. -The first element should be the `default` identifier, and the second you can set it up to identify the validator. +It takes two `dependencies` since we can have several validators for the `default` use case. +The first element should be the fixed `default` identifier, and the second you can set it up to identify the validator itself. In the case of the example, this other dependency is `minLength`. It can be any string. @@ -48,7 +48,7 @@ config.registerComponent({ ``` It takes two `dependencies` since we can potentially have several validators for the same `type`. -The first element should be the `type`, and the second you can set it up to identify the validator. +The first element should be the `type`, and the second you can set it up to identify the validator itself. You should specify the `type` in the JSON schema of the block (in a content type, this is included in the default serialization of the field). The next example is for the use case of a block JSON schema: From 34490f9b383c6fda8956b6711b45e16ed5b1eae2 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Tue, 16 Jul 2024 12:24:49 +0200 Subject: [PATCH 23/51] Add more down to earth examples --- docs/source/configuration/validation.md | 39 +++++++++++++++---------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md index 8ffd779cb3..5e7a0e74b4 100644 --- a/docs/source/configuration/validation.md +++ b/docs/source/configuration/validation.md @@ -30,8 +30,7 @@ config.registerComponent({ }); ``` -It takes two `dependencies` since we can have several validators for the `default` use case. -The first element should be the fixed `default` identifier, and the second you can set it up to identify the validator itself. +It takes two `dependencies` being the first element a fixed `default` identifier, and the second you can set it up to identify the validator itself. In the case of the example, this other dependency is `minLength`. It can be any string. @@ -48,18 +47,20 @@ config.registerComponent({ ``` It takes two `dependencies` since we can potentially have several validators for the same `type`. -The first element should be the `type`, and the second you can set it up to identify the validator itself. -You should specify the `type` in the JSON schema of the block (in a content type, this is included in the default serialization of the field). -The next example is for the use case of a block JSON schema: +The first element should be the field `type`, and the second you should set it up to identify the validator itself. +You should specify the `type` in the JSON schema of the block (in a content type, it is included in the default serialization of the field). +If a field does not specify type, it assumes a `string` type as validator. +The next example is for the use case of JSON schema defined in a block: ```ts let blockSchema = { + // ... fieldset definition in here properties: { ...schema.properties, customField: { title: 'My custom field', description: '', - type: 'integer' + type: 'integer', }, }, required: [], @@ -73,24 +74,25 @@ These validators are applied depending on the specified `widget` of the field. ```ts config.registerComponent({ name: 'fieldValidator', - dependencies: ['myCustomURLWidget', 'maximum'], - component: maximumValidator, + dependencies: ['phoneNumber', 'isValidPhone'], + component: phoneValidator, }); ``` It takes two `dependencies` since we can potentially have several validators for the same `widget`. -The first element should be the `widget`, and the second you can set it up to identify the validator. +The first element should be the name of the `widget`, and the second you can set it up to identify the validator. You should specify the `widget` in the JSON schema of the block (or as additional data in the content type definition). The next example is for the use case of a block JSON schema: ```ts let blockSchema = { + // ... fieldset definition in here properties: { ...schema.properties, - customField: { - title: 'My custom field', + phone: { + title: 'Phone number', description: '', - widget: 'myCustomURLWidget', + widget: 'phoneNumber', }, }, required: [], @@ -104,11 +106,13 @@ These validators are applied depending on the behavior (usually coming from a co ```ts config.registerComponent({ name: 'fieldValidator', - dependencies: ['plone.eventbasic', 'start'], - component: urlValidator, + dependencies: ['plone.eventbasic', 'start', 'startValidator'], + component: startEventDateRangeValidator, }); ``` + The first dependency should be the name of the behavior, and the second the name (`id`) of the field. +It can get a third dependency in case you want to specify several validators for the same behavior - field id combination. This type of validator only applies to content-type validators. ### Per block type and field name validator @@ -118,11 +122,13 @@ These validators are applied depending on the block type in combination with the ```ts config.registerComponent({ name: 'fieldValidator', - dependencies: ['slider', 'url'], + dependencies: ['slider', 'url', 'isURL'], component: urlValidator, }); ``` + The first dependency should be the `id` of the block, and the second the `id` of the field. +It can get a third dependency in case you want to specify several validators for the same block type - field id combination. This type of validator only applies to blocks. ### Specific validator using the `validator` key in the field @@ -142,6 +148,7 @@ You should specify the validator in the JSON schema of the block (or as addition ```ts let blockSchema = { + // ... fieldset definition in here properties: { ...schema.properties, customField: { @@ -155,7 +162,7 @@ let blockSchema = { ``` It does not need to be tied to any field `type` or `widget` definition. -It runs in addition to all the above, so it complements the normal validators if any apply. +It runs in addition to all the above, so it complements the other validators if any apply. ## Volto's default validators From 315285419271126e38c579fb68d65c183f593cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20de=20Alba?= Date: Thu, 18 Jul 2024 09:45:45 +0200 Subject: [PATCH 24/51] Apply suggestions from code review Co-authored-by: David Glick --- docs/source/upgrade-guide/index.md | 2 +- packages/volto/src/helpers/MessageLabels/MessageLabels.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/upgrade-guide/index.md b/docs/source/upgrade-guide/index.md index 170156d438..243f382161 100644 --- a/docs/source/upgrade-guide/index.md +++ b/docs/source/upgrade-guide/index.md @@ -379,7 +379,7 @@ If you still use it, bring it back as your main add-on dependency, bring back th ### Refactor of `FormValidation` module The `packages/volto/src/helpers/FormValidation/FormValidation.jsx` module has been heavily refactored. -Some helper functions have been moved to `packages/volto/src/helpers/FormValidation/validators.ts`, however, none of that functions were exported in the first place, so no imports will be broken. +Some helper functions have been moved to `packages/volto/src/helpers/FormValidation/validators.ts`, however, none of those functions were exported in the first place, so no imports will be broken. In case that you've shadowed the `packages/volto/src/helpers/FormValidation/FormValidation.jsx` module, you should revisit it and update it with the latest refactor. If you added more validators manually in that shadow, please refer to the documentation to add the validators in the new way: {doc}`../configuration/validation`. diff --git a/packages/volto/src/helpers/MessageLabels/MessageLabels.js b/packages/volto/src/helpers/MessageLabels/MessageLabels.js index bcf614ce15..043129be1c 100644 --- a/packages/volto/src/helpers/MessageLabels/MessageLabels.js +++ b/packages/volto/src/helpers/MessageLabels/MessageLabels.js @@ -381,10 +381,10 @@ export const messages = defineMessages({ }, startEventRange: { id: 'Start event date happens later than the end event date', - defaultMessage: 'Start event date happens later than the end event date', + defaultMessage: 'Event start date happens later than the event end date', }, endEventRange: { id: 'End event date happens before than the start event date', - defaultMessage: 'End event date happens before than the start event date', + defaultMessage: 'Event end date happens before the event start date', }, }); From b89d0ce2112aeb157159594410b08aa16d456a61 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Mon, 22 Jul 2024 16:32:19 +0200 Subject: [PATCH 25/51] Refactor and re-document for clarity and simplicity --- docs/source/configuration/validation.md | 243 +++++++++++------- packages/registry/src/index.ts | 76 ++++++ packages/registry/src/registry.test.tsx | 144 +++++++++++ packages/types/src/config/Utilities.d.ts | 4 + packages/types/src/config/index.d.ts | 11 +- packages/volto/src/config/index.js | 4 +- packages/volto/src/config/validation.ts | 131 ++++++---- .../helpers/FormValidation/FormValidation.jsx | 100 +++---- .../FormValidation/FormValidation.test.js | 141 ++++++---- packages/volto/test-setup-config.jsx | 4 +- 10 files changed, 600 insertions(+), 258 deletions(-) create mode 100644 packages/types/src/config/Utilities.d.ts diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md index 5e7a0e74b4..a03a34f72d 100644 --- a/docs/source/configuration/validation.md +++ b/docs/source/configuration/validation.md @@ -10,46 +10,101 @@ myst: # Client side form field validation Volto provides a mechanism for delivering form field validation in an extensible way. -This extensibility is based on the Volto component registry. +This extensibility is based on the Volto registry. +It applies to content types, custom programatically generated forms and blocks schema settings. +All of them are serialized using JSON schema standard and then Volto generates the resultant form out of it. ## Registering a validator -You can register a validator using the component registry API from your add-on configuration. -All validators are registered under the name `fieldValidator`. -The validators are registered using the `dependencies` array of the `registerComponent` API to differentiate the kind of validator to be registered. +You can register a validator using the registry API from your add-on configuration. +The validators are registered using the `registerUtility` API method. -### `default` validators +### Registering and declaring a simple validator -These validators are registered and applied to all fields. +The most common thing is to have a field that you want to validate with a specific validator. +Volto provide some default validators, see at the end of this chapter for more information. + +#### For Volto custom forms and block schema forms + +When you define them programatically, in your core, using JSON schema, you can register a custom validator using the `format` property. +This is the case of creating the schema for a block: ```ts -config.registerComponent({ - name: 'fieldValidator', - dependencies: ['default', 'minLength'], - component: minLengthValidator, -}); +let blockSchema = { + // ... fieldset definition in here + properties: { + ...schema.properties, + customField: { + title: 'My custom URL field', + description: '', + format: 'url' + }, + }, + required: [], +}; ``` -It takes two `dependencies` being the first element a fixed `default` identifier, and the second you can set it up to identify the validator itself. -In the case of the example, this other dependency is `minLength`. -It can be any string. +`url` named validator should be registered by this name as a Volto validator utility: -### Per field `type` validators +```ts +config.registerUtility({ + type: 'validator', + name: 'url', + method: urlValidator, +}) +``` -These validators are applied depending on the specified `type` of the field. +In this case, the `urlValidator` method validator will be applied for the block field `customField`. -```ts -config.registerComponent({ - name: 'fieldValidator', - dependencies: ['integer', 'maximum'], - component: maximumValidator, -}); +#### For content types + +Content types also can specify the `format` using the schema hints in the backend in the `frontendOptions`: + +```python +from plone.supermodel import model +from zope import schema + +class IMyContent(model.Schema): + directives.widget( + "customField", + frontendOptions={ + "format": "url", + }, + ) + customField = schema.TextLine( + title="Custom URL field", + required=False, + ) + # Rest of your content type definition ``` -It takes two `dependencies` since we can potentially have several validators for the same `type`. -The first element should be the field `type`, and the second you should set it up to identify the validator itself. -You should specify the `type` in the JSON schema of the block (in a content type, it is included in the default serialization of the field). -If a field does not specify type, it assumes a `string` type as validator. +For the record, the resultant `plone.restapi` response will be something like the following, a bit different than in blocks JSON schema. +But the validation engine will take care too: + +```json +{ + "properties": { + "customField": { + "title": "Custom URL field", + "widgetOptions": { + "frontendOptions": { + "format": "url" + } + } + } + } +} +``` + +and the `urlValidator` method validator will be applied for the content type field `customField`. + +### Advanced scenarios + +In case you need more granularity and you don't have access to modify the existing implementation of the JSON schema definitions for existing content types, blocks or forms (might be in third party add-ons), you can use the following advanced validator registrations, using `field`, `widget`, `behaviorName` or `blockType` validator registrations. + +#### Per field `type` validators + +These validators are applied depending on the specified `type` of the field in the JSON schema from content types, forms or blocks. The next example is for the use case of JSON schema defined in a block: ```ts @@ -61,36 +116,38 @@ let blockSchema = { title: 'My custom field', description: '', type: 'integer', + maximum: 30 }, }, required: [], }; ``` -### Per field `widget` validators - -These validators are applied depending on the specified `widget` of the field. - ```ts -config.registerComponent({ - name: 'fieldValidator', - dependencies: ['phoneNumber', 'isValidPhone'], - component: phoneValidator, -}); +config.registerUtility({ + type: 'validator', + name: 'maximum', + dependencies: { + fieldType: 'integer', + }, + method: maximumValidator, +}) ``` -It takes two `dependencies` since we can potentially have several validators for the same `widget`. -The first element should be the name of the `widget`, and the second you can set it up to identify the validator. -You should specify the `widget` in the JSON schema of the block (or as additional data in the content type definition). -The next example is for the use case of a block JSON schema: +You should specify the `type` in the JSON schema of the block (in a content type, it is included in the default serialization of the field). +If a field does not specify type, it assumes a `string` type as validator. + +#### Per field `widget` validators + +These validators are applied depending on the specified `widget` of the field. ```ts let blockSchema = { // ... fieldset definition in here properties: { ...schema.properties, - phone: { - title: 'Phone number', + customField: { + title: 'My custom field', description: '', widget: 'phoneNumber', }, @@ -99,76 +156,85 @@ let blockSchema = { }; ``` -### Per behavior and field name validator - -These validators are applied depending on the behavior (usually coming from a content type definition) in combination with the name of the field. - ```ts -config.registerComponent({ - name: 'fieldValidator', - dependencies: ['plone.eventbasic', 'start', 'startValidator'], - component: startEventDateRangeValidator, -}); +config.registerUtility({ + type: 'validator', + name: 'phoneNumber', + dependencies: { + widgetName: 'phoneNumber', + }, + method: phoneValidator, +}) ``` -The first dependency should be the name of the behavior, and the second the name (`id`) of the field. -It can get a third dependency in case you want to specify several validators for the same behavior - field id combination. -This type of validator only applies to content-type validators. -### Per block type and field name validator +You should specify the `widget` in the JSON schema of the block, or as additional data in the content type definition. +Content types also can specify the `widget` to be used using the schema hints in the backend in the `frontendOptions`: -These validators are applied depending on the block type in combination with the name of the field in the block settings JSON schema. -```ts -config.registerComponent({ - name: 'fieldValidator', - dependencies: ['slider', 'url', 'isURL'], - component: urlValidator, -}); +```python +from plone.supermodel import model +from zope import schema + +class IMyContent(model.Schema): + directives.widget( + "customField", + frontendOptions={ + "widget": "url", + }, + ) + customField = schema.TextLine( + title="Custom URL field", + required=False, + ) + # Rest of your content type definition ``` -The first dependency should be the `id` of the block, and the second the `id` of the field. -It can get a third dependency in case you want to specify several validators for the same block type - field id combination. -This type of validator only applies to blocks. +the validation engine will take care too, and the `urlValidator` method validator will be applied for the content type field `customField`. -### Specific validator using the `validator` key in the field +#### Per behavior and field name validator -A final type of validator is applied to the field if the `validator` key is present in the JSON schema definition of the form field. +These validators are applied depending on the behavior (usually coming from a content type definition) in combination with the name of the field. ```ts -config.registerComponent({ - name: 'fieldValidator', - dependencies: ['isURL'], - component: urlValidator, -}); +config.registerUtility({ + type: 'validator', + name: 'dateRange', + dependencies: { + behaviorName: 'plone.eventbasic', + fieldName: 'start' + }, + method: startEventDateRangeValidator, +}) ``` -The dependencies take one single name, in this case, the name of the validator. -You should specify the validator in the JSON schema of the block (or as additional data in the content type definition). +It takes the `behaviorName` and the `fieldName` as dependencies. +This type of validator only applies to content type validators. + +#### Per block type and field name validator + +These validators are applied depending on the block type in combination with the name of the field in the block settings JSON schema. ```ts -let blockSchema = { - // ... fieldset definition in here - properties: { - ...schema.properties, - customField: { - title: 'Default field', - description: '', - validator: 'isURL', - }, +config.registerUtility({ + type: 'validator', + name: 'url', + dependencies: { + blockType: 'slider', + fieldName: 'url' }, - required: [], -}; + method: urlValidator, +}) ``` -It does not need to be tied to any field `type` or `widget` definition. -It runs in addition to all the above, so it complements the other validators if any apply. +It takes the `blockType` and the `fieldName` as dependencies. +This type of validator only applies to blocks. ## Volto's default validators Volto provides a set of validators by default, you can find them in this module: `packages/volto/src/config/validators.ts` -### How to override them +### How to override a validator You can override them in your add-on as any other component defined in the registry, by redefining them using the same `dependencies`, and providing your own. @@ -200,5 +266,8 @@ export const isNumber = ({ value, formatMessage }: Validator) => { }; ``` +## Invariants + Using the `formData` you can perform validation checks using other field data as source. This is interesting in the case that two fields are related, like `start` and `end` dates. +You can create invariant type of validators thanks to this. diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index f94a8b24fe..e6d5c9b8ad 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -10,6 +10,7 @@ import type { SlotComponent, SlotPredicate, SlotsConfig, + UtilitiesConfig, ViewsConfig, WidgetsConfig, } from '@plone/types'; @@ -23,6 +24,7 @@ export type ConfigData = { addonRoutes: AddonRoutesConfig; slots: SlotsConfig; components: ComponentsConfig; + utilities: UtilitiesConfig; experimental: ExperimentalConfig; }; @@ -30,6 +32,10 @@ type GetComponentResult = { component: React.ComponentType; }; +type GetUtilityResult = { + method: (...args: any[]) => any; +}; + export type ConfigType = InstanceType; class Config { @@ -128,6 +134,14 @@ class Config { this._data.components = components; } + get utilities() { + return this._data.utilities; + } + + set utilities(utilities) { + this._data.utilities = utilities; + } + getComponent( options: { name: string; dependencies?: string[] | string } | string, ): GetComponentResult { @@ -439,6 +453,68 @@ class Config { const result = currentSlotComponents.slice(); currentSlot.data[name] = result.splice(position, 1); } + + registerUtility(options: { + name: string; + type: string; + dependencies?: Record; + method: (args: any) => any; + }) { + const { name, type, method, dependencies = {} } = options; + let depsString: string = ''; + if (!method) { + throw new Error('No method provided'); + } else { + depsString = Object.keys(dependencies) + .map((key) => `${key}:${dependencies[key]}`) + .join('+'); + } + const utilityName = `${depsString ? `|${depsString}` : ''}${name}`; + + let utilityType = this._data.utilities[type]; + if (!utilityType) { + this._data.utilities[type] = {}; + utilityType = this._data.utilities[type]; + } + utilityType[utilityName] = { method }; + } + + getUtility(options: { + name: string; + type: string; + dependencies?: Record; + }): GetUtilityResult { + const { name, type, dependencies = {} } = options; + let depsString: string = ''; + depsString = Object.keys(dependencies) + .map((key) => `${key}:${dependencies[key]}`) + .join('+'); + + const utilityName = `${depsString ? `|${depsString}` : ''}${name}`; + + return this._data.utilities[type][utilityName] || {}; + } + + getUtilities(options: { + type: string; + dependencies?: Record; + }): Array { + const { type, dependencies = {} } = options; + let depsString: string = ''; + depsString = Object.keys(dependencies) + .map((key) => `${key}:${dependencies[key]}`) + .join('+'); + + const utilityName = `${depsString ? `|${depsString}` : ''}`; + const utilitiesKeys = Object.keys(this._data.utilities[type]).filter( + (key) => key.startsWith(utilityName), + ); + const utilities = utilitiesKeys.map( + (key) => this._data.utilities[type][key], + ); + + return utilities; + } } const instance = new Config(); diff --git a/packages/registry/src/registry.test.tsx b/packages/registry/src/registry.test.tsx index 20af23c27d..3ac2fb6295 100644 --- a/packages/registry/src/registry.test.tsx +++ b/packages/registry/src/registry.test.tsx @@ -8,6 +8,7 @@ beforeEach(() => { 'Teaser|News Item': { component: 'This is the News Item Teaser component' }, }); config.set('slots', {}); + config.set('utilities', {}); }); describe('Component registry', () => { @@ -953,3 +954,146 @@ describe('Slots registry', () => { }); }); }); + +describe('Utilities registry', () => { + afterEach(() => { + config.set('utilities', {}); + }); + + it('registers a simple utility', () => { + config.registerUtility({ + name: 'url', + type: 'validator', + method: () => 'this is a simple validator utility', + }); + + expect( + config.getUtility({ name: 'url', type: 'validator' }).method(), + ).toEqual('this is a simple validator utility'); + }); + + it('registers an utility with dependencies', () => { + config.registerUtility({ + name: 'email', + type: 'validator', + dependencies: { fieldType: 'email' }, + method: () => 'this is a validator utility with dependencies', + }); + + expect( + config + .getUtility({ + name: 'email', + dependencies: { fieldType: 'email' }, + type: 'validator', + }) + .method(), + ).toEqual('this is a validator utility with dependencies'); + }); + + it('registers both an utility with dependencies and with no dependencies', () => { + config.registerUtility({ + name: 'email', + type: 'validator', + method: () => 'this is a simple validator utility', + }); + + config.registerUtility({ + name: 'email', + type: 'validator', + dependencies: { fieldType: 'email' }, + method: () => 'this is a validator utility with dependencies', + }); + + expect( + config.getUtility({ name: 'email', type: 'validator' }).method(), + ).toEqual('this is a simple validator utility'); + + expect( + config + .getUtility({ + name: 'email', + dependencies: { fieldType: 'email' }, + type: 'validator', + }) + .method(), + ).toEqual('this is a validator utility with dependencies'); + }); + + it('registers an utility with dependencies and another with different dependencies', () => { + config.registerUtility({ + name: 'email', + type: 'validator', + dependencies: { fieldType: 'email' }, + method: () => 'this is a validator utility with dependencies for email', + }); + + config.registerUtility({ + name: 'email', + type: 'validator', + dependencies: { fieldType: 'string' }, + method: () => 'this is a validator utility with dependencies for string', + }); + + expect( + config + .getUtility({ + name: 'email', + dependencies: { fieldType: 'string' }, + type: 'validator', + }) + .method(), + ).toEqual('this is a validator utility with dependencies for string'); + + expect( + config + .getUtility({ + name: 'email', + dependencies: { fieldType: 'email' }, + type: 'validator', + }) + .method(), + ).toEqual('this is a validator utility with dependencies for email'); + }); + + it('getUtilities - registers two utilities with the same dependencies and different names', () => { + config.registerUtility({ + name: 'minLength', + type: 'validator', + dependencies: { fieldType: 'string' }, + method: () => 'this is a validator for minLength', + }); + + config.registerUtility({ + name: 'maxLength', + type: 'validator', + dependencies: { fieldType: 'string' }, + method: () => 'this is a validator for maxLength', + }); + + expect( + config.getUtilities({ + dependencies: { fieldType: 'string' }, + type: 'validator', + }).length, + ).toEqual(2); + + expect( + config + .getUtilities({ + dependencies: { fieldType: 'string' }, + type: 'validator', + })[0] + .method(), + ).toEqual('this is a validator for minLength'); + + expect( + config + .getUtilities({ + dependencies: { fieldType: 'string' }, + type: 'validator', + })[1] + .method(), + ).toEqual('this is a validator for maxLength'); + }); +}); diff --git a/packages/types/src/config/Utilities.d.ts b/packages/types/src/config/Utilities.d.ts new file mode 100644 index 0000000000..14bf92d8dc --- /dev/null +++ b/packages/types/src/config/Utilities.d.ts @@ -0,0 +1,4 @@ +// export type Utility = Record any }>; +export type Utility = Record any }>; + +export type UtilitiesConfig = Record; diff --git a/packages/types/src/config/index.d.ts b/packages/types/src/config/index.d.ts index 670b9636f9..58be498702 100644 --- a/packages/types/src/config/index.d.ts +++ b/packages/types/src/config/index.d.ts @@ -3,6 +3,7 @@ import type { BlocksConfig } from './Blocks'; import type { ViewsConfig } from './Views'; import type { WidgetsConfig } from './Widgets'; import type { SlotsConfig } from './Slots'; +import type { UtilitiesConfig } from './Utilities'; export type AddonReducersConfig = Record; @@ -33,12 +34,20 @@ export type ConfigData = { addonRoutes: AddonRoutesConfig; slots: SlotsConfig; components: ComponentsConfig; + utilities: UtilitiesConfig; experimental: ExperimentalConfig; }; -export { SettingsConfig, BlocksConfig, ViewsConfig, WidgetsConfig }; +export { + BlocksConfig, + SettingsConfig, + UtilitiesConfig, + ViewsConfig, + WidgetsConfig, +}; export * from './Blocks'; export * from './Settings'; export * from './Slots'; +export * from './Utilities'; export * from './Views'; export * from './Widgets'; diff --git a/packages/volto/src/config/index.js b/packages/volto/src/config/index.js index 5a0cf59a13..ddce0abd9f 100644 --- a/packages/volto/src/config/index.js +++ b/packages/volto/src/config/index.js @@ -210,8 +210,9 @@ let config = { }, addonRoutes: [], addonReducers: {}, - slots: {}, components, + slots: {}, + utilities: {}, }; // The apiExpanders depends on a config of the object, so it's done here @@ -239,6 +240,7 @@ ConfigRegistry.addonRoutes = config.addonRoutes; ConfigRegistry.addonReducers = config.addonReducers; ConfigRegistry.components = config.components; ConfigRegistry.slots = config.slots; +ConfigRegistry.utilities = config.utilities; registerValidators(ConfigRegistry); diff --git a/packages/volto/src/config/validation.ts b/packages/volto/src/config/validation.ts index 0fc7d8aad7..84b75f8d31 100644 --- a/packages/volto/src/config/validation.ts +++ b/packages/volto/src/config/validation.ts @@ -15,82 +15,109 @@ import { } from '@plone/volto/helpers/FormValidation/validators'; const registerValidators = (config: ConfigType) => { - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['default', 'minLength'], - component: minLengthValidator, + config.registerUtility({ + name: 'minLength', + type: 'validator', + dependencies: { fieldType: 'string' }, + method: minLengthValidator, }); - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['default', 'maxLength'], - component: maxLengthValidator, + config.registerUtility({ + name: 'maxLength', + type: 'validator', + dependencies: { fieldType: 'string' }, + method: maxLengthValidator, }); - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['email', 'isValidEmail'], - component: emailValidator, + config.registerUtility({ + name: 'minLength', + type: 'validator', + dependencies: { fieldType: 'password' }, + method: minLengthValidator, }); - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['url', 'isValidURL'], - component: urlValidator, + config.registerUtility({ + name: 'maxLength', + type: 'validator', + dependencies: { fieldType: 'password' }, + method: maxLengthValidator, }); - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['number', 'isNumber'], - component: isNumber, + config.registerUtility({ + name: 'email', + type: 'validator', + dependencies: { widgetName: 'email' }, + method: emailValidator, }); - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['number', 'minimum'], - component: minimumValidator, + config.registerUtility({ + name: 'url', + type: 'validator', + dependencies: { widgetName: 'url' }, + method: urlValidator, }); - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['number', 'maximum'], - component: maximumValidator, + config.registerUtility({ + name: 'number', + type: 'validator', + dependencies: { fieldType: 'number' }, + method: isNumber, }); - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['integer', 'isNumber'], - component: isInteger, + config.registerUtility({ + name: 'minimum', + type: 'validator', + dependencies: { fieldType: 'number' }, + method: minimumValidator, }); - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['integer', 'minimum'], - component: minimumValidator, + config.registerUtility({ + name: 'maximum', + type: 'validator', + dependencies: { fieldType: 'number' }, + method: maximumValidator, }); - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['integer', 'maximum'], - component: maximumValidator, + config.registerUtility({ + name: 'integer', + type: 'validator', + dependencies: { fieldType: 'integer' }, + method: isInteger, }); - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['array', 'uniqueItems'], - component: hasUniqueItems, + config.registerUtility({ + name: 'minimum', + type: 'validator', + dependencies: { fieldType: 'integer' }, + method: minimumValidator, }); - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['plone.eventbasic', 'start'], - component: startEventDateRangeValidator, + config.registerUtility({ + name: 'maximum', + type: 'validator', + dependencies: { fieldType: 'integer' }, + method: maximumValidator, }); - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['plone.eventbasic', 'end'], - component: endEventDateRangeValidator, + config.registerUtility({ + name: 'uniqueItems', + type: 'validator', + dependencies: { fieldType: 'array' }, + method: hasUniqueItems, + }); + + config.registerUtility({ + name: 'dateRangeValidator', + type: 'validator', + dependencies: { behaviorName: 'plone.eventbasic', fieldName: 'start' }, + method: startEventDateRangeValidator, + }); + + config.registerUtility({ + name: 'dateRangeValidator', + type: 'validator', + dependencies: { behaviorName: 'plone.eventbasic', fieldName: 'end' }, + method: endEventDateRangeValidator, }); }; diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.jsx b/packages/volto/src/helpers/FormValidation/FormValidation.jsx index 3039f83114..798ff93abd 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.jsx +++ b/packages/volto/src/helpers/FormValidation/FormValidation.jsx @@ -110,7 +110,7 @@ const validateFieldsPerFieldset = ( const errorMessage = fieldData === undefined || fieldData === null ? null - : widgetCriterion.component({ + : widgetCriterion.method({ value: fieldData, field, formData, @@ -124,24 +124,28 @@ const validateFieldsPerFieldset = ( Object.entries(schema.properties).forEach(([fieldId, field]) => { let fieldData = formData[fieldId]; - // Default validation for all fields (required, minLength, maxLength) - const defaultFieldValidationCriteria = config.getComponents({ - name: 'fieldValidator', - dependencies: ['default'], - }); + // 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 }, + }); - const defaultFieldErrors = checkFieldErrors( - defaultFieldValidationCriteria, - field, - fieldData, - ); + specificFieldErrors = checkFieldErrors( + specificValidationCriteria, + field, + fieldData, + ); + } // Validation per field type - const fieldType = field.type || 'string'; - // test each criterion eg. maximum, isEmail, isUrl, etc - const fieldTypeValidationCriteria = config.getComponents({ - name: 'fieldValidator', - dependencies: [fieldType], + const fieldType = field.type || 'string'; // defaults to string + const fieldTypeValidationCriteria = config.getUtilities({ + type: 'validator', + dependencies: { fieldType }, }); const fieldErrors = checkFieldErrors( @@ -151,66 +155,46 @@ const validateFieldsPerFieldset = ( ); // Validation per field widget - const fieldWidget = + const widgetName = field.widgetOptions?.frontendOptions?.widget || field.widget || ''; let widgetErrors = []; - if (fieldWidget) { - const fieldWidgetValidationCriteria = config.getComponents({ - name: 'fieldValidator', - dependencies: [fieldWidget], + if (widgetName) { + const widgetNameValidationCriteria = config.getUtilities({ + type: 'validator', + dependencies: { widgetName }, }); widgetErrors = checkFieldErrors( - fieldWidgetValidationCriteria, + widgetNameValidationCriteria, field, fieldData, ); } - // Validation per specific behavior and fieldId - const perBehaviorSpecificValidator = field.behavior; + // Validation per specific behavior and field name (for content types) + const behaviorName = field.behavior; let perBehaviorFieldErrors = []; - // test each criterion eg. maximum, isEmail, isUrl, etc - if (perBehaviorSpecificValidator) { - const specificFieldValidationCriteria = config.getComponents({ - name: 'fieldValidator', - dependencies: [perBehaviorSpecificValidator, fieldId], + if (behaviorName) { + const specificPerBehaviorFieldValidationCriteria = config.getUtilities({ + type: 'validator', + dependencies: { behaviorName, fieldName: fieldId }, }); perBehaviorFieldErrors = checkFieldErrors( - specificFieldValidationCriteria, - field, - fieldData, - ); - } - - // Validation per specific validator - const hasSpecificValidator = - field.widgetOptions?.frontendOptions?.validator || field.validator; - let specificFieldErrors = []; - // test each criterion eg. maximum, isEmail, isUrl, etc - if (hasSpecificValidator) { - const specificFieldValidationCriteria = config.getComponents({ - name: 'fieldValidator', - dependencies: [hasSpecificValidator], - }); - - specificFieldErrors = checkFieldErrors( - specificFieldValidationCriteria, + specificPerBehaviorFieldValidationCriteria, field, fieldData, ); } - // Validation per block type validator - const hasBlockType = formData['@type']; + // Validation per block type validator (for blocks) + const blockType = formData['@type']; let blockTypeFieldErrors = []; - // test each criterion eg. maximum, isEmail, isUrl, etc - if (hasBlockType) { - const blockTypeFieldValidationCriteria = config.getComponents({ - name: 'fieldValidator', - dependencies: [hasBlockType, fieldId], + if (blockType) { + const blockTypeFieldValidationCriteria = config.getUtilities({ + type: 'validator', + dependencies: { blockType, fieldName: fieldId }, }); blockTypeFieldErrors = checkFieldErrors( @@ -221,22 +205,20 @@ const validateFieldsPerFieldset = ( } const mergedErrors = [ - ...defaultFieldErrors, + ...specificFieldErrors, ...fieldErrors, ...widgetErrors, ...perBehaviorFieldErrors, - ...specificFieldErrors, ...blockTypeFieldErrors, ]; if (mergedErrors.length > 0) { errors[fieldId] = [ ...(errors[fieldId] || []), - ...defaultFieldErrors, + ...specificFieldErrors, ...fieldErrors, ...widgetErrors, ...perBehaviorFieldErrors, - ...specificFieldErrors, ...blockTypeFieldErrors, ]; } diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.test.js b/packages/volto/src/helpers/FormValidation/FormValidation.test.js index 057d6e0399..c82f7529a6 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.test.js +++ b/packages/volto/src/helpers/FormValidation/FormValidation.test.js @@ -167,23 +167,19 @@ describe('FormValidation', () => { ).toEqual({}); }); - it('default - widget validator from block - Fails', () => { + it('widget - validator from block - Fails', () => { let newSchema = { properties: { ...schema.properties, customField: { title: 'Default field', description: '', - widget: 'isURL', + widget: 'url', }, }, required: [], }; - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['isURL'], - component: urlValidator, - }); + expect( FormValidation.validateFieldsPerFieldset({ schema: newSchema, @@ -198,7 +194,7 @@ describe('FormValidation', () => { }); }); - it('default - type and widget validator from block - Fails', () => { + it('type + widget - validator from block - Fails', () => { let newSchema = { properties: { ...schema.properties, @@ -206,20 +202,16 @@ describe('FormValidation', () => { title: 'Default field', description: '', type: 'customfieldtype', - widget: 'isURL', + widget: 'url', }, }, required: [], }; - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['customfieldtype', 'willFail'], - component: () => 'Fails', - }); - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['isURL'], - component: urlValidator, + config.registerUtility({ + type: 'validator', + name: 'alwaysFail', + dependencies: { fieldType: 'customfieldtype' }, + method: () => 'Fails', }); expect( FormValidation.validateFieldsPerFieldset({ @@ -235,7 +227,7 @@ describe('FormValidation', () => { }); }); - it('default - widget validator from content type set - Fails', () => { + it('widget - validator from content type set - Fails', () => { let newSchema = { properties: { ...schema.properties, @@ -244,18 +236,13 @@ describe('FormValidation', () => { description: '', widgetOptions: { frontendOptions: { - widget: 'isURL', + widget: 'url', }, }, }, }, required: [], }; - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['isURL'], - component: urlValidator, - }); expect( FormValidation.validateFieldsPerFieldset({ schema: newSchema, @@ -461,7 +448,7 @@ describe('FormValidation', () => { ...schema.properties, customField: { title: 'Integer field', - type: 'number', + type: 'integer', description: '', maximum: 8, }, @@ -594,22 +581,23 @@ describe('FormValidation', () => { ).toEqual({}); }); - it('default - specific validator set - Errors', () => { + it('format - specific validator set - Errors', () => { let newSchema = { properties: { ...schema.properties, customField: { title: 'Default field', description: '', - validator: 'isURL', + format: 'url', }, }, required: [], }; - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['isURL'], - component: urlValidator, + config.registerUtility({ + type: 'validator', + name: 'url', + dependencies: { format: 'url' }, + method: urlValidator, }); expect( FormValidation.validateFieldsPerFieldset({ @@ -625,22 +613,23 @@ describe('FormValidation', () => { }); }); - it('default - specific validator set - Succeeds', () => { + it('format - specific validator set - Succeeds', () => { let newSchema = { properties: { ...schema.properties, customField: { title: 'Default field', description: '', - validator: 'isURL', + format: 'url', }, }, required: [], }; - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['isURL'], - component: urlValidator, + config.registerUtility({ + type: 'validator', + name: 'url', + dependencies: { format: 'url' }, + method: urlValidator, }); expect( FormValidation.validateFieldsPerFieldset({ @@ -654,7 +643,7 @@ describe('FormValidation', () => { ).toEqual({}); }); - it('default - specific validator from content type set - Succeeds', () => { + it('format - specific validator from content type set - Fails', () => { let newSchema = { properties: { ...schema.properties, @@ -663,17 +652,54 @@ describe('FormValidation', () => { description: '', widgetOptions: { frontendOptions: { - validator: 'isURL', + format: 'url', }, }, }, }, required: [], }; - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['isURL'], - component: urlValidator, + config.registerUtility({ + type: 'validator', + name: 'url', + dependencies: { format: 'url' }, + method: urlValidator, + }); + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 'asdasd', + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.isValidURL.defaultMessage], + }); + }); + + it('format - specific validator from content type set - Succeeds', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Default field', + description: '', + widgetOptions: { + frontendOptions: { + format: 'url', + }, + }, + }, + }, + required: [], + }; + config.registerUtility({ + type: 'validator', + name: 'url', + dependencies: { format: 'url' }, + method: urlValidator, }); expect( FormValidation.validateFieldsPerFieldset({ @@ -687,7 +713,7 @@ describe('FormValidation', () => { ).toEqual({}); }); - it('default - per behavior specific - Fails', () => { + it('behavior + fieldName - Fails', () => { let newSchema = { properties: { ...schema.properties, @@ -699,10 +725,15 @@ describe('FormValidation', () => { }, required: [], }; - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['plone.event', 'customField'], - component: urlValidator, + config.registerUtility({ + type: 'validator', + name: 'url', + dependencies: { + behaviorName: 'plone.event', + fieldName: 'customField', + format: 'url', + }, + method: urlValidator, }); expect( FormValidation.validateFieldsPerFieldset({ @@ -729,10 +760,10 @@ describe('FormValidation', () => { }, required: [], }; - config.registerComponent({ - name: 'fieldValidator', - dependencies: ['slider', 'customField'], - component: urlValidator, + config.registerUtility({ + type: 'validator', + dependencies: { blockType: 'slider', fieldName: 'customField' }, + method: urlValidator, }); expect( FormValidation.validateFieldsPerFieldset({ @@ -749,8 +780,4 @@ describe('FormValidation', () => { }); }); }); - - // describe('validateBlockDataFields', () => { - - // }); }); diff --git a/packages/volto/test-setup-config.jsx b/packages/volto/test-setup-config.jsx index 33ef175b17..b42bd47dc4 100644 --- a/packages/volto/test-setup-config.jsx +++ b/packages/volto/test-setup-config.jsx @@ -155,7 +155,7 @@ config.set('components', { }, }); -registerValidators(config); +config.set('utilities', {}); config.set('experimental', { addBlockButton: { @@ -164,3 +164,5 @@ config.set('experimental', { }); config.set('slots', {}); + +registerValidators(config); From 11c24c7dde096a1ae3ee0886ad6d210698a7ea01 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Mon, 22 Jul 2024 16:44:53 +0200 Subject: [PATCH 26/51] locales --- packages/volto/locales/ca/LC_MESSAGES/volto.po | 4 ++-- packages/volto/locales/de/LC_MESSAGES/volto.po | 4 ++-- packages/volto/locales/en/LC_MESSAGES/volto.po | 4 ++-- packages/volto/locales/es/LC_MESSAGES/volto.po | 4 ++-- packages/volto/locales/eu/LC_MESSAGES/volto.po | 4 ++-- packages/volto/locales/fi/LC_MESSAGES/volto.po | 4 ++-- packages/volto/locales/fr/LC_MESSAGES/volto.po | 4 ++-- packages/volto/locales/hi/LC_MESSAGES/volto.po | 4 ++-- packages/volto/locales/it/LC_MESSAGES/volto.po | 4 ++-- packages/volto/locales/ja/LC_MESSAGES/volto.po | 4 ++-- packages/volto/locales/nl/LC_MESSAGES/volto.po | 4 ++-- packages/volto/locales/pt/LC_MESSAGES/volto.po | 4 ++-- packages/volto/locales/pt_BR/LC_MESSAGES/volto.po | 4 ++-- packages/volto/locales/ro/LC_MESSAGES/volto.po | 4 ++-- packages/volto/locales/volto.pot | 6 +++--- packages/volto/locales/zh_CN/LC_MESSAGES/volto.po | 4 ++-- 16 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/volto/locales/ca/LC_MESSAGES/volto.po b/packages/volto/locales/ca/LC_MESSAGES/volto.po index 1ef04b6b5d..408c20eebb 100644 --- a/packages/volto/locales/ca/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ca/LC_MESSAGES/volto.po @@ -1268,7 +1268,7 @@ msgstr "" msgid "End Date" msgstr "Data de finalització" -#. Default: "End event date happens before than the start event date" +#. Default: "Event end date happens before the event start date" #: helpers/MessageLabels/MessageLabels msgid "End event date happens before than the start event date" msgstr "" @@ -3442,7 +3442,7 @@ msgstr "Dividir" msgid "Start Date" msgstr "Data d'inici" -#. Default: "Start event date happens later than the end event date" +#. Default: "Event start date happens later than the event end date" #: helpers/MessageLabels/MessageLabels msgid "Start event date happens later than the end event date" msgstr "" diff --git a/packages/volto/locales/de/LC_MESSAGES/volto.po b/packages/volto/locales/de/LC_MESSAGES/volto.po index 6157a2e0c1..c7960976dd 100644 --- a/packages/volto/locales/de/LC_MESSAGES/volto.po +++ b/packages/volto/locales/de/LC_MESSAGES/volto.po @@ -1267,7 +1267,7 @@ msgstr "Aktiviert?" msgid "End Date" msgstr "Enddatum" -#. Default: "End event date happens before than the start event date" +#. Default: "Event end date happens before the event start date" #: helpers/MessageLabels/MessageLabels msgid "End event date happens before than the start event date" msgstr "" @@ -3441,7 +3441,7 @@ msgstr "Aufsplitten" msgid "Start Date" msgstr "Anfangsdatum" -#. Default: "Start event date happens later than the end event date" +#. Default: "Event start date happens later than the event end date" #: helpers/MessageLabels/MessageLabels msgid "Start event date happens later than the end event date" msgstr "" diff --git a/packages/volto/locales/en/LC_MESSAGES/volto.po b/packages/volto/locales/en/LC_MESSAGES/volto.po index 447c2509be..c8451383be 100644 --- a/packages/volto/locales/en/LC_MESSAGES/volto.po +++ b/packages/volto/locales/en/LC_MESSAGES/volto.po @@ -1262,7 +1262,7 @@ msgstr "" msgid "End Date" msgstr "" -#. Default: "End event date happens before than the start event date" +#. Default: "Event end date happens before the event start date" #: helpers/MessageLabels/MessageLabels msgid "End event date happens before than the start event date" msgstr "" @@ -3436,7 +3436,7 @@ msgstr "" msgid "Start Date" msgstr "" -#. Default: "Start event date happens later than the end event date" +#. Default: "Event start date happens later than the event end date" #: helpers/MessageLabels/MessageLabels msgid "Start event date happens later than the end event date" msgstr "" diff --git a/packages/volto/locales/es/LC_MESSAGES/volto.po b/packages/volto/locales/es/LC_MESSAGES/volto.po index 1b28b346b9..ebe5bbcc9f 100644 --- a/packages/volto/locales/es/LC_MESSAGES/volto.po +++ b/packages/volto/locales/es/LC_MESSAGES/volto.po @@ -1269,7 +1269,7 @@ msgstr "¿Activado?" msgid "End Date" msgstr "Fecha final" -#. Default: "End event date happens before than the start event date" +#. Default: "Event end date happens before the event start date" #: helpers/MessageLabels/MessageLabels msgid "End event date happens before than the start event date" msgstr "" @@ -3443,7 +3443,7 @@ msgstr "División" msgid "Start Date" msgstr "Fecha de inicio" -#. Default: "Start event date happens later than the end event date" +#. Default: "Event start date happens later than the event end date" #: helpers/MessageLabels/MessageLabels msgid "Start event date happens later than the end event date" msgstr "" diff --git a/packages/volto/locales/eu/LC_MESSAGES/volto.po b/packages/volto/locales/eu/LC_MESSAGES/volto.po index e6d1d7cc65..bf229077d4 100644 --- a/packages/volto/locales/eu/LC_MESSAGES/volto.po +++ b/packages/volto/locales/eu/LC_MESSAGES/volto.po @@ -1269,7 +1269,7 @@ msgstr "Aktibatuta?" msgid "End Date" msgstr "Bukaera data" -#. Default: "End event date happens before than the start event date" +#. Default: "Event end date happens before the event start date" #: helpers/MessageLabels/MessageLabels msgid "End event date happens before than the start event date" msgstr "" @@ -3443,7 +3443,7 @@ msgstr "Banatu" msgid "Start Date" msgstr "Hasiera-data" -#. Default: "Start event date happens later than the end event date" +#. Default: "Event start date happens later than the event end date" #: helpers/MessageLabels/MessageLabels msgid "Start event date happens later than the end event date" msgstr "" diff --git a/packages/volto/locales/fi/LC_MESSAGES/volto.po b/packages/volto/locales/fi/LC_MESSAGES/volto.po index b47a21d524..683cfd39bf 100644 --- a/packages/volto/locales/fi/LC_MESSAGES/volto.po +++ b/packages/volto/locales/fi/LC_MESSAGES/volto.po @@ -1267,7 +1267,7 @@ msgstr "Aktivoitu?" msgid "End Date" msgstr "Päättymispäivä" -#. Default: "End event date happens before than the start event date" +#. Default: "Event end date happens before the event start date" #: helpers/MessageLabels/MessageLabels msgid "End event date happens before than the start event date" msgstr "" @@ -3441,7 +3441,7 @@ msgstr "Halkaise" msgid "Start Date" msgstr "Aloituspäivä" -#. Default: "Start event date happens later than the end event date" +#. Default: "Event start date happens later than the event end date" #: helpers/MessageLabels/MessageLabels msgid "Start event date happens later than the end event date" msgstr "" diff --git a/packages/volto/locales/fr/LC_MESSAGES/volto.po b/packages/volto/locales/fr/LC_MESSAGES/volto.po index 59686d2042..b9816e87a2 100644 --- a/packages/volto/locales/fr/LC_MESSAGES/volto.po +++ b/packages/volto/locales/fr/LC_MESSAGES/volto.po @@ -1269,7 +1269,7 @@ msgstr "Activé ?" msgid "End Date" msgstr "Date de fin" -#. Default: "End event date happens before than the start event date" +#. Default: "Event end date happens before the event start date" #: helpers/MessageLabels/MessageLabels msgid "End event date happens before than the start event date" msgstr "" @@ -3443,7 +3443,7 @@ msgstr "Divisé" msgid "Start Date" msgstr "Date de début" -#. Default: "Start event date happens later than the end event date" +#. Default: "Event start date happens later than the event end date" #: helpers/MessageLabels/MessageLabels msgid "Start event date happens later than the end event date" msgstr "" diff --git a/packages/volto/locales/hi/LC_MESSAGES/volto.po b/packages/volto/locales/hi/LC_MESSAGES/volto.po index bad6113aab..dc98703c6c 100644 --- a/packages/volto/locales/hi/LC_MESSAGES/volto.po +++ b/packages/volto/locales/hi/LC_MESSAGES/volto.po @@ -1262,7 +1262,7 @@ msgstr "सक्षम?" msgid "End Date" msgstr "अंतिम तिथि" -#. Default: "End event date happens before than the start event date" +#. Default: "Event end date happens before the event start date" #: helpers/MessageLabels/MessageLabels msgid "End event date happens before than the start event date" msgstr "" @@ -3436,7 +3436,7 @@ msgstr "विभाजित करें" msgid "Start Date" msgstr "प्रारंभ तिथि" -#. Default: "Start event date happens later than the end event date" +#. Default: "Event start date happens later than the event end date" #: helpers/MessageLabels/MessageLabels msgid "Start event date happens later than the end event date" msgstr "" diff --git a/packages/volto/locales/it/LC_MESSAGES/volto.po b/packages/volto/locales/it/LC_MESSAGES/volto.po index 511d8a3797..a8bda3b015 100644 --- a/packages/volto/locales/it/LC_MESSAGES/volto.po +++ b/packages/volto/locales/it/LC_MESSAGES/volto.po @@ -1262,7 +1262,7 @@ msgstr "Abilitato?" msgid "End Date" msgstr "Data di fine" -#. Default: "End event date happens before than the start event date" +#. Default: "Event end date happens before the event start date" #: helpers/MessageLabels/MessageLabels msgid "End event date happens before than the start event date" msgstr "" @@ -3436,7 +3436,7 @@ msgstr "Dividi" msgid "Start Date" msgstr "Data di inizio" -#. Default: "Start event date happens later than the end event date" +#. Default: "Event start date happens later than the event end date" #: helpers/MessageLabels/MessageLabels msgid "Start event date happens later than the end event date" msgstr "" diff --git a/packages/volto/locales/ja/LC_MESSAGES/volto.po b/packages/volto/locales/ja/LC_MESSAGES/volto.po index 3143057326..b8b91b70dd 100644 --- a/packages/volto/locales/ja/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ja/LC_MESSAGES/volto.po @@ -1267,7 +1267,7 @@ msgstr "" msgid "End Date" msgstr "終了日付" -#. Default: "End event date happens before than the start event date" +#. Default: "Event end date happens before the event start date" #: helpers/MessageLabels/MessageLabels msgid "End event date happens before than the start event date" msgstr "" @@ -3441,7 +3441,7 @@ msgstr "分割" msgid "Start Date" msgstr "開始日付" -#. Default: "Start event date happens later than the end event date" +#. Default: "Event start date happens later than the event end date" #: helpers/MessageLabels/MessageLabels msgid "Start event date happens later than the end event date" msgstr "" diff --git a/packages/volto/locales/nl/LC_MESSAGES/volto.po b/packages/volto/locales/nl/LC_MESSAGES/volto.po index a69cb7f9b6..f90db006ff 100644 --- a/packages/volto/locales/nl/LC_MESSAGES/volto.po +++ b/packages/volto/locales/nl/LC_MESSAGES/volto.po @@ -1266,7 +1266,7 @@ msgstr "" msgid "End Date" msgstr "" -#. Default: "End event date happens before than the start event date" +#. Default: "Event end date happens before the event start date" #: helpers/MessageLabels/MessageLabels msgid "End event date happens before than the start event date" msgstr "" @@ -3440,7 +3440,7 @@ msgstr "" msgid "Start Date" msgstr "" -#. Default: "Start event date happens later than the end event date" +#. Default: "Event start date happens later than the event end date" #: helpers/MessageLabels/MessageLabels msgid "Start event date happens later than the end event date" msgstr "" diff --git a/packages/volto/locales/pt/LC_MESSAGES/volto.po b/packages/volto/locales/pt/LC_MESSAGES/volto.po index d559c41160..c4ba392540 100644 --- a/packages/volto/locales/pt/LC_MESSAGES/volto.po +++ b/packages/volto/locales/pt/LC_MESSAGES/volto.po @@ -1267,7 +1267,7 @@ msgstr "" msgid "End Date" msgstr "" -#. Default: "End event date happens before than the start event date" +#. Default: "Event end date happens before the event start date" #: helpers/MessageLabels/MessageLabels msgid "End event date happens before than the start event date" msgstr "" @@ -3441,7 +3441,7 @@ msgstr "Dividir" msgid "Start Date" msgstr "" -#. Default: "Start event date happens later than the end event date" +#. Default: "Event start date happens later than the event end date" #: helpers/MessageLabels/MessageLabels msgid "Start event date happens later than the end event date" msgstr "" diff --git a/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po b/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po index 7ddb7ec3a3..e2d9d584cf 100644 --- a/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po +++ b/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po @@ -1268,7 +1268,7 @@ msgstr "Ativada?" msgid "End Date" msgstr "Data Final" -#. Default: "End event date happens before than the start event date" +#. Default: "Event end date happens before the event start date" #: helpers/MessageLabels/MessageLabels msgid "End event date happens before than the start event date" msgstr "" @@ -3442,7 +3442,7 @@ msgstr "Dividir" msgid "Start Date" msgstr "Data de Início" -#. Default: "Start event date happens later than the end event date" +#. Default: "Event start date happens later than the event end date" #: helpers/MessageLabels/MessageLabels msgid "Start event date happens later than the end event date" msgstr "" diff --git a/packages/volto/locales/ro/LC_MESSAGES/volto.po b/packages/volto/locales/ro/LC_MESSAGES/volto.po index d73b6498d4..2eb99f79f2 100644 --- a/packages/volto/locales/ro/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ro/LC_MESSAGES/volto.po @@ -1262,7 +1262,7 @@ msgstr "" msgid "End Date" msgstr "Data de încheiere" -#. Default: "End event date happens before than the start event date" +#. Default: "Event end date happens before the event start date" #: helpers/MessageLabels/MessageLabels msgid "End event date happens before than the start event date" msgstr "" @@ -3436,7 +3436,7 @@ msgstr "Împărțire" msgid "Start Date" msgstr "Data de început" -#. Default: "Start event date happens later than the end event date" +#. Default: "Event start date happens later than the event end date" #: helpers/MessageLabels/MessageLabels msgid "Start event date happens later than the end event date" msgstr "" diff --git a/packages/volto/locales/volto.pot b/packages/volto/locales/volto.pot index f3d97a00f4..cd651ef9f3 100644 --- a/packages/volto/locales/volto.pot +++ b/packages/volto/locales/volto.pot @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: Plone\n" -"POT-Creation-Date: 2024-07-15T15:43:39.835Z\n" +"POT-Creation-Date: 2024-07-22T14:32:27.896Z\n" "Last-Translator: Plone i18n \n" "Language-Team: Plone i18n \n" "Content-Type: text/plain; charset=utf-8\n" @@ -1264,7 +1264,7 @@ msgstr "" msgid "End Date" msgstr "" -#. Default: "End event date happens before than the start event date" +#. Default: "Event end date happens before the event start date" #: helpers/MessageLabels/MessageLabels msgid "End event date happens before than the start event date" msgstr "" @@ -3438,7 +3438,7 @@ msgstr "" msgid "Start Date" msgstr "" -#. Default: "Start event date happens later than the end event date" +#. Default: "Event start date happens later than the event end date" #: helpers/MessageLabels/MessageLabels msgid "Start event date happens later than the end event date" msgstr "" diff --git a/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po b/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po index 3ea60cec70..7f0beedde6 100644 --- a/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po +++ b/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po @@ -1268,7 +1268,7 @@ msgstr "启用?" msgid "End Date" msgstr "结束日期" -#. Default: "End event date happens before than the start event date" +#. Default: "Event end date happens before the event start date" #: helpers/MessageLabels/MessageLabels msgid "End event date happens before than the start event date" msgstr "" @@ -3442,7 +3442,7 @@ msgstr "" msgid "Start Date" msgstr "开始日期" -#. Default: "Start event date happens later than the end event date" +#. Default: "Event start date happens later than the event end date" #: helpers/MessageLabels/MessageLabels msgid "Start event date happens later than the end event date" msgstr "" From e11e9d598e75193e2baa52b0fc0326f15076c845 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Mon, 22 Jul 2024 16:46:39 +0200 Subject: [PATCH 27/51] Remove cruft --- packages/registry/src/index.ts | 33 ------------------------- packages/registry/src/registry.test.tsx | 33 ------------------------- 2 files changed, 66 deletions(-) diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index e6d5c9b8ad..f188297a3f 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -163,39 +163,6 @@ class Config { } } - getComponents( - options: { name: string; dependencies?: string[] | string } | string, - ): Array { - if (typeof options === 'object') { - const { name, dependencies = '' } = options; - let depsString: string = ''; - if (dependencies && Array.isArray(dependencies)) { - depsString = dependencies.join('+'); - } else if (typeof dependencies === 'string') { - depsString = dependencies; - } - const componentName = `${name}${depsString ? `|${depsString}` : ''}`; - const componentsKeys = Object.keys(this._data.components).filter((key) => - key.startsWith(componentName), - ); - const components = componentsKeys.map( - (key) => this._data.components[key], - ); - - return components; - } else { - // Shortcut notation, accepting a lonely string as argument - const componentName = options; - const componentsKeys = Object.keys(this._data.components).filter((key) => - key.startsWith(componentName), - ); - const components = componentsKeys.map( - (key) => this._data.components[key], - ); - return components; - } - } - registerComponent(options: { name: string; dependencies?: string[] | string; diff --git a/packages/registry/src/registry.test.tsx b/packages/registry/src/registry.test.tsx index 3ac2fb6295..fe274f4e48 100644 --- a/packages/registry/src/registry.test.tsx +++ b/packages/registry/src/registry.test.tsx @@ -113,39 +113,6 @@ describe('Component registry', () => { }).component, ).toEqual('this is a Bar component'); }); - it('getComponents - get a collection of component registration', () => { - expect(config.getComponents('Toolbar').length).toEqual(2); - expect(config.getComponents('Toolbar')[0].component).toEqual( - 'this is the Toolbar component', - ); - expect(config.getComponents('Toolbar')[1].component).toEqual( - 'this is the Types component', - ); - }); - it('getComponents - get a collection of component registration and deps', () => { - config.registerComponent({ - name: 'Toolbar', - component: 'this is a StringFieldWidget component', - dependencies: ['News Item', 'StringFieldWidget'], - }); - config.registerComponent({ - name: 'Toolbar', - component: 'this is a AnotherWidget component', - dependencies: ['News Item', 'AnotherWidget'], - }); - expect( - config.getComponents({ name: 'Toolbar', dependencies: ['News Item'] }) - .length, - ).toEqual(2); - expect( - config.getComponents({ name: 'Toolbar', dependencies: ['News Item'] })[0] - .component, - ).toEqual('this is a StringFieldWidget component'); - expect( - config.getComponents({ name: 'Toolbar', dependencies: ['News Item'] })[1] - .component, - ).toEqual('this is a AnotherWidget component'); - }); }); describe('Slots registry', () => { From 8094fa42135c11cd8e269a6dfaf7c19d19a53d5d Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Mon, 22 Jul 2024 16:49:17 +0200 Subject: [PATCH 28/51] Bring back correct typing --- packages/registry/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index f188297a3f..68877bcea7 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -166,7 +166,7 @@ class Config { registerComponent(options: { name: string; dependencies?: string[] | string; - component: (args: any) => any; + component: React.ComponentType; }) { const { name, component, dependencies = '' } = options; let depsString: string = ''; From 0b4f253f3c9b3c674e2354b3d659b1465e4e233d Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 22 Jul 2024 23:28:00 +0200 Subject: [PATCH 29/51] carry over suggestions from #6161 to index.md --- docs/source/upgrade-guide/index.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/source/upgrade-guide/index.md b/docs/source/upgrade-guide/index.md index 243f382161..fc7b819a17 100644 --- a/docs/source/upgrade-guide/index.md +++ b/docs/source/upgrade-guide/index.md @@ -379,9 +379,14 @@ If you still use it, bring it back as your main add-on dependency, bring back th ### Refactor of `FormValidation` module The `packages/volto/src/helpers/FormValidation/FormValidation.jsx` module has been heavily refactored. -Some helper functions have been moved to `packages/volto/src/helpers/FormValidation/validators.ts`, however, none of those functions were exported in the first place, so no imports will be broken. -In case that you've shadowed the `packages/volto/src/helpers/FormValidation/FormValidation.jsx` module, you should revisit it and update it with the latest refactor. -If you added more validators manually in that shadow, please refer to the documentation to add the validators in the new way: {doc}`../configuration/validation`. +Some helper functions have been moved to `packages/volto/src/helpers/FormValidation/validators.ts`. +None of those functions were exported in the first place, so no imports will be broken. +If you shadowed the module {file}`packages/volto/src/helpers/FormValidation/FormValidation.jsx`, you should review it and update it accordingly. + +```{seealso} +{doc}`../configuration/validation` +``` + (volto-upgrade-guide-17.x.x)= From ca2bdd20b25e7791b87ccb3cdd7c2e3e9683c58e Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 23 Jul 2024 00:45:31 +0200 Subject: [PATCH 30/51] Steve's review and edits of validation.md --- docs/source/configuration/validation.md | 116 ++++++++++++++---------- 1 file changed, 70 insertions(+), 46 deletions(-) diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md index a03a34f72d..e3e4bb74a5 100644 --- a/docs/source/configuration/validation.md +++ b/docs/source/configuration/validation.md @@ -3,7 +3,7 @@ myst: html_meta: "description": "Client side form field validation" "property=og:description": "Client side form field validation" - "property=og:title": "Form fields validation" + "property=og:title": "Client side form field validation" "keywords": "Volto, Plone, frontend, React, configuration, form, fields, validation" --- @@ -11,23 +11,30 @@ myst: Volto provides a mechanism for delivering form field validation in an extensible way. This extensibility is based on the Volto registry. -It applies to content types, custom programatically generated forms and blocks schema settings. -All of them are serialized using JSON schema standard and then Volto generates the resultant form out of it. +It applies to content types, custom programatically generated forms, and blocks schema settings. +The mechanism serializes all of them according to the [JSON schema standard](https://json-schema.org/draft/2020-12/json-schema-validation). +Finally Volto generates the form from the serialization. -## Registering a validator -You can register a validator using the registry API from your add-on configuration. -The validators are registered using the `registerUtility` API method. +## Register a validator -### Registering and declaring a simple validator +You can register a validator using the `registerUtility` method in the registry API from your add-on configuration. -The most common thing is to have a field that you want to validate with a specific validator. -Volto provide some default validators, see at the end of this chapter for more information. -#### For Volto custom forms and block schema forms +### Register and declare a simple validator -When you define them programatically, in your core, using JSON schema, you can register a custom validator using the `format` property. -This is the case of creating the schema for a block: +This section describes how to validate a field with a specific validator, a common use case. +Volto also provides some default validators. + +```{seealso} +{ref}`voltos-default-validators-label` +``` + +#### Volto custom forms and block schema forms + +When you define custom forms and block schema forms programatically, you can register a custom validator using the `format` property in your core using JSON schema. + +The following example shows how to create the schema for a block. ```ts let blockSchema = { @@ -44,7 +51,8 @@ let blockSchema = { }; ``` -`url` named validator should be registered by this name as a Volto validator utility: +You should register the `url` named validator as a Volto validator utility. +In the following example, the `urlValidator` method validator will be applied for the block field `customField` in the previous example. ```ts config.registerUtility({ @@ -54,11 +62,10 @@ config.registerUtility({ }) ``` -In this case, the `urlValidator` method validator will be applied for the block field `customField`. -#### For content types +#### Content types -Content types also can specify the `format` using the schema hints in the backend in the `frontendOptions`: +You can also specify the `format` of content types using the schema hints in the backend using `frontendOptions`. ```python from plone.supermodel import model @@ -78,8 +85,9 @@ class IMyContent(model.Schema): # Rest of your content type definition ``` -For the record, the resultant `plone.restapi` response will be something like the following, a bit different than in blocks JSON schema. -But the validation engine will take care too: +The response from `plone.restapi` will be something like the following. +It is slightly different from blocks JSON schema, but the validation engine will behave the same. +The `urlValidator` method validator will be applied for the content type field `customField` from the earlier example. ```json { @@ -96,16 +104,22 @@ But the validation engine will take care too: } ``` -and the `urlValidator` method validator will be applied for the content type field `customField`. ### Advanced scenarios -In case you need more granularity and you don't have access to modify the existing implementation of the JSON schema definitions for existing content types, blocks or forms (might be in third party add-ons), you can use the following advanced validator registrations, using `field`, `widget`, `behaviorName` or `blockType` validator registrations. +In case you need more granularity, and you don't have access to modify the existing implementation of the JSON schema definitions for existing content types, blocks, or forms, you can use the following advanced validator registrations, using `field`, `widget`, `behaviorName`, or `blockType` validator registrations. + + +#### Field `type` validators -#### Per field `type` validators +Field `type` validators are applied depending on the specified `type` of the field in the JSON schema from content types, forms, or blocks. -These validators are applied depending on the specified `type` of the field in the JSON schema from content types, forms or blocks. -The next example is for the use case of JSON schema defined in a block: +You should specify the `type` in the JSON schema of the block. +In a content type, it is included in the default serialization of the field. + +If a field does not specify `type`, it assumes a `string` type as validator. + +The next example shows how to define the JSON schema in a block. ```ts let blockSchema = { @@ -134,12 +148,13 @@ config.registerUtility({ }) ``` -You should specify the `type` in the JSON schema of the block (in a content type, it is included in the default serialization of the field). -If a field does not specify type, it assumes a `string` type as validator. -#### Per field `widget` validators +#### Field `widget` validators + +Field `widget` validators are applied depending on the specified `widget` of the field. +You should specify the `widget` either in the JSON schema of the block or as additional data in the content type definition. -These validators are applied depending on the specified `widget` of the field. +The following example shows how to specify the `widget` either in the JSON schema of the block. ```ts let blockSchema = { @@ -167,10 +182,8 @@ config.registerUtility({ }) ``` - -You should specify the `widget` in the JSON schema of the block, or as additional data in the content type definition. -Content types also can specify the `widget` to be used using the schema hints in the backend in the `frontendOptions`: - +The following example shows how to specify the `widget` in the content type definition in the schema hints in the backend using `frontendOptions`. +The validation engine will behave the same, applying the `urlValidator` method validator for the content type field `customField` in the previous example. ```python from plone.supermodel import model @@ -190,11 +203,13 @@ class IMyContent(model.Schema): # Rest of your content type definition ``` -the validation engine will take care too, and the `urlValidator` method validator will be applied for the content type field `customField`. -#### Per behavior and field name validator +#### Behavior and field name validators -These validators are applied depending on the behavior (usually coming from a content type definition) in combination with the name of the field. +Behavior and field name validators are applied depending on the behavior in combination with the name of the field. +These usually come from a content type definition. +This type of validator only applies to content type validators. +It takes the `behaviorName` and the `fieldName` as dependencies. ```ts config.registerUtility({ @@ -208,12 +223,12 @@ config.registerUtility({ }) ``` -It takes the `behaviorName` and the `fieldName` as dependencies. -This type of validator only applies to content type validators. -#### Per block type and field name validator +#### Block type and field name validators -These validators are applied depending on the block type in combination with the name of the field in the block settings JSON schema. +Block type and field name validators are applied depending on the block type in combination with the name of the field in the block settings JSON schema. +This type of validator only applies to blocks. +It takes the `blockType` and the `fieldName` as dependencies. ```ts config.registerUtility({ @@ -227,16 +242,20 @@ config.registerUtility({ }) ``` -It takes the `blockType` and the `fieldName` as dependencies. -This type of validator only applies to blocks. + +(voltos-default-validators-label)= ## Volto's default validators -Volto provides a set of validators by default, you can find them in this module: `packages/volto/src/config/validators.ts` +Volto provides a set of validators by default. +You can find them in the module {file}`packages/volto/src/config/validators.ts`. + + +### Override a validator -### How to override a validator +You can override a validator in your add-on in the same way as any other component defined in the registry. +You can redefine them using the same `dependencies` and provide your own validator. -You can override them in your add-on as any other component defined in the registry, by redefining them using the same `dependencies`, and providing your own. ## Signature of a validator @@ -266,8 +285,13 @@ export const isNumber = ({ value, formatMessage }: Validator) => { }; ``` + ## Invariants -Using the `formData` you can perform validation checks using other field data as source. -This is interesting in the case that two fields are related, like `start` and `end` dates. -You can create invariant type of validators thanks to this. +Using `formData`, you can perform validation checks using other field data as source. +This is useful when you want to validate two related fields, such as ensuring the end date of an event is after its start date. +You can create invariant validator types. + +```{todo} +Needs example. +``` From 641ce694e770cd6b4a856263ec6b0742a7956911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20de=20Alba?= Date: Tue, 23 Jul 2024 09:12:29 +0200 Subject: [PATCH 31/51] Apply suggestions from code review Co-authored-by: Steve Piercy --- packages/registry/src/registry.test.tsx | 6 +++--- packages/types/news/6161.bugfix | 2 +- packages/volto/news/6161.breaking | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/registry/src/registry.test.tsx b/packages/registry/src/registry.test.tsx index fe274f4e48..ab189a9368 100644 --- a/packages/registry/src/registry.test.tsx +++ b/packages/registry/src/registry.test.tsx @@ -939,7 +939,7 @@ describe('Utilities registry', () => { ).toEqual('this is a simple validator utility'); }); - it('registers an utility with dependencies', () => { + it('registers a utility with dependencies', () => { config.registerUtility({ name: 'email', type: 'validator', @@ -958,7 +958,7 @@ describe('Utilities registry', () => { ).toEqual('this is a validator utility with dependencies'); }); - it('registers both an utility with dependencies and with no dependencies', () => { + it('registers utilities, one with and one without dependencies', () => { config.registerUtility({ name: 'email', type: 'validator', @@ -987,7 +987,7 @@ describe('Utilities registry', () => { ).toEqual('this is a validator utility with dependencies'); }); - it('registers an utility with dependencies and another with different dependencies', () => { + it('registers utilities with the same name, but different dependencies', () => { config.registerUtility({ name: 'email', type: 'validator', diff --git a/packages/types/news/6161.bugfix b/packages/types/news/6161.bugfix index 3e8c99230d..dc4ac6ff7c 100644 --- a/packages/types/news/6161.bugfix +++ b/packages/types/news/6161.bugfix @@ -1 +1 @@ -Add `errors` shape to the `BlockEditProps` @sneridagh +Add `errors` shape to the `BlockEditProps`. @sneridagh diff --git a/packages/volto/news/6161.breaking b/packages/volto/news/6161.breaking index 17a66bab4e..6c3a087353 100644 --- a/packages/volto/news/6161.breaking +++ b/packages/volto/news/6161.breaking @@ -1,5 +1,5 @@ -Add foundations for extensible validation in forms @sneridagh +Add foundations for extensible validation in forms. @sneridagh Breaking: `packages/volto/src/helpers/FormValidation/FormValidation.jsx` has been heavily refactored. -If you have shadowed this component in your project/add-on, please review and update your shadow. +If you shadowed this component in your project or add-on, you should review it and update it accordingly. From 2a6130034ee9f0f9b077dd9c9ecef418687d412f Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Tue, 23 Jul 2024 10:31:54 +0200 Subject: [PATCH 32/51] Unify validator names, ending in `Validator` --- packages/volto/src/config/validation.ts | 12 ++++++------ .../volto/src/helpers/FormValidation/validators.ts | 10 +++++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/volto/src/config/validation.ts b/packages/volto/src/config/validation.ts index 84b75f8d31..6e7df47b29 100644 --- a/packages/volto/src/config/validation.ts +++ b/packages/volto/src/config/validation.ts @@ -5,11 +5,11 @@ import { maxLengthValidator, urlValidator, emailValidator, - isNumber, + isNumberValidator, maximumValidator, minimumValidator, - isInteger, - hasUniqueItems, + isIntegerValidator, + hasUniqueItemsValidator, startEventDateRangeValidator, endEventDateRangeValidator, } from '@plone/volto/helpers/FormValidation/validators'; @@ -61,7 +61,7 @@ const registerValidators = (config: ConfigType) => { name: 'number', type: 'validator', dependencies: { fieldType: 'number' }, - method: isNumber, + method: isNumberValidator, }); config.registerUtility({ @@ -82,7 +82,7 @@ const registerValidators = (config: ConfigType) => { name: 'integer', type: 'validator', dependencies: { fieldType: 'integer' }, - method: isInteger, + method: isIntegerValidator, }); config.registerUtility({ @@ -103,7 +103,7 @@ const registerValidators = (config: ConfigType) => { name: 'uniqueItems', type: 'validator', dependencies: { fieldType: 'array' }, - method: hasUniqueItems, + method: hasUniqueItemsValidator, }); config.registerUtility({ diff --git a/packages/volto/src/helpers/FormValidation/validators.ts b/packages/volto/src/helpers/FormValidation/validators.ts index f6e4ed3057..6a0590023f 100644 --- a/packages/volto/src/helpers/FormValidation/validators.ts +++ b/packages/volto/src/helpers/FormValidation/validators.ts @@ -83,7 +83,7 @@ export const emailValidator = ({ value, formatMessage }: Validator): string => { return !isValid ? formatMessage(messages.isValidEmail) : null; }; -export const isNumber = ({ value, formatMessage }: Validator) => { +export const isNumberValidator = ({ value, formatMessage }: Validator) => { const floatRegex = /^[+-]?\d+(\.\d+)?$/; const isValid = typeof value === 'number' && !isNaN(value) && floatRegex.test(value); @@ -106,14 +106,18 @@ export const maximumValidator = ({ value, field, formatMessage }: Validator) => formatMessage, }); -export const isInteger = ({ value, formatMessage }: Validator) => { +export const isIntegerValidator = ({ value, formatMessage }: Validator) => { const intRegex = /^-?[0-9]+$/; const isValid = typeof value === 'number' && !isNaN(value) && intRegex.test(value); return !isValid ? formatMessage(messages.isInteger) : null; }; -export const hasUniqueItems = ({ value, field, formatMessage }: Validator) => { +export const hasUniqueItemsValidator = ({ + value, + field, + formatMessage, +}: Validator) => { if (!field.uniqueItems) { return null; } From 091711a999edc47fc9d71f52dd915f23b00d9038 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Tue, 23 Jul 2024 11:05:11 +0200 Subject: [PATCH 33/51] Fix start-end date validator i18n msg, add tests --- .../FormValidation/FormValidation.test.js | 58 ++++++++++++++++++- .../src/helpers/FormValidation/validators.ts | 14 ++++- .../helpers/MessageLabels/MessageLabels.js | 10 ++-- 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.test.js b/packages/volto/src/helpers/FormValidation/FormValidation.test.js index c82f7529a6..635f6b7a57 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.test.js +++ b/packages/volto/src/helpers/FormValidation/FormValidation.test.js @@ -718,7 +718,7 @@ describe('FormValidation', () => { properties: { ...schema.properties, customField: { - behavior: 'plone.event', + behavior: 'plone.eventbasic', title: 'Default field', description: '', }, @@ -729,7 +729,7 @@ describe('FormValidation', () => { type: 'validator', name: 'url', dependencies: { - behaviorName: 'plone.event', + behaviorName: 'plone.eventbasic', fieldName: 'customField', format: 'url', }, @@ -749,6 +749,60 @@ describe('FormValidation', () => { }); }); + it('behavior + fieldName - start date in Event - Fails', () => { + let contentTypeSchema = { + properties: { + ...schema.properties, + start: { + behavior: 'plone.eventbasic', + type: 'string', + title: 'Start date', + description: '', + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: contentTypeSchema, + formData: { + start: '2024-08-01T11:00:00+00:00', + end: '2024-04-01T11:00:00+00:00', + }, + formatMessage, + }), + ).toEqual({ + start: [messages.startEventRange.defaultMessage], + }); + }); + + it('behavior + fieldName - end date in Event - Fails', () => { + let contentTypeSchema = { + properties: { + ...schema.properties, + end: { + behavior: 'plone.eventbasic', + type: 'string', + title: 'End date', + description: '', + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: contentTypeSchema, + formData: { + start: '2024-08-01T11:00:00+00:00', + end: '2024-04-01T11:00:00+00:00', + }, + formatMessage, + }), + ).toEqual({ + end: [messages.endEventRange.defaultMessage], + }); + }); + it('block - per block type and fieldID specific - Fails', () => { let newSchema = { properties: { diff --git a/packages/volto/src/helpers/FormValidation/validators.ts b/packages/volto/src/helpers/FormValidation/validators.ts index 6a0590023f..dd75998ef7 100644 --- a/packages/volto/src/helpers/FormValidation/validators.ts +++ b/packages/volto/src/helpers/FormValidation/validators.ts @@ -137,7 +137,11 @@ export const startEventDateRangeValidator = ({ }: Validator) => { const isValid = value && formData.end && new Date(value) < new Date(formData.end); - return !isValid ? formatMessage(messages.startEventRange) : null; + return !isValid + ? formatMessage(messages.startEventRange, { + endDateValueOrEndFieldName: formData.end || 'end', + }) + : null; }; export const endEventDateRangeValidator = ({ @@ -147,6 +151,10 @@ export const endEventDateRangeValidator = ({ formatMessage, }: Validator) => { const isValid = - value && formData.start && new Date(value) < new Date(formData.start); - return !isValid ? formatMessage(messages.endEventRange) : null; + value && formData.start && new Date(value) > new Date(formData.start); + return !isValid + ? formatMessage(messages.endEventRange, { + startDateValueOrStartFieldName: formData.start || 'start', + }) + : null; }; diff --git a/packages/volto/src/helpers/MessageLabels/MessageLabels.js b/packages/volto/src/helpers/MessageLabels/MessageLabels.js index 043129be1c..640b00443d 100644 --- a/packages/volto/src/helpers/MessageLabels/MessageLabels.js +++ b/packages/volto/src/helpers/MessageLabels/MessageLabels.js @@ -380,11 +380,13 @@ export const messages = defineMessages({ defaultMessage: 'Error in the block field {errorField}.', }, startEventRange: { - id: 'Start event date happens later than the end event date', - defaultMessage: 'Event start date happens later than the event end date', + id: 'Event start date must be on or before {endDateValueOrEndFieldName}', + defaultMessage: + 'Event start date must be on or before {endDateValueOrEndFieldName}', }, endEventRange: { - id: 'End event date happens before than the start event date', - defaultMessage: 'Event end date happens before the event start date', + id: 'Event end date must be on or after {startDateValueOrStartFieldName}', + defaultMessage: + 'Event end date must be on or after {startDateValueOrStartFieldName}', }, }); From 79b9a0fbe8f70e549a4abd32c6beeba63669974d Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Tue, 23 Jul 2024 11:14:09 +0200 Subject: [PATCH 34/51] Add pattern validator --- packages/volto/src/config/validation.ts | 8 +++ .../FormValidation/FormValidation.test.js | 54 ++++++++++++++++++- .../src/helpers/FormValidation/validators.ts | 10 ++++ .../helpers/MessageLabels/MessageLabels.js | 4 ++ 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/packages/volto/src/config/validation.ts b/packages/volto/src/config/validation.ts index 6e7df47b29..15ce08c70f 100644 --- a/packages/volto/src/config/validation.ts +++ b/packages/volto/src/config/validation.ts @@ -12,6 +12,7 @@ import { hasUniqueItemsValidator, startEventDateRangeValidator, endEventDateRangeValidator, + patternValidator, } from '@plone/volto/helpers/FormValidation/validators'; const registerValidators = (config: ConfigType) => { @@ -29,6 +30,13 @@ const registerValidators = (config: ConfigType) => { method: maxLengthValidator, }); + config.registerUtility({ + name: 'pattern', + type: 'validator', + dependencies: { fieldType: 'string' }, + method: patternValidator, + }); + config.registerUtility({ name: 'minLength', type: 'validator', diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.test.js b/packages/volto/src/helpers/FormValidation/FormValidation.test.js index 635f6b7a57..47e646cf6e 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.test.js +++ b/packages/volto/src/helpers/FormValidation/FormValidation.test.js @@ -257,7 +257,7 @@ describe('FormValidation', () => { }); }); - it('default - min lenght', () => { + it('string - min lenght', () => { let newSchema = { properties: { ...schema.properties, @@ -283,7 +283,7 @@ describe('FormValidation', () => { }); }); - it('default - max lenght', () => { + it('string - max lenght', () => { let newSchema = { properties: { ...schema.properties, @@ -309,6 +309,56 @@ describe('FormValidation', () => { }); }); + it('string - pattern - Fail', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'password', + description: '', + pattern: '^[a-zA-Z0-9]*$', + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 'as#', + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.pattern.defaultMessage], + }); + }); + + it('string - pattern - Succeeds', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'password', + description: '', + pattern: '^[a-zA-Z0-9]*$', + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: 'asasd', + }, + formatMessage, + }), + ).toEqual({}); + }); + it('number - isNumber', () => { let newSchema = { properties: { diff --git a/packages/volto/src/helpers/FormValidation/validators.ts b/packages/volto/src/helpers/FormValidation/validators.ts index dd75998ef7..6d2e56a441 100644 --- a/packages/volto/src/helpers/FormValidation/validators.ts +++ b/packages/volto/src/helpers/FormValidation/validators.ts @@ -158,3 +158,13 @@ export const endEventDateRangeValidator = ({ }) : null; }; + +export const patternValidator = ({ + value, + field, + formatMessage, +}: Validator) => { + const regex = new RegExp(field.pattern); + const isValid = regex.test(value); + return !isValid ? formatMessage(messages.pattern) : null; +}; diff --git a/packages/volto/src/helpers/MessageLabels/MessageLabels.js b/packages/volto/src/helpers/MessageLabels/MessageLabels.js index 640b00443d..b0a6f3ab49 100644 --- a/packages/volto/src/helpers/MessageLabels/MessageLabels.js +++ b/packages/volto/src/helpers/MessageLabels/MessageLabels.js @@ -389,4 +389,8 @@ export const messages = defineMessages({ defaultMessage: 'Event end date must be on or after {startDateValueOrStartFieldName}', }, + pattern: { + id: 'The value does not match the pattern {pattern}', + defaultMessage: 'The value does not match the pattern {pattern}', + }, }); From 6cb3f2b99289f83c9f113036db140be8287e0310 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Tue, 23 Jul 2024 11:25:12 +0200 Subject: [PATCH 35/51] Implemented default `maxItems`/`minItems` --- packages/volto/src/config/validation.ts | 16 ++++++ .../FormValidation/FormValidation.test.js | 56 ++++++++++++++++++- .../src/helpers/FormValidation/validators.ts | 31 ++++++++++ .../helpers/MessageLabels/MessageLabels.js | 10 ++++ 4 files changed, 112 insertions(+), 1 deletion(-) diff --git a/packages/volto/src/config/validation.ts b/packages/volto/src/config/validation.ts index 15ce08c70f..74a17532ab 100644 --- a/packages/volto/src/config/validation.ts +++ b/packages/volto/src/config/validation.ts @@ -9,6 +9,8 @@ import { maximumValidator, minimumValidator, isIntegerValidator, + maxItemsValidator, + minItemsValidator, hasUniqueItemsValidator, startEventDateRangeValidator, endEventDateRangeValidator, @@ -107,6 +109,20 @@ const registerValidators = (config: ConfigType) => { method: maximumValidator, }); + config.registerUtility({ + name: 'maxItems', + type: 'validator', + dependencies: { fieldType: 'array' }, + method: maxItemsValidator, + }); + + config.registerUtility({ + name: 'minItems', + type: 'validator', + dependencies: { fieldType: 'array' }, + method: minItemsValidator, + }); + config.registerUtility({ name: 'uniqueItems', type: 'validator', diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.test.js b/packages/volto/src/helpers/FormValidation/FormValidation.test.js index 47e646cf6e..cc59661bfe 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.test.js +++ b/packages/volto/src/helpers/FormValidation/FormValidation.test.js @@ -569,6 +569,60 @@ describe('FormValidation', () => { }); }); + it('array - maxItems', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Array field', + type: 'array', + description: '', + maxItems: 1, + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: [1, 2], + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.maxItems.defaultMessage], + }); + }); + + it('array - minItems', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Array field', + type: 'array', + description: '', + minItems: 3, + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + customField: [1], + }, + formatMessage, + }), + ).toEqual({ + customField: [messages.minItems.defaultMessage], + }); + }); + it('array - uniqueItems', () => { let newSchema = { properties: { @@ -596,7 +650,7 @@ describe('FormValidation', () => { }); }); - it('array - uniqueItems false', () => { + it('array - uniqueItems - false', () => { let newSchema = { properties: { ...schema.properties, diff --git a/packages/volto/src/helpers/FormValidation/validators.ts b/packages/volto/src/helpers/FormValidation/validators.ts index 6d2e56a441..0771b77978 100644 --- a/packages/volto/src/helpers/FormValidation/validators.ts +++ b/packages/volto/src/helpers/FormValidation/validators.ts @@ -164,7 +164,38 @@ export const patternValidator = ({ field, formatMessage, }: Validator) => { + if (!field.pattern) { + return null; + } const regex = new RegExp(field.pattern); const isValid = regex.test(value); return !isValid ? formatMessage(messages.pattern) : null; }; + +export const maxItemsValidator = ({ + value, + field, + formatMessage, +}: Validator) => { + if (!field.maxItems) { + return null; + } + const isValid = Array.isArray(value) && value.length <= field.maxItems; + return !isValid + ? formatMessage(messages.maxItems, { maxItems: field.maxItems }) + : null; +}; + +export const minItemsValidator = ({ + value, + field, + formatMessage, +}: Validator) => { + if (!field.minItems) { + return null; + } + const isValid = Array.isArray(value) && value.length >= field.minItems; + return !isValid + ? formatMessage(messages.minItems, { minItems: field.minItems }) + : null; +}; diff --git a/packages/volto/src/helpers/MessageLabels/MessageLabels.js b/packages/volto/src/helpers/MessageLabels/MessageLabels.js index b0a6f3ab49..7549e09043 100644 --- a/packages/volto/src/helpers/MessageLabels/MessageLabels.js +++ b/packages/volto/src/helpers/MessageLabels/MessageLabels.js @@ -393,4 +393,14 @@ export const messages = defineMessages({ id: 'The value does not match the pattern {pattern}', defaultMessage: 'The value does not match the pattern {pattern}', }, + maxItems: { + id: 'The number of items must be less than or equal to {maxItems}', + defaultMessage: + 'The number of items must be less than or equal to {maxItems}', + }, + minItems: { + id: 'The number of items must be greater than or equal to {minItems}', + defaultMessage: + 'The number of items must be greater than or equal to {minItems}', + }, }); From 0999019e7a8a041c078649bdca56c12199c5f870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20de=20Alba?= Date: Wed, 24 Jul 2024 11:22:28 +0200 Subject: [PATCH 36/51] Update docs/source/configuration/validation.md Co-authored-by: David Glick --- docs/source/configuration/validation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md index e3e4bb74a5..12f67e7725 100644 --- a/docs/source/configuration/validation.md +++ b/docs/source/configuration/validation.md @@ -9,7 +9,7 @@ myst: # Client side form field validation -Volto provides a mechanism for delivering form field validation in an extensible way. +Volto provides an extensible way to validate form field values. This extensibility is based on the Volto registry. It applies to content types, custom programatically generated forms, and blocks schema settings. The mechanism serializes all of them according to the [JSON schema standard](https://json-schema.org/draft/2020-12/json-schema-validation). From e787a8ac936f34fa6a54aba25bf6e8867699f650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20de=20Alba?= Date: Wed, 24 Jul 2024 11:50:20 +0200 Subject: [PATCH 37/51] Apply suggestions from code review Co-authored-by: Steve Piercy --- docs/source/configuration/validation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md index 12f67e7725..95003f9a50 100644 --- a/docs/source/configuration/validation.md +++ b/docs/source/configuration/validation.md @@ -154,7 +154,7 @@ config.registerUtility({ Field `widget` validators are applied depending on the specified `widget` of the field. You should specify the `widget` either in the JSON schema of the block or as additional data in the content type definition. -The following example shows how to specify the `widget` either in the JSON schema of the block. +The following example shows how to specify the `widget` in the JSON schema of the block. ```ts let blockSchema = { @@ -183,7 +183,7 @@ config.registerUtility({ ``` The following example shows how to specify the `widget` in the content type definition in the schema hints in the backend using `frontendOptions`. -The validation engine will behave the same, applying the `urlValidator` method validator for the content type field `customField` in the previous example. +The validation engine will behave the same as in the JSON schema of the block, applying the `urlValidator` method validator for the content type field `customField` in the previous example. ```python from plone.supermodel import model From 44001cbd976f15f7db56d6aa84d2396d7c703520 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 24 Jul 2024 11:18:01 +0200 Subject: [PATCH 38/51] Added missing docs for utilities in types --- packages/types/news/6161.bugfix | 1 - packages/types/news/6161.feature | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 packages/types/news/6161.bugfix create mode 100644 packages/types/news/6161.feature diff --git a/packages/types/news/6161.bugfix b/packages/types/news/6161.bugfix deleted file mode 100644 index dc4ac6ff7c..0000000000 --- a/packages/types/news/6161.bugfix +++ /dev/null @@ -1 +0,0 @@ -Add `errors` shape to the `BlockEditProps`. @sneridagh diff --git a/packages/types/news/6161.feature b/packages/types/news/6161.feature new file mode 100644 index 0000000000..9b53b2fc4e --- /dev/null +++ b/packages/types/news/6161.feature @@ -0,0 +1,2 @@ +Added `errors` shape to the `BlockEditProps`. +Added typings for Utilities registry @sneridagh From aff001084aa4382a455eab9e59c6208c42ef2e2e Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 24 Jul 2024 11:19:36 +0200 Subject: [PATCH 39/51] Add initial sort for keys when hashing them into the depsString --- packages/registry/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index 68877bcea7..830202f797 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -433,6 +433,7 @@ class Config { throw new Error('No method provided'); } else { depsString = Object.keys(dependencies) + .sort() .map((key) => `${key}:${dependencies[key]}`) .join('+'); } From 443f9c76d1c67a31d9b4c99b8887861d32f250e6 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 24 Jul 2024 11:22:00 +0200 Subject: [PATCH 40/51] Fix out of date registry changelog --- packages/registry/news/6161.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/registry/news/6161.feature b/packages/registry/news/6161.feature index 75641a0fa7..3a6270005c 100644 --- a/packages/registry/news/6161.feature +++ b/packages/registry/news/6161.feature @@ -1 +1 @@ -Add `getComponents` that match a partial set of dependencies, given a name. @sneridagh +Added Utilities registry, `registerUtility`, `getUtility` and `getUtilities` @sneridagh From 01a14cf74fca4bade35e6c5452c5a8590b0b177b Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 24 Jul 2024 11:34:05 +0200 Subject: [PATCH 41/51] Include in the documentation all the default validators in Volto --- docs/source/configuration/validation.md | 35 ++++++++++++++++++++++++- packages/volto/src/config/validation.ts | 7 +++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md index 95003f9a50..a8e06c58cd 100644 --- a/docs/source/configuration/validation.md +++ b/docs/source/configuration/validation.md @@ -247,7 +247,40 @@ config.registerUtility({ ## Volto's default validators -Volto provides a set of validators by default. +Volto provides a set of validators by default: + +### Strings +- minLength +- maxLength +- pattern + +### Password +- minLength +- maxLength +- pattern + +### Numbers +- isNumber +- minimum +- maximum + +### Integers +- isInteger +- minimum +- maximum + +### Arrays +- maxItems +- minItems +- uniqueItems + +### Per widget +- email +- url + +### Event content type +- start/end dates check + You can find them in the module {file}`packages/volto/src/config/validators.ts`. diff --git a/packages/volto/src/config/validation.ts b/packages/volto/src/config/validation.ts index 74a17532ab..17961b6a06 100644 --- a/packages/volto/src/config/validation.ts +++ b/packages/volto/src/config/validation.ts @@ -53,6 +53,13 @@ const registerValidators = (config: ConfigType) => { method: maxLengthValidator, }); + config.registerUtility({ + name: 'pattern', + type: 'validator', + dependencies: { fieldType: 'password' }, + method: patternValidator, + }); + config.registerUtility({ name: 'email', type: 'validator', From f8b72876e747b4c53d2b31a9b5f1162821985a97 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 24 Jul 2024 11:39:24 +0200 Subject: [PATCH 42/51] Change the name of the dependency key from `widgetName` to `widget` --- docs/source/configuration/validation.md | 2 +- packages/volto/src/config/validation.ts | 4 ++-- packages/volto/src/helpers/FormValidation/FormValidation.jsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md index a8e06c58cd..b89a71c629 100644 --- a/docs/source/configuration/validation.md +++ b/docs/source/configuration/validation.md @@ -176,7 +176,7 @@ config.registerUtility({ type: 'validator', name: 'phoneNumber', dependencies: { - widgetName: 'phoneNumber', + widget: 'phoneNumber', }, method: phoneValidator, }) diff --git a/packages/volto/src/config/validation.ts b/packages/volto/src/config/validation.ts index 17961b6a06..5a92e004e9 100644 --- a/packages/volto/src/config/validation.ts +++ b/packages/volto/src/config/validation.ts @@ -63,14 +63,14 @@ const registerValidators = (config: ConfigType) => { config.registerUtility({ name: 'email', type: 'validator', - dependencies: { widgetName: 'email' }, + dependencies: { widget: 'email' }, method: emailValidator, }); config.registerUtility({ name: 'url', type: 'validator', - dependencies: { widgetName: 'url' }, + dependencies: { widget: 'url' }, method: urlValidator, }); diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.jsx b/packages/volto/src/helpers/FormValidation/FormValidation.jsx index 798ff93abd..f423f49d5a 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.jsx +++ b/packages/volto/src/helpers/FormValidation/FormValidation.jsx @@ -162,7 +162,7 @@ const validateFieldsPerFieldset = ( if (widgetName) { const widgetNameValidationCriteria = config.getUtilities({ type: 'validator', - dependencies: { widgetName }, + dependencies: { widget: widgetName }, }); widgetErrors = checkFieldErrors( From f7dee7e4f632988388c4bb8ab29f1b0f67a3a52d Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 24 Jul 2024 11:46:52 +0200 Subject: [PATCH 43/51] Remove granularity from the paragraph --- docs/source/configuration/validation.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md index b89a71c629..fad68dff03 100644 --- a/docs/source/configuration/validation.md +++ b/docs/source/configuration/validation.md @@ -107,8 +107,7 @@ The `urlValidator` method validator will be applied for the content type field ` ### Advanced scenarios -In case you need more granularity, and you don't have access to modify the existing implementation of the JSON schema definitions for existing content types, blocks, or forms, you can use the following advanced validator registrations, using `field`, `widget`, `behaviorName`, or `blockType` validator registrations. - +If, for some reason, you can't modify the existing implementation of the JSON schema definitions for existing content types, blocks, or forms, you can use the following advanced validator registrations, which allows you to register validators depending on the `field`, the `widget`, the `behaviorName` (in case of a content type), or the `blockType` (in case of a block). #### Field `type` validators From 2578cf5fcdf86202d40ccd47c2bba52c58c30134 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 24 Jul 2024 11:47:02 +0200 Subject: [PATCH 44/51] locales --- .../volto/locales/ca/LC_MESSAGES/volto.po | 35 +++++++++++++----- .../volto/locales/de/LC_MESSAGES/volto.po | 35 +++++++++++++----- .../volto/locales/en/LC_MESSAGES/volto.po | 35 +++++++++++++----- .../volto/locales/es/LC_MESSAGES/volto.po | 35 +++++++++++++----- .../volto/locales/eu/LC_MESSAGES/volto.po | 35 +++++++++++++----- .../volto/locales/fi/LC_MESSAGES/volto.po | 35 +++++++++++++----- .../volto/locales/fr/LC_MESSAGES/volto.po | 35 +++++++++++++----- .../volto/locales/hi/LC_MESSAGES/volto.po | 35 +++++++++++++----- .../volto/locales/it/LC_MESSAGES/volto.po | 35 +++++++++++++----- .../volto/locales/ja/LC_MESSAGES/volto.po | 35 +++++++++++++----- .../volto/locales/nl/LC_MESSAGES/volto.po | 35 +++++++++++++----- .../volto/locales/pt/LC_MESSAGES/volto.po | 35 +++++++++++++----- .../volto/locales/pt_BR/LC_MESSAGES/volto.po | 35 +++++++++++++----- .../volto/locales/ro/LC_MESSAGES/volto.po | 35 +++++++++++++----- packages/volto/locales/volto.pot | 37 +++++++++++++------ .../volto/locales/zh_CN/LC_MESSAGES/volto.po | 35 +++++++++++++----- 16 files changed, 401 insertions(+), 161 deletions(-) diff --git a/packages/volto/locales/ca/LC_MESSAGES/volto.po b/packages/volto/locales/ca/LC_MESSAGES/volto.po index 408c20eebb..97299c5c52 100644 --- a/packages/volto/locales/ca/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ca/LC_MESSAGES/volto.po @@ -1268,11 +1268,6 @@ msgstr "" msgid "End Date" msgstr "Data de finalització" -#. Default: "Event end date happens before the event start date" -#: helpers/MessageLabels/MessageLabels -msgid "End event date happens before than the start event date" -msgstr "" - #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -1360,11 +1355,21 @@ msgstr "" msgid "Event" msgstr "" +#. Default: "Event end date must be on or after {startDateValueOrStartFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event end date must be on or after {startDateValueOrStartFieldName}" +msgstr "" + #. Default: "Event listing" #: config/Views msgid "Event listing" msgstr "Llista d'esdeveniments" +#. Default: "Event start date must be on or before {endDateValueOrEndFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event start date must be on or before {endDateValueOrEndFieldName}" +msgstr "" + #. Default: "Event view" #: config/Views msgid "Event view" @@ -3442,11 +3447,6 @@ msgstr "Dividir" msgid "Start Date" msgstr "Data d'inici" -#. Default: "Event start date happens later than the event end date" -#: helpers/MessageLabels/MessageLabels -msgid "Start event date happens later than the end event date" -msgstr "" - #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" @@ -3675,6 +3675,16 @@ msgstr "No s'ha pogut suprimir l'element." msgid "The link address is:" msgstr "L'adreça de l'enllaç és:" +#. Default: "The number of items must be greater than or equal to {minItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be greater than or equal to {minItems}" +msgstr "" + +#. Default: "The number of items must be less than or equal to {maxItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be less than or equal to {maxItems}" +msgstr "" + #. Default: "The provided alternative url already exists!" #: components/manage/Aliases/Aliases msgid "The provided alternative url already exists!" @@ -3691,6 +3701,11 @@ msgstr "El procés de registre ha estat satisfactori. Si us plau, comproveu la v msgid "The site configuration is outdated and needs to be upgraded." msgstr "" +#. Default: "The value does not match the pattern {pattern}" +#: helpers/MessageLabels/MessageLabels +msgid "The value does not match the pattern {pattern}" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/de/LC_MESSAGES/volto.po b/packages/volto/locales/de/LC_MESSAGES/volto.po index c7960976dd..3ae526f295 100644 --- a/packages/volto/locales/de/LC_MESSAGES/volto.po +++ b/packages/volto/locales/de/LC_MESSAGES/volto.po @@ -1267,11 +1267,6 @@ msgstr "Aktiviert?" msgid "End Date" msgstr "Enddatum" -#. Default: "Event end date happens before the event start date" -#: helpers/MessageLabels/MessageLabels -msgid "End event date happens before than the start event date" -msgstr "" - #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -1359,11 +1354,21 @@ msgstr "Fehler" msgid "Event" msgstr "Ereignis" +#. Default: "Event end date must be on or after {startDateValueOrStartFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event end date must be on or after {startDateValueOrStartFieldName}" +msgstr "" + #. Default: "Event listing" #: config/Views msgid "Event listing" msgstr "Termine" +#. Default: "Event start date must be on or before {endDateValueOrEndFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event start date must be on or before {endDateValueOrEndFieldName}" +msgstr "" + #. Default: "Event view" #: config/Views msgid "Event view" @@ -3441,11 +3446,6 @@ msgstr "Aufsplitten" msgid "Start Date" msgstr "Anfangsdatum" -#. Default: "Event start date happens later than the event end date" -#: helpers/MessageLabels/MessageLabels -msgid "Start event date happens later than the end event date" -msgstr "" - #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" @@ -3674,6 +3674,16 @@ msgstr "Das Objekt kann nicht gelöscht werden." msgid "The link address is:" msgstr "Die Linkadresse lautet:" +#. Default: "The number of items must be greater than or equal to {minItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be greater than or equal to {minItems}" +msgstr "" + +#. Default: "The number of items must be less than or equal to {maxItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be less than or equal to {maxItems}" +msgstr "" + #. Default: "The provided alternative url already exists!" #: components/manage/Aliases/Aliases msgid "The provided alternative url already exists!" @@ -3690,6 +3700,11 @@ msgstr "Bitte prüfen Sie Ihr E-Mail Postfach. Sie sollten eine E-Mail erhalten msgid "The site configuration is outdated and needs to be upgraded." msgstr "Die Seitenkonfiguration ist veraltet und muss aktualisiert werden." +#. Default: "The value does not match the pattern {pattern}" +#: helpers/MessageLabels/MessageLabels +msgid "The value does not match the pattern {pattern}" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/en/LC_MESSAGES/volto.po b/packages/volto/locales/en/LC_MESSAGES/volto.po index c8451383be..309d263b01 100644 --- a/packages/volto/locales/en/LC_MESSAGES/volto.po +++ b/packages/volto/locales/en/LC_MESSAGES/volto.po @@ -1262,11 +1262,6 @@ msgstr "" msgid "End Date" msgstr "" -#. Default: "Event end date happens before the event start date" -#: helpers/MessageLabels/MessageLabels -msgid "End event date happens before than the start event date" -msgstr "" - #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -1354,11 +1349,21 @@ msgstr "" msgid "Event" msgstr "" +#. Default: "Event end date must be on or after {startDateValueOrStartFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event end date must be on or after {startDateValueOrStartFieldName}" +msgstr "" + #. Default: "Event listing" #: config/Views msgid "Event listing" msgstr "" +#. Default: "Event start date must be on or before {endDateValueOrEndFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event start date must be on or before {endDateValueOrEndFieldName}" +msgstr "" + #. Default: "Event view" #: config/Views msgid "Event view" @@ -3436,11 +3441,6 @@ msgstr "" msgid "Start Date" msgstr "" -#. Default: "Event start date happens later than the event end date" -#: helpers/MessageLabels/MessageLabels -msgid "Start event date happens later than the end event date" -msgstr "" - #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" @@ -3669,6 +3669,16 @@ msgstr "" msgid "The link address is:" msgstr "" +#. Default: "The number of items must be greater than or equal to {minItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be greater than or equal to {minItems}" +msgstr "" + +#. Default: "The number of items must be less than or equal to {maxItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be less than or equal to {maxItems}" +msgstr "" + #. Default: "The provided alternative url already exists!" #: components/manage/Aliases/Aliases msgid "The provided alternative url already exists!" @@ -3685,6 +3695,11 @@ msgstr "" msgid "The site configuration is outdated and needs to be upgraded." msgstr "" +#. Default: "The value does not match the pattern {pattern}" +#: helpers/MessageLabels/MessageLabels +msgid "The value does not match the pattern {pattern}" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/es/LC_MESSAGES/volto.po b/packages/volto/locales/es/LC_MESSAGES/volto.po index ebe5bbcc9f..bcd3892d27 100644 --- a/packages/volto/locales/es/LC_MESSAGES/volto.po +++ b/packages/volto/locales/es/LC_MESSAGES/volto.po @@ -1269,11 +1269,6 @@ msgstr "¿Activado?" msgid "End Date" msgstr "Fecha final" -#. Default: "Event end date happens before the event start date" -#: helpers/MessageLabels/MessageLabels -msgid "End event date happens before than the start event date" -msgstr "" - #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -1361,11 +1356,21 @@ msgstr "Error" msgid "Event" msgstr "Evento" +#. Default: "Event end date must be on or after {startDateValueOrStartFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event end date must be on or after {startDateValueOrStartFieldName}" +msgstr "" + #. Default: "Event listing" #: config/Views msgid "Event listing" msgstr "Listado de eventos" +#. Default: "Event start date must be on or before {endDateValueOrEndFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event start date must be on or before {endDateValueOrEndFieldName}" +msgstr "" + #. Default: "Event view" #: config/Views msgid "Event view" @@ -3443,11 +3448,6 @@ msgstr "División" msgid "Start Date" msgstr "Fecha de inicio" -#. Default: "Event start date happens later than the event end date" -#: helpers/MessageLabels/MessageLabels -msgid "Start event date happens later than the end event date" -msgstr "" - #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" @@ -3676,6 +3676,16 @@ msgstr "El elemento no se ha podido eliminar." msgid "The link address is:" msgstr "La dirección del enlace es:" +#. Default: "The number of items must be greater than or equal to {minItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be greater than or equal to {minItems}" +msgstr "" + +#. Default: "The number of items must be less than or equal to {maxItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be less than or equal to {maxItems}" +msgstr "" + #. Default: "The provided alternative url already exists!" #: components/manage/Aliases/Aliases msgid "The provided alternative url already exists!" @@ -3692,6 +3702,11 @@ msgstr "El registro fue exitoso. Por favor, verifique su bandeja de entrada para msgid "The site configuration is outdated and needs to be upgraded." msgstr "La configuración del sitio está anticuada y debe ser actualizada." +#. Default: "The value does not match the pattern {pattern}" +#: helpers/MessageLabels/MessageLabels +msgid "The value does not match the pattern {pattern}" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/eu/LC_MESSAGES/volto.po b/packages/volto/locales/eu/LC_MESSAGES/volto.po index bf229077d4..db6c019cb4 100644 --- a/packages/volto/locales/eu/LC_MESSAGES/volto.po +++ b/packages/volto/locales/eu/LC_MESSAGES/volto.po @@ -1269,11 +1269,6 @@ msgstr "Aktibatuta?" msgid "End Date" msgstr "Bukaera data" -#. Default: "Event end date happens before the event start date" -#: helpers/MessageLabels/MessageLabels -msgid "End event date happens before than the start event date" -msgstr "" - #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -1361,11 +1356,21 @@ msgstr "Errorea" msgid "Event" msgstr "Hitzordua" +#. Default: "Event end date must be on or after {startDateValueOrStartFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event end date must be on or after {startDateValueOrStartFieldName}" +msgstr "" + #. Default: "Event listing" #: config/Views msgid "Event listing" msgstr "Hitzorduen zerrenda" +#. Default: "Event start date must be on or before {endDateValueOrEndFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event start date must be on or before {endDateValueOrEndFieldName}" +msgstr "" + #. Default: "Event view" #: config/Views msgid "Event view" @@ -3443,11 +3448,6 @@ msgstr "Banatu" msgid "Start Date" msgstr "Hasiera-data" -#. Default: "Event start date happens later than the event end date" -#: helpers/MessageLabels/MessageLabels -msgid "Start event date happens later than the end event date" -msgstr "" - #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" @@ -3676,6 +3676,16 @@ msgstr "Elementua ezin izan da ezabatu." msgid "The link address is:" msgstr "Loturaren helbidea hauxe da:" +#. Default: "The number of items must be greater than or equal to {minItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be greater than or equal to {minItems}" +msgstr "" + +#. Default: "The number of items must be less than or equal to {maxItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be less than or equal to {maxItems}" +msgstr "" + #. Default: "The provided alternative url already exists!" #: components/manage/Aliases/Aliases msgid "The provided alternative url already exists!" @@ -3692,6 +3702,11 @@ msgstr "Izen-emate prozesua ondo egin duzu. Begiratu zure eposta, kontua aktibat msgid "The site configuration is outdated and needs to be upgraded." msgstr "Atariaren konfigurazioa zaharkituta dago eta eguneratu egin behar da." +#. Default: "The value does not match the pattern {pattern}" +#: helpers/MessageLabels/MessageLabels +msgid "The value does not match the pattern {pattern}" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/fi/LC_MESSAGES/volto.po b/packages/volto/locales/fi/LC_MESSAGES/volto.po index 683cfd39bf..a894243780 100644 --- a/packages/volto/locales/fi/LC_MESSAGES/volto.po +++ b/packages/volto/locales/fi/LC_MESSAGES/volto.po @@ -1267,11 +1267,6 @@ msgstr "Aktivoitu?" msgid "End Date" msgstr "Päättymispäivä" -#. Default: "Event end date happens before the event start date" -#: helpers/MessageLabels/MessageLabels -msgid "End event date happens before than the start event date" -msgstr "" - #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -1359,11 +1354,21 @@ msgstr "" msgid "Event" msgstr "Tapahtuma" +#. Default: "Event end date must be on or after {startDateValueOrStartFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event end date must be on or after {startDateValueOrStartFieldName}" +msgstr "" + #. Default: "Event listing" #: config/Views msgid "Event listing" msgstr "Tapahtumalista" +#. Default: "Event start date must be on or before {endDateValueOrEndFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event start date must be on or before {endDateValueOrEndFieldName}" +msgstr "" + #. Default: "Event view" #: config/Views msgid "Event view" @@ -3441,11 +3446,6 @@ msgstr "Halkaise" msgid "Start Date" msgstr "Aloituspäivä" -#. Default: "Event start date happens later than the event end date" -#: helpers/MessageLabels/MessageLabels -msgid "Start event date happens later than the end event date" -msgstr "" - #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" @@ -3674,6 +3674,16 @@ msgstr "Tätä kohdetta ei voitu poistaa." msgid "The link address is:" msgstr "Linkin osoite on:" +#. Default: "The number of items must be greater than or equal to {minItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be greater than or equal to {minItems}" +msgstr "" + +#. Default: "The number of items must be less than or equal to {maxItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be less than or equal to {maxItems}" +msgstr "" + #. Default: "The provided alternative url already exists!" #: components/manage/Aliases/Aliases msgid "The provided alternative url already exists!" @@ -3690,6 +3700,11 @@ msgstr "Rekisteröinti onnistui. Tarkista, saitko sähköpostiisi ohjeet käytt msgid "The site configuration is outdated and needs to be upgraded." msgstr "Sivuston konfiguraatio on vanhentunut ja se pitää päivittää." +#. Default: "The value does not match the pattern {pattern}" +#: helpers/MessageLabels/MessageLabels +msgid "The value does not match the pattern {pattern}" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/fr/LC_MESSAGES/volto.po b/packages/volto/locales/fr/LC_MESSAGES/volto.po index b9816e87a2..92c6836cb5 100644 --- a/packages/volto/locales/fr/LC_MESSAGES/volto.po +++ b/packages/volto/locales/fr/LC_MESSAGES/volto.po @@ -1269,11 +1269,6 @@ msgstr "Activé ?" msgid "End Date" msgstr "Date de fin" -#. Default: "Event end date happens before the event start date" -#: helpers/MessageLabels/MessageLabels -msgid "End event date happens before than the start event date" -msgstr "" - #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -1361,11 +1356,21 @@ msgstr "Erreur" msgid "Event" msgstr "Événement" +#. Default: "Event end date must be on or after {startDateValueOrStartFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event end date must be on or after {startDateValueOrStartFieldName}" +msgstr "" + #. Default: "Event listing" #: config/Views msgid "Event listing" msgstr "Liste des événements" +#. Default: "Event start date must be on or before {endDateValueOrEndFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event start date must be on or before {endDateValueOrEndFieldName}" +msgstr "" + #. Default: "Event view" #: config/Views msgid "Event view" @@ -3443,11 +3448,6 @@ msgstr "Divisé" msgid "Start Date" msgstr "Date de début" -#. Default: "Event start date happens later than the event end date" -#: helpers/MessageLabels/MessageLabels -msgid "Start event date happens later than the end event date" -msgstr "" - #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" @@ -3676,6 +3676,16 @@ msgstr "L'élement n'a pas pu être supprimé." msgid "The link address is:" msgstr "L'adresse du lien est :" +#. Default: "The number of items must be greater than or equal to {minItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be greater than or equal to {minItems}" +msgstr "" + +#. Default: "The number of items must be less than or equal to {maxItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be less than or equal to {maxItems}" +msgstr "" + #. Default: "The provided alternative url already exists!" #: components/manage/Aliases/Aliases msgid "The provided alternative url already exists!" @@ -3692,6 +3702,11 @@ msgstr "Le processus d'inscription a réussi. Veuillez vérifier votre boîte e- msgid "The site configuration is outdated and needs to be upgraded." msgstr "La configuration du site nécessite une mise à niveau." +#. Default: "The value does not match the pattern {pattern}" +#: helpers/MessageLabels/MessageLabels +msgid "The value does not match the pattern {pattern}" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/hi/LC_MESSAGES/volto.po b/packages/volto/locales/hi/LC_MESSAGES/volto.po index dc98703c6c..7acbc22305 100644 --- a/packages/volto/locales/hi/LC_MESSAGES/volto.po +++ b/packages/volto/locales/hi/LC_MESSAGES/volto.po @@ -1262,11 +1262,6 @@ msgstr "सक्षम?" msgid "End Date" msgstr "अंतिम तिथि" -#. Default: "Event end date happens before the event start date" -#: helpers/MessageLabels/MessageLabels -msgid "End event date happens before than the start event date" -msgstr "" - #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -1354,11 +1349,21 @@ msgstr "त्रुटि" msgid "Event" msgstr "आयोजन" +#. Default: "Event end date must be on or after {startDateValueOrStartFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event end date must be on or after {startDateValueOrStartFieldName}" +msgstr "" + #. Default: "Event listing" #: config/Views msgid "Event listing" msgstr "आयोजन सूची" +#. Default: "Event start date must be on or before {endDateValueOrEndFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event start date must be on or before {endDateValueOrEndFieldName}" +msgstr "" + #. Default: "Event view" #: config/Views msgid "Event view" @@ -3436,11 +3441,6 @@ msgstr "विभाजित करें" msgid "Start Date" msgstr "प्रारंभ तिथि" -#. Default: "Event start date happens later than the event end date" -#: helpers/MessageLabels/MessageLabels -msgid "Start event date happens later than the end event date" -msgstr "" - #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" @@ -3669,6 +3669,16 @@ msgstr "आइटम को हटाया नहीं जा सका।" msgid "The link address is:" msgstr "लिंक पता है:" +#. Default: "The number of items must be greater than or equal to {minItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be greater than or equal to {minItems}" +msgstr "" + +#. Default: "The number of items must be less than or equal to {maxItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be less than or equal to {maxItems}" +msgstr "" + #. Default: "The provided alternative url already exists!" #: components/manage/Aliases/Aliases msgid "The provided alternative url already exists!" @@ -3685,6 +3695,11 @@ msgstr "रजिस्ट्रेशन प्रक्रिया सफल msgid "The site configuration is outdated and needs to be upgraded." msgstr "साइट कॉन्फ़िगरेशन पुरानी है और अपग्रेड की आवश्यकता है।" +#. Default: "The value does not match the pattern {pattern}" +#: helpers/MessageLabels/MessageLabels +msgid "The value does not match the pattern {pattern}" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/it/LC_MESSAGES/volto.po b/packages/volto/locales/it/LC_MESSAGES/volto.po index a8bda3b015..11f9e88370 100644 --- a/packages/volto/locales/it/LC_MESSAGES/volto.po +++ b/packages/volto/locales/it/LC_MESSAGES/volto.po @@ -1262,11 +1262,6 @@ msgstr "Abilitato?" msgid "End Date" msgstr "Data di fine" -#. Default: "Event end date happens before the event start date" -#: helpers/MessageLabels/MessageLabels -msgid "End event date happens before than the start event date" -msgstr "" - #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -1354,11 +1349,21 @@ msgstr "Errore" msgid "Event" msgstr "Evento" +#. Default: "Event end date must be on or after {startDateValueOrStartFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event end date must be on or after {startDateValueOrStartFieldName}" +msgstr "" + #. Default: "Event listing" #: config/Views msgid "Event listing" msgstr "Elenco eventi" +#. Default: "Event start date must be on or before {endDateValueOrEndFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event start date must be on or before {endDateValueOrEndFieldName}" +msgstr "" + #. Default: "Event view" #: config/Views msgid "Event view" @@ -3436,11 +3441,6 @@ msgstr "Dividi" msgid "Start Date" msgstr "Data di inizio" -#. Default: "Event start date happens later than the event end date" -#: helpers/MessageLabels/MessageLabels -msgid "Start event date happens later than the end event date" -msgstr "" - #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" @@ -3669,6 +3669,16 @@ msgstr "L'elemento non può essere eliminato." msgid "The link address is:" msgstr "L'indirizzo del collegamento è:" +#. Default: "The number of items must be greater than or equal to {minItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be greater than or equal to {minItems}" +msgstr "" + +#. Default: "The number of items must be less than or equal to {maxItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be less than or equal to {maxItems}" +msgstr "" + #. Default: "The provided alternative url already exists!" #: components/manage/Aliases/Aliases msgid "The provided alternative url already exists!" @@ -3685,6 +3695,11 @@ msgstr "La registrazione è avvenuta correttamente. Per favore controlla la tua msgid "The site configuration is outdated and needs to be upgraded." msgstr "La configurazione del sito è obsoleta e deve essere aggiornata." +#. Default: "The value does not match the pattern {pattern}" +#: helpers/MessageLabels/MessageLabels +msgid "The value does not match the pattern {pattern}" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/ja/LC_MESSAGES/volto.po b/packages/volto/locales/ja/LC_MESSAGES/volto.po index b8b91b70dd..b9b6ceb98f 100644 --- a/packages/volto/locales/ja/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ja/LC_MESSAGES/volto.po @@ -1267,11 +1267,6 @@ msgstr "" msgid "End Date" msgstr "終了日付" -#. Default: "Event end date happens before the event start date" -#: helpers/MessageLabels/MessageLabels -msgid "End event date happens before than the start event date" -msgstr "" - #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -1359,11 +1354,21 @@ msgstr "" msgid "Event" msgstr "" +#. Default: "Event end date must be on or after {startDateValueOrStartFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event end date must be on or after {startDateValueOrStartFieldName}" +msgstr "" + #. Default: "Event listing" #: config/Views msgid "Event listing" msgstr "" +#. Default: "Event start date must be on or before {endDateValueOrEndFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event start date must be on or before {endDateValueOrEndFieldName}" +msgstr "" + #. Default: "Event view" #: config/Views msgid "Event view" @@ -3441,11 +3446,6 @@ msgstr "分割" msgid "Start Date" msgstr "開始日付" -#. Default: "Event start date happens later than the event end date" -#: helpers/MessageLabels/MessageLabels -msgid "Start event date happens later than the end event date" -msgstr "" - #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" @@ -3674,6 +3674,16 @@ msgstr "" msgid "The link address is:" msgstr "" +#. Default: "The number of items must be greater than or equal to {minItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be greater than or equal to {minItems}" +msgstr "" + +#. Default: "The number of items must be less than or equal to {maxItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be less than or equal to {maxItems}" +msgstr "" + #. Default: "The provided alternative url already exists!" #: components/manage/Aliases/Aliases msgid "The provided alternative url already exists!" @@ -3690,6 +3700,11 @@ msgstr "The registration process has been successful. Please check your e-mail i msgid "The site configuration is outdated and needs to be upgraded." msgstr "" +#. Default: "The value does not match the pattern {pattern}" +#: helpers/MessageLabels/MessageLabels +msgid "The value does not match the pattern {pattern}" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/nl/LC_MESSAGES/volto.po b/packages/volto/locales/nl/LC_MESSAGES/volto.po index f90db006ff..efdef3cd6f 100644 --- a/packages/volto/locales/nl/LC_MESSAGES/volto.po +++ b/packages/volto/locales/nl/LC_MESSAGES/volto.po @@ -1266,11 +1266,6 @@ msgstr "" msgid "End Date" msgstr "" -#. Default: "Event end date happens before the event start date" -#: helpers/MessageLabels/MessageLabels -msgid "End event date happens before than the start event date" -msgstr "" - #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -1358,11 +1353,21 @@ msgstr "" msgid "Event" msgstr "" +#. Default: "Event end date must be on or after {startDateValueOrStartFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event end date must be on or after {startDateValueOrStartFieldName}" +msgstr "" + #. Default: "Event listing" #: config/Views msgid "Event listing" msgstr "" +#. Default: "Event start date must be on or before {endDateValueOrEndFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event start date must be on or before {endDateValueOrEndFieldName}" +msgstr "" + #. Default: "Event view" #: config/Views msgid "Event view" @@ -3440,11 +3445,6 @@ msgstr "" msgid "Start Date" msgstr "" -#. Default: "Event start date happens later than the event end date" -#: helpers/MessageLabels/MessageLabels -msgid "Start event date happens later than the end event date" -msgstr "" - #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" @@ -3673,6 +3673,16 @@ msgstr "" msgid "The link address is:" msgstr "" +#. Default: "The number of items must be greater than or equal to {minItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be greater than or equal to {minItems}" +msgstr "" + +#. Default: "The number of items must be less than or equal to {maxItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be less than or equal to {maxItems}" +msgstr "" + #. Default: "The provided alternative url already exists!" #: components/manage/Aliases/Aliases msgid "The provided alternative url already exists!" @@ -3689,6 +3699,11 @@ msgstr "" msgid "The site configuration is outdated and needs to be upgraded." msgstr "" +#. Default: "The value does not match the pattern {pattern}" +#: helpers/MessageLabels/MessageLabels +msgid "The value does not match the pattern {pattern}" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/pt/LC_MESSAGES/volto.po b/packages/volto/locales/pt/LC_MESSAGES/volto.po index c4ba392540..d245b26dfb 100644 --- a/packages/volto/locales/pt/LC_MESSAGES/volto.po +++ b/packages/volto/locales/pt/LC_MESSAGES/volto.po @@ -1267,11 +1267,6 @@ msgstr "" msgid "End Date" msgstr "" -#. Default: "Event end date happens before the event start date" -#: helpers/MessageLabels/MessageLabels -msgid "End event date happens before than the start event date" -msgstr "" - #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -1359,11 +1354,21 @@ msgstr "" msgid "Event" msgstr "" +#. Default: "Event end date must be on or after {startDateValueOrStartFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event end date must be on or after {startDateValueOrStartFieldName}" +msgstr "" + #. Default: "Event listing" #: config/Views msgid "Event listing" msgstr "" +#. Default: "Event start date must be on or before {endDateValueOrEndFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event start date must be on or before {endDateValueOrEndFieldName}" +msgstr "" + #. Default: "Event view" #: config/Views msgid "Event view" @@ -3441,11 +3446,6 @@ msgstr "Dividir" msgid "Start Date" msgstr "" -#. Default: "Event start date happens later than the event end date" -#: helpers/MessageLabels/MessageLabels -msgid "Start event date happens later than the end event date" -msgstr "" - #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" @@ -3674,6 +3674,16 @@ msgstr "" msgid "The link address is:" msgstr "" +#. Default: "The number of items must be greater than or equal to {minItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be greater than or equal to {minItems}" +msgstr "" + +#. Default: "The number of items must be less than or equal to {maxItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be less than or equal to {maxItems}" +msgstr "" + #. Default: "The provided alternative url already exists!" #: components/manage/Aliases/Aliases msgid "The provided alternative url already exists!" @@ -3690,6 +3700,11 @@ msgstr "O processo de registo foi bem sucedido. Por favor verifique no seu e-mai msgid "The site configuration is outdated and needs to be upgraded." msgstr "" +#. Default: "The value does not match the pattern {pattern}" +#: helpers/MessageLabels/MessageLabels +msgid "The value does not match the pattern {pattern}" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po b/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po index e2d9d584cf..364922d608 100644 --- a/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po +++ b/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po @@ -1268,11 +1268,6 @@ msgstr "Ativada?" msgid "End Date" msgstr "Data Final" -#. Default: "Event end date happens before the event start date" -#: helpers/MessageLabels/MessageLabels -msgid "End event date happens before than the start event date" -msgstr "" - #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -1360,11 +1355,21 @@ msgstr "Erro" msgid "Event" msgstr "Evento" +#. Default: "Event end date must be on or after {startDateValueOrStartFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event end date must be on or after {startDateValueOrStartFieldName}" +msgstr "" + #. Default: "Event listing" #: config/Views msgid "Event listing" msgstr "Listagem de Evento" +#. Default: "Event start date must be on or before {endDateValueOrEndFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event start date must be on or before {endDateValueOrEndFieldName}" +msgstr "" + #. Default: "Event view" #: config/Views msgid "Event view" @@ -3442,11 +3447,6 @@ msgstr "Dividir" msgid "Start Date" msgstr "Data de Início" -#. Default: "Event start date happens later than the event end date" -#: helpers/MessageLabels/MessageLabels -msgid "Start event date happens later than the end event date" -msgstr "" - #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" @@ -3675,6 +3675,16 @@ msgstr "O item não pôde ser excluído." msgid "The link address is:" msgstr "O endereço do link é:" +#. Default: "The number of items must be greater than or equal to {minItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be greater than or equal to {minItems}" +msgstr "" + +#. Default: "The number of items must be less than or equal to {maxItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be less than or equal to {maxItems}" +msgstr "" + #. Default: "The provided alternative url already exists!" #: components/manage/Aliases/Aliases msgid "The provided alternative url already exists!" @@ -3691,6 +3701,11 @@ msgstr "O processo de registro foi bem sucedido. Verifique sua caixa de entrada msgid "The site configuration is outdated and needs to be upgraded." msgstr "A configuração do site está desatualizada e precisa ser atualizada." +#. Default: "The value does not match the pattern {pattern}" +#: helpers/MessageLabels/MessageLabels +msgid "The value does not match the pattern {pattern}" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/ro/LC_MESSAGES/volto.po b/packages/volto/locales/ro/LC_MESSAGES/volto.po index 2eb99f79f2..7c203620ef 100644 --- a/packages/volto/locales/ro/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ro/LC_MESSAGES/volto.po @@ -1262,11 +1262,6 @@ msgstr "" msgid "End Date" msgstr "Data de încheiere" -#. Default: "Event end date happens before the event start date" -#: helpers/MessageLabels/MessageLabels -msgid "End event date happens before than the start event date" -msgstr "" - #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -1354,11 +1349,21 @@ msgstr "" msgid "Event" msgstr "" +#. Default: "Event end date must be on or after {startDateValueOrStartFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event end date must be on or after {startDateValueOrStartFieldName}" +msgstr "" + #. Default: "Event listing" #: config/Views msgid "Event listing" msgstr "" +#. Default: "Event start date must be on or before {endDateValueOrEndFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event start date must be on or before {endDateValueOrEndFieldName}" +msgstr "" + #. Default: "Event view" #: config/Views msgid "Event view" @@ -3436,11 +3441,6 @@ msgstr "Împărțire" msgid "Start Date" msgstr "Data de început" -#. Default: "Event start date happens later than the event end date" -#: helpers/MessageLabels/MessageLabels -msgid "Start event date happens later than the end event date" -msgstr "" - #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" @@ -3669,6 +3669,16 @@ msgstr "Elementul nu a putut fi șters." msgid "The link address is:" msgstr "Adresa linkului este:" +#. Default: "The number of items must be greater than or equal to {minItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be greater than or equal to {minItems}" +msgstr "" + +#. Default: "The number of items must be less than or equal to {maxItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be less than or equal to {maxItems}" +msgstr "" + #. Default: "The provided alternative url already exists!" #: components/manage/Aliases/Aliases msgid "The provided alternative url already exists!" @@ -3685,6 +3695,11 @@ msgstr "Procesul de înregistrare a avut succes. Vă rugăm să verificați căs msgid "The site configuration is outdated and needs to be upgraded." msgstr "" +#. Default: "The value does not match the pattern {pattern}" +#: helpers/MessageLabels/MessageLabels +msgid "The value does not match the pattern {pattern}" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/volto.pot b/packages/volto/locales/volto.pot index cd651ef9f3..d38fea965b 100644 --- a/packages/volto/locales/volto.pot +++ b/packages/volto/locales/volto.pot @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: Plone\n" -"POT-Creation-Date: 2024-07-22T14:32:27.896Z\n" +"POT-Creation-Date: 2024-07-24T09:39:31.887Z\n" "Last-Translator: Plone i18n \n" "Language-Team: Plone i18n \n" "Content-Type: text/plain; charset=utf-8\n" @@ -1264,11 +1264,6 @@ msgstr "" msgid "End Date" msgstr "" -#. Default: "Event end date happens before the event start date" -#: helpers/MessageLabels/MessageLabels -msgid "End event date happens before than the start event date" -msgstr "" - #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -1356,11 +1351,21 @@ msgstr "" msgid "Event" msgstr "" +#. Default: "Event end date must be on or after {startDateValueOrStartFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event end date must be on or after {startDateValueOrStartFieldName}" +msgstr "" + #. Default: "Event listing" #: config/Views msgid "Event listing" msgstr "" +#. Default: "Event start date must be on or before {endDateValueOrEndFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event start date must be on or before {endDateValueOrEndFieldName}" +msgstr "" + #. Default: "Event view" #: config/Views msgid "Event view" @@ -3438,11 +3443,6 @@ msgstr "" msgid "Start Date" msgstr "" -#. Default: "Event start date happens later than the event end date" -#: helpers/MessageLabels/MessageLabels -msgid "Start event date happens later than the end event date" -msgstr "" - #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" @@ -3671,6 +3671,16 @@ msgstr "" msgid "The link address is:" msgstr "" +#. Default: "The number of items must be greater than or equal to {minItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be greater than or equal to {minItems}" +msgstr "" + +#. Default: "The number of items must be less than or equal to {maxItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be less than or equal to {maxItems}" +msgstr "" + #. Default: "The provided alternative url already exists!" #: components/manage/Aliases/Aliases msgid "The provided alternative url already exists!" @@ -3687,6 +3697,11 @@ msgstr "" msgid "The site configuration is outdated and needs to be upgraded." msgstr "" +#. Default: "The value does not match the pattern {pattern}" +#: helpers/MessageLabels/MessageLabels +msgid "The value does not match the pattern {pattern}" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" diff --git a/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po b/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po index 7f0beedde6..50d2927f3a 100644 --- a/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po +++ b/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po @@ -1268,11 +1268,6 @@ msgstr "启用?" msgid "End Date" msgstr "结束日期" -#. Default: "Event end date happens before the event start date" -#: helpers/MessageLabels/MessageLabels -msgid "End event date happens before than the start event date" -msgstr "" - #. Default: "Enter URL or select an item" #: components/manage/AnchorPlugin/components/LinkButton/AddLinkForm msgid "Enter URL or select an item" @@ -1360,11 +1355,21 @@ msgstr "" msgid "Event" msgstr "事件" +#. Default: "Event end date must be on or after {startDateValueOrStartFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event end date must be on or after {startDateValueOrStartFieldName}" +msgstr "" + #. Default: "Event listing" #: config/Views msgid "Event listing" msgstr "事件列表" +#. Default: "Event start date must be on or before {endDateValueOrEndFieldName}" +#: helpers/MessageLabels/MessageLabels +msgid "Event start date must be on or before {endDateValueOrEndFieldName}" +msgstr "" + #. Default: "Event view" #: config/Views msgid "Event view" @@ -3442,11 +3447,6 @@ msgstr "" msgid "Start Date" msgstr "开始日期" -#. Default: "Event start date happens later than the event end date" -#: helpers/MessageLabels/MessageLabels -msgid "Start event date happens later than the end event date" -msgstr "" - #. Default: "Start of the recurrence" #: components/manage/Widgets/RecurrenceWidget/Occurences msgid "Start of the recurrence" @@ -3675,6 +3675,16 @@ msgstr "此条目无法被删除。" msgid "The link address is:" msgstr "链接地址为:" +#. Default: "The number of items must be greater than or equal to {minItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be greater than or equal to {minItems}" +msgstr "" + +#. Default: "The number of items must be less than or equal to {maxItems}" +#: helpers/MessageLabels/MessageLabels +msgid "The number of items must be less than or equal to {maxItems}" +msgstr "" + #. Default: "The provided alternative url already exists!" #: components/manage/Aliases/Aliases msgid "The provided alternative url already exists!" @@ -3691,6 +3701,11 @@ msgstr "注册过程成功完成。请在您的电子邮箱中查看有关如何 msgid "The site configuration is outdated and needs to be upgraded." msgstr "网站配置已过时,需要升级。" +#. Default: "The value does not match the pattern {pattern}" +#: helpers/MessageLabels/MessageLabels +msgid "The value does not match the pattern {pattern}" +msgstr "" + #. Default: "The working copy was discarded" #: components/manage/Toolbar/More msgid "The working copy was discarded" From a2afa51f0c34b68abcb821d28992808313b12661 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 24 Jul 2024 11:57:46 +0200 Subject: [PATCH 45/51] Add an example of how to build an invariant --- docs/source/configuration/validation.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md index fad68dff03..ecd679d1d2 100644 --- a/docs/source/configuration/validation.md +++ b/docs/source/configuration/validation.md @@ -323,7 +323,21 @@ export const isNumber = ({ value, formatMessage }: Validator) => { Using `formData`, you can perform validation checks using other field data as source. This is useful when you want to validate two related fields, such as ensuring the end date of an event is after its start date. You can create invariant validator types. +In the following code snippet you can check how to create a validator method for checking if the Event content type `start`/`end` fields are valid: -```{todo} -Needs example. +```ts +export const startEventDateRangeValidator = ({ + value, + field, + formData, + formatMessage, +}: Validator) => { + const isValid = + value && formData.end && new Date(value) < new Date(formData.end); + return !isValid + ? formatMessage(messages.startEventRange, { + endDateValueOrEndFieldName: formData.end || 'end', + }) + : null; +}; ``` From 0b2c26e4273a59fe5ca230934df5031a427e7bab Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 24 Jul 2024 12:00:38 +0200 Subject: [PATCH 46/51] Move the default validators section to the top --- docs/source/configuration/validation.md | 86 ++++++++++++------------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md index ecd679d1d2..23d3a8a3de 100644 --- a/docs/source/configuration/validation.md +++ b/docs/source/configuration/validation.md @@ -16,6 +16,47 @@ The mechanism serializes all of them according to the [JSON schema standard](htt Finally Volto generates the form from the serialization. +(voltos-default-validators-label)= + +## Volto's default validators + +Volto provides a set of validators by default: + +### Strings +- minLength +- maxLength +- pattern + +### Password +- minLength +- maxLength +- pattern + +### Numbers +- isNumber +- minimum +- maximum + +### Integers +- isInteger +- minimum +- maximum + +### Arrays +- maxItems +- minItems +- uniqueItems + +### Per widget +- email +- url + +### Event content type +- start/end dates check + +You can find them in the module {file}`packages/volto/src/config/validators.ts`. + + ## Register a validator You can register a validator using the `registerUtility` method in the registry API from your add-on configuration. @@ -24,11 +65,7 @@ You can register a validator using the `registerUtility` method in the registry ### Register and declare a simple validator This section describes how to validate a field with a specific validator, a common use case. -Volto also provides some default validators. -```{seealso} -{ref}`voltos-default-validators-label` -``` #### Volto custom forms and block schema forms @@ -242,47 +279,6 @@ config.registerUtility({ ``` -(voltos-default-validators-label)= - -## Volto's default validators - -Volto provides a set of validators by default: - -### Strings -- minLength -- maxLength -- pattern - -### Password -- minLength -- maxLength -- pattern - -### Numbers -- isNumber -- minimum -- maximum - -### Integers -- isInteger -- minimum -- maximum - -### Arrays -- maxItems -- minItems -- uniqueItems - -### Per widget -- email -- url - -### Event content type -- start/end dates check - -You can find them in the module {file}`packages/volto/src/config/validators.ts`. - - ### Override a validator You can override a validator in your add-on in the same way as any other component defined in the registry. From c01bb39b4ae89e8eb90c9eb795620ca7ecabf333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20de=20Alba?= Date: Fri, 26 Jul 2024 11:31:36 +0200 Subject: [PATCH 47/51] Apply suggestions from code review Co-authored-by: Steve Piercy --- docs/source/configuration/validation.md | 7 ++++--- packages/registry/news/6161.feature | 2 +- packages/types/news/6161.feature | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md index 23d3a8a3de..75efa153bb 100644 --- a/docs/source/configuration/validation.md +++ b/docs/source/configuration/validation.md @@ -52,7 +52,7 @@ Volto provides a set of validators by default: - url ### Event content type -- start/end dates check +- event end date must be on or after its start date You can find them in the module {file}`packages/volto/src/config/validators.ts`. @@ -144,7 +144,8 @@ The `urlValidator` method validator will be applied for the content type field ` ### Advanced scenarios -If, for some reason, you can't modify the existing implementation of the JSON schema definitions for existing content types, blocks, or forms, you can use the following advanced validator registrations, which allows you to register validators depending on the `field`, the `widget`, the `behaviorName` (in case of a content type), or the `blockType` (in case of a block). +If, for some reason, you can't modify the existing implementation of the JSON schema definitions for existing content types, blocks, or forms, you can use the following advanced validator registrations. +These allow you to register validators according to whether it is a `field`, `widget`, `behaviorName` (for content types), or `blockType` (for blocks). #### Field `type` validators @@ -319,7 +320,7 @@ export const isNumber = ({ value, formatMessage }: Validator) => { Using `formData`, you can perform validation checks using other field data as source. This is useful when you want to validate two related fields, such as ensuring the end date of an event is after its start date. You can create invariant validator types. -In the following code snippet you can check how to create a validator method for checking if the Event content type `start`/`end` fields are valid: +The following code snippet shows how to create a validator method that ensures the event content type's end date is after its start date. ```ts export const startEventDateRangeValidator = ({ diff --git a/packages/registry/news/6161.feature b/packages/registry/news/6161.feature index 3a6270005c..005ffce826 100644 --- a/packages/registry/news/6161.feature +++ b/packages/registry/news/6161.feature @@ -1 +1 @@ -Added Utilities registry, `registerUtility`, `getUtility` and `getUtilities` @sneridagh +Added `Utilities` registry for `registerUtility`, `getUtility`, and `getUtilities`. @sneridagh diff --git a/packages/types/news/6161.feature b/packages/types/news/6161.feature index 9b53b2fc4e..c78c615f1d 100644 --- a/packages/types/news/6161.feature +++ b/packages/types/news/6161.feature @@ -1,2 +1,2 @@ Added `errors` shape to the `BlockEditProps`. -Added typings for Utilities registry @sneridagh +Added typings for `Utilities` registry. @sneridagh From f619cd815970b03aed189a34278c58b209574b3c Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Mon, 29 Jul 2024 12:53:53 +0200 Subject: [PATCH 48/51] Pass `blocksErrors` as a separate prop, aside from the whole errors object --- packages/coresandbox/src/components/Blocks/TestBlock/Data.tsx | 4 ++-- packages/types/src/blocks/index.d.ts | 3 ++- .../volto/src/components/manage/Blocks/Block/BlocksForm.jsx | 4 +++- packages/volto/src/components/manage/Form/Form.jsx | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/coresandbox/src/components/Blocks/TestBlock/Data.tsx b/packages/coresandbox/src/components/Blocks/TestBlock/Data.tsx index 0a89035bdd..0485429876 100644 --- a/packages/coresandbox/src/components/Blocks/TestBlock/Data.tsx +++ b/packages/coresandbox/src/components/Blocks/TestBlock/Data.tsx @@ -10,7 +10,7 @@ const TestBlockData = (props: BlockEditProps) => { data, navRoot, onChangeBlock, - errors, + blocksErrors, } = props; const intl = useIntl(); const schema = blocksConfig[data['@type']].blockSchema({ intl, props }); @@ -31,7 +31,7 @@ const TestBlockData = (props: BlockEditProps) => { blocksConfig={blocksConfig} navRoot={navRoot} contentType={contentType} - errors={errors} + errors={blocksErrors} /> ); }; diff --git a/packages/types/src/blocks/index.d.ts b/packages/types/src/blocks/index.d.ts index 5c40dff0d6..2c0a2e65de 100644 --- a/packages/types/src/blocks/index.d.ts +++ b/packages/types/src/blocks/index.d.ts @@ -116,5 +116,6 @@ export interface BlockEditProps { history: History; location: Location; token: string; - errors: Record; + errors: Record>; + blocksErrors: Record>>; } diff --git a/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx b/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx index 40e15faff8..65d3f6fb0d 100644 --- a/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx +++ b/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx @@ -59,6 +59,7 @@ const BlocksForm = (props) => { location, token, errors, + blocksErrors, } = props; const [isClient, setIsClient] = useState(false); @@ -282,7 +283,7 @@ const BlocksForm = (props) => { onDeleteBlock={onDeleteBlock} onSelectBlock={onSelectBlock} removable - errors={errors} + errors={blocksErrors} />
    , document.getElementById('sidebar-order'), @@ -357,6 +358,7 @@ const BlocksForm = (props) => { location, token, errors, + blocksErrors, }; return editBlockWrapper( dragProps, diff --git a/packages/volto/src/components/manage/Form/Form.jsx b/packages/volto/src/components/manage/Form/Form.jsx index 151a2b542b..8b108d7dde 100644 --- a/packages/volto/src/components/manage/Form/Form.jsx +++ b/packages/volto/src/components/manage/Form/Form.jsx @@ -793,7 +793,8 @@ class Form extends Component { history={this.props.history} location={this.props.location} token={this.props.token} - errors={this.state.errors.blocks} + errors={this.state.errors} + blocksErrors={this.state.errors.blocks} /> {this.state.isClient && this.state.sidebarMetadataIsAvailable && From 461b18806a5a8992d13ec0c6fc4448b9ba02c91a Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Mon, 29 Jul 2024 13:38:00 +0200 Subject: [PATCH 49/51] Add new `blocksErrors` prop to all stock blocks --- docs/source/upgrade-guide/index.md | 39 +++++++++++++++++++ .../manage/Blocks/Container/Data.jsx | 12 +++++- .../manage/Blocks/Image/ImageSidebar.jsx | 12 +++++- .../manage/Blocks/Listing/ListingData.jsx | 12 +++++- .../manage/Blocks/Maps/MapsSidebar.jsx | 4 +- .../manage/Blocks/Search/SearchBlockEdit.jsx | 2 + .../components/manage/Blocks/Teaser/Data.jsx | 12 +++++- .../src/components/manage/Blocks/ToC/Edit.jsx | 1 + .../manage/Blocks/Video/VideoSidebar.jsx | 4 +- 9 files changed, 88 insertions(+), 10 deletions(-) diff --git a/docs/source/upgrade-guide/index.md b/docs/source/upgrade-guide/index.md index ec349fc0bb..94e7353656 100644 --- a/docs/source/upgrade-guide/index.md +++ b/docs/source/upgrade-guide/index.md @@ -389,6 +389,45 @@ If you shadowed the module {file}`packages/volto/src/helpers/FormValidation/Form {doc}`../configuration/validation` ``` +### Field validation for blocks + +`BlockDataForm` component now gets a new prop `errors`. +This prop must be assigned with the new prop passed down from the blocks engine `blocksErrors`. +If not passed down, the block can't display any field validation error. + +```tsx +// More component code above here + + const { + block, + blocksConfig, + contentType, + data, + navRoot, + onChangeBlock, + blocksErrors, + } = props; + +return ( + { + onChangeBlock(block, { + ...data, + [id]: value, + }); + }} + onChangeBlock={onChangeBlock} + formData={data} + blocksConfig={blocksConfig} + navRoot={navRoot} + contentType={contentType} + errors={blocksErrors} + /> +) +``` ### `SchemaWidget` widget registration change diff --git a/packages/volto/src/components/manage/Blocks/Container/Data.jsx b/packages/volto/src/components/manage/Blocks/Container/Data.jsx index d9b24bf6e0..83e75b469c 100644 --- a/packages/volto/src/components/manage/Blocks/Container/Data.jsx +++ b/packages/volto/src/components/manage/Blocks/Container/Data.jsx @@ -2,8 +2,15 @@ import { useIntl } from 'react-intl'; import { BlockDataForm } from '@plone/volto/components/manage/Form'; const ContainerData = (props) => { - const { block, blocksConfig, data, onChangeBlock, navRoot, contentType } = - props; + const { + block, + blocksConfig, + blocksErrors, + data, + onChangeBlock, + navRoot, + contentType, + } = props; const intl = useIntl(); const schema = blocksConfig[data['@type']].blockSchema({ intl }); @@ -28,6 +35,7 @@ const ContainerData = (props) => { blocksConfig={blocksConfig} navRoot={navRoot} contentType={contentType} + errors={blocksErrors} /> ); }; diff --git a/packages/volto/src/components/manage/Blocks/Image/ImageSidebar.jsx b/packages/volto/src/components/manage/Blocks/Image/ImageSidebar.jsx index 94a43e1d3c..dd35733475 100644 --- a/packages/volto/src/components/manage/Blocks/Image/ImageSidebar.jsx +++ b/packages/volto/src/components/manage/Blocks/Image/ImageSidebar.jsx @@ -10,8 +10,15 @@ import imageSVG from '@plone/volto/icons/image.svg'; import trashSVG from '@plone/volto/icons/delete.svg'; const ImageSidebar = (props) => { - const { blocksConfig, data, block, onChangeBlock, navRoot, contentType } = - props; + const { + blocksConfig, + blocksErrors, + data, + block, + onChangeBlock, + navRoot, + contentType, + } = props; const intl = useIntl(); const schema = ImageSchema({ formData: data, intl }); return ( @@ -98,6 +105,7 @@ const ImageSidebar = (props) => { blocksConfig={blocksConfig} navRoot={navRoot} contentType={contentType} + errors={blocksErrors} /> ); diff --git a/packages/volto/src/components/manage/Blocks/Listing/ListingData.jsx b/packages/volto/src/components/manage/Blocks/Listing/ListingData.jsx index a531009b3f..f19ba3ff3e 100644 --- a/packages/volto/src/components/manage/Blocks/Listing/ListingData.jsx +++ b/packages/volto/src/components/manage/Blocks/Listing/ListingData.jsx @@ -4,8 +4,15 @@ import { useIntl } from 'react-intl'; import { BlockDataForm } from '@plone/volto/components/manage/Form'; const ListingData = (props) => { - const { data, block, blocksConfig, onChangeBlock, navRoot, contentType } = - props; + const { + data, + block, + blocksConfig, + blocksErrors, + onChangeBlock, + navRoot, + contentType, + } = props; const intl = useIntl(); const schema = blocksConfig.listing.blockSchema({ ...props, @@ -28,6 +35,7 @@ const ListingData = (props) => { block={block} navRoot={navRoot} contentType={contentType} + errors={blocksErrors} /> ); }; diff --git a/packages/volto/src/components/manage/Blocks/Maps/MapsSidebar.jsx b/packages/volto/src/components/manage/Blocks/Maps/MapsSidebar.jsx index 1bc2179f0b..977b7dc870 100644 --- a/packages/volto/src/components/manage/Blocks/Maps/MapsSidebar.jsx +++ b/packages/volto/src/components/manage/Blocks/Maps/MapsSidebar.jsx @@ -18,7 +18,8 @@ const messages = defineMessages({ }); const MapsSidebar = (props) => { - const { data, block, onChangeBlock, navRoot, contentType } = props; + const { data, block, blocksErrors, onChangeBlock, navRoot, contentType } = + props; const intl = useIntl(); const schema = MapsSchema({ ...props, intl }); @@ -44,6 +45,7 @@ const MapsSidebar = (props) => { block={block} navRoot={navRoot} contentType={contentType} + errors={blocksErrors} /> )} diff --git a/packages/volto/src/components/manage/Blocks/Search/SearchBlockEdit.jsx b/packages/volto/src/components/manage/Blocks/Search/SearchBlockEdit.jsx index 184ee3579b..56ac5769e4 100644 --- a/packages/volto/src/components/manage/Blocks/Search/SearchBlockEdit.jsx +++ b/packages/volto/src/components/manage/Blocks/Search/SearchBlockEdit.jsx @@ -23,6 +23,7 @@ const messages = defineMessages({ const SearchBlockEdit = (props) => { const { block, + blocksErrors, onChangeBlock, data, selected, @@ -94,6 +95,7 @@ const SearchBlockEdit = (props) => { formData={data} navRoot={navRoot} contentType={contentType} + errors={blocksErrors} /> diff --git a/packages/volto/src/components/manage/Blocks/Teaser/Data.jsx b/packages/volto/src/components/manage/Blocks/Teaser/Data.jsx index f57bf762a5..69bb3db42a 100644 --- a/packages/volto/src/components/manage/Blocks/Teaser/Data.jsx +++ b/packages/volto/src/components/manage/Blocks/Teaser/Data.jsx @@ -31,8 +31,15 @@ const messages = defineMessages({ }); const TeaserData = (props) => { - const { block, blocksConfig, data, onChangeBlock, navRoot, contentType } = - props; + const { + block, + blocksConfig, + blocksErrors, + data, + onChangeBlock, + navRoot, + contentType, + } = props; const dispatch = useDispatch(); const intl = useIntl(); @@ -161,6 +168,7 @@ const TeaserData = (props) => { actionButton={data.overwrite && ActionButton} navRoot={navRoot} contentType={contentType} + errors={blocksErrors} /> ); }; diff --git a/packages/volto/src/components/manage/Blocks/ToC/Edit.jsx b/packages/volto/src/components/manage/Blocks/ToC/Edit.jsx index d9c68c6ca4..dc369719a6 100644 --- a/packages/volto/src/components/manage/Blocks/ToC/Edit.jsx +++ b/packages/volto/src/components/manage/Blocks/ToC/Edit.jsx @@ -29,6 +29,7 @@ class Edit extends Component { block={this.props.block} navRoot={this.props.navRoot} contentType={this.props.contentType} + errors={this.props.blocksErrors} /> diff --git a/packages/volto/src/components/manage/Blocks/Video/VideoSidebar.jsx b/packages/volto/src/components/manage/Blocks/Video/VideoSidebar.jsx index 1a5b110f44..bec7228200 100644 --- a/packages/volto/src/components/manage/Blocks/Video/VideoSidebar.jsx +++ b/packages/volto/src/components/manage/Blocks/Video/VideoSidebar.jsx @@ -18,7 +18,8 @@ const messages = defineMessages({ }); const VideoSidebar = (props) => { - const { data, block, onChangeBlock, navRoot, contentType } = props; + const { data, block, blocksErrors, onChangeBlock, navRoot, contentType } = + props; const intl = useIntl(); const schema = VideoBlockSchema({ ...props, intl }); @@ -44,6 +45,7 @@ const VideoSidebar = (props) => { block={block} navRoot={navRoot} contentType={contentType} + errors={blocksErrors} /> )} From 5f32593d473b0c13823ff81a57f677818f0ccbff Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Mon, 29 Jul 2024 13:47:25 +0200 Subject: [PATCH 50/51] Remove the optional from Validator type, since the engine always push for it --- docs/source/configuration/validation.md | 2 +- packages/volto/src/helpers/FormValidation/validators.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/validation.md b/docs/source/configuration/validation.md index 75efa153bb..c76e95e42d 100644 --- a/docs/source/configuration/validation.md +++ b/docs/source/configuration/validation.md @@ -297,7 +297,7 @@ type Validator = { // The field schema definition object field: Record; // The form data - formData?: any; + formData: any; // The intl formatMessage function formatMessage: Function; }; diff --git a/packages/volto/src/helpers/FormValidation/validators.ts b/packages/volto/src/helpers/FormValidation/validators.ts index 0771b77978..97d6af7544 100644 --- a/packages/volto/src/helpers/FormValidation/validators.ts +++ b/packages/volto/src/helpers/FormValidation/validators.ts @@ -11,7 +11,7 @@ type MinMaxValidator = { type Validator = { value: string; field: Record; - formData?: any; + formData: any; formatMessage: Function; }; From d50cda024c5bde3a2a3cf9db8d01db6179287757 Mon Sep 17 00:00:00 2001 From: Tisha Soumya Date: Tue, 30 Jul 2024 20:23:29 +0530 Subject: [PATCH 51/51] Cypress Test for Validation of Field Types in CoreSandBox (#6217) Co-authored-by: Victor Fernandez de Alba --- .../src/components/Blocks/FormBlock/Data.tsx | 31 ++++ .../src/components/Blocks/FormBlock/Edit.tsx | 122 +++++++++++++ .../src/components/Blocks/FormBlock/View.tsx | 9 + .../src/components/Blocks/FormBlock/schema.ts | 19 +++ .../coresandbox/src/components/TestForm.jsx | 137 +++++++++++++++ packages/coresandbox/src/index.ts | 29 +++- .../tests/coresandbox/fieldTypeValidation.js | 160 ++++++++++++++++++ .../tests/coresandbox/formBlockValidation.js | 109 ++++++++++++ .../coresandbox/formFieldTypeValidation.js | 103 +++++++++++ packages/volto/news/6217.internal | 1 + .../FormValidation/FormValidation.test.js | 55 +++++- .../src/helpers/FormValidation/validators.ts | 10 +- 12 files changed, 778 insertions(+), 7 deletions(-) create mode 100644 packages/coresandbox/src/components/Blocks/FormBlock/Data.tsx create mode 100644 packages/coresandbox/src/components/Blocks/FormBlock/Edit.tsx create mode 100644 packages/coresandbox/src/components/Blocks/FormBlock/View.tsx create mode 100644 packages/coresandbox/src/components/Blocks/FormBlock/schema.ts create mode 100644 packages/coresandbox/src/components/TestForm.jsx create mode 100644 packages/volto/cypress/tests/coresandbox/fieldTypeValidation.js create mode 100644 packages/volto/cypress/tests/coresandbox/formBlockValidation.js create mode 100644 packages/volto/cypress/tests/coresandbox/formFieldTypeValidation.js create mode 100644 packages/volto/news/6217.internal diff --git a/packages/coresandbox/src/components/Blocks/FormBlock/Data.tsx b/packages/coresandbox/src/components/Blocks/FormBlock/Data.tsx new file mode 100644 index 0000000000..a0f7ce8d68 --- /dev/null +++ b/packages/coresandbox/src/components/Blocks/FormBlock/Data.tsx @@ -0,0 +1,31 @@ +import { useIntl } from 'react-intl'; +import { BlockDataForm } from '@plone/volto/components/manage/Form'; +import type { BlockEditProps } from '@plone/types'; + +const FormBlockData = (props: BlockEditProps) => { + const { block, blocksConfig, contentType, data, navRoot, onChangeBlock } = + props; + const intl = useIntl(); + const schema = blocksConfig[data['@type']].blockSchema({ intl, props }); + + return ( + { + onChangeBlock(block, { + ...data, + [id]: value, + }); + }} + onChangeBlock={onChangeBlock} + formData={data} + blocksConfig={blocksConfig} + navRoot={navRoot} + contentType={contentType} + /> + ); +}; + +export default FormBlockData; diff --git a/packages/coresandbox/src/components/Blocks/FormBlock/Edit.tsx b/packages/coresandbox/src/components/Blocks/FormBlock/Edit.tsx new file mode 100644 index 0000000000..225b91ae4f --- /dev/null +++ b/packages/coresandbox/src/components/Blocks/FormBlock/Edit.tsx @@ -0,0 +1,122 @@ +import { SidebarPortal } from '@plone/volto/components'; +import Data from './Data'; +import type { BlockEditProps } from '@plone/types'; +import { Helmet } from '@plone/volto/helpers'; + +import { defineMessages, useIntl } from 'react-intl'; +import { Container } from 'semantic-ui-react'; +import { Form } from '@plone/volto/components/manage/Form'; + +const messages = defineMessages({ + testForm: { + id: 'Test Form', + defaultMessage: 'Test Form', + }, + default: { + id: 'Default', + defaultMessage: 'Default', + }, + textlineTitle: { + id: 'Title', + defaultMessage: 'Title', + }, + emailTitle: { + id: 'Email', + defaultMessage: 'Email', + }, + + urlTitle: { + id: 'URL', + defaultMessage: 'Enter URL', + }, + + datetimeTitle: { + id: 'Date/Time', + defaultMessage: 'Enter Date/Time', + }, + + idTitle: { + id: 'Id', + defaultMessage: 'Enter ID', + }, + + richTextTitle: { + id: 'RichText', + defaultMessage: 'Enter RichText', + }, + + PasswordTitle: { + id: 'password', + defaultMessage: 'Password', + }, +}); +const FormBlockEdit = (props: BlockEditProps) => { + const intl = useIntl(); + const { selected } = props; + + return ( + <> +

    + Form Block +

    + + +
    + + + + + + ); +}; + +export default FormBlockEdit; diff --git a/packages/coresandbox/src/components/Blocks/FormBlock/View.tsx b/packages/coresandbox/src/components/Blocks/FormBlock/View.tsx new file mode 100644 index 0000000000..3d44816ed0 --- /dev/null +++ b/packages/coresandbox/src/components/Blocks/FormBlock/View.tsx @@ -0,0 +1,9 @@ +const FormBlockView = () => { + return ( +
    +
    Form Block View
    +
    + ); +}; + +export default FormBlockView; diff --git a/packages/coresandbox/src/components/Blocks/FormBlock/schema.ts b/packages/coresandbox/src/components/Blocks/FormBlock/schema.ts new file mode 100644 index 0000000000..3903ac86c2 --- /dev/null +++ b/packages/coresandbox/src/components/Blocks/FormBlock/schema.ts @@ -0,0 +1,19 @@ +import type { BlockConfigBase } from '@plone/types'; + +export const formBlockSchema: BlockConfigBase['blockSchema'] = ({ intl }) => ({ + title: 'form Block', + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: ['title'], + }, + ], + properties: { + title: { + widget: 'textLine', + title: 'Title', + }, + }, + required: [], +}); diff --git a/packages/coresandbox/src/components/TestForm.jsx b/packages/coresandbox/src/components/TestForm.jsx new file mode 100644 index 0000000000..23fa15787e --- /dev/null +++ b/packages/coresandbox/src/components/TestForm.jsx @@ -0,0 +1,137 @@ +import React from 'react'; +import { Helmet } from '@plone/volto/helpers'; + +import { defineMessages, useIntl } from 'react-intl'; +import { Container } from 'semantic-ui-react'; +import { Form } from '@plone/volto/components/manage/Form'; + +const messages = defineMessages({ + testForm: { + id: 'Test Form', + defaultMessage: 'Test Form', + }, + default: { + id: 'Default', + defaultMessage: 'Default', + }, + textlineTitle: { + id: 'Title', + defaultMessage: 'Title', + }, + emailTitle: { + id: 'Email', + defaultMessage: 'Email', + }, + + urlTitle: { + id: 'URL', + defaultMessage: 'Enter URL', + }, + + datetimeTitle: { + id: 'Date/Time', + defaultMessage: 'Enter Date/Time', + }, + + idTitle: { + id: 'Id', + defaultMessage: 'Enter ID', + }, + + linkTitle: { + id: 'Link', + defaultMessage: 'Link to Document/Event/News', + }, + + linkDescription: { + id: 'Enter Link', + defaultMessage: 'Enter Link', + }, + + richTextTitle: { + id: 'RichText', + defaultMessage: 'Enter RichText', + }, + + PasswordTitle: { + id: 'password', + defaultMessage: 'Password', + }, +}); + +const TestForm = (props) => { + const intl = useIntl(); + /** + * Cancel handler + * @method onCancel + * @returns {undefined} + */ + const onCancel = () => { + props.history.goBack(); + }; + + return ( + + + + + ); +}; + +export default TestForm; diff --git a/packages/coresandbox/src/index.ts b/packages/coresandbox/src/index.ts index 264be43d9b..bad373a9b0 100644 --- a/packages/coresandbox/src/index.ts +++ b/packages/coresandbox/src/index.ts @@ -15,7 +15,10 @@ import type { ConfigType } from '@plone/registry'; import SlotComponentTest from './components/Slots/SlotTest'; import { ContentTypeCondition } from '@plone/volto/helpers'; import { RouteCondition } from '@plone/volto/helpers/Slots'; - +import TestForm from './components/TestForm'; +import FormBlockView from './components/Blocks/FormBlock/View'; +import FormBlockEdit from './components/Blocks/FormBlock/Edit'; +import { formBlockSchema } from './components/Blocks/FormBlock/schema'; const testBlock: BlockConfigBase = { id: 'testBlock', title: 'testBlock', @@ -133,6 +136,20 @@ const testBlockDefaultView: BlockConfigBase = { ], extensions: {}, }; +const testformBlock: BlockConfigBase = { + id: 'testformBlock', + title: 'Form Block', + icon: codeSVG, + group: 'common', + view: FormBlockView, + edit: FormBlockEdit, + blockSchema: formBlockSchema, + restricted: false, + mostUsed: true, + sidebarTab: 1, + + extensions: {}, +}; const listing = (config: ConfigType) => { return { @@ -177,10 +194,19 @@ declare module '@plone/types' { testBlockMultipleFieldsets: BlockConfigBase; testBlockDefaultEdit: BlockConfigBase; testBlockDefaultView: BlockConfigBase; + testformBlock: BlockConfigBase; } } const applyConfig = (config: ConfigType) => { + config.addonRoutes = [ + ...config.addonRoutes, + { + path: '/form', + component: TestForm, + exact: false, + }, + ]; config.blocks.blocksConfig.testBlock = testBlock; config.blocks.blocksConfig.inputBlock = inputBlock; config.blocks.blocksConfig.testBlockConditional = testBlockConditional; @@ -190,6 +216,7 @@ const applyConfig = (config: ConfigType) => { testBlockMultipleFieldsets; config.blocks.blocksConfig.testBlockDefaultEdit = testBlockDefaultEdit; config.blocks.blocksConfig.testBlockDefaultView = testBlockDefaultView; + config.blocks.blocksConfig.testformBlock = testformBlock; config.blocks.blocksConfig.listing = listing(config); config.views.contentTypesViews.Folder = NewsAndEvents; diff --git a/packages/volto/cypress/tests/coresandbox/fieldTypeValidation.js b/packages/volto/cypress/tests/coresandbox/fieldTypeValidation.js new file mode 100644 index 0000000000..e97c7bd132 --- /dev/null +++ b/packages/volto/cypress/tests/coresandbox/fieldTypeValidation.js @@ -0,0 +1,160 @@ +context('Test field types in example content', () => { + describe('Test', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/@types/example').as('schema'); + cy.intercept('POST', '/**/').as('create'); + + // given a logged in editor and a page in edit mode + cy.autologin(); + cy.visit('/'); + cy.wait('@content'); + + // We always add a new example content type, fill the required + cy.navigate('/add?type=example'); + cy.wait('@schema'); + + cy.get('#field-title').type('An Example'); + }); + + it('Test Email field by entering email address without a domain', function () { + cy.get('#field-email_field').type('plone'); + cy.findAllByText('Email field').click(); + + cy.get('.form-error-label') + .contains('Input must be valid email (something@domain.com)') + .should('be.visible'); + cy.get('#toolbar-save').click(); + cy.wait(100); + cy.get('.toast-inner-content') + .contains('Input must be valid email (something@domain.com)') + .should('be.visible'); + }); + + it('Test Text Field', function () { + cy.get('#field-description').type('Volto Coresandbox fixture'); + cy.findAllByText('Description (Textline)').click(); + cy.get('.form-error-label').should('not.exist'); + cy.get('#toolbar-save').click(); + cy.wait(100); + cy.get('.toast-inner-content').should('not.exist'); + }); + + it('Test Integer & Float Field', function () { + cy.findByText('Number fields').click(); + cy.wait(500); + cy.get('#field-int_field').type('121'); + cy.get('#field-float_field').type('121.121'); + cy.findAllByText('Integer Field (e.g. 12)').click(); + cy.get('.form-error-label').should('not.exist'); + cy.get('#toolbar-save').click(); + cy.wait(100); + cy.get('.toast-inner-content').should('not.exist'); + }); + it('Test Date & Time Field', function () { + const date = new Date(); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + + const currentTime = date.toLocaleTimeString([], { + hour: 'numeric', + minute: 'numeric', + hour12: true, + }); + + cy.findByText('Date and time fields').click(); + cy.wait(100); + //Date + cy.get('#datetime_field-date').type(`${month}/${day}/${year}`); + cy.get('#datetime_field-date').should( + 'have.value', + `${month}/${day}/${year}`, + ); + + //Time + cy.get('#datetime_field-time').type(`${currentTime} `); + cy.get('#datetime_field-time').should('have.value', `${currentTime}`); + + cy.get('.form-error-label').should('not.exist'); + cy.get('#toolbar-save').click(); + cy.wait(100); + cy.get('.toast-inner-content').should('not.exist'); + }); + + it('Test List Field', function () { + cy.findAllByText('Choice and Multiple Choice fields').click(); + cy.wait(500); // We allow the Select component to lazy load + + cy.get('.react-select__placeholder') + .should('be.visible') + .contains('Select'); + + // We select the choice 'Beginner' of the field and remove it + cy.get('#field-list_field').click(); + cy.findAllByText('Beginner').click(); + cy.get( + '#field-list_field > .react-select__control > .react-select__value-container > .react-select__multi-value', + ) + .first('Beginner') + .get('.react-select__multi-value__remove') + .click(); + + // We select the choice 'Advanced' of the field + cy.get('#field-list_field').click(); + cy.findAllByText('Advanced').click(); + cy.get( + '#field-list_field > .react-select__control > .react-select__value-container > .react-select__multi-value', + ).should('have.text', 'Advanced'); + + cy.get('.form-error-label').should('not.exist'); + cy.get('#toolbar-save').click(); + cy.wait(100); + cy.get('.toast-inner-content').should('not.exist'); + }); + + it('Test Relationship Field', function () { + cy.findAllByText('Relation fields').click(); + cy.wait(500); // We allow the Select component to lazy load + cy.get('.react-select__placeholder') + .should('be.visible') + .contains('Select'); + + // We select the choice 'Beginner' of the field and remove it + + cy.get('#field-relationchoice_field > .react-select__control ') + .click() + .get('.react-select__menu-list > #react-select-6-option-4') + .click(); + + cy.wait(100); + cy.get('.form-error-label').should('not.exist'); + cy.get('#toolbar-save').click(); + cy.wait(100); + cy.get('.toast-inner-content').should('not.exist'); + cy.wait('@create'); + cy.wait('@content'); + cy.get('.relation').should('have.attr', 'href'); + }); + //relation widget Folder private + + it('Test URI Field by entering invalid URI', function () { + cy.findAllByText('Other fields').click(); + cy.get('#field-uri_field').type('plone'); + cy.findAllByText('URI field').click(); + + cy.get('.form-error-label') + .contains( + 'Input must be valid url (www.something.com or http(s)://www.something.com)', + ) + .should('be.visible'); + cy.get('#toolbar-save').click(); + cy.wait(100); + cy.get('.toast-inner-content') + .contains( + 'Input must be valid url (www.something.com or http(s)://www.something.com)', + ) + .should('be.visible'); + }); + }); +}); diff --git a/packages/volto/cypress/tests/coresandbox/formBlockValidation.js b/packages/volto/cypress/tests/coresandbox/formBlockValidation.js new file mode 100644 index 0000000000..1de78bc7e8 --- /dev/null +++ b/packages/volto/cypress/tests/coresandbox/formBlockValidation.js @@ -0,0 +1,109 @@ +context('Test Field Type in form block', () => { + describe('Test', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + // given a logged in editor and a page in edit mode + cy.autologin(); + cy.createContent({ + contentType: 'Document', + contentId: 'document', + contentTitle: 'Test document', + }); + cy.visit('/'); + cy.wait('@content'); + cy.navigate('/document'); + cy.wait('@content'); + cy.navigate('/document/edit'); + cy.wait('@schema'); + cy.getSlateTitle(); + + cy.getSlate().click(); + cy.get('.button .block-add-button').click({ force: true }); + + cy.get('.blocks-chooser .mostUsed .button.testformBlock').click(); + }); + + it('Test Email field by entering email address without a domain', function () { + cy.get('#field-email').click({ force: true }).type('plone'); + cy.findAllByText('Email').click(); + cy.get('.form-error-label') + .contains('Input must be valid email (something@domain.com)') + .should('be.visible'); + }); + + it('Test Text Field', function () { + cy.get('#field-textline') + .click({ force: true }) + .type('Volto Coresandbox fixture'); + cy.get('.form-error-label').should('not.exist'); + }); + + it('Test Date & Time Field', function () { + const date = new Date(); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + + const currentTime = date.toLocaleTimeString([], { + hour: 'numeric', + minute: 'numeric', + hour12: true, + }); + + cy.findByText('Enter Date/Time').click(); + cy.wait(100); + //Date + cy.get('#datetime-date').type(`${month}/${day}/${year}`); + cy.get('#datetime-date').should('have.value', `${month}/${day}/${year}`); + + //Time + cy.get('#datetime-time').type(`${currentTime} `); + cy.get('#datetime-time').should('have.value', `${currentTime}`); + + cy.get('.form-error-label').should('not.exist'); + cy.findByText('Enter Date/Time').click(); + }); + + it('Test URI Field by entering invalid URI', function () { + cy.get('#field-url').click({ force: true }).type('plone'); + cy.findAllByText('Enter URL').click(); + cy.get('.form-error-label') + .contains( + 'Input must be valid url (www.something.com or http(s)://www.something.com)', + ) + .should('be.visible'); + }); + + it('Test ID Field Type', function () { + cy.get('#field-id').click({ force: true }).type('Plone'); + cy.findAllByText('Enter ID').click(); + cy.get('.form-error-label') + .contains( + 'Only 7-bit bytes characters are allowed. Cannot contain uppercase letters, special characters: <, >, &, #, /, ?, or others that are illegal in URLs. Cannot start with: _, aq_, @@, ++. Cannot end with __. Cannot be: request,contributors, ., .., "". Cannot contain new lines.', + ) + .should('be.visible'); + }); + + it('Test RichText Field Type', function () { + cy.get('p[data-slate-node="element"]') + .click({ force: true }) + .type('Plone{selectall}'); + cy.get('a[title="Bold"]').click(); + cy.get('a[title="Italic"]').click(); + cy.get('.slate_wysiwyg_box').click(); + }); + it('Missing required field error', function () { + cy.get('#field-textline').type('Volto Coresandbox fixture'); + cy.get('#field-email').type('plone@org.com'); + cy.get('#field-password').click(); + cy.get('#field-email').click(); + cy.get('.form-error-label') + .contains('Required input is missing.') + .should('be.visible'); + cy.get('.ui.icon.negative.attached.message > .content ').should( + 'be.visible', + ); + }); + }); +}); diff --git a/packages/volto/cypress/tests/coresandbox/formFieldTypeValidation.js b/packages/volto/cypress/tests/coresandbox/formFieldTypeValidation.js new file mode 100644 index 0000000000..09d9397229 --- /dev/null +++ b/packages/volto/cypress/tests/coresandbox/formFieldTypeValidation.js @@ -0,0 +1,103 @@ +context('Test Field Type in form ', () => { + describe('Test', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/@types/example').as('schema'); + cy.intercept('POST', '/**/').as('create'); + + // given a logged in editor and a page in edit mode + cy.autologin(); + cy.visit('/'); + cy.wait('@content'); + cy.navigate('/form'); + }); + + it('Test Email field by entering email address without a domain', function () { + cy.get('#field-email').type('plone'); + cy.findAllByText('Email').click(); + + cy.get('.form-error-label') + .contains('Input must be valid email (something@domain.com)') + .should('be.visible'); + }); + + it('Test Text Field', function () { + cy.get('#field-textline').type('Volto Coresandbox fixture'); + cy.findAllByText('Title').click(); + cy.get('.form-error-label').should('not.exist'); + }); + + it('Test Date & Time Field', function () { + const date = new Date(); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + + const currentTime = date.toLocaleTimeString([], { + hour: 'numeric', + minute: 'numeric', + hour12: true, + }); + + cy.findByText('Enter Date/Time').click(); + cy.wait(100); + //Date + cy.get('#datetime-date').type(`${month}/${day}/${year}`); + cy.get('#datetime-date').should('have.value', `${month}/${day}/${year}`); + + //Time + cy.get('#datetime-time').type(`${currentTime} `); + cy.get('#datetime-time').should('have.value', `${currentTime}`); + + cy.get('.form-error-label').should('not.exist'); + cy.findByText('Enter Date/Time').click(); + }); + + it('Test URI Field by entering invalid URI', function () { + cy.get('#field-url').type('plone'); + cy.findAllByText('Enter URL').click(); + cy.get('.form-error-label') + .contains( + 'Input must be valid url (www.something.com or http(s)://www.something.com)', + ) + .should('be.visible'); + }); + + it('Test ID Field Type', function () { + cy.get('#field-id').type('Plone'); + cy.findAllByText('Enter ID').click(); + cy.get('.form-error-label') + .contains( + 'Only 7-bit bytes characters are allowed. Cannot contain uppercase letters, special characters: <, >, &, #, /, ?, or others that are illegal in URLs. Cannot start with: _, aq_, @@, ++. Cannot end with __. Cannot be: request,contributors, ., .., "". Cannot contain new lines.', + ) + .should('be.visible'); + }); + it('Test Link Document/News/Event Field Type', function () { + cy.get('.objectbrowser-field > .selected-values').click(); + cy.get('svg.icon.home-icon').click(); + cy.get('li').last().click(); + cy.findAllByText('Link to Document/Event/News').click(); + cy.get('.objectbrowser-field > .selected-values > div.ui.label').should( + 'be.visible', + ); + }); + it('Test RichText Field Type', function () { + cy.get('.slate_wysiwyg_box').type('Plone{selectall}'); + cy.get('a[title="Bold"]').click(); + cy.get('a[title="Italic"]').click(); + cy.get('.slate_wysiwyg_box').click(); + }); + it('Missing required field error', function () { + cy.get('#field-textline').type('Volto Coresandbox fixture'); + cy.get('#field-email').type('plone@org.com'); + cy.get('#field-password').click(); + cy.get('#field-email').click(); + cy.get('.form-error-label') + .contains('Required input is missing.') + .should('be.visible'); + cy.get('.ui.icon.negative.attached.message > .content ').should( + 'be.visible', + ); + }); + }); +}); diff --git a/packages/volto/news/6217.internal b/packages/volto/news/6217.internal new file mode 100644 index 0000000000..75258c4a17 --- /dev/null +++ b/packages/volto/news/6217.internal @@ -0,0 +1 @@ +Added Cypress test for field types in example content - @Tishasoumya-02 \ No newline at end of file diff --git a/packages/volto/src/helpers/FormValidation/FormValidation.test.js b/packages/volto/src/helpers/FormValidation/FormValidation.test.js index cc59661bfe..b5c40d4dd2 100644 --- a/packages/volto/src/helpers/FormValidation/FormValidation.test.js +++ b/packages/volto/src/helpers/FormValidation/FormValidation.test.js @@ -359,7 +359,7 @@ describe('FormValidation', () => { ).toEqual({}); }); - it('number - isNumber', () => { + it('number - isNumber - fails (not string|number as number)', () => { let newSchema = { properties: { ...schema.properties, @@ -376,7 +376,8 @@ describe('FormValidation', () => { schema: newSchema, formData: { username: 'test username', - customField: '1', + //since 'number' can accept digits in string & number format hence testing it with an alphabet + customField: 'n', }, formatMessage, }), @@ -385,6 +386,56 @@ describe('FormValidation', () => { }); }); + it('number - isNumber - as string', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Number field', + type: 'number', + description: '', + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + //since 'number' can accept digits in string & number format hence testing it with an alphabet + customField: '1', + }, + formatMessage, + }), + ).toEqual({}); + }); + + it('number - isNumber - as number', () => { + let newSchema = { + properties: { + ...schema.properties, + customField: { + title: 'Number field', + type: 'number', + description: '', + }, + }, + required: [], + }; + expect( + FormValidation.validateFieldsPerFieldset({ + schema: newSchema, + formData: { + username: 'test username', + //since 'number' can accept digits in string & number format hence testing it with an alphabet + customField: 1, + }, + formatMessage, + }), + ).toEqual({}); + }); + it('number - minimum', () => { let newSchema = { properties: { diff --git a/packages/volto/src/helpers/FormValidation/validators.ts b/packages/volto/src/helpers/FormValidation/validators.ts index 97d6af7544..82e927b927 100644 --- a/packages/volto/src/helpers/FormValidation/validators.ts +++ b/packages/volto/src/helpers/FormValidation/validators.ts @@ -84,9 +84,11 @@ export const emailValidator = ({ value, formatMessage }: Validator): string => { }; export const isNumberValidator = ({ value, formatMessage }: Validator) => { + const isNumeric = (string: string | number) => Number.isFinite(+string); const floatRegex = /^[+-]?\d+(\.\d+)?$/; - const isValid = - typeof value === 'number' && !isNaN(value) && floatRegex.test(value); + const isValid = isNumeric(value) && floatRegex.test(value); + // const isValid = + // typeof value === 'string' && !isNaN(+value) && floatRegex.test(value); return !isValid ? formatMessage(messages.isNumber) : null; }; @@ -107,9 +109,9 @@ export const maximumValidator = ({ value, field, formatMessage }: Validator) => }); export const isIntegerValidator = ({ value, formatMessage }: Validator) => { + const isNumeric = (string: string | number) => Number.isFinite(+string); const intRegex = /^-?[0-9]+$/; - const isValid = - typeof value === 'number' && !isNaN(value) && intRegex.test(value); + const isValid = isNumeric(value) && intRegex.test(value); return !isValid ? formatMessage(messages.isInteger) : null; };