feat: extended validation function arguments (#494)
* feat: WIP extended validation function arguments * chore: optimizes validation extended args * chore: more consistently passes validation args * chore: removes field from form state * chore: passing tests * fix: default point validation allows not required and some edge cases * chore: ensures default validate functions receive field config * chore: demo validation with sibling data * chore: optimize getDatabByPath and getSiblingData * chore: adds tests to validate extra arg options * docs: add validation arguments * chore: export default field validation * chore: top level getSiblingData * fix: #495, avoids appending version to id queries * chore: revises when field validation is run * chore: restore original admin field validation Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
@@ -10,6 +10,34 @@ const Validations: CollectionConfig = {
|
|||||||
read: () => true,
|
read: () => true,
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'validationOptions',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Text with siblingData Validation',
|
||||||
|
required: true,
|
||||||
|
validate: (value: string, { data, siblingData, id, operation, user }) => {
|
||||||
|
if (typeof value === 'undefined') {
|
||||||
|
return 'Validation is missing value';
|
||||||
|
}
|
||||||
|
if (data?.text !== 'test') {
|
||||||
|
return 'The next field should be test';
|
||||||
|
}
|
||||||
|
if (siblingData?.text !== 'test') {
|
||||||
|
return 'The next field should be test';
|
||||||
|
}
|
||||||
|
if (!user) {
|
||||||
|
return 'ValidationOptions is missing "user"';
|
||||||
|
}
|
||||||
|
if (typeof operation === 'undefined') {
|
||||||
|
return 'ValidationOptions is missing "operation"';
|
||||||
|
}
|
||||||
|
if (operation === 'update' && typeof id === 'undefined') {
|
||||||
|
return 'ValidationOptions is missing "id"';
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'text',
|
name: 'text',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -58,7 +86,7 @@ const Validations: CollectionConfig = {
|
|||||||
name: 'atLeast3Rows',
|
name: 'atLeast3Rows',
|
||||||
required: true,
|
required: true,
|
||||||
validate: (value) => {
|
validate: (value) => {
|
||||||
const result = value && value.length >= 3;
|
const result = value >= 3;
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return 'This array needs to have at least 3 rows.';
|
return 'This array needs to have at least 3 rows.';
|
||||||
@@ -96,7 +124,7 @@ const Validations: CollectionConfig = {
|
|||||||
label: 'Number should be less than 20',
|
label: 'Number should be less than 20',
|
||||||
required: true,
|
required: true,
|
||||||
validate: (value) => {
|
validate: (value) => {
|
||||||
const result = value < 30;
|
const result = value < 20;
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return 'This value of this field needs to be less than 20.';
|
return 'This value of this field needs to be less than 20.';
|
||||||
|
|||||||
@@ -62,7 +62,19 @@ In addition to being able to define access control on a document-level, you can
|
|||||||
|
|
||||||
### Validation
|
### Validation
|
||||||
|
|
||||||
You can provide your own validation function for all field types. It will be used on both the frontend and the backend, so it should not rely on any Node-specific packages. The validation function can be either synchronous or asynchronous and accepts the field's value and expects to return either `true` or a string error message to display in both API responses and within the Admin panel.
|
Field validation is enforced automatically based on the field type and other properties such as `required` or `min` and `max` value constraints on certain field types. This default behavior can be replaced by providing your own validate function for any field. It will be used on both the frontend and the backend, so it should not rely on any Node-specific packages. The validation function can be either synchronous or asynchronous and expects to return either `true` or a string error message to display in both API responses and within the Admin panel.
|
||||||
|
|
||||||
|
There are two arguments available to custom validation functions.
|
||||||
|
1. The value which is currently assigned to the field
|
||||||
|
2. An optional object with dynamic properties for more complex validation having the following:
|
||||||
|
|
||||||
|
| Property | Description |
|
||||||
|
| ------------- | -------------|
|
||||||
|
| `data` | An object of the full collection or global document |
|
||||||
|
| `siblingData` | An object of the document data limited to fields within the same parent to the field |
|
||||||
|
| `operation` | Will be "create" or "update" depending on the UI action or API call |
|
||||||
|
| `id` | The value of the collection `id`, will be `undefined` on create request |
|
||||||
|
| `user` | The currently authenticated user object |
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```js
|
```js
|
||||||
@@ -72,7 +84,11 @@ Example:
|
|||||||
{
|
{
|
||||||
name: 'customerNumber',
|
name: 'customerNumber',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
validate: async (val) => {
|
validate: async (val, { operation }) => {
|
||||||
|
if (operation !== 'create') {
|
||||||
|
// skip validation on update
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const response = await fetch(`https://your-api.com/customers/${val}`);
|
const response = await fetch(`https://your-api.com/customers/${val}`);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return true;
|
return true;
|
||||||
@@ -85,6 +101,26 @@ Example:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When supplying a field `validate` function, Payload will use yours in place of the default. To make use of the default field validation in your custom logic you can import, call and return the result as needed.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
```js
|
||||||
|
import { text } from 'payload/fields/validations';
|
||||||
|
const field =
|
||||||
|
{
|
||||||
|
name: 'notBad',
|
||||||
|
type: 'text',
|
||||||
|
validate: (val, args) => {
|
||||||
|
if (value === 'bad') {
|
||||||
|
return 'This cannot be "bad"';
|
||||||
|
}
|
||||||
|
// highlight-start
|
||||||
|
return text(val, args);
|
||||||
|
// highlight-end
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Customizable ID
|
### Customizable ID
|
||||||
|
|
||||||
Collections ID fields are generated automatically by default. An explicit `id` field can be declared in the `fields` array to override this behavior.
|
Collections ID fields are generated automatically by default. An explicit `id` field can be declared in the `fields` array to override this behavior.
|
||||||
@@ -198,7 +234,7 @@ A description can be configured three ways.
|
|||||||
- As a function that accepts an object containing the field's value, which returns a string
|
- As a function that accepts an object containing the field's value, which returns a string
|
||||||
- As a React component that accepts value as a prop
|
- As a React component that accepts value as a prop
|
||||||
|
|
||||||
As shown above, you can simply provide a string that will show by the field, but there are use cases where you may want to create some dynamic feedback. By using a function or a component for the `description` property you can provide rich feedback in realtime the user interacts with the form.
|
As shown above, you can simply provide a string that will show by the field, but there are use cases where you may want to create some dynamic feedback. By using a function or a component for the `description` property you can provide realtime feedback as the user interacts with the form.
|
||||||
|
|
||||||
**Function Example:**
|
**Function Example:**
|
||||||
|
|
||||||
|
|||||||
1
fields/validations.js
Normal file
1
fields/validations.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from '../dist/fields/validations';
|
||||||
@@ -8,7 +8,7 @@ import PositionPanel from './PositionPanel';
|
|||||||
import Button from '../../elements/Button';
|
import Button from '../../elements/Button';
|
||||||
import { NegativeFieldGutterProvider } from '../FieldTypeGutter/context';
|
import { NegativeFieldGutterProvider } from '../FieldTypeGutter/context';
|
||||||
import FieldTypeGutter from '../FieldTypeGutter';
|
import FieldTypeGutter from '../FieldTypeGutter';
|
||||||
import RenderFields, { useRenderedFields } from '../RenderFields';
|
import RenderFields from '../RenderFields';
|
||||||
import { Props } from './types';
|
import { Props } from './types';
|
||||||
import HiddenInput from '../field-types/HiddenInput';
|
import HiddenInput from '../field-types/HiddenInput';
|
||||||
|
|
||||||
@@ -38,7 +38,6 @@ const DraggableSection: React.FC<Props> = (props) => {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const { operation } = useRenderedFields();
|
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
baseClass,
|
baseClass,
|
||||||
@@ -105,7 +104,6 @@ const DraggableSection: React.FC<Props> = (props) => {
|
|||||||
>
|
>
|
||||||
<NegativeFieldGutterProvider allow={false}>
|
<NegativeFieldGutterProvider allow={false}>
|
||||||
<RenderFields
|
<RenderFields
|
||||||
operation={operation}
|
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
fieldTypes={fieldTypes}
|
fieldTypes={fieldTypes}
|
||||||
key={rowIndex}
|
key={rowIndex}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ describe('Form - buildStateFromSchema', () => {
|
|||||||
defaultValue,
|
defaultValue,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const state = await buildStateFromSchema(fieldSchema, {});
|
const state = await buildStateFromSchema({ fieldSchema });
|
||||||
expect(state.text.value).toBe(defaultValue);
|
expect(state.text.value).toBe(defaultValue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import ObjectID from 'bson-objectid';
|
import ObjectID from 'bson-objectid';
|
||||||
import { Field as FieldSchema, fieldAffectsData, FieldAffectingData, fieldIsPresentationalOnly } from '../../../../fields/config/types';
|
import { User } from '../../../../auth';
|
||||||
|
import {
|
||||||
|
Field as FieldSchema,
|
||||||
|
fieldAffectsData,
|
||||||
|
FieldAffectingData,
|
||||||
|
fieldIsPresentationalOnly,
|
||||||
|
ValidateOptions,
|
||||||
|
} from '../../../../fields/config/types';
|
||||||
import { Fields, Field, Data } from './types';
|
import { Fields, Field, Data } from './types';
|
||||||
|
|
||||||
const buildValidationPromise = async (fieldState: Field, field: FieldAffectingData) => {
|
const buildValidationPromise = async (fieldState: Field, options: ValidateOptions<unknown, unknown, unknown>) => {
|
||||||
const validatedFieldState = fieldState;
|
const validatedFieldState = fieldState;
|
||||||
|
|
||||||
let validationResult: boolean | string = true;
|
let validationResult: boolean | string = true;
|
||||||
|
|
||||||
if (typeof field.validate === 'function') {
|
if (typeof fieldState.validate === 'function') {
|
||||||
validationResult = await field.validate(fieldState.value, field);
|
validationResult = await fieldState.validate(fieldState.value, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof validationResult === 'string') {
|
if (typeof validationResult === 'string') {
|
||||||
@@ -19,7 +26,24 @@ const buildValidationPromise = async (fieldState: Field, field: FieldAffectingDa
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data = {}): Promise<Fields> => {
|
type Args = {
|
||||||
|
fieldSchema: FieldSchema[]
|
||||||
|
data?: Data,
|
||||||
|
siblingData?: Data,
|
||||||
|
user?: User,
|
||||||
|
id?: string | number,
|
||||||
|
operation?: 'create' | 'update'
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildStateFromSchema = async (args: Args): Promise<Fields> => {
|
||||||
|
const {
|
||||||
|
fieldSchema,
|
||||||
|
data: fullData = {},
|
||||||
|
user,
|
||||||
|
id,
|
||||||
|
operation,
|
||||||
|
} = args;
|
||||||
|
|
||||||
if (fieldSchema) {
|
if (fieldSchema) {
|
||||||
const validationPromises = [];
|
const validationPromises = [];
|
||||||
|
|
||||||
@@ -35,7 +59,14 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data =
|
|||||||
passesCondition,
|
passesCondition,
|
||||||
};
|
};
|
||||||
|
|
||||||
validationPromises.push(buildValidationPromise(fieldState, field));
|
validationPromises.push(buildValidationPromise(fieldState, {
|
||||||
|
...field,
|
||||||
|
fullData,
|
||||||
|
user,
|
||||||
|
siblingData: data,
|
||||||
|
id,
|
||||||
|
operation,
|
||||||
|
}));
|
||||||
|
|
||||||
return fieldState;
|
return fieldState;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
import { unflatten } from 'flatley';
|
import { unflatten } from 'flatley';
|
||||||
import reduceFieldsToValues from './reduceFieldsToValues';
|
|
||||||
import { Fields } from './types';
|
import { Fields } from './types';
|
||||||
|
|
||||||
const getDataByPath = <T = unknown>(fields: Fields, path: string): T => {
|
const getDataByPath = <T = unknown>(fields: Fields, path: string): T => {
|
||||||
const pathPrefixToRemove = path.substring(0, path.lastIndexOf('.') + 1);
|
const pathPrefixToRemove = path.substring(0, path.lastIndexOf('.') + 1);
|
||||||
const name = path.split('.').pop();
|
const name = path.split('.').pop();
|
||||||
|
|
||||||
const data = Object.keys(fields).reduce((matchedData, key) => {
|
const data = {};
|
||||||
if (key.indexOf(`${path}.`) === 0 || key === path) {
|
Object.keys(fields).forEach((key) => {
|
||||||
return {
|
if (!fields[key].disableFormData && (key.indexOf(`${path}.`) === 0 || key === path)) {
|
||||||
...matchedData,
|
data[key.replace(pathPrefixToRemove, '')] = fields[key].value;
|
||||||
[key.replace(pathPrefixToRemove, '')]: fields[key],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return matchedData;
|
const unflattenedData = unflatten(data);
|
||||||
}, {});
|
|
||||||
|
|
||||||
const values = reduceFieldsToValues(data, true);
|
|
||||||
const unflattenedData = unflatten(values);
|
|
||||||
|
|
||||||
return unflattenedData?.[name];
|
return unflattenedData?.[name];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
import reduceFieldsToValues from './reduceFieldsToValues';
|
import { unflatten } from 'flatley';
|
||||||
import { Fields, Data } from './types';
|
import { Fields, Data } from './types';
|
||||||
|
import reduceFieldsToValues from './reduceFieldsToValues';
|
||||||
|
|
||||||
const getSiblingData = (fields: Fields, path: string): Data => {
|
const getSiblingData = (fields: Fields, path: string): Data => {
|
||||||
let siblingFields = fields;
|
if (path.indexOf('.') === -1) {
|
||||||
|
return reduceFieldsToValues(fields, true);
|
||||||
|
}
|
||||||
|
const siblingFields = {};
|
||||||
|
|
||||||
// If this field is nested
|
// If this field is nested
|
||||||
// We can provide a list of sibling fields
|
// We can provide a list of sibling fields
|
||||||
if (path.indexOf('.') > 0) {
|
|
||||||
const parentFieldPath = path.substring(0, path.lastIndexOf('.') + 1);
|
const parentFieldPath = path.substring(0, path.lastIndexOf('.') + 1);
|
||||||
siblingFields = Object.keys(fields).reduce((siblings, fieldKey) => {
|
Object.keys(fields).forEach((fieldKey) => {
|
||||||
if (fieldKey.indexOf(parentFieldPath) === 0) {
|
if (!fields[fieldKey].disableFormData && fieldKey.indexOf(parentFieldPath) === 0) {
|
||||||
return {
|
siblingFields[fieldKey.replace(parentFieldPath, '')] = fields[fieldKey].value;
|
||||||
...siblings,
|
|
||||||
[fieldKey.replace(parentFieldPath, '')]: fields[fieldKey],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return siblings;
|
return unflatten(siblingFields, { safe: true });
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
return reduceFieldsToValues(siblingFields, true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getSiblingData;
|
export default getSiblingData;
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
import React, {
|
import React, {
|
||||||
useReducer, useEffect, useRef, useState, useCallback,
|
useReducer, useEffect, useRef, useState, useCallback,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import isDeepEqual from 'deep-equal';
|
||||||
import { serialize } from 'object-to-formdata';
|
import { serialize } from 'object-to-formdata';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useAuth } from '@payloadcms/config-provider';
|
import { useAuth } from '@payloadcms/config-provider';
|
||||||
import { useLocale } from '../../utilities/Locale';
|
import { useLocale } from '../../utilities/Locale';
|
||||||
|
import { useDocumentInfo } from '../../utilities/DocumentInfo';
|
||||||
import { requests } from '../../../api';
|
import { requests } from '../../../api';
|
||||||
import useThrottledEffect from '../../../hooks/useThrottledEffect';
|
import useThrottledEffect from '../../../hooks/useThrottledEffect';
|
||||||
import fieldReducer from './fieldReducer';
|
import fieldReducer from './fieldReducer';
|
||||||
@@ -15,13 +17,13 @@ import reduceFieldsToValues from './reduceFieldsToValues';
|
|||||||
import getSiblingDataFunc from './getSiblingData';
|
import getSiblingDataFunc from './getSiblingData';
|
||||||
import getDataByPathFunc from './getDataByPath';
|
import getDataByPathFunc from './getDataByPath';
|
||||||
import wait from '../../../../utilities/wait';
|
import wait from '../../../../utilities/wait';
|
||||||
|
import { Field } from '../../../../fields/config/types';
|
||||||
import buildInitialState from './buildInitialState';
|
import buildInitialState from './buildInitialState';
|
||||||
import errorMessages from './errorMessages';
|
import errorMessages from './errorMessages';
|
||||||
import { Context as FormContextType, Props, SubmitOptions } from './types';
|
import { Context as FormContextType, Props, SubmitOptions } from './types';
|
||||||
|
|
||||||
import { SubmittedContext, ProcessingContext, ModifiedContext, FormContext, FormWatchContext } from './context';
|
import { SubmittedContext, ProcessingContext, ModifiedContext, FormContext, FormWatchContext } from './context';
|
||||||
import buildStateFromSchema from './buildStateFromSchema';
|
import buildStateFromSchema from './buildStateFromSchema';
|
||||||
import { Field } from '../../../../fields/config/types';
|
import { useOperation } from '../../utilities/OperationProvider';
|
||||||
|
|
||||||
const baseClass = 'form';
|
const baseClass = 'form';
|
||||||
|
|
||||||
@@ -45,7 +47,9 @@ const Form: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const { refreshCookie } = useAuth();
|
const { refreshCookie, user } = useAuth();
|
||||||
|
const { id } = useDocumentInfo();
|
||||||
|
const operation = useOperation();
|
||||||
|
|
||||||
const [modified, setModified] = useState(false);
|
const [modified, setModified] = useState(false);
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
@@ -70,6 +74,7 @@ const Form: React.FC<Props> = (props) => {
|
|||||||
const validateForm = useCallback(async () => {
|
const validateForm = useCallback(async () => {
|
||||||
const validatedFieldState = {};
|
const validatedFieldState = {};
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
|
const data = contextRef.current.getData();
|
||||||
|
|
||||||
const validationPromises = Object.entries(contextRef.current.fields).map(async ([path, field]) => {
|
const validationPromises = Object.entries(contextRef.current.fields).map(async ([path, field]) => {
|
||||||
const validatedField = {
|
const validatedField = {
|
||||||
@@ -81,7 +86,13 @@ const Form: React.FC<Props> = (props) => {
|
|||||||
let validationResult: boolean | string = true;
|
let validationResult: boolean | string = true;
|
||||||
|
|
||||||
if (typeof field.validate === 'function') {
|
if (typeof field.validate === 'function') {
|
||||||
validationResult = await field.validate(field.value);
|
validationResult = await field.validate(field.value, {
|
||||||
|
data,
|
||||||
|
siblingData: contextRef.current.getSiblingData(path),
|
||||||
|
user,
|
||||||
|
id,
|
||||||
|
operation,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof validationResult === 'string') {
|
if (typeof validationResult === 'string') {
|
||||||
@@ -96,10 +107,12 @@ const Form: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
await Promise.all(validationPromises);
|
await Promise.all(validationPromises);
|
||||||
|
|
||||||
|
if (!isDeepEqual(contextRef.current.fields, validatedFieldState)) {
|
||||||
dispatchFields({ type: 'REPLACE_STATE', state: validatedFieldState });
|
dispatchFields({ type: 'REPLACE_STATE', state: validatedFieldState });
|
||||||
|
}
|
||||||
|
|
||||||
return isValid;
|
return isValid;
|
||||||
}, [contextRef]);
|
}, [contextRef, id, user, operation]);
|
||||||
|
|
||||||
const submit = useCallback(async (options: SubmitOptions = {}, e): Promise<void> => {
|
const submit = useCallback(async (options: SubmitOptions = {}, e): Promise<void> => {
|
||||||
const {
|
const {
|
||||||
@@ -313,10 +326,10 @@ const Form: React.FC<Props> = (props) => {
|
|||||||
}, [contextRef]);
|
}, [contextRef]);
|
||||||
|
|
||||||
const reset = useCallback(async (fieldSchema: Field[], data: unknown) => {
|
const reset = useCallback(async (fieldSchema: Field[], data: unknown) => {
|
||||||
const state = await buildStateFromSchema(fieldSchema, data);
|
const state = await buildStateFromSchema({ fieldSchema, data, user, id, operation });
|
||||||
contextRef.current = { ...initContextState } as FormContextType;
|
contextRef.current = { ...initContextState } as FormContextType;
|
||||||
dispatchFields({ type: 'REPLACE_STATE', state });
|
dispatchFields({ type: 'REPLACE_STATE', state });
|
||||||
}, []);
|
}, [id, user, operation]);
|
||||||
|
|
||||||
contextRef.current.dispatchFields = dispatchFields;
|
contextRef.current.dispatchFields = dispatchFields;
|
||||||
contextRef.current.submit = submit;
|
contextRef.current.submit = submit;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { unflatten as flatleyUnflatten } from 'flatley';
|
import { unflatten as flatleyUnflatten } from 'flatley';
|
||||||
import { Fields, Data } from './types';
|
import { Data, Fields } from './types';
|
||||||
|
|
||||||
const reduceFieldsToValues = (fields: Fields, unflatten?: boolean): Data => {
|
const reduceFieldsToValues = (fields: Fields, unflatten?: boolean): Data => {
|
||||||
const data = {};
|
const data = {};
|
||||||
@@ -11,8 +11,7 @@ const reduceFieldsToValues = (fields: Fields, unflatten?: boolean): Data => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (unflatten) {
|
if (unflatten) {
|
||||||
const unflattened = flatleyUnflatten(data, { safe: true });
|
return flatleyUnflatten(data, { safe: true });
|
||||||
return unflattened;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Field as FieldConfig, Condition } from '../../../../fields/config/types';
|
import { Field as FieldConfig, Condition, Validate } from '../../../../fields/config/types';
|
||||||
|
|
||||||
export type Field = {
|
export type Field = {
|
||||||
value: unknown
|
value: unknown
|
||||||
initialValue: unknown
|
initialValue: unknown
|
||||||
errorMessage?: string
|
errorMessage?: string
|
||||||
valid: boolean
|
valid: boolean
|
||||||
validate?: (val: unknown) => Promise<boolean | string> | boolean | string
|
validate?: Validate
|
||||||
disableFormData?: boolean
|
disableFormData?: boolean
|
||||||
ignoreWhileFlattening?: boolean
|
ignoreWhileFlattening?: boolean
|
||||||
condition?: Condition
|
condition?: Condition
|
||||||
@@ -38,6 +38,7 @@ export type Props = {
|
|||||||
initialData?: Data
|
initialData?: Data
|
||||||
waitForAutocomplete?: boolean
|
waitForAutocomplete?: boolean
|
||||||
log?: boolean
|
log?: boolean
|
||||||
|
validationOperation?: 'create' | 'update'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SubmitOptions = {
|
export type SubmitOptions = {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { createContext, useEffect, useContext, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
||||||
import useIntersect from '../../../hooks/useIntersect';
|
import useIntersect from '../../../hooks/useIntersect';
|
||||||
import { Props, Context } from './types';
|
import { Props } from './types';
|
||||||
import { fieldAffectsData, fieldIsPresentationalOnly } from '../../../../fields/config/types';
|
import { fieldAffectsData, fieldIsPresentationalOnly } from '../../../../fields/config/types';
|
||||||
|
import { useOperation } from '../../utilities/OperationProvider';
|
||||||
|
|
||||||
const baseClass = 'render-fields';
|
const baseClass = 'render-fields';
|
||||||
|
|
||||||
@@ -10,10 +11,6 @@ const intersectionObserverOptions = {
|
|||||||
rootMargin: '1000px',
|
rootMargin: '1000px',
|
||||||
};
|
};
|
||||||
|
|
||||||
const RenderedFieldContext = createContext({} as Context);
|
|
||||||
|
|
||||||
export const useRenderedFields = (): Context => useContext(RenderedFieldContext);
|
|
||||||
|
|
||||||
const RenderFields: React.FC<Props> = (props) => {
|
const RenderFields: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
@@ -21,30 +18,17 @@ const RenderFields: React.FC<Props> = (props) => {
|
|||||||
filter,
|
filter,
|
||||||
permissions,
|
permissions,
|
||||||
readOnly: readOnlyOverride,
|
readOnly: readOnlyOverride,
|
||||||
operation: operationFromProps,
|
|
||||||
className,
|
className,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [hasRendered, setHasRendered] = useState(false);
|
const [hasRendered, setHasRendered] = useState(false);
|
||||||
const [intersectionRef, entry] = useIntersect(intersectionObserverOptions);
|
const [intersectionRef, entry] = useIntersect(intersectionObserverOptions);
|
||||||
|
const operation = useOperation();
|
||||||
|
|
||||||
const isIntersecting = Boolean(entry?.isIntersecting);
|
const isIntersecting = Boolean(entry?.isIntersecting);
|
||||||
const isAboveViewport = entry?.boundingClientRect?.top < 0;
|
const isAboveViewport = entry?.boundingClientRect?.top < 0;
|
||||||
|
|
||||||
const shouldRender = isIntersecting || isAboveViewport;
|
const shouldRender = isIntersecting || isAboveViewport;
|
||||||
|
|
||||||
const { operation: operationFromContext } = useRenderedFields();
|
|
||||||
|
|
||||||
const operation = operationFromProps || operationFromContext;
|
|
||||||
|
|
||||||
const [contextValue, setContextValue] = useState({
|
|
||||||
operation,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setContextValue({
|
|
||||||
operation,
|
|
||||||
});
|
|
||||||
}, [operation]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldRender && !hasRendered) {
|
if (shouldRender && !hasRendered) {
|
||||||
@@ -64,8 +48,7 @@ const RenderFields: React.FC<Props> = (props) => {
|
|||||||
className={classes}
|
className={classes}
|
||||||
>
|
>
|
||||||
{hasRendered && (
|
{hasRendered && (
|
||||||
<RenderedFieldContext.Provider value={contextValue}>
|
fieldSchema.map((field, i) => {
|
||||||
{fieldSchema.map((field, i) => {
|
|
||||||
const fieldIsPresentational = fieldIsPresentationalOnly(field);
|
const fieldIsPresentational = fieldIsPresentationalOnly(field);
|
||||||
let FieldComponent = fieldTypes[field.type];
|
let FieldComponent = fieldTypes[field.type];
|
||||||
|
|
||||||
@@ -136,8 +119,7 @@ const RenderFields: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
})}
|
})
|
||||||
</RenderedFieldContext.Provider>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,15 +2,8 @@ import { FieldPermissions } from '../../../../auth/types';
|
|||||||
import { FieldWithPath, Field } from '../../../../fields/config/types';
|
import { FieldWithPath, Field } from '../../../../fields/config/types';
|
||||||
import { FieldTypes } from '../field-types';
|
import { FieldTypes } from '../field-types';
|
||||||
|
|
||||||
export type Operation = 'create' | 'update'
|
|
||||||
|
|
||||||
export type Context = {
|
|
||||||
operation: Operation
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
className?: string
|
className?: string
|
||||||
operation?: Operation
|
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
permissions?: {
|
permissions?: {
|
||||||
[field: string]: FieldPermissions
|
[field: string]: FieldPermissions
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useReducer, useCallback, useState } from 'react';
|
import React, { useCallback, useEffect, useReducer, useState } from 'react';
|
||||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
import { useAuth } from '@payloadcms/config-provider';
|
||||||
import withCondition from '../../withCondition';
|
import withCondition from '../../withCondition';
|
||||||
import Button from '../../../elements/Button';
|
import Button from '../../../elements/Button';
|
||||||
import DraggableSection from '../../DraggableSection';
|
import DraggableSection from '../../DraggableSection';
|
||||||
@@ -14,6 +15,9 @@ import Banner from '../../../elements/Banner';
|
|||||||
import FieldDescription from '../../FieldDescription';
|
import FieldDescription from '../../FieldDescription';
|
||||||
import { Props } from './types';
|
import { Props } from './types';
|
||||||
|
|
||||||
|
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
|
||||||
|
import { useOperation } from '../../../utilities/OperationProvider';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
const baseClass = 'field-type array';
|
const baseClass = 'field-type array';
|
||||||
@@ -49,15 +53,17 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const [rows, dispatchRows] = useReducer(reducer, []);
|
const [rows, dispatchRows] = useReducer(reducer, []);
|
||||||
const formContext = useForm();
|
const formContext = useForm();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { id } = useDocumentInfo();
|
||||||
|
const operation = useOperation();
|
||||||
|
|
||||||
const { dispatchFields } = formContext;
|
const { dispatchFields } = formContext;
|
||||||
|
|
||||||
const path = pathFromProps || name;
|
const path = pathFromProps || name;
|
||||||
|
|
||||||
const memoizedValidate = useCallback((value) => {
|
const memoizedValidate = useCallback((value, options) => {
|
||||||
const validationResult = validate(value, { minRows, maxRows, required });
|
return validate(value, { ...options, minRows, maxRows, required });
|
||||||
return validationResult;
|
}, [maxRows, minRows, required, validate]);
|
||||||
}, [validate, maxRows, minRows, required]);
|
|
||||||
|
|
||||||
const [disableFormData, setDisableFormData] = useState(false);
|
const [disableFormData, setDisableFormData] = useState(false);
|
||||||
|
|
||||||
@@ -75,11 +81,11 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const addRow = useCallback(async (rowIndex) => {
|
const addRow = useCallback(async (rowIndex) => {
|
||||||
const subFieldState = await buildStateFromSchema(fields);
|
const subFieldState = await buildStateFromSchema({ fieldSchema: fields, operation, id, user });
|
||||||
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path });
|
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path });
|
||||||
dispatchRows({ type: 'ADD', rowIndex });
|
dispatchRows({ type: 'ADD', rowIndex });
|
||||||
setValue(value as number + 1);
|
setValue(value as number + 1);
|
||||||
}, [dispatchRows, dispatchFields, fields, path, setValue, value]);
|
}, [dispatchRows, dispatchFields, fields, path, setValue, value, operation, id, user]);
|
||||||
|
|
||||||
const removeRow = useCallback((rowIndex) => {
|
const removeRow = useCallback((rowIndex) => {
|
||||||
dispatchRows({ type: 'REMOVE', rowIndex });
|
dispatchRows({ type: 'REMOVE', rowIndex });
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React, {
|
import React, { useCallback, useEffect, useReducer, useState } from 'react';
|
||||||
useEffect, useReducer, useCallback, useState,
|
|
||||||
} from 'react';
|
|
||||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
import { useAuth } from '@payloadcms/config-provider';
|
||||||
import { usePreferences } from '../../../utilities/Preferences';
|
import { usePreferences } from '../../../utilities/Preferences';
|
||||||
import withCondition from '../../withCondition';
|
import withCondition from '../../withCondition';
|
||||||
import Button from '../../../elements/Button';
|
import Button from '../../../elements/Button';
|
||||||
@@ -20,6 +19,7 @@ import Banner from '../../../elements/Banner';
|
|||||||
import FieldDescription from '../../FieldDescription';
|
import FieldDescription from '../../FieldDescription';
|
||||||
import { Props } from './types';
|
import { Props } from './types';
|
||||||
import { DocumentPreferences } from '../../../../../preferences/types';
|
import { DocumentPreferences } from '../../../../../preferences/types';
|
||||||
|
import { useOperation } from '../../../utilities/OperationProvider';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
@@ -57,17 +57,14 @@ const Blocks: React.FC<Props> = (props) => {
|
|||||||
const { getPreference, setPreference } = usePreferences();
|
const { getPreference, setPreference } = usePreferences();
|
||||||
const [rows, dispatchRows] = useReducer(reducer, []);
|
const [rows, dispatchRows] = useReducer(reducer, []);
|
||||||
const formContext = useForm();
|
const formContext = useForm();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { id } = useDocumentInfo();
|
||||||
|
const operation = useOperation();
|
||||||
const { dispatchFields } = formContext;
|
const { dispatchFields } = formContext;
|
||||||
|
|
||||||
const memoizedValidate = useCallback((value) => {
|
const memoizedValidate = useCallback((value, options) => {
|
||||||
const validationResult = validate(
|
return validate(value, { ...options, minRows, maxRows, required });
|
||||||
value,
|
}, [maxRows, minRows, required, validate]);
|
||||||
{
|
|
||||||
minRows, maxRows, labels, blocks, required,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return validationResult;
|
|
||||||
}, [validate, maxRows, minRows, labels, blocks, required]);
|
|
||||||
|
|
||||||
const [disableFormData, setDisableFormData] = useState(false);
|
const [disableFormData, setDisableFormData] = useState(false);
|
||||||
|
|
||||||
@@ -87,12 +84,12 @@ const Blocks: React.FC<Props> = (props) => {
|
|||||||
const addRow = useCallback(async (rowIndex, blockType) => {
|
const addRow = useCallback(async (rowIndex, blockType) => {
|
||||||
const block = blocks.find((potentialBlock) => potentialBlock.slug === blockType);
|
const block = blocks.find((potentialBlock) => potentialBlock.slug === blockType);
|
||||||
|
|
||||||
const subFieldState = await buildStateFromSchema(block.fields);
|
const subFieldState = await buildStateFromSchema({ fieldSchema: block.fields, operation, id, user });
|
||||||
|
|
||||||
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path, blockType });
|
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path, blockType });
|
||||||
dispatchRows({ type: 'ADD', rowIndex, blockType });
|
dispatchRows({ type: 'ADD', rowIndex, blockType });
|
||||||
setValue(value as number + 1);
|
setValue(value as number + 1);
|
||||||
}, [path, setValue, value, blocks, dispatchFields]);
|
}, [path, setValue, value, blocks, dispatchFields, operation, id, user]);
|
||||||
|
|
||||||
const removeRow = useCallback((rowIndex) => {
|
const removeRow = useCallback((rowIndex) => {
|
||||||
dispatchRows({ type: 'REMOVE', rowIndex });
|
dispatchRows({ type: 'REMOVE', rowIndex });
|
||||||
@@ -105,8 +102,8 @@ const Blocks: React.FC<Props> = (props) => {
|
|||||||
dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path });
|
dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path });
|
||||||
}, [dispatchRows, dispatchFields, path]);
|
}, [dispatchRows, dispatchFields, path]);
|
||||||
|
|
||||||
const setCollapse = useCallback(async (id: string, collapsed: boolean) => {
|
const setCollapse = useCallback(async (rowID: string, collapsed: boolean) => {
|
||||||
dispatchRows({ type: 'SET_COLLAPSE', id, collapsed });
|
dispatchRows({ type: 'SET_COLLAPSE', rowID, collapsed });
|
||||||
|
|
||||||
if (preferencesKey) {
|
if (preferencesKey) {
|
||||||
const preferences: DocumentPreferences = await getPreference(preferencesKey);
|
const preferences: DocumentPreferences = await getPreference(preferencesKey);
|
||||||
@@ -116,9 +113,9 @@ const Blocks: React.FC<Props> = (props) => {
|
|||||||
|| [];
|
|| [];
|
||||||
|
|
||||||
if (!collapsed) {
|
if (!collapsed) {
|
||||||
newCollapsedState = newCollapsedState.filter((existingID) => existingID !== id);
|
newCollapsedState = newCollapsedState.filter((existingID) => existingID !== rowID);
|
||||||
} else {
|
} else {
|
||||||
newCollapsedState.push(id);
|
newCollapsedState.push(rowID);
|
||||||
}
|
}
|
||||||
|
|
||||||
setPreference(preferencesKey, {
|
setPreference(preferencesKey, {
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ const Checkbox: React.FC<Props> = (props) => {
|
|||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
path: pathFromProps,
|
path: pathFromProps,
|
||||||
required,
|
|
||||||
validate = checkbox,
|
validate = checkbox,
|
||||||
label,
|
label,
|
||||||
onChange,
|
onChange,
|
||||||
disableFormData,
|
disableFormData,
|
||||||
|
required,
|
||||||
admin: {
|
admin: {
|
||||||
readOnly,
|
readOnly,
|
||||||
style,
|
style,
|
||||||
@@ -32,9 +32,8 @@ const Checkbox: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const path = pathFromProps || name;
|
const path = pathFromProps || name;
|
||||||
|
|
||||||
const memoizedValidate = useCallback((value) => {
|
const memoizedValidate = useCallback((value, options) => {
|
||||||
const validationResult = validate(value, { required });
|
return validate(value, { ...options, required });
|
||||||
return validationResult;
|
|
||||||
}, [validate, required]);
|
}, [validate, required]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ const Code: React.FC<Props> = (props) => {
|
|||||||
condition,
|
condition,
|
||||||
} = {},
|
} = {},
|
||||||
label,
|
label,
|
||||||
minLength,
|
|
||||||
maxLength,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [highlighter] = useState(() => {
|
const [highlighter] = useState(() => {
|
||||||
@@ -43,10 +41,9 @@ const Code: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const path = pathFromProps || name;
|
const path = pathFromProps || name;
|
||||||
|
|
||||||
const memoizedValidate = useCallback((value) => {
|
const memoizedValidate = useCallback((value, options) => {
|
||||||
const validationResult = validate(value, { minLength, maxLength, required });
|
return validate(value, { ...options, required });
|
||||||
return validationResult;
|
}, [validate, required]);
|
||||||
}, [validate, maxLength, minLength, required]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -34,9 +34,8 @@ const DateTime: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const path = pathFromProps || name;
|
const path = pathFromProps || name;
|
||||||
|
|
||||||
const memoizedValidate = useCallback((value) => {
|
const memoizedValidate = useCallback((value, options) => {
|
||||||
const validationResult = validate(value, { required });
|
return validate(value, { ...options, required });
|
||||||
return validationResult;
|
|
||||||
}, [validate, required]);
|
}, [validate, required]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -30,9 +30,8 @@ const Email: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const path = pathFromProps || name;
|
const path = pathFromProps || name;
|
||||||
|
|
||||||
const memoizedValidate = useCallback((value) => {
|
const memoizedValidate = useCallback((value, options) => {
|
||||||
const validationResult = validate(value, { required });
|
return validate(value, { ...options, required });
|
||||||
return validationResult;
|
|
||||||
}, [validate, required]);
|
}, [validate, required]);
|
||||||
|
|
||||||
const fieldType = useField({
|
const fieldType = useField({
|
||||||
|
|||||||
@@ -32,10 +32,9 @@ const NumberField: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const path = pathFromProps || name;
|
const path = pathFromProps || name;
|
||||||
|
|
||||||
const memoizedValidate = useCallback((value) => {
|
const memoizedValidate = useCallback((value, options) => {
|
||||||
const validationResult = validate(value, { min, max, required });
|
return validate(value, { ...options, min, max, required });
|
||||||
return validationResult;
|
}, [validate, min, max, required]);
|
||||||
}, [validate, max, min, required]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ const Password: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const path = pathFromProps || name;
|
const path = pathFromProps || name;
|
||||||
|
|
||||||
const memoizedValidate = useCallback((value) => {
|
const memoizedValidate = useCallback((value, options) => {
|
||||||
const validationResult = validate(value, { required });
|
const validationResult = validate(value, { ...options, required });
|
||||||
return validationResult;
|
return validationResult;
|
||||||
}, [validate, required]);
|
}, [validate, required]);
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,8 @@ const PointField: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const path = pathFromProps || name;
|
const path = pathFromProps || name;
|
||||||
|
|
||||||
const memoizedValidate = useCallback((value) => {
|
const memoizedValidate = useCallback((value, options) => {
|
||||||
const validationResult = validate(value, { required });
|
return validate(value, { ...options, required });
|
||||||
return validationResult;
|
|
||||||
}, [validate, required]);
|
}, [validate, required]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -35,10 +35,9 @@ const RadioGroup: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const path = pathFromProps || name;
|
const path = pathFromProps || name;
|
||||||
|
|
||||||
const memoizedValidate = useCallback((value) => {
|
const memoizedValidate = useCallback((value, validationOptions) => {
|
||||||
const validationResult = validate(value, { required, options });
|
return validate(value, { ...validationOptions, options, required });
|
||||||
return validationResult;
|
}, [validate, options, required]);
|
||||||
}, [validate, required, options]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -62,9 +62,8 @@ const Relationship: React.FC<Props> = (props) => {
|
|||||||
const [hasLoadedValueOptions, setHasLoadedValueOptions] = useState(false);
|
const [hasLoadedValueOptions, setHasLoadedValueOptions] = useState(false);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
const memoizedValidate = useCallback((value) => {
|
const memoizedValidate = useCallback((value, validationOptions) => {
|
||||||
const validationResult = validate(value, { required });
|
return validate(value, { ...validationOptions, required });
|
||||||
return validationResult;
|
|
||||||
}, [validate, required]);
|
}, [validate, required]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import isHotkey from 'is-hotkey';
|
|||||||
import { createEditor, Transforms, Node, Element as SlateElement, Text, BaseEditor } from 'slate';
|
import { createEditor, Transforms, Node, Element as SlateElement, Text, BaseEditor } from 'slate';
|
||||||
import { ReactEditor, Editable, withReact, Slate } from 'slate-react';
|
import { ReactEditor, Editable, withReact, Slate } from 'slate-react';
|
||||||
import { HistoryEditor, withHistory } from 'slate-history';
|
import { HistoryEditor, withHistory } from 'slate-history';
|
||||||
|
import { options } from 'joi';
|
||||||
import { richText } from '../../../../../fields/validations';
|
import { richText } from '../../../../../fields/validations';
|
||||||
import useField from '../../useField';
|
import useField from '../../useField';
|
||||||
import withCondition from '../../withCondition';
|
import withCondition from '../../withCondition';
|
||||||
@@ -117,9 +118,8 @@ const RichText: React.FC<Props> = (props) => {
|
|||||||
);
|
);
|
||||||
}, [enabledLeaves, path, props]);
|
}, [enabledLeaves, path, props]);
|
||||||
|
|
||||||
const memoizedValidate = useCallback((value) => {
|
const memoizedValidate = useCallback((value, validationOptions) => {
|
||||||
const validationResult = validate(value, { required });
|
return validate(value, { ...validationOptions, required });
|
||||||
return validationResult;
|
|
||||||
}, [validate, required]);
|
}, [validate, required]);
|
||||||
|
|
||||||
const fieldType = useField({
|
const fieldType = useField({
|
||||||
@@ -272,7 +272,6 @@ const RichText: React.FC<Props> = (props) => {
|
|||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
>
|
>
|
||||||
<Editable
|
<Editable
|
||||||
className={`${baseClass}__input`}
|
|
||||||
renderElement={renderElement}
|
renderElement={renderElement}
|
||||||
renderLeaf={renderLeaf}
|
renderLeaf={renderLeaf}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||||||
import { Transforms, Element } from 'slate';
|
import { Transforms, Element } from 'slate';
|
||||||
import { ReactEditor, useSlateStatic } from 'slate-react';
|
import { ReactEditor, useSlateStatic } from 'slate-react';
|
||||||
import { Modal } from '@faceless-ui/modal';
|
import { Modal } from '@faceless-ui/modal';
|
||||||
|
import { useAuth } from '@payloadcms/config-provider';
|
||||||
import { SanitizedCollectionConfig } from '../../../../../../../../../collections/config/types';
|
import { SanitizedCollectionConfig } from '../../../../../../../../../collections/config/types';
|
||||||
import buildStateFromSchema from '../../../../../../Form/buildStateFromSchema';
|
import buildStateFromSchema from '../../../../../../Form/buildStateFromSchema';
|
||||||
import MinimalTemplate from '../../../../../../../templates/Minimal';
|
import MinimalTemplate from '../../../../../../../templates/Minimal';
|
||||||
@@ -29,6 +30,7 @@ type Props = {
|
|||||||
export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollectionConfig, fieldSchema, element }) => {
|
export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollectionConfig, fieldSchema, element }) => {
|
||||||
const editor = useSlateStatic();
|
const editor = useSlateStatic();
|
||||||
const [initialState, setInitialState] = useState({});
|
const [initialState, setInitialState] = useState({});
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
const handleUpdateEditData = useCallback((fields) => {
|
const handleUpdateEditData = useCallback((fields) => {
|
||||||
const newNode = {
|
const newNode = {
|
||||||
@@ -47,12 +49,12 @@ export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollection
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const awaitInitialState = async () => {
|
const awaitInitialState = async () => {
|
||||||
const state = await buildStateFromSchema(fieldSchema, element?.fields);
|
const state = await buildStateFromSchema({ fieldSchema, data: element?.fields, user, operation: 'update' });
|
||||||
setInitialState(state);
|
setInitialState(state);
|
||||||
};
|
};
|
||||||
|
|
||||||
awaitInitialState();
|
awaitInitialState();
|
||||||
}, [fieldSchema, element.fields]);
|
}, [fieldSchema, element.fields, user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ const Select: React.FC<Props> = (props) => {
|
|||||||
const {
|
const {
|
||||||
path: pathFromProps,
|
path: pathFromProps,
|
||||||
name,
|
name,
|
||||||
required,
|
|
||||||
validate = select,
|
validate = select,
|
||||||
label,
|
label,
|
||||||
options: optionsFromProps,
|
options: optionsFromProps,
|
||||||
hasMany,
|
hasMany,
|
||||||
|
required,
|
||||||
admin: {
|
admin: {
|
||||||
readOnly,
|
readOnly,
|
||||||
style,
|
style,
|
||||||
@@ -44,10 +44,9 @@ const Select: React.FC<Props> = (props) => {
|
|||||||
setOptions(formatOptions(optionsFromProps));
|
setOptions(formatOptions(optionsFromProps));
|
||||||
}, [optionsFromProps]);
|
}, [optionsFromProps]);
|
||||||
|
|
||||||
const memoizedValidate = useCallback((value) => {
|
const memoizedValidate = useCallback((value, validationOptions) => {
|
||||||
const validationResult = validate(value, { required, options });
|
return validate(value, { ...validationOptions, options, hasMany, required });
|
||||||
return validationResult;
|
}, [validate, required, hasMany, options]);
|
||||||
}, [validate, required, options]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import useField from '../../useField';
|
import useField from '../../useField';
|
||||||
import withCondition from '../../withCondition';
|
import withCondition from '../../withCondition';
|
||||||
import { text } from '../../../../../fields/validations';
|
import { text } from '../../../../../fields/validations';
|
||||||
@@ -12,6 +12,8 @@ const Text: React.FC<Props> = (props) => {
|
|||||||
required,
|
required,
|
||||||
validate = text,
|
validate = text,
|
||||||
label,
|
label,
|
||||||
|
minLength,
|
||||||
|
maxLength,
|
||||||
admin: {
|
admin: {
|
||||||
placeholder,
|
placeholder,
|
||||||
readOnly,
|
readOnly,
|
||||||
@@ -25,9 +27,13 @@ const Text: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const path = pathFromProps || name;
|
const path = pathFromProps || name;
|
||||||
|
|
||||||
|
const memoizedValidate = useCallback((value, options) => {
|
||||||
|
return validate(value, { ...options, minLength, maxLength, required });
|
||||||
|
}, [validate, minLength, maxLength, required]);
|
||||||
|
|
||||||
const field = useField<string>({
|
const field = useField<string>({
|
||||||
path,
|
path,
|
||||||
validate,
|
validate: memoizedValidate,
|
||||||
enableDebouncedValue: true,
|
enableDebouncedValue: true,
|
||||||
condition,
|
condition,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const Textarea: React.FC<Props> = (props) => {
|
|||||||
name,
|
name,
|
||||||
required,
|
required,
|
||||||
validate = textarea,
|
validate = textarea,
|
||||||
|
maxLength,
|
||||||
|
minLength,
|
||||||
admin: {
|
admin: {
|
||||||
readOnly,
|
readOnly,
|
||||||
style,
|
style,
|
||||||
@@ -24,16 +26,13 @@ const Textarea: React.FC<Props> = (props) => {
|
|||||||
condition,
|
condition,
|
||||||
} = {},
|
} = {},
|
||||||
label,
|
label,
|
||||||
minLength,
|
|
||||||
maxLength,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const path = pathFromProps || name;
|
const path = pathFromProps || name;
|
||||||
|
|
||||||
const memoizedValidate = useCallback((value) => {
|
const memoizedValidate = useCallback((value, options) => {
|
||||||
const validationResult = validate(value, { minLength, maxLength, required });
|
return validate(value, { ...options, required, maxLength, minLength });
|
||||||
return validationResult;
|
}, [validate, required, maxLength, minLength]);
|
||||||
}, [validate, maxLength, minLength, required]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const AddUploadModal: React.FC<Props> = (props) => {
|
|||||||
action={`${serverURL}${api}/${collection.slug}`}
|
action={`${serverURL}${api}/${collection.slug}`}
|
||||||
onSuccess={onSuccess}
|
onSuccess={onSuccess}
|
||||||
disableSuccessStatus
|
disableSuccessStatus
|
||||||
|
validationOperation="create"
|
||||||
>
|
>
|
||||||
<header className={`${baseClass}__header`}>
|
<header className={`${baseClass}__header`}>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -37,9 +37,8 @@ const Upload: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const collection = collections.find((coll) => coll.slug === relationTo);
|
const collection = collections.find((coll) => coll.slug === relationTo);
|
||||||
|
|
||||||
const memoizedValidate = useCallback((value) => {
|
const memoizedValidate = useCallback((value, options) => {
|
||||||
const validationResult = validate(value, { required });
|
return validate(value, { ...options, required });
|
||||||
return validationResult;
|
|
||||||
}, [validate, required]);
|
}, [validate, required]);
|
||||||
|
|
||||||
const field = useField({
|
const field = useField({
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { useContext, createContext } from 'react';
|
||||||
|
|
||||||
|
export const OperationContext = createContext(undefined);
|
||||||
|
|
||||||
|
export type Operation = 'create' | 'update'
|
||||||
|
|
||||||
|
export const useOperation = (): Operation | undefined => useContext(OperationContext);
|
||||||
@@ -15,6 +15,7 @@ import Meta from '../../utilities/Meta';
|
|||||||
import Auth from '../collections/Edit/Auth';
|
import Auth from '../collections/Edit/Auth';
|
||||||
import Loading from '../../elements/Loading';
|
import Loading from '../../elements/Loading';
|
||||||
import { Props } from './types';
|
import { Props } from './types';
|
||||||
|
import { OperationContext } from '../../utilities/OperationProvider';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ const DefaultAccount: React.FC<Props> = (props) => {
|
|||||||
<Loading />
|
<Loading />
|
||||||
)}
|
)}
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
|
<OperationContext.Provider value="update">
|
||||||
<Form
|
<Form
|
||||||
className={`${baseClass}__form`}
|
className={`${baseClass}__form`}
|
||||||
method="put"
|
method="put"
|
||||||
@@ -85,7 +87,6 @@ const DefaultAccount: React.FC<Props> = (props) => {
|
|||||||
operation="update"
|
operation="update"
|
||||||
/>
|
/>
|
||||||
<RenderFields
|
<RenderFields
|
||||||
operation="update"
|
|
||||||
permissions={permissions.fields}
|
permissions={permissions.fields}
|
||||||
readOnly={!hasSavePermission}
|
readOnly={!hasSavePermission}
|
||||||
filter={(field) => field?.admin?.position !== 'sidebar'}
|
filter={(field) => field?.admin?.position !== 'sidebar'}
|
||||||
@@ -115,7 +116,6 @@ const DefaultAccount: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={`${baseClass}__sidebar-fields`}>
|
<div className={`${baseClass}__sidebar-fields`}>
|
||||||
<RenderFields
|
<RenderFields
|
||||||
operation="update"
|
|
||||||
permissions={permissions.fields}
|
permissions={permissions.fields}
|
||||||
readOnly={!hasSavePermission}
|
readOnly={!hasSavePermission}
|
||||||
filter={(field) => field?.admin?.position === 'sidebar'}
|
filter={(field) => field?.admin?.position === 'sidebar'}
|
||||||
@@ -164,6 +164,7 @@ const DefaultAccount: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
</OperationContext.Provider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import DefaultAccount from './Default';
|
|||||||
import buildStateFromSchema from '../../forms/Form/buildStateFromSchema';
|
import buildStateFromSchema from '../../forms/Form/buildStateFromSchema';
|
||||||
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
||||||
import { NegativeFieldGutterProvider } from '../../forms/FieldTypeGutter/context';
|
import { NegativeFieldGutterProvider } from '../../forms/FieldTypeGutter/context';
|
||||||
|
import { useDocumentInfo } from '../../utilities/DocumentInfo';
|
||||||
|
|
||||||
const AccountView: React.FC = () => {
|
const AccountView: React.FC = () => {
|
||||||
const { state: locationState } = useLocation<{ data: unknown }>();
|
const { state: locationState } = useLocation<{ data: unknown }>();
|
||||||
@@ -16,6 +17,8 @@ const AccountView: React.FC = () => {
|
|||||||
const { setStepNav } = useStepNav();
|
const { setStepNav } = useStepNav();
|
||||||
const { user, permissions } = useAuth();
|
const { user, permissions } = useAuth();
|
||||||
const [initialState, setInitialState] = useState({});
|
const [initialState, setInitialState] = useState({});
|
||||||
|
const { id } = useDocumentInfo();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
serverURL,
|
serverURL,
|
||||||
routes: { api },
|
routes: { api },
|
||||||
@@ -61,12 +64,12 @@ const AccountView: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const awaitInitialState = async () => {
|
const awaitInitialState = async () => {
|
||||||
const state = await buildStateFromSchema(fields, dataToRender);
|
const state = await buildStateFromSchema({ fieldSchema: fields, data: dataToRender, operation: 'update', id, user });
|
||||||
setInitialState(state);
|
setInitialState(state);
|
||||||
};
|
};
|
||||||
|
|
||||||
awaitInitialState();
|
awaitInitialState();
|
||||||
}, [dataToRender, fields]);
|
}, [dataToRender, fields, id, user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NegativeFieldGutterProvider allow>
|
<NegativeFieldGutterProvider allow>
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ const CreateFirstUser: React.FC<Props> = (props) => {
|
|||||||
method="post"
|
method="post"
|
||||||
redirect={admin}
|
redirect={admin}
|
||||||
action={`${serverURL}${api}/${userSlug}/first-register`}
|
action={`${serverURL}${api}/${userSlug}/first-register`}
|
||||||
|
validationOperation="create"
|
||||||
>
|
>
|
||||||
<NegativeFieldGutterProvider allow>
|
<NegativeFieldGutterProvider allow>
|
||||||
<RenderFields
|
<RenderFields
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import Status from '../../elements/Status';
|
|||||||
import Autosave from '../../elements/Autosave';
|
import Autosave from '../../elements/Autosave';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
import { OperationContext } from '../../utilities/OperationProvider';
|
||||||
|
|
||||||
const baseClass = 'global-edit';
|
const baseClass = 'global-edit';
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ const DefaultGlobalView: React.FC<Props> = (props) => {
|
|||||||
<Loading />
|
<Loading />
|
||||||
)}
|
)}
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
|
<OperationContext.Provider value="update">
|
||||||
<Form
|
<Form
|
||||||
className={`${baseClass}__form`}
|
className={`${baseClass}__form`}
|
||||||
method="post"
|
method="post"
|
||||||
@@ -83,7 +85,6 @@ const DefaultGlobalView: React.FC<Props> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
<RenderFields
|
<RenderFields
|
||||||
operation="update"
|
|
||||||
readOnly={!hasSavePermission}
|
readOnly={!hasSavePermission}
|
||||||
permissions={permissions.fields}
|
permissions={permissions.fields}
|
||||||
filter={(field) => (!field.admin.position || (field.admin.position && field.admin.position !== 'sidebar'))}
|
filter={(field) => (!field.admin.position || (field.admin.position && field.admin.position !== 'sidebar'))}
|
||||||
@@ -137,7 +138,6 @@ const DefaultGlobalView: React.FC<Props> = (props) => {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
<RenderFields
|
<RenderFields
|
||||||
operation="update"
|
|
||||||
readOnly={!hasSavePermission}
|
readOnly={!hasSavePermission}
|
||||||
permissions={permissions.fields}
|
permissions={permissions.fields}
|
||||||
filter={(field) => field.admin.position === 'sidebar'}
|
filter={(field) => field.admin.position === 'sidebar'}
|
||||||
@@ -179,6 +179,7 @@ const DefaultGlobalView: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
</OperationContext.Provider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
|
|||||||
const { state: locationState } = useLocation<{data?: Record<string, unknown>}>();
|
const { state: locationState } = useLocation<{data?: Record<string, unknown>}>();
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const { setStepNav } = useStepNav();
|
const { setStepNav } = useStepNav();
|
||||||
const { permissions } = useAuth();
|
const { permissions, user } = useAuth();
|
||||||
const [initialState, setInitialState] = useState({});
|
const [initialState, setInitialState] = useState({});
|
||||||
const { getVersions } = useDocumentInfo();
|
const { getVersions } = useDocumentInfo();
|
||||||
|
|
||||||
@@ -45,9 +45,9 @@ const GlobalView: React.FC<IndexProps> = (props) => {
|
|||||||
|
|
||||||
const onSave = useCallback(async (json) => {
|
const onSave = useCallback(async (json) => {
|
||||||
getVersions();
|
getVersions();
|
||||||
const state = await buildStateFromSchema(fields, json.result);
|
const state = await buildStateFromSchema({ fieldSchema: fields, data: json.result, operation: 'update', user });
|
||||||
setInitialState(state);
|
setInitialState(state);
|
||||||
}, [getVersions, fields]);
|
}, [getVersions, fields, user]);
|
||||||
|
|
||||||
const [{ data, isLoading }] = usePayloadAPI(
|
const [{ data, isLoading }] = usePayloadAPI(
|
||||||
`${serverURL}${api}/globals/${slug}`,
|
`${serverURL}${api}/globals/${slug}`,
|
||||||
@@ -66,12 +66,12 @@ const GlobalView: React.FC<IndexProps> = (props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const awaitInitialState = async () => {
|
const awaitInitialState = async () => {
|
||||||
const state = await buildStateFromSchema(fields, dataToRender);
|
const state = await buildStateFromSchema({ fieldSchema: fields, data: dataToRender, user, operation: 'update' });
|
||||||
setInitialState(state);
|
setInitialState(state);
|
||||||
};
|
};
|
||||||
|
|
||||||
awaitInitialState();
|
awaitInitialState();
|
||||||
}, [dataToRender, fields]);
|
}, [dataToRender, fields, user]);
|
||||||
|
|
||||||
const globalPermissions = permissions?.globals?.[slug];
|
const globalPermissions = permissions?.globals?.[slug];
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import GenerateConfirmation from '../../../../elements/GenerateConfirmation';
|
|||||||
|
|
||||||
const path = 'apiKey';
|
const path = 'apiKey';
|
||||||
const baseClass = 'api-key';
|
const baseClass = 'api-key';
|
||||||
const validate = (val) => text(val, { minLength: 24, maxLength: 48 });
|
const validate = (val) => text(val, { minLength: 24, maxLength: 48, data: {}, siblingData: {} });
|
||||||
|
|
||||||
const APIKey: React.FC = () => {
|
const APIKey: React.FC = () => {
|
||||||
const [initialAPIKey, setInitialAPIKey] = useState(null);
|
const [initialAPIKey, setInitialAPIKey] = useState(null);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import Status from '../../../elements/Status';
|
|||||||
import Publish from '../../../elements/Publish';
|
import Publish from '../../../elements/Publish';
|
||||||
import SaveDraft from '../../../elements/SaveDraft';
|
import SaveDraft from '../../../elements/SaveDraft';
|
||||||
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
|
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
|
||||||
|
import { OperationContext } from '../../../utilities/OperationProvider';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
@@ -76,6 +77,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
|||||||
<Loading />
|
<Loading />
|
||||||
)}
|
)}
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
|
<OperationContext.Provider value={operation}>
|
||||||
<Form
|
<Form
|
||||||
className={`${baseClass}__form`}
|
className={`${baseClass}__form`}
|
||||||
method={id ? 'put' : 'post'}
|
method={id ? 'put' : 'post'}
|
||||||
@@ -83,6 +85,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
|||||||
onSuccess={onSave}
|
onSuccess={onSave}
|
||||||
disabled={!hasSavePermission}
|
disabled={!hasSavePermission}
|
||||||
initialState={initialState}
|
initialState={initialState}
|
||||||
|
validationOperation={isEditing ? 'update' : 'create'}
|
||||||
>
|
>
|
||||||
<div className={`${baseClass}__main`}>
|
<div className={`${baseClass}__main`}>
|
||||||
<Meta
|
<Meta
|
||||||
@@ -117,7 +120,6 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<RenderFields
|
<RenderFields
|
||||||
operation={operation}
|
|
||||||
readOnly={!hasSavePermission}
|
readOnly={!hasSavePermission}
|
||||||
permissions={permissions.fields}
|
permissions={permissions.fields}
|
||||||
filter={(field) => (!field?.admin?.position || (field?.admin?.position !== 'sidebar'))}
|
filter={(field) => (!field?.admin?.position || (field?.admin?.position !== 'sidebar'))}
|
||||||
@@ -190,7 +192,6 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
<RenderFields
|
<RenderFields
|
||||||
operation={isEditing ? 'update' : 'create'}
|
|
||||||
readOnly={!hasSavePermission}
|
readOnly={!hasSavePermission}
|
||||||
permissions={permissions.fields}
|
permissions={permissions.fields}
|
||||||
filter={(field) => field?.admin?.position === 'sidebar'}
|
filter={(field) => field?.admin?.position === 'sidebar'}
|
||||||
@@ -247,6 +248,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
</OperationContext.Provider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const EditView: React.FC<IndexProps> = (props) => {
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { setStepNav } = useStepNav();
|
const { setStepNav } = useStepNav();
|
||||||
const [initialState, setInitialState] = useState({});
|
const [initialState, setInitialState] = useState({});
|
||||||
const { permissions } = useAuth();
|
const { permissions, user } = useAuth();
|
||||||
const { getVersions } = useDocumentInfo();
|
const { getVersions } = useDocumentInfo();
|
||||||
|
|
||||||
const onSave = useCallback(async (json: any) => {
|
const onSave = useCallback(async (json: any) => {
|
||||||
@@ -50,10 +50,10 @@ const EditView: React.FC<IndexProps> = (props) => {
|
|||||||
if (!isEditing) {
|
if (!isEditing) {
|
||||||
history.push(`${admin}/collections/${collection.slug}/${json?.doc?.id}`);
|
history.push(`${admin}/collections/${collection.slug}/${json?.doc?.id}`);
|
||||||
} else {
|
} else {
|
||||||
const state = await buildStateFromSchema(collection.fields, json.doc);
|
const state = await buildStateFromSchema({ fieldSchema: collection.fields, data: json.doc, user, id, operation: 'update' });
|
||||||
setInitialState(state);
|
setInitialState(state);
|
||||||
}
|
}
|
||||||
}, [admin, collection, history, isEditing, getVersions]);
|
}, [admin, collection, history, isEditing, getVersions, user, id]);
|
||||||
|
|
||||||
const [{ data, isLoading, isError }] = usePayloadAPI(
|
const [{ data, isLoading, isError }] = usePayloadAPI(
|
||||||
(isEditing ? `${serverURL}${api}/${slug}/${id}` : null),
|
(isEditing ? `${serverURL}${api}/${slug}/${id}` : null),
|
||||||
@@ -97,12 +97,12 @@ const EditView: React.FC<IndexProps> = (props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const awaitInitialState = async () => {
|
const awaitInitialState = async () => {
|
||||||
const state = await buildStateFromSchema(fields, dataToRender);
|
const state = await buildStateFromSchema({ fieldSchema: fields, data: dataToRender, user, operation: isEditing ? 'update' : 'create', id });
|
||||||
setInitialState(state);
|
setInitialState(state);
|
||||||
};
|
};
|
||||||
|
|
||||||
awaitInitialState();
|
awaitInitialState();
|
||||||
}, [dataToRender, fields]);
|
}, [dataToRender, fields, isEditing, id, user]);
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ describe('GeoJSON', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Point Field - REST', () => {
|
describe('Point Field - REST', () => {
|
||||||
const location = [10, 20];
|
let location = [10, 20];
|
||||||
const localizedPoint = [30, 40];
|
const localizedPoint = [30, 40];
|
||||||
const group = { point: [15, 25] };
|
const group = { point: [15, 25] };
|
||||||
let doc;
|
let doc;
|
||||||
@@ -115,8 +115,24 @@ describe('GeoJSON', () => {
|
|||||||
expect(hitDocs).toHaveLength(1);
|
expect(hitDocs).toHaveLength(1);
|
||||||
expect(missDocs).toHaveLength(0);
|
expect(missDocs).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should save with non-required point', async () => {
|
||||||
|
location = undefined;
|
||||||
|
|
||||||
|
const create = await fetch(`${serverURL}/api/geolocation`, {
|
||||||
|
body: JSON.stringify({ location }),
|
||||||
|
headers,
|
||||||
|
method: 'post',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { doc } = await create.json();
|
||||||
|
|
||||||
|
expect(doc.id).toBeDefined();
|
||||||
|
expect(doc.location).toStrictEqual(location);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('Point Field - GraphQL', () => {
|
describe('Point Field - GraphQL', () => {
|
||||||
const url = `${serverURL}${routes.api}${routes.graphQL}`;
|
const url = `${serverURL}${routes.api}${routes.graphQL}`;
|
||||||
let client = null;
|
let client = null;
|
||||||
|
|||||||
81
src/collections/tests/validations.spec.js
Normal file
81
src/collections/tests/validations.spec.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import getConfig from '../../config/load';
|
||||||
|
import { email, password } from '../../mongoose/testCredentials';
|
||||||
|
|
||||||
|
require('isomorphic-fetch');
|
||||||
|
|
||||||
|
const { serverURL: url } = getConfig();
|
||||||
|
|
||||||
|
let token = null;
|
||||||
|
let headers = null;
|
||||||
|
|
||||||
|
describe('Validations - REST', () => {
|
||||||
|
beforeAll(async (done) => {
|
||||||
|
const response = await fetch(`${url}/api/admins/login`, {
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
({ token } = data);
|
||||||
|
headers = {
|
||||||
|
Authorization: `JWT ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Validations', () => {
|
||||||
|
let validation;
|
||||||
|
|
||||||
|
beforeAll(async (done) => {
|
||||||
|
const result = await fetch(`${url}/api/validations`, {
|
||||||
|
body: JSON.stringify({
|
||||||
|
validationOptions: 'ok',
|
||||||
|
text: 'test',
|
||||||
|
lessThan10: 9,
|
||||||
|
greaterThan10LessThan50: 20,
|
||||||
|
atLeast3Rows: [
|
||||||
|
{ greaterThan30: 40 },
|
||||||
|
{ greaterThan30: 50 },
|
||||||
|
{ greaterThan30: 60 },
|
||||||
|
],
|
||||||
|
array: [
|
||||||
|
{ lessThan20: 10 },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
headers,
|
||||||
|
method: 'post',
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await result.json();
|
||||||
|
validation = data.doc;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create with custom validation', async () => {
|
||||||
|
expect(validation.id).toBeDefined();
|
||||||
|
});
|
||||||
|
it('should update with custom validation', async () => {
|
||||||
|
const result = await fetch(`${url}/api/validations/${validation.id}`, {
|
||||||
|
body: JSON.stringify({
|
||||||
|
validationOptions: 'update',
|
||||||
|
}),
|
||||||
|
headers,
|
||||||
|
method: 'put',
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await result.json();
|
||||||
|
validation = data.doc;
|
||||||
|
|
||||||
|
expect(validation.validationOptions).toStrictEqual('update');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -43,7 +43,7 @@ const sanitizeFields = (fields, validRelationships: string[]) => {
|
|||||||
if (typeof field.validate === 'undefined') {
|
if (typeof field.validate === 'undefined') {
|
||||||
const defaultValidate = validations[field.type];
|
const defaultValidate = validations[field.type];
|
||||||
if (defaultValidate) {
|
if (defaultValidate) {
|
||||||
field.validate = (val) => defaultValidate(val, field);
|
field.validate = (val, options) => defaultValidate(val, { ...field, ...options });
|
||||||
} else {
|
} else {
|
||||||
field.validate = () => true;
|
field.validate = () => true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
/* eslint-disable no-use-before-define */
|
/* eslint-disable no-use-before-define */
|
||||||
import { CSSProperties } from 'react';
|
import { CSSProperties } from 'react';
|
||||||
import { Editor } from 'slate';
|
import { Editor } from 'slate';
|
||||||
|
import { Operation } from '../../types';
|
||||||
import { TypeWithID } from '../../collections/config/types';
|
import { TypeWithID } from '../../collections/config/types';
|
||||||
import { PayloadRequest } from '../../express/types';
|
import { PayloadRequest } from '../../express/types';
|
||||||
import { ConditionalDateProps } from '../../admin/components/elements/DatePicker/types';
|
import { ConditionalDateProps } from '../../admin/components/elements/DatePicker/types';
|
||||||
import { Description } from '../../admin/components/forms/FieldDescription/types';
|
import { Description } from '../../admin/components/forms/FieldDescription/types';
|
||||||
|
import { User } from '../../auth';
|
||||||
|
|
||||||
export type FieldHookArgs<T extends TypeWithID = any, P = any, S = any> = {
|
export type FieldHookArgs<T extends TypeWithID = any, P = any, S = any> = {
|
||||||
value?: P,
|
value?: P,
|
||||||
@@ -49,7 +51,15 @@ export type Labels = {
|
|||||||
plural: string;
|
plural: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Validate<T = any> = (value?: T, options?: any) => string | true | Promise<string | true>;
|
export type ValidateOptions<T, S, F> = {
|
||||||
|
data: Partial<T>
|
||||||
|
siblingData: Partial<S>
|
||||||
|
id?: string | number
|
||||||
|
user?: Partial<User>
|
||||||
|
operation?: Operation
|
||||||
|
} & F;
|
||||||
|
|
||||||
|
export type Validate<T = any, S = any, F = any> = (value?: T, options?: ValidateOptions<F, S, Partial<F>>) => string | true | Promise<string | true>;
|
||||||
|
|
||||||
export type OptionObject = {
|
export type OptionObject = {
|
||||||
label: string
|
label: string
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import validationPromise from './validationPromise';
|
import validationPromise from './validationPromise';
|
||||||
import accessPromise from './accessPromise';
|
import accessPromise from './accessPromise';
|
||||||
import hookPromise from './hookPromise';
|
import hookPromise from './hookPromise';
|
||||||
import { Field, fieldHasSubFields, fieldIsArrayType, fieldIsBlockType, fieldAffectsData, HookName } from './config/types';
|
import {
|
||||||
|
Field,
|
||||||
|
fieldHasSubFields,
|
||||||
|
fieldIsArrayType,
|
||||||
|
fieldIsBlockType,
|
||||||
|
fieldAffectsData,
|
||||||
|
HookName,
|
||||||
|
} from './config/types';
|
||||||
import { Operation } from '../types';
|
import { Operation } from '../types';
|
||||||
import { PayloadRequest } from '../express/types';
|
import { PayloadRequest } from '../express/types';
|
||||||
import { Payload } from '..';
|
import { Payload } from '..';
|
||||||
@@ -372,21 +379,31 @@ const traverseFields = (args: Arguments): void => {
|
|||||||
validationPromises.push(() => validationPromise({
|
validationPromises.push(() => validationPromise({
|
||||||
errors,
|
errors,
|
||||||
hook,
|
hook,
|
||||||
newData: { [field.name]: newRowCount },
|
data: { [field.name]: newRowCount },
|
||||||
existingData: { [field.name]: existingRowCount },
|
fullData,
|
||||||
|
originalDoc: { [field.name]: existingRowCount },
|
||||||
|
fullOriginalDoc,
|
||||||
field,
|
field,
|
||||||
path,
|
path,
|
||||||
skipValidation: skipValidationFromHere,
|
skipValidation: skipValidationFromHere,
|
||||||
|
user: req.user,
|
||||||
|
operation,
|
||||||
|
id,
|
||||||
}));
|
}));
|
||||||
} else if (fieldAffectsData(field)) {
|
} else if (fieldAffectsData(field)) {
|
||||||
validationPromises.push(() => validationPromise({
|
validationPromises.push(() => validationPromise({
|
||||||
errors,
|
errors,
|
||||||
hook,
|
hook,
|
||||||
newData: data,
|
data,
|
||||||
existingData: originalDoc,
|
fullData,
|
||||||
|
originalDoc,
|
||||||
|
fullOriginalDoc,
|
||||||
field,
|
field,
|
||||||
path,
|
path,
|
||||||
skipValidation: skipValidationFromHere,
|
skipValidation: skipValidationFromHere,
|
||||||
|
user: req.user,
|
||||||
|
operation,
|
||||||
|
id,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import merge from 'deepmerge';
|
||||||
|
import { User } from '../auth';
|
||||||
|
import { Operation } from '../types';
|
||||||
import { HookName, FieldAffectingData } from './config/types';
|
import { HookName, FieldAffectingData } from './config/types';
|
||||||
|
|
||||||
type Arguments = {
|
type Arguments = {
|
||||||
@@ -5,30 +8,47 @@ type Arguments = {
|
|||||||
field: FieldAffectingData
|
field: FieldAffectingData
|
||||||
path: string
|
path: string
|
||||||
errors: {message: string, field: string}[]
|
errors: {message: string, field: string}[]
|
||||||
newData: Record<string, unknown>
|
data: Record<string, unknown>
|
||||||
existingData: Record<string, unknown>
|
fullData: Record<string, unknown>
|
||||||
|
originalDoc: Record<string, unknown>
|
||||||
|
fullOriginalDoc: Record<string, unknown>
|
||||||
|
id?: string | number
|
||||||
skipValidation?: boolean
|
skipValidation?: boolean
|
||||||
|
user: User
|
||||||
|
operation: Operation
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationPromise = async ({
|
const validationPromise = async ({
|
||||||
errors,
|
errors,
|
||||||
hook,
|
hook,
|
||||||
newData,
|
originalDoc,
|
||||||
existingData,
|
fullOriginalDoc,
|
||||||
|
data,
|
||||||
|
fullData,
|
||||||
|
id,
|
||||||
field,
|
field,
|
||||||
path,
|
path,
|
||||||
skipValidation,
|
skipValidation,
|
||||||
|
user,
|
||||||
|
operation,
|
||||||
}: Arguments): Promise<string | boolean> => {
|
}: Arguments): Promise<string | boolean> => {
|
||||||
if (hook !== 'beforeChange' || skipValidation) return true;
|
if (hook !== 'beforeChange' || skipValidation) return true;
|
||||||
|
|
||||||
const hasCondition = field.admin && field.admin.condition;
|
const hasCondition = field.admin && field.admin.condition;
|
||||||
const shouldValidate = field.validate && !hasCondition;
|
const shouldValidate = field.validate && !hasCondition;
|
||||||
|
|
||||||
let valueToValidate = newData?.[field.name];
|
let valueToValidate = data?.[field.name];
|
||||||
if (valueToValidate === undefined) valueToValidate = existingData?.[field.name];
|
if (valueToValidate === undefined) valueToValidate = originalDoc?.[field.name];
|
||||||
if (valueToValidate === undefined) valueToValidate = field.defaultValue;
|
if (valueToValidate === undefined) valueToValidate = field.defaultValue;
|
||||||
|
|
||||||
const result = shouldValidate ? await field.validate(valueToValidate, field) : true;
|
const result = shouldValidate ? await field.validate(valueToValidate, {
|
||||||
|
...field,
|
||||||
|
data: merge(fullOriginalDoc, fullData),
|
||||||
|
siblingData: merge(originalDoc, data),
|
||||||
|
id,
|
||||||
|
operation,
|
||||||
|
user,
|
||||||
|
}) : true;
|
||||||
|
|
||||||
if (typeof result === 'string') {
|
if (typeof result === 'string') {
|
||||||
errors.push({
|
errors.push({
|
||||||
|
|||||||
@@ -1,133 +1,195 @@
|
|||||||
|
import { text, textarea, password, select, point } from './validations';
|
||||||
import { text, textarea, password, select } from './validations';
|
import { ValidateOptions } from './config/types';
|
||||||
|
|
||||||
const minLengthMessage = (length: number) => `This value must be longer than the minimum length of ${length} characters.`;
|
const minLengthMessage = (length: number) => `This value must be longer than the minimum length of ${length} characters.`;
|
||||||
const maxLengthMessage = (length: number) => `This value must be shorter than the max length of ${length} characters.`;
|
const maxLengthMessage = (length: number) => `This value must be shorter than the max length of ${length} characters.`;
|
||||||
const requiredMessage = 'This field is required.';
|
const requiredMessage = 'This field is required.';
|
||||||
|
let options: ValidateOptions<any, any, any> = {
|
||||||
|
operation: 'create',
|
||||||
|
data: undefined,
|
||||||
|
siblingData: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
describe('Field Validations', () => {
|
describe('Field Validations', () => {
|
||||||
describe('text', () => {
|
describe('text', () => {
|
||||||
it('should validate', () => {
|
it('should validate', () => {
|
||||||
const val = 'test';
|
const val = 'test';
|
||||||
const result = text(val);
|
const result = text(val, options);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
it('should show required message', () => {
|
it('should show required message', () => {
|
||||||
const val = undefined;
|
const val = undefined;
|
||||||
const result = text(val, { required: true });
|
const result = text(val, { ...options, required: true });
|
||||||
expect(result).toBe(requiredMessage);
|
expect(result).toBe(requiredMessage);
|
||||||
});
|
});
|
||||||
it('should handle undefined', () => {
|
it('should handle undefined', () => {
|
||||||
const val = undefined;
|
const val = undefined;
|
||||||
const result = text(val);
|
const result = text(val, options);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
it('should validate maxLength', () => {
|
it('should validate maxLength', () => {
|
||||||
const val = 'toolong';
|
const val = 'toolong';
|
||||||
const result = text(val, { maxLength: 5 });
|
const result = text(val, { ...options, maxLength: 5 });
|
||||||
expect(result).toBe(maxLengthMessage(5));
|
expect(result).toBe(maxLengthMessage(5));
|
||||||
});
|
});
|
||||||
it('should validate minLength', () => {
|
it('should validate minLength', () => {
|
||||||
const val = 'short';
|
const val = 'short';
|
||||||
const result = text(val, { minLength: 10 });
|
const result = text(val, { ...options, minLength: 10 });
|
||||||
expect(result).toBe(minLengthMessage(10));
|
expect(result).toBe(minLengthMessage(10));
|
||||||
});
|
});
|
||||||
it('should validate maxLength with no value', () => {
|
it('should validate maxLength with no value', () => {
|
||||||
const val = undefined;
|
const val = undefined;
|
||||||
const result = text(val, { maxLength: 5 });
|
const result = text(val, { ...options, maxLength: 5 });
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
it('should validate minLength with no value', () => {
|
it('should validate minLength with no value', () => {
|
||||||
const val = undefined;
|
const val = undefined;
|
||||||
const result = text(val, { minLength: 10 });
|
const result = text(val, { ...options, minLength: 10 });
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('textarea', () => {
|
describe('textarea', () => {
|
||||||
|
options = { ...options, field: { type: 'textarea', name: 'test' } };
|
||||||
it('should validate', () => {
|
it('should validate', () => {
|
||||||
const val = 'test';
|
const val = 'test';
|
||||||
const result = textarea(val);
|
const result = textarea(val, options);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
it('should show required message', () => {
|
it('should show required message', () => {
|
||||||
const val = undefined;
|
const val = undefined;
|
||||||
const result = textarea(val, { required: true });
|
const result = textarea(val, { ...options, required: true });
|
||||||
expect(result).toBe(requiredMessage);
|
expect(result).toBe(requiredMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle undefined', () => {
|
it('should handle undefined', () => {
|
||||||
const val = undefined;
|
const val = undefined;
|
||||||
const result = textarea(val);
|
const result = textarea(val, options);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
it('should validate maxLength', () => {
|
it('should validate maxLength', () => {
|
||||||
const val = 'toolong';
|
const val = 'toolong';
|
||||||
const result = textarea(val, { maxLength: 5 });
|
const result = textarea(val, { ...options, maxLength: 5 });
|
||||||
expect(result).toBe(maxLengthMessage(5));
|
expect(result).toBe(maxLengthMessage(5));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate minLength', () => {
|
it('should validate minLength', () => {
|
||||||
const val = 'short';
|
const val = 'short';
|
||||||
const result = textarea(val, { minLength: 10 });
|
const result = textarea(val, { ...options, minLength: 10 });
|
||||||
expect(result).toBe(minLengthMessage(10));
|
expect(result).toBe(minLengthMessage(10));
|
||||||
});
|
});
|
||||||
it('should validate maxLength with no value', () => {
|
it('should validate maxLength with no value', () => {
|
||||||
const val = undefined;
|
const val = undefined;
|
||||||
const result = textarea(val, { maxLength: 5 });
|
const result = textarea(val, { ...options, maxLength: 5 });
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
it('should validate minLength with no value', () => {
|
it('should validate minLength with no value', () => {
|
||||||
const val = undefined;
|
const val = undefined;
|
||||||
const result = textarea(val, { minLength: 10 });
|
const result = textarea(val, { ...options, minLength: 10 });
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('password', () => {
|
describe('password', () => {
|
||||||
|
options.type = 'password';
|
||||||
|
options.name = 'test';
|
||||||
it('should validate', () => {
|
it('should validate', () => {
|
||||||
const val = 'test';
|
const val = 'test';
|
||||||
const result = password(val);
|
const result = password(val, options);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
it('should show required message', () => {
|
it('should show required message', () => {
|
||||||
const val = undefined;
|
const val = undefined;
|
||||||
const result = password(val, { required: true });
|
const result = password(val, { ...options, required: true });
|
||||||
expect(result).toBe(requiredMessage);
|
expect(result).toBe(requiredMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle undefined', () => {
|
it('should handle undefined', () => {
|
||||||
const val = undefined;
|
const val = undefined;
|
||||||
const result = password(val);
|
const result = password(val, options);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
it('should validate maxLength', () => {
|
it('should validate maxLength', () => {
|
||||||
const val = 'toolong';
|
const val = 'toolong';
|
||||||
const result = password(val, { maxLength: 5 });
|
const result = password(val, { ...options, maxLength: 5 });
|
||||||
expect(result).toBe(maxLengthMessage(5));
|
expect(result).toBe(maxLengthMessage(5));
|
||||||
});
|
});
|
||||||
it('should validate minLength', () => {
|
it('should validate minLength', () => {
|
||||||
const val = 'short';
|
const val = 'short';
|
||||||
const result = password(val, { minLength: 10 });
|
const result = password(val, { ...options, minLength: 10 });
|
||||||
expect(result).toBe(minLengthMessage(10));
|
expect(result).toBe(minLengthMessage(10));
|
||||||
});
|
});
|
||||||
it('should validate maxLength with no value', () => {
|
it('should validate maxLength with no value', () => {
|
||||||
const val = undefined;
|
const val = undefined;
|
||||||
const result = password(val, { maxLength: 5 });
|
const result = password(val, { ...options, maxLength: 5 });
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
it('should validate minLength with no value', () => {
|
it('should validate minLength with no value', () => {
|
||||||
const val = undefined;
|
const val = undefined;
|
||||||
const result = password(val, { minLength: 10 });
|
const result = password(val, { ...options, minLength: 10 });
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('point', () => {
|
||||||
|
options.type = 'point';
|
||||||
|
options.name = 'point';
|
||||||
|
it('should validate numbers', () => {
|
||||||
|
const val = ['0.1', '0.2'];
|
||||||
|
const result = point(val, options);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
it('should validate strings that could be numbers', () => {
|
||||||
|
const val = ['0.1', '0.2'];
|
||||||
|
const result = point(val, options);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
it('should show required message when undefined', () => {
|
||||||
|
const val = undefined;
|
||||||
|
const result = point(val, { ...options, required: true });
|
||||||
|
expect(result).not.toBe(true);
|
||||||
|
});
|
||||||
|
it('should show required message when array', () => {
|
||||||
|
const val = [];
|
||||||
|
const result = point(val, { ...options, required: true });
|
||||||
|
expect(result).not.toBe(true);
|
||||||
|
});
|
||||||
|
it('should show required message when array of undefined', () => {
|
||||||
|
const val = [undefined, undefined];
|
||||||
|
const result = point(val, { ...options, required: true });
|
||||||
|
expect(result).not.toBe(true);
|
||||||
|
});
|
||||||
|
it('should handle undefined not required', () => {
|
||||||
|
const val = undefined;
|
||||||
|
const result = password(val, options);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
it('should handle empty array not required', () => {
|
||||||
|
const val = [];
|
||||||
|
const result = point(val, options);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
it('should handle array of undefined not required', () => {
|
||||||
|
const val = [undefined, undefined];
|
||||||
|
const result = point(val, options);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
it('should prevent text input', () => {
|
||||||
|
const val = ['bad', 'input'];
|
||||||
|
const result = point(val, options);
|
||||||
|
expect(result).not.toBe(true);
|
||||||
|
});
|
||||||
|
it('should prevent missing value', () => {
|
||||||
|
const val = [0.1];
|
||||||
|
const result = point(val, options);
|
||||||
|
expect(result).not.toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('select', () => {
|
describe('select', () => {
|
||||||
const arrayOptions = {
|
options.type = 'select';
|
||||||
options: ['one', 'two', 'three'],
|
options.options = ['one', 'two', 'three'];
|
||||||
};
|
|
||||||
const optionsRequired = {
|
const optionsRequired = {
|
||||||
|
...options,
|
||||||
required: true,
|
required: true,
|
||||||
options: [{
|
options: [{
|
||||||
value: 'one',
|
value: 'one',
|
||||||
@@ -141,6 +203,7 @@ describe('Field Validations', () => {
|
|||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
const optionsWithEmptyString = {
|
const optionsWithEmptyString = {
|
||||||
|
...options,
|
||||||
options: [{
|
options: [{
|
||||||
value: '',
|
value: '',
|
||||||
label: 'None',
|
label: 'None',
|
||||||
@@ -151,27 +214,27 @@ describe('Field Validations', () => {
|
|||||||
};
|
};
|
||||||
it('should allow valid input', () => {
|
it('should allow valid input', () => {
|
||||||
const val = 'one';
|
const val = 'one';
|
||||||
const result = select(val, arrayOptions);
|
const result = select(val, options);
|
||||||
expect(result).toStrictEqual(true);
|
expect(result).toStrictEqual(true);
|
||||||
});
|
});
|
||||||
it('should prevent invalid input', () => {
|
it('should prevent invalid input', () => {
|
||||||
const val = 'bad';
|
const val = 'bad';
|
||||||
const result = select(val, arrayOptions);
|
const result = select(val, options);
|
||||||
expect(result).not.toStrictEqual(true);
|
expect(result).not.toStrictEqual(true);
|
||||||
});
|
});
|
||||||
it('should allow null input', () => {
|
it('should allow null input', () => {
|
||||||
const val = null;
|
const val = null;
|
||||||
const result = select(val, arrayOptions);
|
const result = select(val, options);
|
||||||
expect(result).toStrictEqual(true);
|
expect(result).toStrictEqual(true);
|
||||||
});
|
});
|
||||||
it('should allow undefined input', () => {
|
it('should allow undefined input', () => {
|
||||||
let val;
|
let val;
|
||||||
const result = select(val, arrayOptions);
|
const result = select(val, options);
|
||||||
expect(result).toStrictEqual(true);
|
expect(result).toStrictEqual(true);
|
||||||
});
|
});
|
||||||
it('should prevent empty string input', () => {
|
it('should prevent empty string input', () => {
|
||||||
const val = '';
|
const val = '';
|
||||||
const result = select(val, arrayOptions);
|
const result = select(val, options);
|
||||||
expect(result).not.toStrictEqual(true);
|
expect(result).not.toStrictEqual(true);
|
||||||
});
|
});
|
||||||
it('should prevent undefined input with required', () => {
|
it('should prevent undefined input with required', () => {
|
||||||
@@ -179,35 +242,41 @@ describe('Field Validations', () => {
|
|||||||
const result = select(val, optionsRequired);
|
const result = select(val, optionsRequired);
|
||||||
expect(result).not.toStrictEqual(true);
|
expect(result).not.toStrictEqual(true);
|
||||||
});
|
});
|
||||||
|
it('should prevent empty string input with required', () => {
|
||||||
|
const result = select('', optionsRequired);
|
||||||
|
expect(result).not.toStrictEqual(true);
|
||||||
|
});
|
||||||
it('should prevent undefined input with required and hasMany', () => {
|
it('should prevent undefined input with required and hasMany', () => {
|
||||||
let val;
|
let val;
|
||||||
const result = select(val, { ...optionsRequired, hasMany: true });
|
options.hasMany = true;
|
||||||
|
const result = select(val, optionsRequired);
|
||||||
expect(result).not.toStrictEqual(true);
|
expect(result).not.toStrictEqual(true);
|
||||||
});
|
});
|
||||||
it('should prevent empty array input with required and hasMany', () => {
|
it('should prevent empty array input with required and hasMany', () => {
|
||||||
const result = select([], { ...optionsRequired, hasMany: true });
|
optionsRequired.hasMany = true;
|
||||||
expect(result).not.toStrictEqual(true);
|
const result = select([], optionsRequired);
|
||||||
});
|
|
||||||
it('should prevent empty string input with required', () => {
|
|
||||||
const result = select('', { ...optionsRequired });
|
|
||||||
expect(result).not.toStrictEqual(true);
|
expect(result).not.toStrictEqual(true);
|
||||||
});
|
});
|
||||||
it('should prevent empty string array input with required and hasMany', () => {
|
it('should prevent empty string array input with required and hasMany', () => {
|
||||||
const result = select([''], { ...optionsRequired, hasMany: true });
|
options.hasMany = true;
|
||||||
|
const result = select([''], optionsRequired);
|
||||||
expect(result).not.toStrictEqual(true);
|
expect(result).not.toStrictEqual(true);
|
||||||
});
|
});
|
||||||
it('should prevent null input with required and hasMany', () => {
|
it('should prevent null input with required and hasMany', () => {
|
||||||
const val = null;
|
const val = null;
|
||||||
const result = select(val, { ...optionsRequired, hasMany: true });
|
options.hasMany = true;
|
||||||
|
const result = select(val, optionsRequired);
|
||||||
expect(result).not.toStrictEqual(true);
|
expect(result).not.toStrictEqual(true);
|
||||||
});
|
});
|
||||||
it('should allow valid input with option objects', () => {
|
it('should allow valid input with option objects', () => {
|
||||||
const val = 'one';
|
const val = 'one';
|
||||||
|
options.hasMany = false;
|
||||||
const result = select(val, optionsRequired);
|
const result = select(val, optionsRequired);
|
||||||
expect(result).toStrictEqual(true);
|
expect(result).toStrictEqual(true);
|
||||||
});
|
});
|
||||||
it('should prevent invalid input with option objects', () => {
|
it('should prevent invalid input with option objects', () => {
|
||||||
const val = 'bad';
|
const val = 'bad';
|
||||||
|
options.hasMany = false;
|
||||||
const result = select(val, optionsRequired);
|
const result = select(val, optionsRequired);
|
||||||
expect(result).not.toStrictEqual(true);
|
expect(result).not.toStrictEqual(true);
|
||||||
});
|
});
|
||||||
@@ -218,27 +287,30 @@ describe('Field Validations', () => {
|
|||||||
});
|
});
|
||||||
it('should allow empty string input with option object and required', () => {
|
it('should allow empty string input with option object and required', () => {
|
||||||
const val = '';
|
const val = '';
|
||||||
const result = select(val, { ...optionsWithEmptyString, required: true });
|
optionsWithEmptyString.required = true;
|
||||||
|
const result = select(val, optionsWithEmptyString);
|
||||||
expect(result).toStrictEqual(true);
|
expect(result).toStrictEqual(true);
|
||||||
});
|
});
|
||||||
it('should allow valid input with hasMany', () => {
|
it('should allow valid input with hasMany', () => {
|
||||||
const val = ['one', 'two'];
|
const val = ['one', 'two'];
|
||||||
const result = select(val, arrayOptions);
|
const result = select(val, options);
|
||||||
expect(result).toStrictEqual(true);
|
expect(result).toStrictEqual(true);
|
||||||
});
|
});
|
||||||
it('should prevent invalid input with hasMany', () => {
|
it('should prevent invalid input with hasMany', () => {
|
||||||
const val = ['one', 'bad'];
|
const val = ['one', 'bad'];
|
||||||
const result = select(val, arrayOptions);
|
const result = select(val, options);
|
||||||
expect(result).not.toStrictEqual(true);
|
expect(result).not.toStrictEqual(true);
|
||||||
});
|
});
|
||||||
it('should allow valid input with hasMany option objects', () => {
|
it('should allow valid input with hasMany option objects', () => {
|
||||||
const val = ['one', 'three'];
|
const val = ['one', 'three'];
|
||||||
const result = select(val, { ...optionsRequired, hasMany: true });
|
optionsRequired.hasMany = true;
|
||||||
|
const result = select(val, optionsRequired);
|
||||||
expect(result).toStrictEqual(true);
|
expect(result).toStrictEqual(true);
|
||||||
});
|
});
|
||||||
it('should prevent invalid input with hasMany option objects', () => {
|
it('should prevent invalid input with hasMany option objects', () => {
|
||||||
const val = ['three', 'bad'];
|
const val = ['three', 'bad'];
|
||||||
const result = select(val, { ...optionsRequired, hasMany: true });
|
optionsRequired.hasMany = true;
|
||||||
|
const result = select(val, optionsRequired);
|
||||||
expect(result).not.toStrictEqual(true);
|
expect(result).not.toStrictEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,40 +1,56 @@
|
|||||||
import defaultRichTextValue from './richText/defaultValue';
|
import defaultRichTextValue from './richText/defaultValue';
|
||||||
import { Validate } from './config/types';
|
import {
|
||||||
|
ArrayField,
|
||||||
|
BlockField,
|
||||||
|
CheckboxField,
|
||||||
|
CodeField, DateField,
|
||||||
|
EmailField,
|
||||||
|
NumberField,
|
||||||
|
PointField,
|
||||||
|
RadioField,
|
||||||
|
RelationshipField,
|
||||||
|
RichTextField,
|
||||||
|
SelectField,
|
||||||
|
TextareaField,
|
||||||
|
TextField,
|
||||||
|
UploadField,
|
||||||
|
Validate,
|
||||||
|
} from './config/types';
|
||||||
|
|
||||||
const defaultMessage = 'This field is required.';
|
const defaultMessage = 'This field is required.';
|
||||||
|
|
||||||
export const number: Validate = (value: string, options = {}) => {
|
export const number: Validate<unknown, unknown, NumberField> = (value: string, { required, min, max }) => {
|
||||||
const parsedValue = parseInt(value, 10);
|
const parsedValue = parseInt(value, 10);
|
||||||
|
|
||||||
if ((value && typeof parsedValue !== 'number') || (options.required && Number.isNaN(parsedValue))) {
|
if ((value && typeof parsedValue !== 'number') || (required && Number.isNaN(parsedValue))) {
|
||||||
return 'Please enter a valid number.';
|
return 'Please enter a valid number.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.max && parsedValue > options.max) {
|
if (max && parsedValue > max) {
|
||||||
return `"${value}" is greater than the max allowed value of ${options.max}.`;
|
return `"${value}" is greater than the max allowed value of ${max}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.min && parsedValue < options.min) {
|
if (min && parsedValue < min) {
|
||||||
return `"${value}" is less than the min allowed value of ${options.min}.`;
|
return `"${value}" is less than the min allowed value of ${min}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.required && typeof parsedValue !== 'number') {
|
if (required && typeof parsedValue !== 'number') {
|
||||||
return defaultMessage;
|
return defaultMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const text: Validate = (value: string, options = {}) => {
|
export const text: Validate<unknown, unknown, TextField> = (value: string, { minLength, maxLength, required }) => {
|
||||||
if (value && options.maxLength && value.length > options.maxLength) {
|
if (value && maxLength && value.length > maxLength) {
|
||||||
return `This value must be shorter than the max length of ${options.maxLength} characters.`;
|
return `This value must be shorter than the max length of ${maxLength} characters.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value && options.minLength && value?.length < options.minLength) {
|
if (value && minLength && value?.length < minLength) {
|
||||||
return `This value must be longer than the minimum length of ${options.minLength} characters.`;
|
return `This value must be longer than the minimum length of ${minLength} characters.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.required) {
|
if (required) {
|
||||||
if (typeof value !== 'string' || value?.length === 0) {
|
if (typeof value !== 'string' || value?.length === 0) {
|
||||||
return defaultMessage;
|
return defaultMessage;
|
||||||
}
|
}
|
||||||
@@ -43,65 +59,57 @@ export const text: Validate = (value: string, options = {}) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const password: Validate = (value: string, options = {}) => {
|
export const password: Validate<unknown, unknown, TextField> = (value: string, { required, maxLength, minLength }) => {
|
||||||
if (value && options.maxLength && value.length > options.maxLength) {
|
if (value && maxLength && value.length > maxLength) {
|
||||||
return `This value must be shorter than the max length of ${options.maxLength} characters.`;
|
return `This value must be shorter than the max length of ${maxLength} characters.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value && options.minLength && value.length < options.minLength) {
|
if (value && minLength && value.length < minLength) {
|
||||||
return `This value must be longer than the minimum length of ${options.minLength} characters.`;
|
return `This value must be longer than the minimum length of ${minLength} characters.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.required && !value) {
|
if (required && !value) {
|
||||||
return defaultMessage;
|
return defaultMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const email: Validate = (value: string, options = {}) => {
|
export const email: Validate<unknown, unknown, EmailField> = (value: string, { required }) => {
|
||||||
if ((value && !/\S+@\S+\.\S+/.test(value))
|
if ((value && !/\S+@\S+\.\S+/.test(value))
|
||||||
|| (!value && options.required)) {
|
|| (!value && required)) {
|
||||||
return 'Please enter a valid email address.';
|
return 'Please enter a valid email address.';
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const textarea: Validate = (value: string, options = {}) => {
|
export const textarea: Validate<unknown, unknown, TextareaField> = (value: string, { required, maxLength, minLength }) => {
|
||||||
if (value && options.maxLength && value.length > options.maxLength) {
|
if (value && maxLength && value.length > maxLength) {
|
||||||
return `This value must be shorter than the max length of ${options.maxLength} characters.`;
|
return `This value must be shorter than the max length of ${maxLength} characters.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value && options.minLength && value.length < options.minLength) {
|
if (value && minLength && value.length < minLength) {
|
||||||
return `This value must be longer than the minimum length of ${options.minLength} characters.`;
|
return `This value must be longer than the minimum length of ${minLength} characters.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.required && !value) {
|
if (required && !value) {
|
||||||
return defaultMessage;
|
return defaultMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const wysiwyg: Validate = (value: string, options = {}) => {
|
export const code: Validate<unknown, unknown, CodeField> = (value: string, { required }) => {
|
||||||
if (options.required && !value) {
|
if (required && value === undefined) {
|
||||||
return defaultMessage;
|
return defaultMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const code: Validate = (value: string, options = {}) => {
|
export const richText: Validate<unknown, unknown, RichTextField> = (value, { required }) => {
|
||||||
if (options.required && value === undefined) {
|
if (required) {
|
||||||
return defaultMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const richText: Validate = (value, options = {}) => {
|
|
||||||
if (options.required) {
|
|
||||||
const stringifiedDefaultValue = JSON.stringify(defaultRichTextValue);
|
const stringifiedDefaultValue = JSON.stringify(defaultRichTextValue);
|
||||||
if (value && JSON.stringify(value) !== stringifiedDefaultValue) return true;
|
if (value && JSON.stringify(value) !== stringifiedDefaultValue) return true;
|
||||||
return 'This field is required.';
|
return 'This field is required.';
|
||||||
@@ -110,16 +118,16 @@ export const richText: Validate = (value, options = {}) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkbox: Validate = (value: boolean, options = {}) => {
|
export const checkbox: Validate<unknown, unknown, CheckboxField> = (value: boolean, { required }) => {
|
||||||
if ((value && typeof value !== 'boolean')
|
if ((value && typeof value !== 'boolean')
|
||||||
|| (options.required && typeof value !== 'boolean')) {
|
|| (required && typeof value !== 'boolean')) {
|
||||||
return 'This field can only be equal to true or false.';
|
return 'This field can only be equal to true or false.';
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const date: Validate = (value, options = {}) => {
|
export const date: Validate<unknown, unknown, DateField> = (value, { required }) => {
|
||||||
if (value && !isNaN(Date.parse(value.toString()))) { /* eslint-disable-line */
|
if (value && !isNaN(Date.parse(value.toString()))) { /* eslint-disable-line */
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -128,50 +136,50 @@ export const date: Validate = (value, options = {}) => {
|
|||||||
return `"${value}" is not a valid date.`;
|
return `"${value}" is not a valid date.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.required) {
|
if (required) {
|
||||||
return defaultMessage;
|
return defaultMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const upload: Validate = (value: string, options = {}) => {
|
export const upload: Validate<unknown, unknown, UploadField> = (value: string, { required }) => {
|
||||||
if (value || !options.required) return true;
|
if (value || !required) return true;
|
||||||
return defaultMessage;
|
return defaultMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const relationship: Validate = (value, options = {}) => {
|
export const relationship: Validate<unknown, unknown, RelationshipField> = (value, { required }) => {
|
||||||
if (value || !options.required) return true;
|
if (value || !required) return true;
|
||||||
return defaultMessage;
|
return defaultMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const array: Validate = (value, options = {}) => {
|
export const array: Validate<unknown, unknown, ArrayField> = (value, { minRows, maxRows, required }) => {
|
||||||
if (options.minRows && value < options.minRows) {
|
if (minRows && value < minRows) {
|
||||||
return `This field requires at least ${options.minRows} row(s).`;
|
return `This field requires at least ${minRows} row(s).`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.maxRows && value > options.maxRows) {
|
if (maxRows && value > maxRows) {
|
||||||
return `This field requires no more than ${options.maxRows} row(s).`;
|
return `This field requires no more than ${maxRows} row(s).`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!value && options.required) {
|
if (!value && required) {
|
||||||
return 'This field requires at least one row.';
|
return 'This field requires at least one row.';
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const select: Validate = (value, options = {}) => {
|
export const select: Validate<unknown, unknown, SelectField> = (value, { options, hasMany, required }) => {
|
||||||
if (Array.isArray(value) && value.some((input) => !options.options.some((option) => (option === input || option.value === input)))) {
|
if (Array.isArray(value) && value.some((input) => !options.some((option) => (option === input || (typeof option !== 'string' && option?.value === input))))) {
|
||||||
return 'This field has an invalid selection';
|
return 'This field has an invalid selection';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'string' && !options.options.some((option) => (option === value || option.value === value))) {
|
if (typeof value === 'string' && !options.some((option) => (option === value || (typeof option !== 'string' && option.value === value)))) {
|
||||||
return 'This field has an invalid selection';
|
return 'This field has an invalid selection';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.required && (
|
if (required && (
|
||||||
(typeof value === 'undefined' || value === null) || (options.hasMany && Array.isArray(value) && (value as [])?.length === 0))
|
(typeof value === 'undefined' || value === null) || (hasMany && Array.isArray(value) && (value as [])?.length === 0))
|
||||||
) {
|
) {
|
||||||
return defaultMessage;
|
return defaultMessage;
|
||||||
}
|
}
|
||||||
@@ -179,41 +187,41 @@ export const select: Validate = (value, options = {}) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const radio: Validate = (value, options = {}) => {
|
export const radio: Validate<unknown, unknown, RadioField> = (value, { options, required }) => {
|
||||||
const stringValue = String(value);
|
const stringValue = String(value);
|
||||||
if ((typeof value !== 'undefined' || !options.required) && (options.options.find((option) => String(option.value) === stringValue))) return true;
|
if ((typeof value !== 'undefined' || !required) && (options.find((option) => String(typeof option !== 'string' && option?.value) === stringValue))) return true;
|
||||||
return defaultMessage;
|
return defaultMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const blocks: Validate = (value, options = {}) => {
|
export const blocks: Validate<unknown, unknown, BlockField> = (value, { maxRows, minRows, required }) => {
|
||||||
if (options.minRows && value < options.minRows) {
|
if (minRows && value < minRows) {
|
||||||
return `This field requires at least ${options.minRows} row(s).`;
|
return `This field requires at least ${minRows} row(s).`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.maxRows && value > options.maxRows) {
|
if (maxRows && value > maxRows) {
|
||||||
return `This field requires no more than ${options.maxRows} row(s).`;
|
return `This field requires no more than ${maxRows} row(s).`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!value && options.required) {
|
if (!value && required) {
|
||||||
return 'This field requires at least one row.';
|
return 'This field requires at least one row.';
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const point: Validate = (value: [number | string, number | string] = ['', ''], options = {}) => {
|
export const point: Validate<unknown, unknown, PointField> = (value: [number | string, number | string] = ['', ''], { required }) => {
|
||||||
const lng = parseFloat(String(value[0]));
|
const lng = parseFloat(String(value[0]));
|
||||||
const lat = parseFloat(String(value[1]));
|
const lat = parseFloat(String(value[1]));
|
||||||
if (
|
if (required && (
|
||||||
(value[0] && value[1] && typeof lng !== 'number' && typeof lat !== 'number')
|
(value[0] && value[1] && typeof lng !== 'number' && typeof lat !== 'number')
|
||||||
|| (options.required && (Number.isNaN(lng) || Number.isNaN(lat)))
|
|| (Number.isNaN(lng) || Number.isNaN(lat))
|
||||||
|| (Array.isArray(value) && value.length !== 2)
|
|| (Array.isArray(value) && value.length !== 2)
|
||||||
) {
|
)) {
|
||||||
return 'This field requires two numbers';
|
return 'This field requires two numbers';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.required && typeof value[0] !== typeof value[1]) {
|
if ((value[1] && Number.isNaN(lng)) || (value[0] && Number.isNaN(lat))) {
|
||||||
return 'This field requires two numbers or both can be empty';
|
return 'This field has an invalid input';
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -226,7 +234,6 @@ export default {
|
|||||||
email,
|
email,
|
||||||
textarea,
|
textarea,
|
||||||
code,
|
code,
|
||||||
wysiwyg,
|
|
||||||
richText,
|
richText,
|
||||||
checkbox,
|
checkbox,
|
||||||
date,
|
date,
|
||||||
|
|||||||
Reference in New Issue
Block a user