Split attributes yup validations

Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
soupette 2021-01-12 15:25:21 +01:00
parent 29e74e9f9f
commit 5366e8f930
7 changed files with 532 additions and 303 deletions

View File

@ -631,6 +631,8 @@ const FormModal = () => {
toggleConfirmModal();
}, [toggleConfirmModal]);
console.log({ modifiedData });
const handleSubmit = async (e, shouldContinue = isCreating) => {
e.preventDefault();

View File

@ -234,6 +234,35 @@ const baseForm = {
],
};
},
string: () => {
return {
items: [
[nameField],
[
{
label: { id: getTrad('modalForm.attribute.text.type-selection') },
name: 'type',
size: 12,
type: 'booleanBox',
options: [
{
headerId: getTrad('form.attribute.text.option.short-text'),
descriptionId: getTrad('form.attribute.text.option.short-text.description'),
value: 'string',
},
{
headerId: getTrad('form.attribute.text.option.long-text'),
descriptionId: getTrad('form.attribute.text.option.long-text.description'),
value: 'text',
},
],
validations: {},
},
],
[uiHelpers.spacerMedium],
],
};
},
text: () => {
return {
items: [

View File

@ -1,3 +1,4 @@
// eslint-disable-next-line import/prefer-default-export
export { default as attributesForm } from './form';
export { default as commonBaseForm } from './commonBaseForm';
export { default as attributeTypes } from './types';

View File

@ -0,0 +1,321 @@
import * as yup from 'yup';
// import { get } from 'lodash';
// import { isEmpty } from 'lodash';
import { translatedErrors as errorsTrads } from 'strapi-helper-plugin';
import getTrad from '../../../../utils/getTrad';
import {
alreadyUsedAttributeNames,
createTextShape,
getUsedContentTypeAttributeNames,
isMinSuperiorThanMax,
isNameAllowed,
validators,
NAME_REGEX,
} from './validation/common';
const types = {
date: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
type: validators.type(),
};
return yup.object(shape);
},
datetime: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
type: validators.type(),
};
return yup.object(shape);
},
time: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
type: validators.type(),
};
return yup.object(shape);
},
default: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
type: validators.type(),
};
return yup.object(shape);
},
biginteger: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
type: validators.type(),
default: yup
.string()
.nullable()
.matches(/^\d*$/),
unique: validators.unique(),
required: validators.required(),
max: yup
.string()
.nullable()
.matches(/^\d*$/, errorsTrads.regex),
min: yup
.string()
.nullable()
.test(isMinSuperiorThanMax)
.matches(/^\d*$/, errorsTrads.regex),
};
return yup.object(shape);
},
boolean: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
default: yup.boolean().nullable(),
required: validators.required(),
unique: validators.unique(),
};
return yup.object(shape);
},
component: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
type: validators.type(),
required: validators.required(),
max: validators.max(),
min: validators.min(),
component: yup.string().required(errorsTrads.required),
};
return yup.object(shape);
},
decimal: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
type: validators.type(),
default: yup.number(),
required: validators.required(),
max: yup.number(),
min: yup.number().test(isMinSuperiorThanMax),
};
return yup.object(shape);
},
dynamiczone: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
type: validators.type(),
required: validators.required(),
max: validators.max(),
min: validators.min(),
};
return yup.object(shape);
},
email: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
type: validators.type(),
default: yup
.string()
.email()
.nullable(),
unique: validators.unique(),
required: validators.required(),
maxLength: validators.maxLength(),
minLength: validators.minLength(),
};
return yup.object(shape);
},
enumeration: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const usedNames = getUsedContentTypeAttributeNames(
contentTypeSchema,
isEdition,
initialData.name
);
const ENUM_REGEX = new RegExp('^[_A-Za-z][_0-9A-Za-z]*$');
const shape = {
name: yup
.string()
.test(alreadyUsedAttributeNames(usedNames))
.test(isNameAllowed(reservedNames))
.matches(ENUM_REGEX, errorsTrads.regex)
.required(errorsTrads.required),
type: validators.type(),
default: validators.default(),
unique: validators.unique(),
required: validators.required(),
enum: yup
.array()
.of(yup.string())
.min(1, errorsTrads.min)
.test({
name: 'areEnumValuesUnique',
message: getTrad('error.validation.enum-duplicate'),
test: values => {
const filtered = [...new Set(values)];
return filtered.length === values.length;
},
})
.test({
name: 'valuesMatchesRegex',
message: errorsTrads.regex,
test: values => {
return values.every(val => val === '' || ENUM_REGEX.test(val));
},
})
.test({
name: 'doesNotHaveEmptyValues',
message: getTrad('error.validation.enum-empty-string'),
test: values => !values.some(val => val === ''),
}),
enumName: yup.string().nullable(),
};
return yup.object(shape);
},
float: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
type: validators.type(),
required: validators.required(),
default: yup.number(),
max: yup.number(),
min: yup.number().test(isMinSuperiorThanMax),
};
return yup.object(shape);
},
integer: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
type: validators.type(),
default: yup.number().integer(),
unique: validators.unique(),
required: validators.required(),
max: validators.max(),
min: validators.min(),
};
return yup.object(shape);
},
json: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
type: validators.type(),
required: validators.required(),
unique: validators.unique(),
};
return yup.object(shape);
},
media: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
type: validators.type(),
multiple: yup.boolean(),
required: validators.required(),
allowedTypes: yup
.array()
.of(yup.string().oneOf(['images', 'videos', 'files']))
.min(1)
.nullable(),
};
return yup.object(shape);
},
password: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
type: validators.type(),
default: validators.default(),
unique: validators.unique(),
required: validators.required(),
maxLength: validators.maxLength(),
minLength: validators.minLength(),
};
return yup.object(shape);
},
relation: (
contentTypeSchema,
initialData,
isEdition,
reservedNames,
data,
alreadyTakenTargetAttributes
) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
target: yup.string().required(errorsTrads.required),
nature: yup.string().required(),
dominant: yup.boolean().nullable(),
unique: yup.boolean().nullable(),
targetAttribute: yup.lazy(() => {
let schema = yup.string().test(isNameAllowed(reservedNames));
const initialForbiddenName = [...alreadyTakenTargetAttributes, data.name];
let forbiddenTargetAttributeName = isEdition
? initialForbiddenName.filter(val => val !== initialData.targetAttribute)
: initialForbiddenName;
if (!['oneWay', 'manyWay'].includes(data.nature)) {
schema = schema.matches(NAME_REGEX, errorsTrads.regex);
}
return schema
.test({
name: 'forbiddenTargetAttributeName',
message: getTrad('error.validation.relation.targetAttribute-taken'),
test: value => {
if (!value) {
return false;
}
return !forbiddenTargetAttributeName.includes(value);
},
})
.required(errorsTrads.required);
}),
};
return yup.object(shape);
},
richtext: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
type: validators.type(),
default: validators.default(),
unique: validators.unique(),
required: validators.required(),
maxLength: validators.maxLength(),
minLength: validators.minLength(),
};
return yup.object(shape);
},
string: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = createTextShape(contentTypeSchema, initialData, isEdition, reservedNames);
return yup.object(shape);
},
text: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = createTextShape(contentTypeSchema, initialData, isEdition, reservedNames);
return yup.object(shape);
},
uid: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = createTextShape(contentTypeSchema, initialData, isEdition, reservedNames);
return yup.object(shape);
},
};
export default types;

View File

@ -0,0 +1,159 @@
import * as yup from 'yup';
import { get, toNumber } from 'lodash';
import { translatedErrors as errorsTrads } from 'strapi-helper-plugin';
import getTrad from '../../../../../utils/getTrad';
const NAME_REGEX = new RegExp('^[A-Za-z][_0-9A-Za-z]*$');
const alreadyUsedAttributeNames = usedNames => {
return {
name: 'attributeNameAlreadyUsed',
message: errorsTrads.unique,
test: value => {
if (!value) {
return false;
}
return !usedNames.includes(value);
},
};
};
const getUsedContentTypeAttributeNames = (ctShema, isEdition, attributeNameToEdit) => {
const attributes = get(ctShema, ['schema', 'attributes'], {});
return Object.keys(attributes).filter(attr => {
if (isEdition) {
return attr !== attributeNameToEdit;
}
return true;
});
};
const isNameAllowed = reservedNames => {
return {
name: 'forbiddenAttributeName',
message: getTrad('error.attributeName.reserved-name'),
test: value => {
if (!value) {
return false;
}
return !reservedNames.includes(value);
},
};
};
const validators = {
default: () => yup.string().nullable(),
max: () =>
yup
.number()
.integer()
.positive()
.nullable(),
min: () =>
yup
.number()
.integer()
.positive()
.when('max', (max, schema) => {
if (max) {
return schema.max(max, getTrad('error.validation.minSupMax'));
}
return schema;
})
.nullable(),
maxLength: () =>
yup
.number()
.integer()
.nullable(),
minLength: () =>
yup
.number()
.integer()
.when('maxLength', (maxLength, schema) => {
if (maxLength) {
return schema.max(maxLength, getTrad('error.validation.minSupMax'));
}
return schema;
})
.nullable(),
name: (contentTypeSchema, initialData, isEdition, reservedNames) => {
const usedNames = getUsedContentTypeAttributeNames(
contentTypeSchema,
isEdition,
initialData.name
);
return yup
.string()
.test(alreadyUsedAttributeNames(usedNames))
.test(isNameAllowed(reservedNames))
.matches(NAME_REGEX, errorsTrads.regex)
.required(errorsTrads.required);
},
required: () => yup.boolean(),
type: () => yup.string().required(errorsTrads.required),
unique: () => yup.boolean().nullable(),
};
const createTextShape = (contentTypeSchema, initialData, isEdition, reservedNames) => {
const shape = {
name: validators.name(contentTypeSchema, initialData, isEdition, reservedNames),
type: validators.type(),
default: validators.default(),
unique: validators.unique(),
required: validators.required(),
maxLength: validators.maxLength(),
minLength: validators.minLength(),
regex: yup
.string()
.test({
name: 'isValidRegExpPattern',
message: getTrad('error.validation.regex'),
test: value => {
return new RegExp(value) !== null;
},
})
.nullable(),
};
return shape;
};
const isMinSuperiorThanMax = {
name: 'isMinSuperiorThanMax',
message: getTrad('error.validation.minSupMax'),
test(value) {
if (!value) {
return false;
}
const { max } = this.parent;
if (!max) {
return false;
}
if (Number.isNaN(toNumber(value))) {
return true;
}
return toNumber(max) >= toNumber(value);
},
};
export {
alreadyUsedAttributeNames,
createTextShape,
getUsedContentTypeAttributeNames,
isMinSuperiorThanMax,
isNameAllowed,
validators,
NAME_REGEX,
};

View File

@ -1,85 +1,11 @@
import * as yup from 'yup';
import { get, isEmpty, toLower, trim, toNumber } from 'lodash';
import { translatedErrors as errorsTrads } from 'strapi-helper-plugin';
import getTrad from '../../../../utils/getTrad';
import { get, toLower } from 'lodash';
import { nameToSlug } from '../createUid';
import { attributesForm, commonBaseForm } from '../attributes';
import { attributesForm, attributeTypes, commonBaseForm } from '../attributes';
import { categoryForm, createCategorySchema } from '../category';
import { contentTypeForm, createContentTypeSchema } from '../contentType';
import { createComponentSchema, componentForm } from '../component';
import { dynamiczoneForm } from '../dynamicZone';
import { NAME_REGEX, ENUM_REGEX } from './regexes';
/* eslint-disable indent */
/* eslint-disable prefer-arrow-callback */
yup.addMethod(yup.mixed, 'defined', function() {
return this.test('defined', errorsTrads.required, value => value !== undefined);
});
yup.addMethod(yup.string, 'unique', function(
message,
alreadyTakenAttributes,
validator,
category = ''
) {
return this.test('unique', message, function(string) {
if (!string) {
return false;
}
return !alreadyTakenAttributes.includes(
typeof validator === 'function' ? validator(string, category) : string.toLowerCase()
);
});
});
yup.addMethod(yup.array, 'hasNotEmptyValues', function(message) {
return this.test('hasNotEmptyValues', message, function(array) {
return !array.some(value => {
return isEmpty(value);
});
});
});
yup.addMethod(yup.string, 'isAllowed', function(message, reservedNames) {
return this.test('isAllowed', message, function(string) {
if (!string) {
return false;
}
return !reservedNames.includes(toLower(trim(string)));
});
});
yup.addMethod(yup.string, 'isInferior', function(message, max) {
return this.test('isInferior', message, function(min) {
if (!min) {
return false;
}
if (Number.isNaN(toNumber(min))) {
return true;
}
return toNumber(max) >= toNumber(min);
});
});
yup.addMethod(yup.array, 'matchesEnumRegex', function(message) {
return this.test('matchesEnumRegex', message, function(array) {
return array.every(value => {
return ENUM_REGEX.test(value);
});
});
});
yup.addMethod(yup.string, 'isValidRegExpPattern', function(message) {
return this.test('isValidRegExpPattern', message, function(string) {
return new RegExp(string) !== null;
});
});
const forms = {
attribute: {
@ -93,235 +19,25 @@ const forms = {
alreadyTakenTargetContentTypeAttributes,
reservedNames
) {
const alreadyTakenAttributes = Object.keys(
get(currentSchema, ['schema', 'attributes'], {})
).filter(attribute => {
if (isEditing) {
return attribute !== attributeToEditName;
}
return true;
});
// For relations
let targetAttributeAlreadyTakenValue = dataToValidate.name
? [...alreadyTakenAttributes, dataToValidate.name]
: alreadyTakenAttributes;
if (
isEditing &&
attributeType === 'relation' &&
dataToValidate.target === currentSchema.uid
) {
targetAttributeAlreadyTakenValue = targetAttributeAlreadyTakenValue.filter(
attribute => attribute !== initialData.targetAttribute
try {
return attributeTypes[attributeType](
currentSchema,
initialData,
isEditing,
reservedNames.attributes,
dataToValidate,
alreadyTakenTargetContentTypeAttributes
);
}
} catch (err) {
console.log(err);
console.log(attributeType);
// Common yup shape for most attributes
const commonShape = {
name: yup
.string()
.unique(errorsTrads.unique, alreadyTakenAttributes)
.matches(NAME_REGEX, errorsTrads.regex)
.isAllowed(getTrad('error.attributeName.reserved-name'), reservedNames.attributes)
.required(errorsTrads.required),
type: yup.string().required(errorsTrads.required),
default: yup.string().nullable(),
unique: yup.boolean().nullable(),
required: yup.boolean(),
};
const numberTypeShape = {
max: yup.lazy(() => {
let schema = yup.number();
if (
attributeType === 'integer' ||
attributeType === 'biginteger' ||
attributeType === 'dynamiczone'
) {
schema = schema.integer();
}
if (attributeType === 'dynamiczone') {
schema = schema.positive();
}
return schema.nullable();
}),
min: yup.lazy(() => {
let schema = yup.number();
if (
attributeType === 'integer' ||
attributeType === 'biginteger' ||
attributeType === 'dynamiczone'
) {
schema = schema.integer();
}
if (attributeType === 'dynamiczone') {
schema = schema.positive();
}
return schema
.nullable()
.when('max', (max, schema) => {
if (max) {
return schema.max(max, getTrad('error.validation.minSupMax'));
}
return schema;
})
.nullable();
}),
};
const fieldsThatSupportMaxAndMinLengthShape = {
maxLength: yup
.number()
.integer()
.nullable(),
minLength: yup
.number()
.integer()
.when('maxLength', (maxLength, schema) => {
if (maxLength) {
return schema.max(maxLength, getTrad('error.validation.minSupMax'));
}
return schema;
})
.nullable(),
};
switch (attributeType) {
case 'component':
return yup.object().shape({
...commonShape,
component: yup.string().required(errorsTrads.required),
...numberTypeShape,
});
case 'dynamiczone':
return yup.object().shape({
...commonShape,
...numberTypeShape,
});
case 'enumeration':
return yup.object().shape({
name: yup
.string()
.isAllowed(getTrad('error.attributeName.reserved-name'), reservedNames.attributes)
.unique(errorsTrads.unique, alreadyTakenAttributes)
.matches(ENUM_REGEX, errorsTrads.regex)
.required(errorsTrads.required),
type: yup.string().required(errorsTrads.required),
default: yup.string().nullable(),
unique: yup.boolean().nullable(),
required: yup.boolean(),
enum: yup
.array()
.of(yup.string())
.min(1, errorsTrads.min)
.test({
name: 'areEnumValuesUnique',
message: getTrad('error.validation.enum-duplicate'),
test: values => {
const filtered = [...new Set(values)];
return filtered.length === values.length;
},
})
.matchesEnumRegex(errorsTrads.regex)
.hasNotEmptyValues('Empty strings are not allowed', dataToValidate.enum),
enumName: yup.string().nullable(),
});
case 'text':
return yup.object().shape({
...commonShape,
...fieldsThatSupportMaxAndMinLengthShape,
regex: yup
.string()
.isValidRegExpPattern(getTrad('error.validation.regex'))
.nullable(),
});
case 'number':
case 'integer':
case 'biginteger':
case 'float':
case 'decimal': {
if (dataToValidate.type === 'biginteger') {
return yup.object().shape({
...commonShape,
default: yup
.string()
.nullable()
.matches(/^\d*$/),
min: yup
.string()
.nullable()
.matches(/^\d*$/)
.when('max', (max, schema) => {
if (max) {
return schema.isInferior(getTrad('error.validation.minSupMax'), max);
}
return schema;
}),
max: yup
.string()
.nullable()
.matches(/^\d*$/),
});
}
let defaultType = yup.number();
if (dataToValidate.type === 'integer') {
defaultType = yup.number().integer('component.Input.error.validation.integer');
}
return yup.object().shape({
...commonShape,
default: defaultType.nullable(),
...numberTypeShape,
});
}
case 'relation':
return yup.object().shape({
name: yup
.string()
.isAllowed(getTrad('error.attributeName.reserved-name'), reservedNames.attributes)
.matches(NAME_REGEX, errorsTrads.regex)
.unique(errorsTrads.unique, alreadyTakenAttributes)
.required(errorsTrads.required),
targetAttribute: yup.lazy(() => {
let schema = yup
.string()
.isAllowed(getTrad('error.attributeName.reserved-name'), reservedNames.attributes);
if (!['oneWay', 'manyWay'].includes(dataToValidate.nature)) {
schema = schema.matches(NAME_REGEX, errorsTrads.regex);
}
return schema
.unique(errorsTrads.unique, targetAttributeAlreadyTakenValue)
.unique(
getTrad('error.validation.relation.targetAttribute-taken'),
alreadyTakenTargetContentTypeAttributes
)
.required(errorsTrads.required);
}),
target: yup.string().required(errorsTrads.required),
nature: yup.string().required(),
dominant: yup.boolean().nullable(),
unique: yup.boolean().nullable(),
});
default:
return yup.object().shape({
...commonShape,
...fieldsThatSupportMaxAndMinLengthShape,
});
return attributeTypes.default(
currentSchema,
initialData,
isEditing,
reservedNames.attributes
);
}
},
form: {

View File

@ -54,6 +54,7 @@
"error.validation.enum-duplicate": "Duplicate values are not allowed",
"error.validation.minSupMax": "Can't be superior",
"error.validation.regex": "Regex pattern is invalid",
"error.validation.enum-empty-string": "Empty strings are not allowed",
"error.validation.relation.targetAttribute-taken": "This name exists in the target",
"form.attribute.component.option.add": "Add a component",
"form.attribute.component.option.create": "Create a new component",