, siblingData: Partial) => boolean;
+export type FilterOptionsProps = {
+ id: string | number,
+ user: Partial,
+ data: unknown,
+ siblingData: unknown,
+ relationTo: string | string[],
+}
+
+export type FilterOptions = Where | ((options: FilterOptionsProps) => Where);
+
type Admin = {
position?: 'sidebar';
width?: string;
@@ -183,6 +193,7 @@ export type UploadField = FieldBase & {
type: 'upload'
relationTo: string
maxDepth?: number
+ filterOptions?: FilterOptions;
}
type CodeAdmin = Admin & {
@@ -207,8 +218,19 @@ export type RelationshipField = FieldBase & {
relationTo: string | string[];
hasMany?: boolean;
maxDepth?: number;
+ filterOptions?: FilterOptions;
}
+export type ValueWithRelation = {
+ relationTo: string
+ value: string | number
+}
+
+export type RelationshipValue = (string | number)
+ | (string | number)[]
+ | ValueWithRelation
+ | ValueWithRelation[]
+
type RichTextPlugin = (editor: Editor) => Editor;
export type RichTextCustomElement = {
diff --git a/src/fields/performFieldOperations.ts b/src/fields/performFieldOperations.ts
index 5a95aeecb4..fe6aa0b5a3 100644
--- a/src/fields/performFieldOperations.ts
+++ b/src/fields/performFieldOperations.ts
@@ -116,8 +116,8 @@ export default async function performFieldOperations(this: Payload, entityConfig
const hookResults = hookPromises.map((promise) => promise());
await Promise.all(hookResults);
- validationPromises.forEach((promise) => promise());
- await Promise.all(validationPromises);
+ const validationResults = validationPromises.map((promise) => promise());
+ await Promise.all(validationResults);
if (errors.length > 0) {
throw new ValidationError(errors);
diff --git a/src/fields/validations.ts b/src/fields/validations.ts
index a8d31688f2..e084bd7717 100644
--- a/src/fields/validations.ts
+++ b/src/fields/validations.ts
@@ -3,12 +3,14 @@ import {
ArrayField,
BlockField,
CheckboxField,
- CodeField, DateField,
+ CodeField,
+ DateField,
EmailField,
NumberField,
PointField,
RadioField,
RelationshipField,
+ RelationshipValue,
RichTextField,
SelectField,
TextareaField,
@@ -16,6 +18,9 @@ import {
UploadField,
Validate,
} from './config/types';
+import { TypeWithID } from '../collections/config/types';
+import canUseDOM from '../utilities/canUseDOM';
+import payload from '../index';
const defaultMessage = 'This field is required.';
@@ -84,7 +89,11 @@ export const email: Validate = (value: string, { r
return true;
};
-export const textarea: Validate = (value: string, { required, maxLength, minLength }) => {
+export const textarea: Validate = (value: string, {
+ required,
+ maxLength,
+ minLength,
+}) => {
if (value && maxLength && value.length > maxLength) {
return `This value must be shorter than the max length of ${maxLength} characters.`;
}
@@ -143,14 +152,96 @@ export const date: Validate = (value, { required })
return true;
};
-export const upload: Validate = (value: string, { required }) => {
- if (value || !required) return true;
- return defaultMessage;
+const validateFilterOptions: Validate = async (value, { filterOptions, id, user, data, siblingData, relationTo }) => {
+ if (!canUseDOM && typeof filterOptions !== 'undefined' && value) {
+ const options: {
+ [collection: string]: (string | number)[]
+ } = {};
+
+ const collections = typeof relationTo === 'string' ? [relationTo] : relationTo;
+ const values = Array.isArray(value) ? value : [value];
+
+ await Promise.all(collections.map(async (collection) => {
+ const optionFilter = typeof filterOptions === 'function' ? filterOptions({
+ id,
+ data,
+ siblingData,
+ user,
+ relationTo: collection,
+ }) : filterOptions;
+
+ const valueIDs: (string | number)[] = [];
+
+ values.forEach((val) => {
+ if (typeof val === 'object' && val?.value) {
+ valueIDs.push(val.value);
+ }
+
+ if (typeof val === 'string' || typeof val === 'number') {
+ valueIDs.push(val);
+ }
+ });
+
+ const result = await payload.find({
+ collection,
+ depth: 0,
+ where: {
+ and: [
+ { id: { in: valueIDs } },
+ optionFilter,
+ ],
+ },
+ });
+
+ options[collection] = result.docs.map((doc) => doc.id);
+ }));
+
+ const invalidRelationships = values.filter((val) => {
+ let collection: string;
+ let requestedID: string | number;
+
+ if (typeof relationTo === 'string') {
+ collection = relationTo;
+
+ if (typeof val === 'string' || typeof val === 'number') {
+ requestedID = val;
+ }
+ }
+
+ if (Array.isArray(relationTo) && typeof val === 'object' && val?.relationTo) {
+ collection = val.relationTo;
+ requestedID = val.value;
+ }
+
+ return options[collection].indexOf(requestedID) === -1;
+ });
+
+ if (invalidRelationships.length > 0) {
+ return invalidRelationships.reduce((err, invalid, i) => {
+ return `${err} ${JSON.stringify(invalid)}${invalidRelationships.length === i + 1 ? ',' : ''} `;
+ }, 'This field has the following invalid selections:') as string;
+ }
+
+ return true;
+ }
+
+ return true;
};
-export const relationship: Validate = (value, { required }) => {
- if (value || !required) return true;
- return defaultMessage;
+export const upload: Validate = (value: string, options) => {
+ if (!value && options.required) {
+ return defaultMessage;
+ }
+
+ return validateFilterOptions(value, options);
+};
+
+export const relationship: Validate = async (value: RelationshipValue, options) => {
+ if ((!value || (Array.isArray(value) && value.length === 0)) && options.required) {
+ return defaultMessage;
+ }
+
+ return validateFilterOptions(value, options);
};
export const array: Validate = (value, { minRows, maxRows, required }) => {
diff --git a/src/webpack/getBaseConfig.ts b/src/webpack/getBaseConfig.ts
index def6a2fa97..50de8bddc8 100644
--- a/src/webpack/getBaseConfig.ts
+++ b/src/webpack/getBaseConfig.ts
@@ -56,6 +56,7 @@ export default (config: SanitizedConfig): Configuration => ({
'payload-user-css': config.admin.css,
'payload-scss-overrides': config.admin.scss,
dotenv: mockDotENVPath,
+ [path.resolve(__dirname, '../index')]: mockModulePath,
},
extensions: ['.ts', '.tsx', '.js', '.json'],
},