merge: relationship options filter
This commit is contained in:
@@ -368,6 +368,10 @@ const Form: React.FC<Props> = (props) => {
|
||||
refreshCookie();
|
||||
}, 15000, [fields]);
|
||||
|
||||
useThrottledEffect(() => {
|
||||
validateForm();
|
||||
}, 1000, [validateForm, fields]);
|
||||
|
||||
useEffect(() => {
|
||||
contextRef.current = { ...contextRef.current }; // triggers rerender of all components that subscribe to form
|
||||
setModified(false);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, {
|
||||
useCallback, useEffect, useState, useReducer,
|
||||
} from 'react';
|
||||
import { useConfig } from '@payloadcms/config-provider';
|
||||
import equal from 'deep-equal';
|
||||
import { useAuth, useConfig } from '@payloadcms/config-provider';
|
||||
import qs from 'qs';
|
||||
import withCondition from '../../withCondition';
|
||||
import ReactSelect from '../../../elements/ReactSelect';
|
||||
@@ -13,11 +14,13 @@ import FieldDescription from '../../FieldDescription';
|
||||
import { relationship } from '../../../../../fields/validations';
|
||||
import { Where } from '../../../../../types';
|
||||
import { PaginatedDocs } from '../../../../../mongoose/types';
|
||||
import { useFormProcessing } from '../../Form/context';
|
||||
import { useFormProcessing, useWatchForm } from '../../Form/context';
|
||||
import optionsReducer from './optionsReducer';
|
||||
import { Props, Option, ValueWithRelation, GetResults } from './types';
|
||||
import { createRelationMap } from './createRelationMap';
|
||||
import { useDebouncedCallback } from '../../../../hooks/useDebouncedCallback';
|
||||
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
|
||||
import { getFilterOptionsQuery } from '../getFilterOptionsQuery';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -34,6 +37,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
required,
|
||||
label,
|
||||
hasMany,
|
||||
filterOptions,
|
||||
admin: {
|
||||
readOnly,
|
||||
style,
|
||||
@@ -52,13 +56,16 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
collections,
|
||||
} = useConfig();
|
||||
|
||||
const { id } = useDocumentInfo();
|
||||
const { user } = useAuth();
|
||||
const { getData, getSiblingData } = useWatchForm();
|
||||
const formProcessing = useFormProcessing();
|
||||
|
||||
const hasMultipleRelations = Array.isArray(relationTo);
|
||||
const [options, dispatchOptions] = useReducer(optionsReducer, required || hasMany ? [] : [{ value: 'null', label: 'None' }]);
|
||||
const [lastFullyLoadedRelation, setLastFullyLoadedRelation] = useState(-1);
|
||||
const [lastLoadedPage, setLastLoadedPage] = useState(1);
|
||||
const [errorLoading, setErrorLoading] = useState('');
|
||||
const [optionFilters, setOptionFilters] = useState<{[relation: string]: Where}>();
|
||||
const [hasLoadedValueOptions, setHasLoadedValueOptions] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
@@ -107,9 +114,13 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
where: Where
|
||||
} = {
|
||||
where: {
|
||||
id: {
|
||||
not_in: relationMap[relation],
|
||||
},
|
||||
and: [
|
||||
{
|
||||
id: {
|
||||
not_in: relationMap[relation],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
limit: maxResultsPerRequest,
|
||||
page: lastLoadedPageToUse,
|
||||
@@ -118,9 +129,15 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
};
|
||||
|
||||
if (searchArg) {
|
||||
query.where[fieldToSearch] = {
|
||||
like: searchArg,
|
||||
};
|
||||
query.where.and.push({
|
||||
[fieldToSearch]: {
|
||||
like: searchArg,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (optionFilters[relation]) {
|
||||
query.where.and.push(optionFilters[relation]);
|
||||
}
|
||||
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`);
|
||||
@@ -148,7 +165,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
}
|
||||
}, Promise.resolve());
|
||||
}
|
||||
}, [api, collections, serverURL, errorLoading, relationTo, hasMany, hasMultipleRelations]);
|
||||
}, [relationTo, hasMany, errorLoading, collections, optionFilters, serverURL, api, hasMultipleRelations]);
|
||||
|
||||
const findOptionsByValue = useCallback((): Option | Option[] => {
|
||||
if (value) {
|
||||
@@ -262,11 +279,29 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
}, [hasMany, hasMultipleRelations, relationTo, initialValue, hasLoadedValueOptions, errorLoading, collections, api, serverURL]);
|
||||
|
||||
useEffect(() => {
|
||||
setHasLoadedValueOptions(false);
|
||||
getResults({
|
||||
value: initialValue,
|
||||
if (!filterOptions) {
|
||||
return;
|
||||
}
|
||||
const newOptionFilters = getFilterOptionsQuery(filterOptions, {
|
||||
id,
|
||||
data: getData(),
|
||||
relationTo,
|
||||
siblingData: getSiblingData(path),
|
||||
user,
|
||||
});
|
||||
}, [initialValue, getResults]);
|
||||
if (!equal(newOptionFilters, optionFilters)) {
|
||||
setOptionFilters(newOptionFilters);
|
||||
}
|
||||
}, [relationTo, filterOptions, optionFilters, id, getData, getSiblingData, path, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (optionFilters) {
|
||||
setHasLoadedValueOptions(false);
|
||||
getResults({
|
||||
value: initialValue,
|
||||
});
|
||||
}
|
||||
}, [initialValue, getResults, optionFilters]);
|
||||
|
||||
const classes = [
|
||||
'field-type',
|
||||
|
||||
@@ -5,7 +5,7 @@ import Label from '../../Label';
|
||||
import Error from '../../Error';
|
||||
import FileDetails from '../../../elements/FileDetails';
|
||||
import FieldDescription from '../../FieldDescription';
|
||||
import { UploadField } from '../../../../../fields/config/types';
|
||||
import { FilterOptions, UploadField } from '../../../../../fields/config/types';
|
||||
import { Description } from '../../FieldDescription/types';
|
||||
import { FieldTypes } from '..';
|
||||
import AddModal from './Add';
|
||||
@@ -33,6 +33,7 @@ export type UploadInputProps = Omit<UploadField, 'type'> & {
|
||||
collection?: SanitizedCollectionConfig
|
||||
serverURL?: string
|
||||
api?: string
|
||||
filterOptions: FilterOptions
|
||||
}
|
||||
|
||||
const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
@@ -54,6 +55,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
api = '/api',
|
||||
collection,
|
||||
errorMessage,
|
||||
filterOptions,
|
||||
} = props;
|
||||
|
||||
const { toggle } = useModal();
|
||||
@@ -160,6 +162,8 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
slug: selectExistingModalSlug,
|
||||
setValue: onChange,
|
||||
addModalSlug,
|
||||
filterOptions,
|
||||
path,
|
||||
}}
|
||||
/>
|
||||
<FieldDescription
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { Fragment, useState, useEffect } from 'react';
|
||||
import equal from 'deep-equal';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { useConfig } from '@payloadcms/config-provider';
|
||||
import { useAuth, useConfig } from '@payloadcms/config-provider';
|
||||
import { Where } from '../../../../../../types';
|
||||
import MinimalTemplate from '../../../../templates/Minimal';
|
||||
import Button from '../../../../elements/Button';
|
||||
import usePayloadAPI from '../../../../../hooks/usePayloadAPI';
|
||||
@@ -12,6 +14,9 @@ import PerPage from '../../../../elements/PerPage';
|
||||
import formatFields from '../../../../views/collections/List/formatFields';
|
||||
|
||||
import './index.scss';
|
||||
import { getFilterOptionsQuery } from '../../getFilterOptionsQuery';
|
||||
import { useDocumentInfo } from '../../../../utilities/DocumentInfo';
|
||||
import { useWatchForm } from '../../../Form/context';
|
||||
|
||||
const baseClass = 'select-existing-upload-modal';
|
||||
|
||||
@@ -29,15 +34,21 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
|
||||
} = {},
|
||||
} = {},
|
||||
slug: modalSlug,
|
||||
path,
|
||||
filterOptions,
|
||||
} = props;
|
||||
|
||||
const { serverURL, routes: { api } } = useConfig();
|
||||
const { id } = useDocumentInfo();
|
||||
const { user } = useAuth();
|
||||
const { getData, getSiblingData } = useWatchForm();
|
||||
const { closeAll, currentModal } = useModal();
|
||||
const [fields] = useState(() => formatFields(collection));
|
||||
const [limit, setLimit] = useState(defaultLimit);
|
||||
const [sort, setSort] = useState(null);
|
||||
const [where, setWhere] = useState(null);
|
||||
const [page, setPage] = useState(null);
|
||||
const [optionFilters, setOptionFilters] = useState<Where>();
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
@@ -58,12 +69,29 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
|
||||
} = {};
|
||||
|
||||
if (page) params.page = page;
|
||||
if (where) params.where = where;
|
||||
if (where) params.where = { and: [where, optionFilters] };
|
||||
if (sort) params.sort = sort;
|
||||
if (limit) params.limit = limit;
|
||||
|
||||
setParams(params);
|
||||
}, [setParams, page, sort, where, limit]);
|
||||
}, [setParams, page, sort, where, limit, optionFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!filterOptions || !isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOptionFilters = getFilterOptionsQuery(filterOptions, {
|
||||
id,
|
||||
relationTo: collectionSlug,
|
||||
data: getData(),
|
||||
siblingData: getSiblingData(path),
|
||||
user,
|
||||
})[collectionSlug];
|
||||
if (!equal(newOptionFilters, optionFilters)) {
|
||||
setOptionFilters(newOptionFilters);
|
||||
}
|
||||
}, [collectionSlug, filterOptions, optionFilters, id, getData, getSiblingData, path, user, isOpen]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
|
||||
import { FilterOptions } from '../../../../../../fields/config/types';
|
||||
|
||||
export type Props = {
|
||||
setValue: (val: { id: string } | null) => void
|
||||
collection: SanitizedCollectionConfig
|
||||
slug: string
|
||||
path
|
||||
filterOptions: FilterOptions
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ const Upload: React.FC<Props> = (props) => {
|
||||
validate = upload,
|
||||
relationTo,
|
||||
fieldTypes,
|
||||
filterOptions,
|
||||
} = props;
|
||||
|
||||
const collection = collections.find((coll) => coll.slug === relationTo);
|
||||
@@ -82,6 +83,7 @@ const Upload: React.FC<Props> = (props) => {
|
||||
fieldTypes={fieldTypes}
|
||||
name={name}
|
||||
relationTo={relationTo}
|
||||
filterOptions={filterOptions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Where } from '../../../../types';
|
||||
import { FilterOptions, FilterOptionsProps } from '../../../../fields/config/types';
|
||||
|
||||
export const getFilterOptionsQuery = (filterOptions: FilterOptions, options: FilterOptionsProps): {[collection: string]: Where } => {
|
||||
const { relationTo } = options;
|
||||
const relations = Array.isArray(relationTo) ? relationTo : [relationTo];
|
||||
const query = {};
|
||||
if (typeof filterOptions !== 'undefined') {
|
||||
relations.forEach((relation) => {
|
||||
query[relation] = typeof filterOptions === 'function' ? filterOptions(options) : filterOptions;
|
||||
});
|
||||
}
|
||||
return query;
|
||||
};
|
||||
@@ -1,9 +1,12 @@
|
||||
import {
|
||||
useCallback, useEffect, useState,
|
||||
} from 'react';
|
||||
import { useAuth } from '@payloadcms/config-provider';
|
||||
import { useFormProcessing, useFormSubmitted, useFormModified, useForm } from '../Form/context';
|
||||
import useDebounce from '../../../hooks/useDebounce';
|
||||
import { Options, FieldType } from './types';
|
||||
import { useDocumentInfo } from '../../utilities/DocumentInfo';
|
||||
import { useOperation } from '../../utilities/OperationProvider';
|
||||
|
||||
const useField = <T extends unknown>(options: Options): FieldType<T> => {
|
||||
const {
|
||||
@@ -19,10 +22,15 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
|
||||
const submitted = useFormSubmitted();
|
||||
const processing = useFormProcessing();
|
||||
const modified = useFormModified();
|
||||
const { user } = useAuth();
|
||||
const { id } = useDocumentInfo();
|
||||
const operation = useOperation();
|
||||
|
||||
const {
|
||||
dispatchFields,
|
||||
getField,
|
||||
getData,
|
||||
getSiblingData,
|
||||
setModified,
|
||||
} = formContext || {};
|
||||
|
||||
@@ -40,43 +48,6 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
|
||||
const valid = (field && typeof field.valid === 'boolean') ? field.valid : true;
|
||||
const showError = valid === false && submitted;
|
||||
|
||||
// Method to send update field values from field component(s)
|
||||
// Should only be used internally
|
||||
const sendField = useCallback(async (valueToSend) => {
|
||||
const fieldToDispatch = {
|
||||
path,
|
||||
disableFormData,
|
||||
ignoreWhileFlattening,
|
||||
initialValue,
|
||||
validate,
|
||||
condition,
|
||||
value: valueToSend,
|
||||
valid: false,
|
||||
errorMessage: undefined,
|
||||
};
|
||||
|
||||
const validationResult = typeof validate === 'function' ? await validate(valueToSend) : true;
|
||||
|
||||
if (typeof validationResult === 'string') {
|
||||
fieldToDispatch.errorMessage = validationResult;
|
||||
fieldToDispatch.valid = false;
|
||||
} else {
|
||||
fieldToDispatch.valid = validationResult;
|
||||
}
|
||||
|
||||
if (typeof dispatchFields === 'function') {
|
||||
dispatchFields(fieldToDispatch);
|
||||
}
|
||||
}, [
|
||||
path,
|
||||
dispatchFields,
|
||||
validate,
|
||||
disableFormData,
|
||||
ignoreWhileFlattening,
|
||||
initialValue,
|
||||
condition,
|
||||
]);
|
||||
|
||||
// Method to return from `useField`, used to
|
||||
// update internal field values from field component(s)
|
||||
// as fast as they arrive. NOTE - this method is NOT debounced
|
||||
@@ -106,13 +77,38 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
|
||||
const valueToSend = enableDebouncedValue ? debouncedValue : internalValue;
|
||||
|
||||
useEffect(() => {
|
||||
if (field?.value !== valueToSend && valueToSend !== undefined) {
|
||||
sendField(valueToSend);
|
||||
}
|
||||
const sendField = async () => {
|
||||
if (field?.value !== valueToSend && valueToSend !== undefined) {
|
||||
if (typeof dispatchFields === 'function') {
|
||||
dispatchFields({
|
||||
...field,
|
||||
path,
|
||||
disableFormData,
|
||||
ignoreWhileFlattening,
|
||||
initialValue,
|
||||
validate,
|
||||
condition,
|
||||
value: valueToSend,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sendField();
|
||||
}, [
|
||||
condition,
|
||||
disableFormData,
|
||||
dispatchFields,
|
||||
getData,
|
||||
getSiblingData,
|
||||
id,
|
||||
ignoreWhileFlattening,
|
||||
initialValue,
|
||||
operation,
|
||||
path,
|
||||
user,
|
||||
validate,
|
||||
valueToSend,
|
||||
sendField,
|
||||
field,
|
||||
]);
|
||||
|
||||
|
||||
@@ -85,7 +85,6 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
||||
onSuccess={onSave}
|
||||
disabled={!hasSavePermission}
|
||||
initialState={initialState}
|
||||
validationOperation={isEditing ? 'update' : 'create'}
|
||||
>
|
||||
<div className={`${baseClass}__main`}>
|
||||
<Meta
|
||||
|
||||
@@ -189,5 +189,29 @@ describe('Collections - REST', () => {
|
||||
expect(custom.doc.id).toBe(parseFloat(customID.id));
|
||||
expect(doc.customID[0].id).toBe(parseFloat(customID.id));
|
||||
});
|
||||
|
||||
it('should use filterOptions to limit relationship options', async () => {
|
||||
// update documentB to disable relations
|
||||
await fetch(`${url}/api/relationship-b/${documentB.id}`, {
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
disableRelation: true,
|
||||
}),
|
||||
method: 'put',
|
||||
});
|
||||
|
||||
// attempt to save relationship to documentB
|
||||
const response = await fetch(`${url}/api/relationship-a/${documentA.id}`, {
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
filterRelationship: documentB.id,
|
||||
}),
|
||||
method: 'put',
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
expect(result.errors).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -185,6 +185,10 @@ export const upload = baseField.keys({
|
||||
relationTo: joi.string().required(),
|
||||
name: joi.string().required(),
|
||||
maxDepth: joi.number(),
|
||||
filterOptions: joi.alternatives().try(
|
||||
joi.object(),
|
||||
joi.func(),
|
||||
),
|
||||
});
|
||||
|
||||
export const checkbox = baseField.keys({
|
||||
@@ -208,6 +212,10 @@ export const relationship = baseField.keys({
|
||||
),
|
||||
name: joi.string().required(),
|
||||
maxDepth: joi.number(),
|
||||
filterOptions: joi.alternatives().try(
|
||||
joi.object(),
|
||||
joi.func(),
|
||||
),
|
||||
});
|
||||
|
||||
export const blocks = baseField.keys({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
import { CSSProperties } from 'react';
|
||||
import { Editor } from 'slate';
|
||||
import { Operation } from '../../types';
|
||||
import { Operation, Where } from '../../types';
|
||||
import { TypeWithID } from '../../collections/config/types';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { ConditionalDateProps } from '../../admin/components/elements/DatePicker/types';
|
||||
@@ -29,6 +29,16 @@ export type FieldAccess<T extends TypeWithID = any, P = any> = (args: {
|
||||
|
||||
export type Condition<T extends TypeWithID = any, P = any> = (data: Partial<T>, siblingData: Partial<P>) => boolean;
|
||||
|
||||
export type FilterOptionsProps = {
|
||||
id: string | number,
|
||||
user: Partial<User>,
|
||||
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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<unknown, unknown, EmailField> = (value: string, { r
|
||||
return true;
|
||||
};
|
||||
|
||||
export const textarea: Validate<unknown, unknown, TextareaField> = (value: string, { required, maxLength, minLength }) => {
|
||||
export const textarea: Validate<unknown, unknown, TextareaField> = (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<unknown, unknown, DateField> = (value, { required })
|
||||
return true;
|
||||
};
|
||||
|
||||
export const upload: Validate<unknown, unknown, UploadField> = (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<TypeWithID>({
|
||||
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<unknown, unknown, RelationshipField> = (value, { required }) => {
|
||||
if (value || !required) return true;
|
||||
return defaultMessage;
|
||||
export const upload: Validate<unknown, unknown, UploadField> = (value: string, options) => {
|
||||
if (!value && options.required) {
|
||||
return defaultMessage;
|
||||
}
|
||||
|
||||
return validateFilterOptions(value, options);
|
||||
};
|
||||
|
||||
export const relationship: Validate<unknown, unknown, RelationshipField> = async (value: RelationshipValue, options) => {
|
||||
if ((!value || (Array.isArray(value) && value.length === 0)) && options.required) {
|
||||
return defaultMessage;
|
||||
}
|
||||
|
||||
return validateFilterOptions(value, options);
|
||||
};
|
||||
|
||||
export const array: Validate<unknown, unknown, ArrayField> = (value, { minRows, maxRows, required }) => {
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user