mirror of
https://github.com/strapi/strapi.git
synced 2025-06-27 00:41:25 +00:00
feat(uid): add regex attribute (#23141)
This commit is contained in:
parent
ebcac74d51
commit
3561ab6db9
@ -42,11 +42,12 @@ import type { Schema } from '@strapi/types';
|
||||
const UID_REGEX = /^[A-Za-z0-9-_.~]*$/;
|
||||
|
||||
interface UIDInputProps extends Omit<InputProps, 'type'> {
|
||||
attribute?: Pick<Schema.Attribute.UIDProperties, 'regex'>;
|
||||
type: Schema.Attribute.TypeOf<Schema.Attribute.UID>;
|
||||
}
|
||||
|
||||
const UIDInput = React.forwardRef<any, UIDInputProps>(
|
||||
({ hint, label, labelAction, name, required, ...props }, ref) => {
|
||||
({ hint, label, labelAction, name, required, attribute = {}, ...props }, ref) => {
|
||||
const { currentDocumentMeta } = useDocumentContext('UIDInput');
|
||||
const allFormValues = useForm('InputUID', (form) => form.values);
|
||||
const [availability, setAvailability] = React.useState<CheckUIDAvailability.Response>();
|
||||
@ -61,6 +62,9 @@ const UIDInput = React.forwardRef<any, UIDInputProps>(
|
||||
const [{ query }] = useQueryParams();
|
||||
const params = React.useMemo(() => buildValidParams(query), [query]);
|
||||
|
||||
const { regex } = attribute;
|
||||
const validationRegExp = regex ? new RegExp(regex) : UID_REGEX;
|
||||
|
||||
const {
|
||||
data: defaultGeneratedUID,
|
||||
isLoading: isGeneratingDefaultUID,
|
||||
@ -143,7 +147,9 @@ const UIDInput = React.forwardRef<any, UIDInputProps>(
|
||||
{
|
||||
// Don't check availability if the value is empty or wasn't changed
|
||||
skip: !Boolean(
|
||||
(hasChanged || isCloning) && debouncedValue && UID_REGEX.test(debouncedValue.trim())
|
||||
(hasChanged || isCloning) &&
|
||||
debouncedValue &&
|
||||
validationRegExp.test(debouncedValue.trim())
|
||||
),
|
||||
}
|
||||
);
|
||||
|
@ -234,7 +234,9 @@ const createAttributeSchema = (
|
||||
case 'text':
|
||||
return yup.string();
|
||||
case 'uid':
|
||||
return yup.string().matches(/^[A-Za-z0-9-_.~]*$/);
|
||||
return yup
|
||||
.string()
|
||||
.matches(attribute.regex ? new RegExp(attribute.regex) : /^[A-Za-z0-9-_.~]*$/);
|
||||
default:
|
||||
/**
|
||||
* This allows any value.
|
||||
|
@ -1,5 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
import { Schema, UID } from '@strapi/types';
|
||||
import { yup, validateYupSchema, errors } from '@strapi/utils';
|
||||
import { ValidateOptions } from 'yup/lib/types';
|
||||
import { TestContext } from 'yup';
|
||||
import createModelConfigurationSchema from './model-configuration';
|
||||
|
||||
const { PaginationError, ValidationError } = errors;
|
||||
@ -27,8 +30,19 @@ const checkUIDAvailabilityInputSchema = yup.object({
|
||||
field: yup.string().required(),
|
||||
value: yup
|
||||
.string()
|
||||
.matches(/^[A-Za-z0-9-_.~]*$/)
|
||||
.required(),
|
||||
.required()
|
||||
.test(
|
||||
'isValueMatchingRegex',
|
||||
`\${path} must match the custom regex or the default one "/^[A-Za-z0-9-_.~]*$/"`,
|
||||
function (value, context: TestContext<{ regex?: string }>) {
|
||||
return (
|
||||
value === '' ||
|
||||
(context.options.context?.regex
|
||||
? new RegExp(context.options?.context.regex).test(value as string)
|
||||
: /^[A-Za-z0-9-_.~]*$/.test(value as string))
|
||||
);
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
const validateUIDField = (contentTypeUID: any, field: any) => {
|
||||
@ -61,7 +75,30 @@ const validatePagination = ({ page, pageSize }: any) => {
|
||||
const validateKind = validateYupSchema(kindSchema);
|
||||
const validateBulkActionInput = validateYupSchema(bulkActionInputSchema);
|
||||
const validateGenerateUIDInput = validateYupSchema(generateUIDInputSchema);
|
||||
const validateCheckUIDAvailabilityInput = validateYupSchema(checkUIDAvailabilityInputSchema);
|
||||
const validateCheckUIDAvailabilityInput = (body: {
|
||||
contentTypeUID: UID.ContentType;
|
||||
field: string;
|
||||
value: string;
|
||||
}) => {
|
||||
const options: ValidateOptions<{ regex?: string }> = {};
|
||||
|
||||
const contentType =
|
||||
body.contentTypeUID in strapi.contentTypes ? strapi.contentTypes[body.contentTypeUID] : null;
|
||||
|
||||
if (
|
||||
contentType?.attributes[body.field] &&
|
||||
`regex` in contentType.attributes[body.field] &&
|
||||
(contentType.attributes[body.field] as Schema.Attribute.UID).regex
|
||||
) {
|
||||
options.context = {
|
||||
regex: (contentType?.attributes[body.field] as Schema.Attribute.UID).regex,
|
||||
};
|
||||
}
|
||||
|
||||
const validator = validateYupSchema(checkUIDAvailabilityInputSchema, options);
|
||||
|
||||
return validator(body);
|
||||
};
|
||||
|
||||
export {
|
||||
createModelConfigurationSchema,
|
||||
|
@ -403,6 +403,7 @@ export const advancedForm = {
|
||||
attributeOptions.maxLength,
|
||||
attributeOptions.minLength,
|
||||
attributeOptions.private,
|
||||
attributeOptions.regex,
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -227,6 +227,23 @@ describe('Type validators', () => {
|
||||
expect(validator.isValidSync(attributes.slug)).toBe(isValid);
|
||||
});
|
||||
|
||||
test('Default value must match regex if a custom regex is defined', () => {
|
||||
const attributes = {
|
||||
slug: {
|
||||
type: 'uid',
|
||||
default: 'some/value',
|
||||
regex: '^[A-Za-z0-9-_.~/]*$',
|
||||
},
|
||||
} satisfies Struct.SchemaAttributes;
|
||||
|
||||
const validator = getTypeValidator(attributes.slug, {
|
||||
types: ['uid'],
|
||||
attributes,
|
||||
});
|
||||
|
||||
expect(validator.isValidSync(attributes.slug)).toBe(true);
|
||||
});
|
||||
|
||||
test('Default should not be defined if targetField is defined', () => {
|
||||
const attributes = {
|
||||
title: {
|
||||
|
@ -523,6 +523,12 @@ const uidSchema = basePropertiesSchema.extend({
|
||||
preserveLeadingUnderscore: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
regex: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((value) => {
|
||||
return value === '' || !!new RegExp(value as string);
|
||||
}, 'Invalid regular expression pattern'),
|
||||
});
|
||||
|
||||
const customFieldSchema = basePropertiesSchema.extend({
|
||||
|
@ -11,8 +11,8 @@ import {
|
||||
isValidDefaultJSON,
|
||||
isValidName,
|
||||
isValidEnum,
|
||||
isValidUID,
|
||||
isValidRegExpPattern,
|
||||
UID_REGEX,
|
||||
} from './common';
|
||||
|
||||
export type GetTypeValidatorOptions = {
|
||||
@ -84,7 +84,19 @@ const getTypeShape = (attribute: Schema.Attribute.AnyAttribute, { attributes }:
|
||||
return !!(_.isNil(targetField) || _.isNil(value));
|
||||
}
|
||||
)
|
||||
.test(isValidUID),
|
||||
.test(
|
||||
'isValidDefaultRegexUID',
|
||||
`\${path} must match the custom regex or the default one "${UID_REGEX}"`,
|
||||
function (value) {
|
||||
const { regex } = this.parent;
|
||||
|
||||
if (regex) {
|
||||
return !_.isNil(value) && (value === '' || new RegExp(regex).test(value));
|
||||
}
|
||||
|
||||
return value === '' || UID_REGEX.test(value as string);
|
||||
}
|
||||
),
|
||||
minLength: validators.minLength,
|
||||
maxLength: validators.maxLength.max(256).test(maxLengthIsGreaterThanOrEqualToMinLength),
|
||||
options: yup.object().shape({
|
||||
@ -94,6 +106,7 @@ const getTypeShape = (attribute: Schema.Attribute.AnyAttribute, { attributes }:
|
||||
customReplacements: yup.array().of(yup.array().of(yup.string()).min(2).max(2)),
|
||||
preserveLeadingUnderscore: yup.boolean(),
|
||||
}),
|
||||
regex: yup.string().test(isValidRegExpPattern),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -443,6 +443,10 @@ export const uidValidator = (
|
||||
return schema;
|
||||
}
|
||||
|
||||
if (metas.attr.regex) {
|
||||
return schema.matches(new RegExp(metas.attr.regex));
|
||||
}
|
||||
|
||||
return schema.matches(/^[A-Za-z0-9-_.~]*$/);
|
||||
};
|
||||
|
||||
|
@ -15,6 +15,7 @@ export interface UIDProperties<
|
||||
> {
|
||||
targetField?: TTargetAttribute;
|
||||
options?: UIDOptions & TOptions;
|
||||
regex?: RegExp['source'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -321,11 +321,12 @@ describe('Content Manager single types', () => {
|
||||
error: {
|
||||
status: 400,
|
||||
name: 'ValidationError',
|
||||
message: 'value must match the following: "/^[A-Za-z0-9-_.~]*$/"',
|
||||
message: 'value must match the custom regex or the default one "/^[A-Za-z0-9-_.~]*$/"',
|
||||
details: {
|
||||
errors: [
|
||||
{
|
||||
message: 'value must match the following: "/^[A-Za-z0-9-_.~]*$/"',
|
||||
message:
|
||||
'value must match the custom regex or the default one "/^[A-Za-z0-9-_.~]*$/"',
|
||||
name: 'ValidationError',
|
||||
path: ['value'],
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user