|
|
|
|
@@ -1,5 +1,5 @@
|
|
|
|
|
'use client'
|
|
|
|
|
import type { ClientCollectionConfig, PaginatedDocs, Where } from 'payload'
|
|
|
|
|
import type { ClientCollectionConfig, PaginatedDocs } from 'payload'
|
|
|
|
|
|
|
|
|
|
import * as qs from 'qs-esm'
|
|
|
|
|
import React, { useCallback, useEffect, useReducer, useState } from 'react'
|
|
|
|
|
@@ -16,7 +16,7 @@ import optionsReducer from './optionsReducer.js'
|
|
|
|
|
|
|
|
|
|
const baseClass = 'condition-value-relationship'
|
|
|
|
|
|
|
|
|
|
const maxResultsPerRequest = 10
|
|
|
|
|
const maxResultsPerRequest = 50
|
|
|
|
|
|
|
|
|
|
export const RelationshipField: React.FC<Props> = (props) => {
|
|
|
|
|
const {
|
|
|
|
|
@@ -39,23 +39,13 @@ export const RelationshipField: React.FC<Props> = (props) => {
|
|
|
|
|
const [options, dispatchOptions] = useReducer(optionsReducer, [])
|
|
|
|
|
const [search, setSearch] = useState('')
|
|
|
|
|
const [errorLoading, setErrorLoading] = useState('')
|
|
|
|
|
const [hasLoadedFirstOptions, setHasLoadedFirstOptions] = useState(false)
|
|
|
|
|
const debouncedSearch = useDebounce(search, 300)
|
|
|
|
|
const { i18n, t } = useTranslation()
|
|
|
|
|
const relationSlugs = hasMultipleRelations ? relationTo : [relationTo]
|
|
|
|
|
const initialRelationMap = () => {
|
|
|
|
|
const map: Map<string, number> = new Map()
|
|
|
|
|
relationSlugs.forEach((relation) => {
|
|
|
|
|
map.set(relation, 1)
|
|
|
|
|
})
|
|
|
|
|
return map
|
|
|
|
|
}
|
|
|
|
|
const nextPageByRelationshipRef = React.useRef<Map<string, number>>(initialRelationMap())
|
|
|
|
|
const partiallyLoadedRelationshipSlugs = React.useRef<string[]>(relationSlugs)
|
|
|
|
|
|
|
|
|
|
const addOptions = useCallback(
|
|
|
|
|
const setOptions = useCallback(
|
|
|
|
|
(data, relation) => {
|
|
|
|
|
const collection = getEntityConfig({ collectionSlug: relation }) as ClientCollectionConfig
|
|
|
|
|
dispatchOptions({ type: 'CLEAR', i18n, required: false })
|
|
|
|
|
dispatchOptions({ type: 'ADD', collection, data, hasMultipleRelations, i18n, relation })
|
|
|
|
|
},
|
|
|
|
|
[hasMultipleRelations, i18n, getEntityConfig],
|
|
|
|
|
@@ -69,22 +59,18 @@ export const RelationshipField: React.FC<Props> = (props) => {
|
|
|
|
|
abortController: AbortController
|
|
|
|
|
relationSlug: string
|
|
|
|
|
}) => {
|
|
|
|
|
if (relationSlug && partiallyLoadedRelationshipSlugs.current.includes(relationSlug)) {
|
|
|
|
|
if (relationSlug) {
|
|
|
|
|
const collection = getEntityConfig({
|
|
|
|
|
collectionSlug: relationSlug,
|
|
|
|
|
}) as ClientCollectionConfig
|
|
|
|
|
const fieldToSearch = collection?.admin?.useAsTitle || 'id'
|
|
|
|
|
const pageIndex = nextPageByRelationshipRef.current.get(relationSlug)
|
|
|
|
|
|
|
|
|
|
const query: {
|
|
|
|
|
depth?: number
|
|
|
|
|
limit?: number
|
|
|
|
|
page?: number
|
|
|
|
|
where: Where
|
|
|
|
|
} = {
|
|
|
|
|
const query = {
|
|
|
|
|
depth: 0,
|
|
|
|
|
limit: maxResultsPerRequest,
|
|
|
|
|
page: pageIndex,
|
|
|
|
|
select: {
|
|
|
|
|
[fieldToSearch]: true,
|
|
|
|
|
},
|
|
|
|
|
where: {
|
|
|
|
|
and: [],
|
|
|
|
|
},
|
|
|
|
|
@@ -113,18 +99,7 @@ export const RelationshipField: React.FC<Props> = (props) => {
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
const data: PaginatedDocs = await response.json()
|
|
|
|
|
if (data.docs.length > 0) {
|
|
|
|
|
addOptions(data, relationSlug)
|
|
|
|
|
|
|
|
|
|
if (!debouncedSearch) {
|
|
|
|
|
if (data.nextPage) {
|
|
|
|
|
nextPageByRelationshipRef.current.set(relationSlug, data.nextPage)
|
|
|
|
|
} else {
|
|
|
|
|
partiallyLoadedRelationshipSlugs.current =
|
|
|
|
|
partiallyLoadedRelationshipSlugs.current.filter(
|
|
|
|
|
(partiallyLoadedRelation) => partiallyLoadedRelation !== relationSlug,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
setOptions(data, relationSlug)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
setErrorLoading(t('error:unspecific'))
|
|
|
|
|
@@ -135,22 +110,10 @@ export const RelationshipField: React.FC<Props> = (props) => {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setHasLoadedFirstOptions(true)
|
|
|
|
|
},
|
|
|
|
|
[addOptions, api, collections, debouncedSearch, i18n.language, serverURL, t],
|
|
|
|
|
[setOptions, api, collections, debouncedSearch, i18n.language, serverURL, t],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const loadMoreOptions = React.useCallback(() => {
|
|
|
|
|
if (partiallyLoadedRelationshipSlugs.current.length > 0) {
|
|
|
|
|
const abortController = new AbortController()
|
|
|
|
|
void loadRelationOptions({
|
|
|
|
|
abortController,
|
|
|
|
|
relationSlug: partiallyLoadedRelationshipSlugs.current[0],
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}, [loadRelationOptions])
|
|
|
|
|
|
|
|
|
|
const findOptionsByValue = useCallback((): Option | Option[] => {
|
|
|
|
|
if (value) {
|
|
|
|
|
if (hasMany) {
|
|
|
|
|
@@ -208,37 +171,6 @@ export const RelationshipField: React.FC<Props> = (props) => {
|
|
|
|
|
return undefined
|
|
|
|
|
}, [hasMany, hasMultipleRelations, value, options])
|
|
|
|
|
|
|
|
|
|
const handleInputChange = useCallback(
|
|
|
|
|
(newSearch) => {
|
|
|
|
|
if (search !== newSearch) {
|
|
|
|
|
setSearch(newSearch)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[search],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const addOptionByID = useCallback(
|
|
|
|
|
async (id, relation) => {
|
|
|
|
|
if (!errorLoading && id !== 'null' && id && relation) {
|
|
|
|
|
const response = await fetch(`${serverURL}${api}/${relation}/${id}?depth=0`, {
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
headers: {
|
|
|
|
|
'Accept-Language': i18n.language,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
addOptions({ docs: [data] }, relation)
|
|
|
|
|
} else {
|
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
|
console.error(t('error:loadingDocument', { id }))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[i18n, addOptions, api, errorLoading, serverURL, t],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 1. Trigger initial relationship options fetch
|
|
|
|
|
* 2. When search changes, loadRelationOptions will
|
|
|
|
|
@@ -269,47 +201,6 @@ export const RelationshipField: React.FC<Props> = (props) => {
|
|
|
|
|
}
|
|
|
|
|
}, [i18n, loadRelationOptions, relationTo])
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load any options that were not returned
|
|
|
|
|
* in the first 10 of each relation fetch
|
|
|
|
|
*/
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (value && hasLoadedFirstOptions) {
|
|
|
|
|
if (hasMany) {
|
|
|
|
|
const matchedOptions = findOptionsByValue()
|
|
|
|
|
|
|
|
|
|
;((matchedOptions as Option[]) || []).forEach((option, i) => {
|
|
|
|
|
if (!option) {
|
|
|
|
|
if (hasMultipleRelations) {
|
|
|
|
|
void addOptionByID(value[i].value, value[i].relationTo)
|
|
|
|
|
} else {
|
|
|
|
|
void addOptionByID(value[i], relationTo)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
const matchedOption = findOptionsByValue()
|
|
|
|
|
|
|
|
|
|
if (!matchedOption) {
|
|
|
|
|
if (hasMultipleRelations) {
|
|
|
|
|
const valueWithRelation = value as ValueWithRelation
|
|
|
|
|
void addOptionByID(valueWithRelation.value, valueWithRelation.relationTo)
|
|
|
|
|
} else {
|
|
|
|
|
void addOptionByID(value, relationTo)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [
|
|
|
|
|
addOptionByID,
|
|
|
|
|
findOptionsByValue,
|
|
|
|
|
hasMany,
|
|
|
|
|
hasMultipleRelations,
|
|
|
|
|
relationTo,
|
|
|
|
|
value,
|
|
|
|
|
hasLoadedFirstOptions,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const classes = ['field-type', baseClass, errorLoading && 'error-loading']
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join(' ')
|
|
|
|
|
@@ -352,8 +243,7 @@ export const RelationshipField: React.FC<Props> = (props) => {
|
|
|
|
|
onChange(selected?.value)
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onInputChange={handleInputChange}
|
|
|
|
|
onMenuScrollToBottom={loadMoreOptions}
|
|
|
|
|
onInputChange={setSearch}
|
|
|
|
|
options={options}
|
|
|
|
|
placeholder={t('general:selectValue')}
|
|
|
|
|
value={valueToRender}
|
|
|
|
|
|