perf: optimize getEntityConfig lookups (#10665)

Replaces array-based lookups in `getEntityConfig` with a map, reducing
time complexity from O(n) to O(1).
This commit is contained in:
Alessio Gravili
2025-01-20 12:32:38 -07:00
committed by GitHub
parent 56667cdc8d
commit c07c9e9129
50 changed files with 183 additions and 181 deletions

View File

@@ -831,6 +831,22 @@ const MyComponent: React.FC = () => {
}
```
If you need to retrieve a specific collection or global config by its slug, `getEntityConfig` is the most efficient way to do so:
```tsx
'use client'
import { useConfig } from '@payloadcms/ui'
const MyComponent: React.FC = () => {
// highlight-start
const { getEntityConfig } = useConfig()
const mediaConfig = getEntityConfig({ collectionSlug: 'media'})
// highlight-end
return <span>The media collection has {mediaConfig.fields.length} fields.</span>
}
```
## useEditDepth
Sends back how many editing levels "deep" the current component is. Edit depth is relevant while adding new documents / editing documents in modal windows and other cases.

View File

@@ -44,7 +44,7 @@ export function getRouteInfo({
let idType = defaultIDType
if (collectionSlug) {
collectionConfig = config.collections.find((collection) => collection.slug === collectionSlug)
collectionConfig = payload.collections?.[collectionSlug]?.config
}
if (globalSlug) {

View File

@@ -1,7 +1,5 @@
'use client'
import type { ClientCollectionConfig, ClientGlobalConfig } from 'payload'
import {
CheckboxField,
CopyToClipboard,
@@ -41,8 +39,8 @@ export const APIViewClient: React.FC = () => {
getEntityConfig,
} = useConfig()
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
const globalConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
const collectionConfig = getEntityConfig({ collectionSlug })
const globalConfig = getEntityConfig({ globalSlug })
const localeOptions =
localization &&

View File

@@ -42,7 +42,7 @@ export const Account: React.FC<AdminViewProps> = async ({
serverURL,
} = config
const collectionConfig = config.collections.find((collection) => collection.slug === userSlug)
const collectionConfig = payload?.collections?.[userSlug]?.config
if (collectionConfig && user?.id) {
// Fetch the data required for the view

View File

@@ -1,7 +1,6 @@
'use client'
import type { FormProps, UserWithToken } from '@payloadcms/ui'
import type {
ClientCollectionConfig,
DocumentPreferences,
FormState,
LoginWithUsernameOptions,
@@ -45,7 +44,7 @@ export const CreateFirstUserClient: React.FC<{
const abortOnChangeRef = React.useRef<AbortController>(null)
const collectionConfig = getEntityConfig({ collectionSlug: userSlug }) as ClientCollectionConfig
const collectionConfig = getEntityConfig({ collectionSlug: userSlug })
const onChange: FormProps['onChange'][0] = React.useCallback(
async ({ formState: prevFormState }) => {

View File

@@ -17,15 +17,15 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
req,
req: {
payload: {
collections,
config: {
admin: { user: userSlug },
},
config,
},
},
} = initPageResult
const collectionConfig = config.collections?.find((collection) => collection?.slug === userSlug)
const collectionConfig = collections?.[userSlug]?.config
const { auth: authOptions } = collectionConfig
const loginWithUsername = authOptions.loginWithUsername

View File

@@ -148,9 +148,7 @@ export const renderDocumentHandler = async (args: {
importMap: payload.importMap,
initialData,
initPageResult: {
collectionConfig: payload.config.collections.find(
(collection) => collection.slug === collectionSlug,
),
collectionConfig: payload?.collections?.[collectionSlug]?.config,
cookies,
docID,
globalConfig: payload.config.globals.find((global) => global.slug === collectionSlug),

View File

@@ -10,7 +10,7 @@ import React, { useState } from 'react'
import { FormHeader } from '../../../elements/FormHeader/index.js'
export const ForgotPasswordForm: React.FC = () => {
const { config } = useConfig()
const { config, getEntityConfig } = useConfig()
const {
admin: { user: userSlug },
@@ -19,7 +19,7 @@ export const ForgotPasswordForm: React.FC = () => {
const { t } = useTranslation()
const [hasSubmitted, setHasSubmitted] = useState(false)
const collectionConfig = config.collections?.find((collection) => collection?.slug === userSlug)
const collectionConfig = getEntityConfig({ collectionSlug: userSlug })
const loginWithUsername = collectionConfig?.auth?.loginWithUsername
const handleResponse: FormProps['handleResponse'] = (res, successToast, errorToast) => {

View File

@@ -139,9 +139,7 @@ export const renderListHandler = async (args: {
enableRowSelections,
importMap: payload.importMap,
initPageResult: {
collectionConfig: payload.config.collections.find(
(collection) => collection.slug === collectionSlug,
),
collectionConfig: payload?.collections?.[collectionSlug]?.config,
cookies,
globalConfig: payload.config.globals.find((global) => global.slug === collectionSlug),
languageOptions: undefined, // TODO

View File

@@ -567,9 +567,9 @@ export const LivePreviewClient: React.FC<
url,
})
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
const collectionConfig = getEntityConfig({ collectionSlug })
const globalConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
const globalConfig = getEntityConfig({ globalSlug })
const schemaPath = collectionSlug || globalSlug

View File

@@ -24,7 +24,7 @@ export const LoginForm: React.FC<{
prefillUsername?: string
searchParams: { [key: string]: string | string[] | undefined }
}> = ({ prefillEmail, prefillPassword, prefillUsername, searchParams }) => {
const { config } = useConfig()
const { config, getEntityConfig } = useConfig()
const {
admin: {
@@ -34,7 +34,7 @@ export const LoginForm: React.FC<{
routes: { admin: adminRoute, api: apiRoute },
} = config
const collectionConfig = config.collections?.find((collection) => collection?.slug === userSlug)
const collectionConfig = getEntityConfig({ collectionSlug: userSlug })
const { auth: authOptions } = collectionConfig
const loginWithUsername = authOptions.loginWithUsername
const { canLoginWithEmail, canLoginWithUsername } = getLoginOptions(loginWithUsername)

View File

@@ -32,7 +32,7 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
redirect((searchParams.redirect as string) || admin)
}
const collectionConfig = collections.find(({ slug }) => slug === userSlug)
const collectionConfig = payload?.collections?.[userSlug]?.config
const prefillAutoLogin =
typeof config.admin?.autoLogin === 'object' && config.admin?.autoLogin.prefillOnly

View File

@@ -1,5 +1,5 @@
'use client'
import type { ClientCollectionConfig, ClientGlobalConfig, OptionObject } from 'payload'
import type { OptionObject } from 'payload'
import { Gutter, useConfig, useDocumentInfo, usePayloadAPI, useTranslation } from '@payloadcms/ui'
import { formatDate } from '@payloadcms/ui/shared'
@@ -31,11 +31,9 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
const { i18n } = useTranslation()
const { id, collectionSlug, globalSlug } = useDocumentInfo()
const [collectionConfig] = useState(
() => getEntityConfig({ collectionSlug }) as ClientCollectionConfig,
)
const [collectionConfig] = useState(() => getEntityConfig({ collectionSlug }))
const [globalConfig] = useState(() => getEntityConfig({ globalSlug }) as ClientGlobalConfig)
const [globalConfig] = useState(() => getEntityConfig({ globalSlug }))
const [locales, setLocales] = useState<OptionObject[]>(localeOptions)

View File

@@ -35,13 +35,13 @@ const Restore: React.FC<Props> = ({
}) => {
const {
config: {
collections,
routes: { admin: adminRoute, api: apiRoute },
serverURL,
},
getEntityConfig,
} = useConfig()
const collectionConfig = collections.find((collection) => collection.slug === collectionSlug)
const collectionConfig = getEntityConfig({ collectionSlug })
const { toggleModal } = useModal()
const [processing, setProcessing] = useState(false)

View File

@@ -18,9 +18,7 @@ export async function getAccessResults({
const isLoggedIn = !!user
const userCollectionConfig =
user && user.collection
? payload.config.collections.find((collection) => collection.slug === user.collection)
: null
user && user.collection ? payload?.collections?.[user.collection]?.config : null
if (userCollectionConfig && payload.config.admin.user === user?.collection) {
results.canAccessAdmin = userCollectionConfig.access.admin

View File

@@ -23,9 +23,8 @@ export const previewHandler: PayloadHandler = async (req) => {
let previewURL: string
const generatePreviewURL = req.payload.config.collections.find(
(config) => config.slug === collection.config.slug,
)?.admin?.preview
const generatePreviewURL =
req.payload?.collections?.[collection.config.slug]?.config?.admin?.preview
const token = extractJWT(req)

View File

@@ -158,9 +158,7 @@ export const promise = async <T>({
if (Array.isArray(field.relationTo)) {
if (Array.isArray(value)) {
value.forEach((relatedDoc: { relationTo: string; value: JsonValue }, i) => {
const relatedCollection = req.payload.config.collections.find(
(collection) => collection.slug === relatedDoc.relationTo,
)
const relatedCollection = req.payload.collections?.[relatedDoc.relationTo]?.config
if (
typeof relatedDoc.value === 'object' &&
@@ -185,9 +183,7 @@ export const promise = async <T>({
})
}
if (field.hasMany !== true && valueIsValueWithRelation(value)) {
const relatedCollection = req.payload.config.collections.find(
(collection) => collection.slug === value.relationTo,
)
const relatedCollection = req.payload.collections?.[value.relationTo]?.config
if (typeof value.value === 'object' && value.value && 'id' in value.value) {
value.value = (value.value as TypeWithID).id
@@ -206,9 +202,9 @@ export const promise = async <T>({
} else {
if (Array.isArray(value)) {
value.forEach((relatedDoc: unknown, i) => {
const relatedCollection = req.payload.config.collections.find(
(collection) => collection.slug === field.relationTo,
)
const relatedCollection = Array.isArray(field.relationTo)
? undefined
: req.payload.collections?.[field.relationTo]?.config
if (typeof relatedDoc === 'object' && relatedDoc && 'id' in relatedDoc) {
value[i] = relatedDoc.id
@@ -226,9 +222,7 @@ export const promise = async <T>({
})
}
if (field.hasMany !== true && value) {
const relatedCollection = req.payload.config.collections.find(
(collection) => collection.slug === field.relationTo,
)
const relatedCollection = req.payload.collections?.[field.relationTo]?.config
if (typeof value === 'object' && value && 'id' in value) {
siblingData[field.name] = value.id

View File

@@ -166,7 +166,7 @@ export const email: EmailFieldValidation = (
{
collectionSlug,
req: {
payload: { config },
payload: { collections },
t,
},
required,
@@ -174,7 +174,7 @@ export const email: EmailFieldValidation = (
},
) => {
if (collectionSlug) {
const collection = config.collections.find(({ slug }) => slug === collectionSlug)
const collection = collections?.[collectionSlug]?.config
if (
collection.auth.loginWithUsername &&
@@ -201,7 +201,7 @@ export const username: UsernameFieldValidation = (
{
collectionSlug,
req: {
payload: { config },
payload: { collections, config },
t,
},
required,
@@ -211,7 +211,7 @@ export const username: UsernameFieldValidation = (
let maxLength: number
if (collectionSlug) {
const collection = config.collections.find(({ slug }) => slug === collectionSlug)
const collection = collections?.[collectionSlug]?.config
if (
collection.auth.loginWithUsername &&

View File

@@ -27,7 +27,7 @@ export const checkDocumentLockStatus = async ({
// Retrieve the lockDocuments property for either collection or global
const lockDocumentsProp = collectionSlug
? payload.config?.collections?.find((c) => c.slug === collectionSlug)?.lockDocuments
? payload.collections?.[collectionSlug]?.config?.lockDocuments
: payload.config?.globals?.find((g) => g.slug === globalSlug)?.lockDocuments
const isLockingEnabled = lockDocumentsProp !== false

View File

@@ -36,10 +36,10 @@ export const MetaImageComponent: React.FC<MetaImageProps> = (props) => {
const {
config: {
collections,
routes: { api },
serverURL,
},
getEntityConfig,
} = useConfig()
const field: FieldType<string> = useField({ ...props, path } as Options)
@@ -109,7 +109,7 @@ export const MetaImageComponent: React.FC<MetaImageProps> = (props) => {
const hasImage = Boolean(value)
const collection = collections?.find((coll) => coll.slug === relationTo) || undefined
const collection = getEntityConfig({ collectionSlug: relationTo })
return (
<div

View File

@@ -62,7 +62,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
uuid,
} = useEditorConfigContext()
const { config } = useConfig()
const { config, getEntityConfig } = useConfig()
const { i18n, t } = useTranslation<object, 'lexical:link:loadingWithEllipsis'>()
@@ -150,7 +150,9 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
}`,
)
const relatedField = config.collections.find((coll) => coll.slug === fields?.doc?.relationTo)
const relatedField = fields?.doc?.relationTo
? getEntityConfig({ collectionSlug: fields?.doc?.relationTo })
: undefined
if (!relatedField) {
// Usually happens if the user removed all default fields. In this case, we let them specify the label or do not display the label at all.
// label could be a virtual field the user added. This is useful if they want to use the link feature for things other than links.

View File

@@ -59,14 +59,14 @@ const Component: React.FC<Props> = (props) => {
} = useEditorConfigContext()
const {
config: {
collections,
routes: { api },
serverURL,
},
getEntityConfig,
} = useConfig()
const [relatedCollection, setRelatedCollection] = useState(
() => collections.find((coll) => coll.slug === relationTo)!,
const [relatedCollection, setRelatedCollection] = useState(() =>
getEntityConfig({ collectionSlug: relationTo }),
)
const { i18n, t } = useTranslation()

View File

@@ -64,10 +64,10 @@ const Component: React.FC<ElementProps> = (props) => {
const {
config: {
collections,
routes: { api },
serverURL,
},
getEntityConfig,
} = useConfig()
const uploadRef = useRef<HTMLDivElement | null>(null)
const { uuid } = useEditorConfigContext()
@@ -82,8 +82,8 @@ const Component: React.FC<ElementProps> = (props) => {
const { i18n, t } = useTranslation()
const [cacheBust, dispatchCacheBust] = useReducer((state) => state + 1, 0)
const [relatedCollection] = useState<ClientCollectionConfig>(
() => collections.find((coll) => coll.slug === relationTo)!,
const [relatedCollection] = useState<ClientCollectionConfig>(() =>
getEntityConfig({ collectionSlug: relationTo }),
)
const componentID = useId()

View File

@@ -69,7 +69,7 @@ export const LinkElement = () => {
const { id, collectionSlug, docPermissions, getDocPreferences, globalSlug } = useDocumentInfo()
const editor = useSlate()
const { config } = useConfig()
const { config, getEntityConfig } = useConfig()
const { code: locale } = useLocale()
const { i18n, t } = useTranslation()
const { closeModal, openModal, toggleModal } = useModal()
@@ -179,8 +179,7 @@ export const LinkElement = () => {
t={t}
variables={{
label: getTranslation(
config.collections.find(({ slug }) => slug === element.doc.relationTo)?.labels
?.singular,
getEntityConfig({ collectionSlug: element.doc.relationTo })?.labels?.singular,
i18n,
),
}}

View File

@@ -40,6 +40,7 @@ const RelationshipElementComponent: React.FC = () => {
routes: { api },
serverURL,
},
getEntityConfig,
} = useConfig()
const [enabledCollectionSlugs] = useState(() =>
collections
@@ -47,7 +48,7 @@ const RelationshipElementComponent: React.FC = () => {
.map(({ slug }) => slug),
)
const [relatedCollection, setRelatedCollection] = useState(() =>
collections.find((coll) => coll.slug === relationTo),
getEntityConfig({ collectionSlug: relationTo }),
)
const selected = useSelected()
@@ -117,7 +118,7 @@ const RelationshipElementComponent: React.FC = () => {
{ at: elementPath },
)
setRelatedCollection(collections.find((coll) => coll.slug === collectionSlug))
setRelatedCollection(getEntityConfig({ collectionSlug }))
setParams({
...initialParams,
@@ -127,7 +128,7 @@ const RelationshipElementComponent: React.FC = () => {
closeListDrawer()
dispatchCacheBust()
},
[closeListDrawer, editor, element, cacheBust, setParams, collections],
[closeListDrawer, editor, element, cacheBust, setParams, getEntityConfig],
)
return (

View File

@@ -46,15 +46,15 @@ const UploadElementComponent: React.FC<{ enabledCollectionSlugs?: string[] }> =
const {
config: {
collections,
routes: { api },
serverURL,
},
getEntityConfig,
} = useConfig()
const { i18n, t } = useTranslation()
const [cacheBust, dispatchCacheBust] = useReducer((state) => state + 1, 0)
const [relatedCollection, setRelatedCollection] = useState<ClientCollectionConfig>(() =>
collections.find((coll) => coll.slug === relationTo),
getEntityConfig({ collectionSlug: relationTo }),
)
const drawerSlug = useDrawerSlug('upload-drawer')
@@ -121,14 +121,14 @@ const UploadElementComponent: React.FC<{ enabledCollectionSlugs?: string[] }> =
const elementPath = ReactEditor.findPath(editor, element)
setRelatedCollection(collections.find((coll) => coll.slug === collectionSlug))
setRelatedCollection(getEntityConfig({ collectionSlug }))
Transforms.setNodes(editor, newNode, { at: elementPath })
dispatchCacheBust()
closeListDrawer()
},
[closeListDrawer, editor, element, collections],
[closeListDrawer, editor, element, getEntityConfig],
)
const relatedFieldSchemaPath = `${uploadFieldsSchemaPath}.${relatedCollection.slug}`

View File

@@ -6,14 +6,12 @@ import { useState } from 'react'
import { useConfig } from '../../providers/Config/index.js'
export const useRelatedCollections = (relationTo: string | string[]): ClientCollectionConfig[] => {
const { config } = useConfig()
const { getEntityConfig } = useConfig()
const [relatedCollections] = useState(() => {
if (relationTo) {
const relations = typeof relationTo === 'string' ? [relationTo] : relationTo
return relations.map((relation) =>
config.collections.find((collection) => collection.slug === relation),
)
return relations.map((relation) => getEntityConfig({ collectionSlug: relation }))
}
return []
})

View File

@@ -1,7 +1,5 @@
'use client'
import type { ClientCollectionConfig } from 'payload'
import { useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations'
import { reduceFieldsToValues } from 'payload/shared'
@@ -38,7 +36,7 @@ export function AddingFilesView() {
const { user } = useAuth()
const { openModal } = useModal()
const collection = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
const collection = getEntityConfig({ collectionSlug })
return (
<div className={baseClass}>

View File

@@ -58,7 +58,7 @@ export function EditForm({ submitted }: EditFormProps) {
const abortOnChangeRef = React.useRef<AbortController>(null)
const collectionConfig = getEntityConfig({ collectionSlug: docSlug }) as ClientCollectionConfig
const collectionConfig = getEntityConfig({ collectionSlug: docSlug })
const router = useRouter()
const depth = useEditDepth()
const params = useSearchParams()

View File

@@ -20,10 +20,10 @@ function DrawerContent() {
const { addFiles, forms, isInitializing } = useFormsManager()
const { closeModal } = useModal()
const { collectionSlug, drawerSlug } = useBulkUpload()
const { config } = useConfig()
const { getEntityConfig } = useConfig()
const { t } = useTranslation()
const uploadCollection = config.collections.find((col) => col.slug === collectionSlug)
const uploadCollection = getEntityConfig({ collectionSlug })
const uploadConfig = uploadCollection?.upload
const uploadMimeTypes = uploadConfig?.mimeTypes

View File

@@ -54,7 +54,7 @@ export const DeleteDocument: React.FC<Props> = (props) => {
getEntityConfig,
} = useConfig()
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
const collectionConfig = getEntityConfig({ collectionSlug })
const { setModified } = useForm()
const [deleting, setDeleting] = useState(false)

View File

@@ -98,9 +98,9 @@ export const DocumentControls: React.FC<{
const { config, getEntityConfig } = useConfig()
const collectionConfig = getEntityConfig({ collectionSlug: slug }) as ClientCollectionConfig
const collectionConfig = getEntityConfig({ collectionSlug: slug })
const globalConfig = getEntityConfig({ globalSlug: slug }) as ClientGlobalConfig
const globalConfig = getEntityConfig({ globalSlug: slug })
const {
admin: { dateFormat },

View File

@@ -30,14 +30,10 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
redirectAfterDelete,
redirectAfterDuplicate,
}) => {
const {
config: { collections },
} = useConfig()
const { getEntityConfig } = useConfig()
const locale = useLocale()
const [collectionConfig] = useState(() =>
collections.find((collection) => collection.slug === collectionSlug),
)
const [collectionConfig] = useState(() => getEntityConfig({ collectionSlug }))
const abortGetDocumentViewRef = React.useRef<AbortController>(null)

View File

@@ -53,7 +53,7 @@ export const DuplicateDocument: React.FC<Props> = ({
getEntityConfig,
} = useConfig()
const collectionConfig = getEntityConfig({ collectionSlug: slug }) as ClientCollectionConfig
const collectionConfig = getEntityConfig({ collectionSlug: slug })
const [hasClicked, setHasClicked] = useState<boolean>(false)
const { i18n, t } = useTranslation()

View File

@@ -1,5 +1,5 @@
'use client'
import type { ClientCollectionConfig, ListQuery } from 'payload'
import type { ListQuery } from 'payload'
import { useModal } from '@faceless-ui/modal'
import React, { useCallback, useEffect, useState } from 'react'
@@ -45,7 +45,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
const [selectedOption, setSelectedOption] = useState<Option<string>>(() => {
const initialSelection = selectedCollectionFromProps || enabledCollections[0]?.slug
const found = getEntityConfig({ collectionSlug: initialSelection }) as ClientCollectionConfig
const found = getEntityConfig({ collectionSlug: initialSelection })
return found
? {
@@ -63,7 +63,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
() => {
if (selectedCollectionFromProps && selectedCollectionFromProps !== selectedOption?.value) {
setSelectedOption({
label: collections.find(({ slug }) => slug === selectedCollectionFromProps).labels,
label: getEntityConfig({ collectionSlug: selectedCollectionFromProps })?.labels,
value: selectedCollectionFromProps,
})
}

View File

@@ -18,8 +18,9 @@ export const Localizer: React.FC<{
className?: string
}> = (props) => {
const { className } = props
const { config } = useConfig()
const { localization } = config
const {
config: { localization },
} = useConfig()
const searchParams = useSearchParams()
const { setLocaleIsLoading } = useLocaleLoading()

View File

@@ -31,7 +31,7 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
uploadStatus,
} = useDocumentInfo()
const { config } = useConfig()
const { config, getEntityConfig } = useConfig()
const { submit } = useForm()
const modified = useFormModified()
const editDepth = useEditDepth()
@@ -51,13 +51,13 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
const entityConfig = React.useMemo(() => {
if (collectionSlug) {
return config.collections.find(({ slug }) => slug === collectionSlug)
return getEntityConfig({ collectionSlug })
}
if (globalSlug) {
return config.globals.find(({ slug }) => slug === globalSlug)
return getEntityConfig({ globalSlug })
}
}, [collectionSlug, globalSlug, config])
}, [collectionSlug, globalSlug, getEntityConfig])
const hasNewerVersions = unpublishedVersionCount > 0

View File

@@ -91,9 +91,7 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
const [query, setQuery] = useState<ListQuery>()
const [openColumnSelector, setOpenColumnSelector] = useState(false)
const [collectionConfig] = useState(
() => getEntityConfig({ collectionSlug: relationTo }) as ClientCollectionConfig,
)
const [collectionConfig] = useState(() => getEntityConfig({ collectionSlug: relationTo }))
const [isLoadingTable, setIsLoadingTable] = useState(!disableTable)
const [data, setData] = useState<PaginatedDocs>(initialData)

View File

@@ -1,6 +1,5 @@
'use client'
import type {
ClientCollectionConfig,
DefaultCellComponentProps,
JoinFieldClient,
RelationshipFieldClient,
@@ -98,7 +97,7 @@ export const RelationshipCell: React.FC<RelationshipCellProps> = ({
const document = documents[relationTo][value]
const relatedCollection = getEntityConfig({
collectionSlug: relationTo,
}) as ClientCollectionConfig
})
const label = formatDocTitle({
collectionConfig: relatedCollection,

View File

@@ -67,7 +67,7 @@ export const TableColumnsProvider: React.FC<Props> = ({
const { admin: { defaultColumns, useAsTitle } = {}, fields } = getEntityConfig({
collectionSlug,
}) as ClientCollectionConfig
})
const prevCollection = React.useRef<SanitizedCollectionConfig['slug']>(collectionSlug)
const { getPreference } = usePreferences()

View File

@@ -1,5 +1,5 @@
'use client'
import type { ClientCollectionConfig, PaginatedDocs, Where } from 'payload'
import type { PaginatedDocs, Where } from 'payload'
import * as qs from 'qs-esm'
import React, { useCallback, useEffect, useReducer, useState } from 'react'
@@ -28,7 +28,6 @@ export const RelationshipField: React.FC<Props> = (props) => {
const {
config: {
collections,
routes: { api },
serverURL,
},
@@ -55,7 +54,7 @@ export const RelationshipField: React.FC<Props> = (props) => {
const addOptions = useCallback(
(data, relation) => {
const collection = getEntityConfig({ collectionSlug: relation }) as ClientCollectionConfig
const collection = getEntityConfig({ collectionSlug: relation })
dispatchOptions({ type: 'ADD', collection, data, hasMultipleRelations, i18n, relation })
},
[hasMultipleRelations, i18n, getEntityConfig],
@@ -72,7 +71,7 @@ export const RelationshipField: React.FC<Props> = (props) => {
if (relationSlug && partiallyLoadedRelationshipSlugs.current.includes(relationSlug)) {
const collection = getEntityConfig({
collectionSlug: relationSlug,
}) as ClientCollectionConfig
})
const fieldToSearch = collection?.admin?.useAsTitle || 'id'
const pageIndex = nextPageByRelationshipRef.current.get(relationSlug)
@@ -135,7 +134,7 @@ export const RelationshipField: React.FC<Props> = (props) => {
setHasLoadedFirstOptions(true)
},
[addOptions, api, collections, debouncedSearch, i18n.language, serverURL, t],
[addOptions, api, debouncedSearch, getEntityConfig, i18n.language, serverURL, t],
)
const loadMoreOptions = React.useCallback(() => {

View File

@@ -127,9 +127,7 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
const { id: docID, docConfig } = useDocumentInfo()
const {
config: { collections },
} = useConfig()
const { getEntityConfig } = useConfig()
const { customComponents: { AfterInput, BeforeInput, Description, Label } = {}, value } =
useField<PaginatedDocs>({
@@ -166,7 +164,7 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
}, [docID, field.targetField.relationTo, field.where, on, docConfig.slug])
const initialDrawerData = useMemo(() => {
const relatedCollection = collections.find((collection) => collection.slug === field.collection)
const relatedCollection = getEntityConfig({ collectionSlug: field.collection })
return getInitialDrawerData({
collectionSlug: docConfig.slug,
@@ -174,7 +172,7 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
fields: relatedCollection.fields,
segments: field.on.split('.'),
})
}, [collections, field.on, field.collection, docConfig.slug, docID])
}, [getEntityConfig, field.collection, field.on, docConfig.slug, docID])
return (
<div

View File

@@ -60,10 +60,9 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
validate,
} = props
const { config } = useConfig()
const { config, getEntityConfig } = useConfig()
const {
collections,
routes: { api },
serverURL,
} = config
@@ -169,7 +168,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
}
if (resultsFetched < 10) {
const collection = collections.find((coll) => coll.slug === relation)
const collection = getEntityConfig({ collectionSlug: relation })
const fieldToSearch = collection?.admin?.useAsTitle || 'id'
let fieldToSort = collection?.defaultSort || 'id'
if (typeof sortOptions === 'string') {
@@ -275,7 +274,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
hasMany,
errorLoading,
search,
collections,
getEntityConfig,
locale,
serverURL,
sortOptions,
@@ -354,7 +353,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
method: 'POST',
})
const collection = collections.find((coll) => coll.slug === relation)
const collection = getEntityConfig({ collectionSlug: relation })
let docs = []
if (response.ok) {
@@ -380,7 +379,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
options,
hasMany,
errorLoading,
collections,
getEntityConfig,
hasMultipleRelations,
serverURL,
api,
@@ -395,12 +394,12 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
useEffect(() => {
const relations = Array.isArray(relationTo) ? relationTo : [relationTo]
const isIdOnly = relations.reduce((idOnly, relation) => {
const collection = collections.find((coll) => coll.slug === relation)
const collection = getEntityConfig({ collectionSlug: relation })
const fieldToSearch = collection?.admin?.useAsTitle || 'id'
return fieldToSearch === 'id' && idOnly
}, true)
setEnableWordBoundarySearch(!isIdOnly)
}, [relationTo, collections])
}, [relationTo, getEntityConfig])
// When (`relationTo` || `filterOptions` || `locale`) changes, reset component
// Note - effect should not run on first run

View File

@@ -1,26 +1,42 @@
/* eslint-disable perfectionist/sort-object-types */ // Need to disable this rule because the order of the overloads is important
'use client'
import type { ClientCollectionConfig, ClientConfig, ClientGlobalConfig } from 'payload'
import type {
ClientCollectionConfig,
ClientConfig,
ClientGlobalConfig,
CollectionSlug,
GlobalSlug,
} from 'payload'
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
type GetEntityConfigFn = {
// Overload #1: collectionSlug only
// @todo remove "{} |" in 4.0, which would be a breaking change
(args: { collectionSlug: {} | CollectionSlug; globalSlug?: never }): ClientCollectionConfig
// Overload #2: globalSlug only
// @todo remove "{} |" in 4.0, which would be a breaking change
(args: { collectionSlug?: never; globalSlug: {} | GlobalSlug }): ClientGlobalConfig
// Overload #3: both/none (fall back to union | null)
(args: {
collectionSlug?: {} | CollectionSlug
globalSlug?: {} | GlobalSlug
}): ClientCollectionConfig | ClientGlobalConfig | null
}
export type ClientConfigContext = {
config: ClientConfig
getEntityConfig: (args: {
collectionSlug?: string
globalSlug?: string
}) => ClientCollectionConfig | ClientGlobalConfig | null
/**
* Get a collection or global config by its slug. This is preferred over
* using `config.collections.find` or `config.globals.find`, because
* getEntityConfig uses a lookup map for O(1) lookups.
*/
getEntityConfig: GetEntityConfigFn
setConfig: (config: ClientConfig) => void
}
export type EntityConfigContext = {
collectionConfig?: ClientCollectionConfig
globalConfig?: ClientGlobalConfig
setEntityConfig: (args: {
collectionConfig?: ClientCollectionConfig | null
globalConfig?: ClientGlobalConfig | null
}) => void
}
const RootConfigContext = createContext<ClientConfigContext | undefined>(undefined)
export const ConfigProvider: React.FC<{
@@ -35,19 +51,32 @@ export const ConfigProvider: React.FC<{
setConfig(configFromProps)
}, [configFromProps])
const getEntityConfig = useCallback(
({ collectionSlug, globalSlug }: { collectionSlug?: string; globalSlug?: string }) => {
if (collectionSlug) {
return config.collections.find((collection) => collection.slug === collectionSlug)
}
// Build lookup maps for collections and globals so we can do O(1) lookups by slug
const { collectionsBySlug, globalsBySlug } = useMemo(() => {
const collectionsBySlug: Record<string, ClientCollectionConfig> = {}
const globalsBySlug: Record<string, ClientGlobalConfig> = {}
if (globalSlug) {
return config.globals.find((global) => global.slug === globalSlug)
}
for (const collection of config.collections) {
collectionsBySlug[collection.slug] = collection
}
for (const global of config.globals) {
globalsBySlug[global.slug] = global
}
return null
return { collectionsBySlug, globalsBySlug }
}, [config])
const getEntityConfig = useCallback<GetEntityConfigFn>(
(args) => {
if ('collectionSlug' in args) {
return collectionsBySlug[args.collectionSlug] ?? null
}
if ('globalSlug' in args) {
return globalsBySlug[args.globalSlug] ?? null
}
return null as any
},
[config],
[collectionsBySlug, globalsBySlug],
)
return (

View File

@@ -1,11 +1,5 @@
'use client'
import type {
ClientCollectionConfig,
ClientGlobalConfig,
ClientUser,
DocumentPreferences,
SanitizedDocumentPermissions,
} from 'payload'
import type { ClientUser, DocumentPreferences, SanitizedDocumentPermissions } from 'payload'
import * as qs from 'qs-esm'
import React, {
@@ -79,8 +73,8 @@ const DocumentInfo: React.FC<
getEntityConfig,
} = useConfig()
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
const globalConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
const collectionConfig = getEntityConfig({ collectionSlug })
const globalConfig = getEntityConfig({ globalSlug })
const abortControllerRef = useRef(new AbortController())
const docConfig = collectionConfig || globalConfig

View File

@@ -14,6 +14,8 @@ type Result = {
user: TypedUser
}
const lockDurationDefault = 300 // Default 5 minutes in seconds
export const handleFormStateLocking = async ({
id,
collectionSlug,
@@ -39,9 +41,8 @@ export const handleFormStateLocking = async ({
}
}
const lockDurationDefault = 300 // Default 5 minutes in seconds
const lockDocumentsProp = collectionSlug
? req.payload.config.collections.find((c) => c.slug === collectionSlug)?.lockDocuments
? req.payload.collections?.[collectionSlug]?.config.lockDocuments
: req.payload.config.globals.find((g) => g.slug === globalSlug)?.lockDocuments
const lockDuration =

View File

@@ -8,7 +8,6 @@ import { v4 as uuidv4 } from 'uuid'
import { CopyToClipboard } from '../../../elements/CopyToClipboard/index.js'
import { GenerateConfirmation } from '../../../elements/GenerateConfirmation/index.js'
import { FieldLabel } from '../../../fields/FieldLabel/index.js'
import { useFormFields } from '../../../forms/Form/context.js'
import { useField } from '../../../forms/useField/index.js'
import { useConfig } from '../../../providers/Config/index.js'
@@ -26,16 +25,14 @@ export const APIKey: React.FC<{ readonly enabled: boolean; readonly readOnly?: b
const [initialAPIKey] = useState(uuidv4())
const [highlightedField, setHighlightedField] = useState(false)
const { i18n, t } = useTranslation()
const { config } = useConfig()
const { config, getEntityConfig } = useConfig()
const { collectionSlug } = useDocumentInfo()
const apiKey = useFormFields(([fields]) => (fields && fields[path]) || null)
const apiKeyField: TextFieldClient = config.collections
.find((collection) => {
return collection.slug === collectionSlug
})
?.fields?.find((field) => 'name' in field && field.name === 'apiKey') as TextFieldClient
const apiKeyField: TextFieldClient = getEntityConfig({ collectionSlug })?.fields?.find(
(field) => 'name' in field && field.name === 'apiKey',
) as TextFieldClient
const validate = (val) =>
text(val, {

View File

@@ -1,12 +1,6 @@
'use client'
import type {
ClientCollectionConfig,
ClientGlobalConfig,
ClientSideEditViewProps,
ClientUser,
FormState,
} from 'payload'
import type { ClientSideEditViewProps, ClientUser, FormState } from 'payload'
import { useRouter, useSearchParams } from 'next/navigation.js'
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -109,8 +103,8 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
getEntityConfig,
} = useConfig()
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
const globalConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
const collectionConfig = getEntityConfig({ collectionSlug })
const globalConfig = getEntityConfig({ globalSlug })
const depth = useEditDepth()

View File

@@ -90,6 +90,7 @@ const ListDrawerHeader: React.FC<ListHeaderProps> = ({
}) => {
const {
config: { collections },
getEntityConfig,
} = useConfig()
const { closeModal } = useModal()
@@ -102,7 +103,7 @@ const ListDrawerHeader: React.FC<ListHeaderProps> = ({
setSelectedOption,
} = useListDrawerContext()
const collectionConfig = collections.find(({ slug }) => slug === selectedOption.value)
const collectionConfig = getEntityConfig({ collectionSlug: selectedOption.value })
const enabledCollectionConfigs = collections.filter(({ slug }) =>
enabledCollections.includes(slug),

View File

@@ -1,6 +1,6 @@
'use client'
import type { ClientCollectionConfig, ListPreferences } from 'payload'
import type { ListPreferences } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import LinkImport from 'next/link.js'
@@ -113,7 +113,7 @@ export const DefaultListView: React.FC<ListViewClientProps> = (props) => {
const { setCollectionSlug, setCurrentActivePath, setOnSuccess } = useBulkUpload()
const { drawerSlug: bulkUploadDrawerSlug } = useBulkUpload()
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
const collectionConfig = getEntityConfig({ collectionSlug })
const { labels, upload } = collectionConfig