From df934dfeff8b12857d133b2da9f441bc0285dee4 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 4 Apr 2022 21:20:21 -0400 Subject: [PATCH] feat: working PoC for reusing relationship filters in validate --- src/admin/components/forms/Form/types.ts | 1 - .../forms/field-types/Relationship/index.tsx | 14 +++-- .../forms/field-types/Upload/Add/index.tsx | 1 - .../views/CreateFirstUser/index.tsx | 1 - .../views/collections/Edit/Default.tsx | 1 - src/fields/config/types.ts | 10 +++ src/fields/performFieldOperations.ts | 4 +- src/fields/validations.ts | 63 ++++++++++++++++--- src/webpack/getBaseConfig.ts | 2 +- 9 files changed, 76 insertions(+), 21 deletions(-) diff --git a/src/admin/components/forms/Form/types.ts b/src/admin/components/forms/Form/types.ts index 2c2ef60e18..2755de8e6b 100644 --- a/src/admin/components/forms/Form/types.ts +++ b/src/admin/components/forms/Form/types.ts @@ -38,7 +38,6 @@ export type Props = { initialData?: Data waitForAutocomplete?: boolean log?: boolean - validationOperation?: 'create' | 'update' } export type SubmitOptions = { diff --git a/src/admin/components/forms/field-types/Relationship/index.tsx b/src/admin/components/forms/field-types/Relationship/index.tsx index d4ab3355ff..6c2ce3b80c 100644 --- a/src/admin/components/forms/field-types/Relationship/index.tsx +++ b/src/admin/components/forms/field-types/Relationship/index.tsx @@ -78,7 +78,7 @@ const Relationship: React.FC = (props) => { const [lastFullyLoadedRelation, setLastFullyLoadedRelation] = useState(-1); const [lastLoadedPage, setLastLoadedPage] = useState(1); const [errorLoading, setErrorLoading] = useState(''); - const [optionFilters, setOptionFilters] = useState<{[relation: string]: Where}>({}); + const [optionFilters, setOptionFilters] = useState<{[relation: string]: Where}>(); const [hasLoadedValueOptions, setHasLoadedValueOptions] = useState(false); const [search, setSearch] = useState(''); @@ -311,11 +311,13 @@ const Relationship: React.FC = (props) => { }, [relationTo, filterOptions, optionFilters, id, getData, getSiblingData, path, user]); useEffect(() => { - setHasLoadedValueOptions(false); - getResults({ - value: initialValue, - }); - }, [initialValue, getResults]); + if (optionFilters) { + setHasLoadedValueOptions(false); + getResults({ + value: initialValue, + }); + } + }, [initialValue, getResults, optionFilters]); const classes = [ 'field-type', diff --git a/src/admin/components/forms/field-types/Upload/Add/index.tsx b/src/admin/components/forms/field-types/Upload/Add/index.tsx index 4318ce14d5..3fa0127430 100644 --- a/src/admin/components/forms/field-types/Upload/Add/index.tsx +++ b/src/admin/components/forms/field-types/Upload/Add/index.tsx @@ -53,7 +53,6 @@ const AddUploadModal: React.FC = (props) => { action={`${serverURL}${api}/${collection.slug}`} onSuccess={onSuccess} disableSuccessStatus - validationOperation="create" >
diff --git a/src/admin/components/views/CreateFirstUser/index.tsx b/src/admin/components/views/CreateFirstUser/index.tsx index bab615a805..65007628fb 100644 --- a/src/admin/components/views/CreateFirstUser/index.tsx +++ b/src/admin/components/views/CreateFirstUser/index.tsx @@ -65,7 +65,6 @@ const CreateFirstUser: React.FC = (props) => { method="post" redirect={admin} action={`${serverURL}${api}/${userSlug}/first-register`} - validationOperation="create" > = (props) => { onSuccess={onSave} disabled={!hasSavePermission} initialState={initialState} - validationOperation={isEditing ? 'update' : 'create'} >
Where); } +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 f39626b922..7ceffffe24 100644 --- a/src/fields/validations.ts +++ b/src/fields/validations.ts @@ -16,6 +16,8 @@ import { TextField, UploadField, Validate, + RelationshipValue, + ValueWithRelation, } from './config/types'; import canUseDOM from '../utilities/canUseDOM'; import payload from '../index'; @@ -152,14 +154,19 @@ export const upload: Validate = (value: string, { return defaultMessage; }; -export const relationship: Validate = async (value: string | string[], { required, relationTo, filterOptions, id, data, siblingData, user }) => { +export const relationship: Validate = async (value: RelationshipValue, { required, relationTo, filterOptions, id, data, siblingData, user }) => { if ((!value || (Array.isArray(value) && value.length === 0)) && required) { return defaultMessage; } + if (!canUseDOM && typeof filterOptions !== 'undefined' && value) { - const options = []; + const options: { + [collection: string]: (string | number)[] + } = {}; + const collections = typeof relationTo === 'string' ? [relationTo] : relationTo; - const values: string[] = typeof value === 'string' ? [value] : value; + const values = Array.isArray(value) ? value : [value]; + await Promise.all(collections.map(async (collection) => { const optionFilter = typeof filterOptions === 'function' ? filterOptions({ id, @@ -168,23 +175,63 @@ export const relationship: Validate = async user, relationTo: collection, }) : filterOptions; - const result = await payload.find({ + + 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: values } }, + { id: { in: valueIDs } }, optionFilter, ], }, }); - options.concat(result.docs.map((item: TypeWithID) => String(item.id))); + + options[collection] = result.docs.map((doc) => doc.id); })); - if (values.some((input) => options.some((option) => (option === input)))) { - return 'This field has an invalid selection'; + + 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 array: Validate = (value, { minRows, maxRows, required }) => { diff --git a/src/webpack/getBaseConfig.ts b/src/webpack/getBaseConfig.ts index 9bd8ed813e..50de8bddc8 100644 --- a/src/webpack/getBaseConfig.ts +++ b/src/webpack/getBaseConfig.ts @@ -56,7 +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.ts')]: mockModulePath, + [path.resolve(__dirname, '../index')]: mockModulePath, }, extensions: ['.ts', '.tsx', '.js', '.json'], },