diff --git a/demo/collections/Admin.ts b/demo/collections/Admin.ts index 66b2e29560..9fdef8f80b 100644 --- a/demo/collections/Admin.ts +++ b/demo/collections/Admin.ts @@ -44,23 +44,17 @@ const Admin: PayloadCollectionConfig = { saveToJWT: true, hasMany: true, }, + { + name: 'publicUser', + type: 'relationship', + hasMany: true, + relationTo: 'public-users', + }, { name: 'apiKey', type: 'text', access: { - read: ({ req: { user } }) => { - if (checkRole(['admin'], user)) { - return true; - } - - if (user) { - return { - email: user.email, - }; - } - - return false; - }, + read: ({ req: { user } }) => checkRole(['admin'], user), }, }, ], diff --git a/demo/collections/RelationshipB.ts b/demo/collections/RelationshipB.ts index bced99d8c9..faa780fd0e 100644 --- a/demo/collections/RelationshipB.ts +++ b/demo/collections/RelationshipB.ts @@ -31,7 +31,7 @@ const RelationshipB: PayloadCollectionConfig = { label: 'Localized Posts', type: 'relationship', hasMany: true, - relationTo: 'localized-posts', + relationTo: ['localized-posts', 'previewable-post'], }, ], timestamps: true, diff --git a/src/admin/components/forms/field-types/Relationship/index.tsx b/src/admin/components/forms/field-types/Relationship/index.tsx index 3744d8e1e5..163ab80c3a 100644 --- a/src/admin/components/forms/field-types/Relationship/index.tsx +++ b/src/admin/components/forms/field-types/Relationship/index.tsx @@ -14,6 +14,7 @@ import { PaginatedDocs } from '../../../../../collections/config/types'; import { useFormProcessing } from '../../Form/context'; import optionsReducer from './optionsReducer'; import { Props, OptionsPage, Option, ValueWithRelation } from './types'; +import useDebounce from '../../../../hooks/useDebounce'; import './index.scss'; @@ -57,6 +58,7 @@ const Relationship: React.FC = (props) => { const [search, setSearch] = useState(''); const [errorLoading, setErrorLoading] = useState(false); const [hasLoadedFirstOptions, setHasLoadedFirstOptions] = useState(false); + const debouncedSearch = useDebounce(search, 120); const memoizedValidate = useCallback((value) => { const validationResult = validate(value, { required }); @@ -79,6 +81,41 @@ const Relationship: React.FC = (props) => { dispatchOptions({ type: 'ADD', data, relation, hasMultipleRelations, collection }); }, [collections, hasMultipleRelations]); + const getResults = useCallback(({ relations: relationsArg, lastLoadedPage: lastLoadedPageArg }) => { + if (relationsArg.length > 0) { + some(relationsArg, async (relation, callback) => { + const collection = collections.find((coll) => coll.slug === relation); + const fieldToSearch = collection?.admin?.useAsTitle || 'id'; + const searchParam = search ? `&where[${fieldToSearch}][like]=${search}` : ''; + const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPageArg}&depth=0${searchParam}`); + const data: PaginatedDocs = await response.json(); + + if (response.ok) { + if (data.hasNextPage) { + return callback(false, { + data, + relation, + }); + } + + return callback({ relation, data }); + } + + return setErrorLoading(true); + }, (lastPage: OptionsPage, nextPage: OptionsPage) => { + if (nextPage) { + const { data, relation } = nextPage; + addOptions(data, relation); + setLastLoadedPage((l) => l + 1); + } else { + const { data, relation } = lastPage; + addOptions(data, relation); + setLastFullyLoadedRelation(relations.indexOf(relation)); + setLastLoadedPage(1); + } + }); + } + }, [addOptions, api, collections, relations, search, serverURL]); const getNextOptions = useCallback((params = {} as Record) => { const clear = params?.clear; @@ -95,41 +132,12 @@ const Relationship: React.FC = (props) => { if (!errorLoading) { const relationsToSearch = lastFullyLoadedRelation === -1 ? relations : relations.slice(lastFullyLoadedRelation + 1); - if (relationsToSearch.length > 0) { - some(relationsToSearch, async (relation, callback) => { - const collection = collections.find((coll) => coll.slug === relation); - const fieldToSearch = collection?.admin?.useAsTitle || 'id'; - const searchParam = search ? `&where[${fieldToSearch}][like]=${search}` : ''; - const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPage}${searchParam}`); - const data: PaginatedDocs = await response.json(); - - if (response.ok) { - if (data.hasNextPage) { - return callback(false, { - data, - relation, - }); - } - - return callback({ relation, data }); - } - - return setErrorLoading(true); - }, (lastPage: OptionsPage, nextPage: OptionsPage) => { - if (nextPage) { - const { data, relation } = nextPage; - addOptions(data, relation); - setLastLoadedPage(lastLoadedPage + 1); - } else { - const { data, relation } = lastPage; - addOptions(data, relation); - setLastFullyLoadedRelation(relations.indexOf(relation)); - setLastLoadedPage(1); - } - }); - } + getResults({ + relations: relationsToSearch, + lastLoadedPage, + }); } - }, [addOptions, api, collections, errorLoading, lastFullyLoadedRelation, lastLoadedPage, relations, required, search, serverURL]); + }, [errorLoading, required, lastFullyLoadedRelation, relations, getResults, lastLoadedPage]); const findOptionsByValue = useCallback((): Option | Option[] => { if (value) { @@ -191,13 +199,11 @@ const Relationship: React.FC = (props) => { const handleInputChange = useCallback((newSearch) => { if (search !== newSearch) { setSearch(newSearch); - setLastFullyLoadedRelation(-1); - setLastLoadedPage(1); } }, [search]); const addOptionByID = useCallback(async (id, relation) => { - if (!errorLoading) { + if (!errorLoading && id !== 'null') { const response = await fetch(`${serverURL}${api}/${relation}/${id}?depth=0`); if (response.ok) { @@ -248,6 +254,7 @@ const Relationship: React.FC = (props) => { addOptions(data, relation); + if (!data.hasNextPage) { setLastFullyLoadedRelation(relations.indexOf(relation)); } else { @@ -261,6 +268,23 @@ const Relationship: React.FC = (props) => { getFirstResults(); }, [addOptions, api, relations, serverURL]); + useEffect(() => { + if (debouncedSearch) { + dispatchOptions({ + type: 'REPLACE', + payload: required ? [] : [{ value: 'null', label: 'None' }], + }); + + setLastLoadedPage(1); + setLastFullyLoadedRelation(-1); + + getResults({ + relations, + lastLoadedPage: 1, + }); + } + }, [getResults, debouncedSearch, relations, required]); + const classes = [ 'field-type', baseClass, diff --git a/src/admin/components/forms/useFieldType/index.tsx b/src/admin/components/forms/useFieldType/index.tsx index 360d483569..65dd9d77c7 100644 --- a/src/admin/components/forms/useFieldType/index.tsx +++ b/src/admin/components/forms/useFieldType/index.tsx @@ -34,7 +34,6 @@ const useFieldType = (options: Options): FieldType => { // Get field by path const field = getField(path); - const fieldExists = Boolean(field); const initialValue = field?.initialValue; diff --git a/src/fields/traverseFields.ts b/src/fields/traverseFields.ts index 1eb241d074..ec2375889d 100644 --- a/src/fields/traverseFields.ts +++ b/src/fields/traverseFields.ts @@ -77,6 +77,10 @@ const traverseFields = (args: Arguments): void => { dataCopy[field.name] = null; } + if (field.type === 'relationship' && field.hasMany && (data[field.name]?.[0] === '' || data[field.name]?.[0] === 'none' || data[field.name]?.[0] === 'null')) { + dataCopy[field.name] = []; + } + if (field.type === 'number' && typeof data[field.name] === 'string') { dataCopy[field.name] = parseFloat(data[field.name]); }