+}
+
+export default Page
diff --git a/app/(payload)/admin/[[...segments]]/not-found.tsx b/app/(payload)/admin/[[...segments]]/not-found.tsx
index e7723f49a..f2492a309 100644
--- a/app/(payload)/admin/[[...segments]]/not-found.tsx
+++ b/app/(payload)/admin/[[...segments]]/not-found.tsx
@@ -5,6 +5,8 @@ import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
+import { importMap } from '../importMap.js'
+
type Args = {
params: {
segments: string[]
@@ -17,6 +19,7 @@ type Args = {
export const generateMetadata = ({ params, searchParams }: Args): Promise =>
generatePageMetadata({ config, params, searchParams })
-const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
+const NotFound = ({ params, searchParams }: Args) =>
+ NotFoundPage({ config, importMap, params, searchParams })
export default NotFound
diff --git a/app/(payload)/admin/[[...segments]]/page.tsx b/app/(payload)/admin/[[...segments]]/page.tsx
index 61be15c88..0bfe41079 100644
--- a/app/(payload)/admin/[[...segments]]/page.tsx
+++ b/app/(payload)/admin/[[...segments]]/page.tsx
@@ -5,6 +5,8 @@ import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
+import { importMap } from '../importMap.js'
+
type Args = {
params: {
segments: string[]
@@ -17,6 +19,7 @@ type Args = {
export const generateMetadata = ({ params, searchParams }: Args): Promise =>
generatePageMetadata({ config, params, searchParams })
-const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
+const Page = ({ params, searchParams }: Args) =>
+ RootPage({ config, importMap, params, searchParams })
export default Page
diff --git a/app/(payload)/admin/importMap.js b/app/(payload)/admin/importMap.js
deleted file mode 100644
index 8ef702138..000000000
--- a/app/(payload)/admin/importMap.js
+++ /dev/null
@@ -1 +0,0 @@
-export const importMap = {}
diff --git a/app/(payload)/layout.tsx b/app/(payload)/layout.tsx
index 57c61a3a6..ee2762cc3 100644
--- a/app/(payload)/layout.tsx
+++ b/app/(payload)/layout.tsx
@@ -1,6 +1,9 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import configPromise from '@payload-config'
import { RootLayout } from '@payloadcms/next/layouts'
+
+import { importMap } from './admin/importMap.js'
+
// import '@payloadcms/ui/styles.css' // Uncomment this line if `@payloadcms/ui` in `tsconfig.json` points to `/ui/dist` instead of `/ui/src`
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import React from 'react'
@@ -11,6 +14,10 @@ type Args = {
children: React.ReactNode
}
-const Layout = ({ children }: Args) => {children}
+const Layout = ({ children }: Args) => (
+
+ {children}
+
+)
export default Layout
diff --git a/docs/admin/collections.mdx b/docs/admin/collections.mdx
index bf168ca99..5f1189373 100644
--- a/docs/admin/collections.mdx
+++ b/docs/admin/collections.mdx
@@ -51,7 +51,6 @@ To override Collection Components, use the `admin.components` property in your [
```ts
import type { SanitizedCollectionConfig } from 'payload'
-import { CustomSaveButton } from './CustomSaveButton'
export const MyCollection: SanitizedCollectionConfig = {
// ...
diff --git a/docs/admin/components.mdx b/docs/admin/components.mdx
index 86228b279..a1d660e2b 100644
--- a/docs/admin/components.mdx
+++ b/docs/admin/components.mdx
@@ -33,8 +33,6 @@ To override Root Components, use the `admin.components` property in your [Payloa
```ts
import { buildConfig } from 'payload'
-import { MyCustomLogo } from './MyCustomLogo'
-
export default buildConfig({
// ...
admin: {
@@ -81,13 +79,11 @@ To add a Custom Provider, use the `admin.components.providers` property in your
```ts
import { buildConfig } from 'payload'
-import { MyProvider } from './MyProvider'
-
export default buildConfig({
// ...
admin: {
components: {
- providers: [MyProvider], // highlight-line
+ providers: ['/path/to/MyProvider'], // highlight-line
},
},
})
@@ -207,7 +203,7 @@ import React from 'react'
import { useConfig } from '@payloadcms/ui'
export const MyClientComponent: React.FC = () => {
- const { serverURL } = useConfig() // highlight-line
+ const { config: { serverURL } } = useConfig() // highlight-line
return (
@@ -221,6 +217,22 @@ export const MyClientComponent: React.FC = () => {
See [Using Hooks](#using-hooks) for more details.
+All [Field Components](./fields) automatically receive their respective Client Field Config through a common [`field`](./fields#the-field-prop) prop:
+
+```tsx
+'use client'
+import React from 'react'
+import type { TextFieldProps } from 'payload'
+
+export const MyClientFieldComponent: TextFieldProps = ({ field: { name } }) => {
+ return (
+
+ {`This field's name is ${name}`}
+
+ )
+}
+```
+
### Using Hooks
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](./hooks) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts:
diff --git a/docs/admin/fields.mdx b/docs/admin/fields.mdx
index 21ae64100..641611b94 100644
--- a/docs/admin/fields.mdx
+++ b/docs/admin/fields.mdx
@@ -117,7 +117,7 @@ export const CollectionConfig: CollectionConfig = {
// ...
admin: {
components: {
- Field: MyFieldComponent, // highlight-line
+ Field: '/path/to/MyFieldComponent', // highlight-line
},
},
}
@@ -135,32 +135,12 @@ All Field Components receive the following props:
| Property | Description |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **`AfterInput`** | The rendered result of the `admin.components.afterInput` property. [More details](#afterinput-and-beforeinput). |
-| **`BeforeInput`** | The rendered result of the `admin.components.beforeInput` property. [More details](#afterinput-and-beforeinput). |
-| **`CustomDescription`** | The rendered result of the `admin.components.Description` property. [More details](#the-description-component). |
-| **`CustomError`** | The rendered result of the `admin.components.Error` property. [More details](#the-error-component). |
-| **`CustomLabel`** | The rendered result of the `admin.components.Label` property. [More details](#the-label-component).
-| **`path`** | The static path of the field at render time. [More details](./hooks#usefieldprops). |
-| **`disabled`** | The `admin.disabled` property defined in the [Field Config](../fields/overview). |
-| **`required`** | The `admin.required` property defined in the [Field Config](../fields/overview). |
-| **`className`** | The `admin.className` property defined in the [Field Config](../fields/overview). |
-| **`style`** | The `admin.style` property defined in the [Field Config](../fields/overview). |
-| **`custom`** | The `admin.custom` property defined in the [Field Config](../fields/overview).
-| **`placeholder`** | The `admin.placeholder` property defined in the [Field Config](../fields/overview). |
-| **`descriptionProps`** | An object that contains the props for the `FieldDescription` component. |
-| **`labelProps`** | An object that contains the props needed for the `FieldLabel` component. |
-| **`errorProps`** | An object that contains the props for the `FieldError` component. |
-| **`docPreferences`** | An object that contains the preferences for the document. |
-| **`label`** | The label value provided in the field, it can be used with i18n. |
+| **`docPreferences`** | An object that contains the [Preferences](./preferences) for the document.
+| **`field`** | The sanitized, client-friendly version of the field's config. [More details](#the-field-prop) |
| **`locale`** | The locale of the field. [More details](../configuration/localization). |
-| **`localized`** | A boolean value that represents if the field is localized or not. [More details](../fields/localized). |
| **`readOnly`** | A boolean value that represents if the field is read-only or not. |
-| **`rtl`** | A boolean value that represents if the field should be rendered right-to-left or not. [More details](../configuration/i18n). |
| **`user`** | The currently authenticated user. [More details](../authentication/overview). |
| **`validate`** | A function that can be used to validate the field. |
-| **`hasMany`** | If a [`relationship`](../fields/relationship) field, the `hasMany` property defined in the [Field Config](../fields/overview). |
-| **`maxLength`** | If a [`text`](../fields/text) field, the `maxLength` property defined in the [Field Config](../fields/overview). |
-| **`minLength`** | If a [`text`](../fields/text) field, the `minLength` property defined in the [Field Config](../fields/overview). |
Reminder:
@@ -193,6 +173,99 @@ export const CustomTextField: React.FC = () => {
For a complete list of all available React hooks, see the [Payload React Hooks](./hooks) documentation. For additional help, see [Building Custom Components](./components#building-custom-components).
+#### TypeScript
+
+When building Custom Field Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Field Component, one for every [Field Type](../fields/overview). The convention is to append `Props` to the type of field, i.e. `TextFieldProps`.
+
+```tsx
+import type {
+ ArrayFieldProps,
+ BlocksFieldProps,
+ CheckboxFieldProps,
+ CodeFieldProps,
+ CollapsibleFieldProps,
+ DateFieldProps,
+ EmailFieldProps,
+ GroupFieldProps,
+ HiddenFieldProps,
+ JSONFieldProps,
+ NumberFieldProps,
+ PointFieldProps,
+ RadioFieldProps,
+ RelationshipFieldProps,
+ RichTextFieldProps,
+ RowFieldProps,
+ SelectFieldProps,
+ TabsFieldProps,
+ TextFieldProps,
+ TextareaFieldProps,
+ UploadFieldProps
+} from 'payload'
+```
+
+### The `field` Prop
+
+All Field Components are passed a client-friendly version of their Field Config through a common `field` prop. Since the raw Field Config is [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types), Payload sanitized it into a [Client Config](./components#accessing-the-payload-config) that is safe to pass into Client Components.
+
+The exact shape of this prop is unique to the specific [Field Type](../fields/overview) being rendered, minus all non-serializable properties. Any [Custom Components](../components) are also resolved into a "mapped component" that is safe to pass.
+
+```tsx
+'use client'
+import React from 'react'
+import type { TextFieldProps } from 'payload'
+
+export const MyClientFieldComponent: TextFieldProps = ({ field: { name } }) => {
+ return (
+
+ {`This field's name is ${name}`}
+
+ )
+}
+```
+
+The following additional properties are also provided to the `field` prop:
+
+| Property | Description |
+| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **`_isPresentational`** | A boolean indicating that the field is purely visual and does not directly affect data or change data shape, i.e. the [UI Field](../fields/ui). |
+| **`_path`** | A string representing the direct, dynamic path to the field at runtime, i.e. `myGroup.myArray[0].myField`. |
+| **`_schemaPath`** | A string representing the direct, static path to the [Field Config](../fields/overview), i.e. `myGroup.myArray.myField` |
+
+
+ Note:
+ These properties are underscored to denote that they are not part of the original Field Config, and instead are attached during client sanitization to make fields easier to work with on the front-end.
+
+
+#### TypeScript
+
+When building Custom Field Components, you can import the client field props to ensure type safety in your component. There is an explicit type for the Field Component, one for every [Field Type](../fields/overview). The convention is to append `Client` to the type of field, i.e. `TextFieldClient`.
+
+```tsx
+import type {
+ ArrayFieldClient,
+ BlocksFieldClient,
+ CheckboxFieldClient,
+ CodeFieldClient,
+ CollapsibleFieldClient,
+ DateFieldClient,
+ EmailFieldClient,
+ GroupFieldClient,
+ HiddenFieldClient,
+ JSONFieldClient,
+ NumberFieldClient,
+ PointFieldClient,
+ RadioFieldClient,
+ RelationshipFieldClient,
+ RichTextFieldClient,
+ RowFieldClient,
+ SelectFieldClient,
+ TabsFieldClient,
+ TextFieldClient,
+ TextareaFieldClient,
+ UploadFieldClient
+} from 'payload'
+```
+
### The Cell Component
The Cell Component is rendered in the table of the List View. It represents the value of the field when displayed in a table cell.
@@ -207,7 +280,7 @@ export const myField: Field = {
type: 'text',
admin: {
components: {
- Cell: MyCustomCell, // highlight-line
+ Cell: '/path/to/MyCustomCellComponent', // highlight-line
},
},
}
@@ -219,20 +292,9 @@ All Cell Components receive the following props:
| Property | Description |
| ---------------- | ----------------------------------------------------------------- |
-| **`name`** | The name of the field. |
-| **`className`** | The `admin.className` property defined in the [Field Config](../fields/overview). |
-| **`fieldType`** | The `type` property defined in the [Field Config](../fields/overview). |
-| **`schemaPath`** | The path to the field in the schema. Similar to `path`, but without dynamic indices. |
-| **`isFieldAffectingData`** | A boolean value that represents if the field is affecting the data or not. |
-| **`label`** | The label value provided in the field, it can be used with i18n. |
-| **`labels`** | An object that contains the labels for the field. |
+| **`field`** | The sanitized, client-friendly version of the field's config. [More details](#the-field-prop) |
| **`link`** | A boolean representing whether this cell should be wrapped in a link. |
| **`onClick`** | A function that is called when the cell is clicked. |
-| **`dateDisplayFormat`** | If a [`date`](../fields/date) field, the `admin.dateDisplayFormat` property defined in the [Field Config](../fields/overview). |
-| **`options`** | If a [`select`](../fields/select) field, this is an array of options defined in the [Field Config](../fields/overview). [More details](../fields/select). |
-| **`relationTo`** | If a [`relationship`](../fields/relationship). or [`upload`](../fields/upload) field, this is the collection(s) the field is related to. |
-| **`richTextComponentMap`** | If a [`richText`](../fields/rich-text) field, this is an object that maps the rich text components. [More details](../fields/rich-text). |
-| **`blocks`** | If a [`blocks`](../fields/blocks) field, this is an array of labels and slugs representing the blocks defined in the [Field Config](../fields/overview). [More details](../fields/blocks). |
Tip:
@@ -258,7 +320,7 @@ export const myField: Field = {
type: 'text',
admin: {
components: {
- Label: MyCustomLabel, // highlight-line
+ Label: '/path/to/MyCustomLabelComponent', // highlight-line
},
},
}
@@ -270,7 +332,7 @@ Custom Label Components receive all [Field Component](#the-field-component) prop
| Property | Description |
| -------------- | ---------------------------------------------------------------- |
-| **`schemaPath`** | The path to the field in the schema. Similar to `path`, but without dynamic indices. |
+| **`field`** | The sanitized, client-friendly version of the field's config. [More details](#the-field-prop) |
Reminder:
@@ -279,7 +341,7 @@ Custom Label Components receive all [Field Component](#the-field-component) prop
#### TypeScript
-When building Custom Error Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Error Component, one for every [Field Type](../fields/overview). The convention is to append `ErrorComponent` to the type of field, i.e. `TextFieldErrorComponent`.
+When building Custom Label Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Label Component, one for every [Field Type](../fields/overview). The convention is to append `LabelComponent` to the type of field, i.e. `TextFieldLabelComponent`.
```tsx
import type {
@@ -321,7 +383,7 @@ export const myField: Field = {
type: 'text',
admin: {
components: {
- Error: MyCustomError, // highlight-line
+ Error: '/path/to/MyCustomErrorComponent', // highlight-line
},
},
}
@@ -333,7 +395,7 @@ Custom Error Components receive all [Field Component](#the-field-component) prop
| Property | Description |
| --------------- | ------------------------------------------------------------- |
-| **`path`*** | The static path of the field at render time. [More details](./hooks#usefieldprops). |
+| **`field`** | The sanitized, client-friendly version of the field's config. [More details](#the-field-prop) |
Reminder:
@@ -443,7 +505,6 @@ To easily add a Description Component to a field, use the `admin.components.Desc
```ts
import type { SanitizedCollectionConfig } from 'payload'
-import { MyCustomDescription } from './MyCustomDescription'
export const MyCollectionConfig: SanitizedCollectionConfig = {
// ...
@@ -454,7 +515,7 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
type: 'text',
admin: {
components: {
- Description: MyCustomDescription, // highlight-line
+ Description: '/path/to/MyCustomDescriptionComponent', // highlight-line
}
}
}
@@ -468,7 +529,7 @@ Custom Description Components receive all [Field Component](#the-field-component
| Property | Description |
| -------------- | ---------------------------------------------------------------- |
-| **`description`** | The `description` property defined in the [Field Config](../fields/overview). |
+| **`field`** | The sanitized, client-friendly version of the field's config. [More details](#the-field-prop) |
Reminder:
@@ -524,8 +585,8 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
admin: {
components: {
// highlight-start
- beforeInput: [MyCustomComponent],
- afterInput: [MyOtherCustomComponent],
+ beforeInput: ['/path/to/MyCustomComponent'],
+ afterInput: ['/path/to/MyOtherCustomComponent'],
// highlight-end
}
}
diff --git a/docs/admin/globals.mdx b/docs/admin/globals.mdx
index 6ed7f5e4d..ae54495d9 100644
--- a/docs/admin/globals.mdx
+++ b/docs/admin/globals.mdx
@@ -43,7 +43,6 @@ To override Global Components, use the `admin.components` property in your [Glob
```ts
import type { SanitizedGlobalConfig } from 'payload'
-import { CustomSaveButton } from './CustomSaveButton'
export const MyGlobal: SanitizedGlobalConfig = {
// ...
diff --git a/docs/admin/hooks.mdx b/docs/admin/hooks.mdx
index d06ed7e5b..a75748821 100644
--- a/docs/admin/hooks.mdx
+++ b/docs/admin/hooks.mdx
@@ -52,7 +52,7 @@ The `useField` hook accepts the following arguments:
The `useField` hook returns the following object:
```ts
-type FieldResult = {
+type FieldType = {
errorMessage?: string
errorPaths?: string[]
filterOptions?: FilterOptionsResult
@@ -65,7 +65,7 @@ type FieldResult = {
readOnly?: boolean
rows?: Row[]
schemaPath: string
- setValue: (val: unknown, disableModifyingForm?: boolean) => voi
+ setValue: (val: unknown, disableModifyingForm?: boolean) => void
showError: boolean
valid?: boolean
value: T
@@ -463,7 +463,7 @@ export const CustomArrayManager = () => {
name: "customArrayManager",
admin: {
components: {
- Field: CustomArrayManager,
+ Field: '/path/to/CustomArrayManagerField',
},
},
},
@@ -560,7 +560,7 @@ export const CustomArrayManager = () => {
name: "customArrayManager",
admin: {
components: {
- Field: CustomArrayManager,
+ Field: '/path/to/CustomArrayManagerField',
},
},
},
@@ -670,7 +670,7 @@ export const CustomArrayManager = () => {
name: "customArrayManager",
admin: {
components: {
- Field: CustomArrayManager,
+ Field: '/path/to/CustomArrayManagerField',
},
},
},
@@ -818,7 +818,7 @@ import { useConfig } from '@payloadcms/ui'
const MyComponent: React.FC = () => {
// highlight-start
- const config = useConfig()
+ const { config } = useConfig()
// highlight-end
return {config.serverURL}
diff --git a/docs/admin/overview.mdx b/docs/admin/overview.mdx
index 53ddeb9f6..aed4c7c97 100644
--- a/docs/admin/overview.mdx
+++ b/docs/admin/overview.mdx
@@ -69,6 +69,128 @@ All auto-generated files will contain the following comments at the top of each
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
```
+## Defining Custom Components in the Payload Config
+
+In the Payload Config, you can define custom React Components to enhance the admin interface. However, these components should not be imported directly into the server-only Payload Config to avoid including client-side code. Instead, you specify the path to the component. Here’s how you can do it:
+
+
+src/components/Logout.tsx
+```tsx
+'use client'
+import React from 'react'
+
+export const MyComponent = () => {
+ return (
+
+ )
+}
+```
+
+payload.config.ts:
+```ts
+import { buildConfig } from 'payload'
+
+const config = buildConfig({
+ // ...
+ admin: { // highlight-line
+ components: {
+ logout: {
+ Button: '/src/components/Logout#MyComponent'
+ }
+ }
+ },
+})
+```
+
+In the path `/src/components/Logout#MyComponent`, `/src/components/Logout` is the file path, and `MyComponent` is the named export. If the component is the default export, the export name can be omitted. Path and export name are separated by a `#`.
+
+### Configuring the Base Directory
+
+Component paths, by default, are relative to your working directory - this is usually where your Next.js config lies. To simplify component paths, you have the option to configure the *base directory* using the `admin.baseDir.baseDir` property:
+
+```ts
+import { buildConfig } from 'payload'
+import { fileURLToPath } from 'node:url'
+import path from 'path'
+const filename = fileURLToPath(import.meta.url)
+const dirname = path.dirname(filename)
+
+const config = buildConfig({
+ // ...
+ admin: { // highlight-line
+ importMap: {
+ baseDir: path.resolve(dirname, 'src'),
+ },
+ components: {
+ logout: {
+ Button: '/components/Logout#MyComponent'
+ }
+ }
+ },
+})
+```
+
+In this example, we set the base directory to the `src` directory - thus we can omit the `/src/` part of our component path string.
+
+### Passing Props
+
+Each React Component in the Payload Config is typed as `PayloadComponent`. This usually is a string, but can also be an object containing the following properties:
+
+| Property | Description |
+|---------------|-------------------------------------------------------------------------------------------------------------------------------|
+| `clientProps` | Props to be passed to the React Component if it's a Client Component |
+| `exportName` | Instead of declaring named exports using `#` in the component path, you can also omit them from `path` and pass them in here. |
+| `path` | Path to the React Component. Named exports can be appended to the end of the path, separated by a `#` |
+| `serverProps` | Props to be passed to the React Component if it's a Server Component |
+
+To pass in props from the config, you can use the `clientProps` and/or `serverProps` properties. This alleviates the need to use an HOC (Higher-Order-Component) to declare a React Component with props passed in.
+
+Here is an example:
+
+src/components/Logout.tsx
+```tsx
+'use client'
+import React from 'react'
+
+export const MyComponent = ({ text }: { text: string }) => {
+ return (
+
+ )
+}
+```
+
+payload.config.ts:
+```ts
+import { buildConfig } from 'payload'
+
+const config = buildConfig({
+ // ...
+ admin: { // highlight-line
+ components: {
+ logout: {
+ Button: {
+ path: '/src/components/Logout',
+ clientProps: {
+ text: 'Some Text.'
+ },
+ exportName: 'MyComponent'
+ }
+ }
+ }
+ },
+})
+```
+
+### Import Maps
+
+It's essential to understand how `PayloadComponent` paths function behind the scenes. Directly importing React Components into your Payload Config using import statements can introduce client-only modules like CSS into your server-only config. This could error when attempting to load the Payload Config in server-only environments and unnecessarily increase the size of the Payload Config, which should remain streamlined and efficient for server use.
+
+Instead, we utilize component paths to reference React Components. This method enhances the Payload Config with actual React Component imports on the client side, without affecting server-side usage. A script is deployed to scan the Payload Config, collecting all component paths and creating an `importMap.js`. This file, located in app/(payload)/admin/importMap.js, must be statically imported by your Next.js root page and layout. The script imports all the React Components from the specified paths into a Map, associating them with their respective paths (the ones you defined).
+
+When constructing the `ClientConfig`, Payload uses the component paths as keys to fetch the corresponding React Component imports from the Import Map. It then substitutes the `PayloadComponent` with a `MappedComponent`. A `MappedComponent` includes the React Component and additional metadata, such as whether it's a server or a client component and which props it should receive. These components are then rendered through the `` component within the Payload Admin Panel.
+
+Import maps are regenerated whenever you modify any element related to component paths. This regeneration occurs at startup and whenever Hot Module Replacement (HMR) runs. If the import maps fail to regenerate during HMR, you can restart your application and execute the `payload generate:importmap` command to manually create a new import map.
+
## Admin Options
All options for the Admin Panel are defined in your [Payload Config](../configuration/overview) under the `admin` property:
@@ -167,12 +289,12 @@ const config = buildConfig({
The following options are available:
-| Option | Default route | Description |
-| ------------------ | ----------------------- | ------------------------------------- |
-| `admin` | `/admin` | The Admin Panel itself. |
-| `api` | `/api` | The [REST API](../rest-api/overview) base path. |
-| `graphQL` | `/graphql` | The [GraphQL API](../graphql/overview) base path. |
-| `graphQLPlayground`| `/graphql-playground` | The GraphQL Playground. |
+| Option | Default route | Description |
+|---------------------|-----------------------|---------------------------------------------------|
+| `admin` | `/admin` | The Admin Panel itself. |
+| `api` | `/api` | The [REST API](../rest-api/overview) base path. |
+| `graphQL` | `/graphql` | The [GraphQL API](../graphql/overview) base path. |
+| `graphQLPlayground` | `/graphql-playground` | The GraphQL Playground. |
Tip:
diff --git a/docs/admin/views.mdx b/docs/admin/views.mdx
index ac24a003f..3e78a17c9 100644
--- a/docs/admin/views.mdx
+++ b/docs/admin/views.mdx
@@ -31,7 +31,9 @@ const config = buildConfig({
admin: {
components: {
views: {
- Dashboard: MyCustomDashboardView, // highlight-line
+ dashboard: {
+ Component: '/path/to/MyCustomDashboardView#MyCustomDashboardViewComponent', // highlight-line
+ }
},
},
},
@@ -44,8 +46,8 @@ The following options are available:
| Property | Description |
| --------------- | ----------------------------------------------------------------------------- |
-| **`Account`** | The Account view is used to show the currently logged in user's Account page. |
-| **`Dashboard`** | The main landing page of the [Admin Panel](./overview). |
+| **`account`** | The Account view is used to show the currently logged in user's Account page. |
+| **`dashboard`** | The main landing page of the [Admin Panel](./overview). |
For more granular control, pass a configuration object instead. Payload exposes the following properties for each view:
@@ -72,9 +74,9 @@ const config = buildConfig({
components: {
views: {
// highlight-start
- MyCustomView: {
+ myCustomView: {
// highlight-end
- Component: MyCustomView,
+ Component: '/path/to/MyCustomView#MyCustomViewComponent',
path: '/my-custom-view',
},
},
@@ -108,7 +110,9 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
admin: {
components: {
views: {
- Edit: MyCustomEditView, // highlight-line
+ edit: {
+ Component: '/path/to/MyCustomEditView', // highlight-line
+ }
},
},
},
@@ -126,8 +130,8 @@ The following options are available:
| Property | Description |
| ---------- | ----------------------------------------------------------------------------------------------------------------- |
-| **`Edit`** | The Edit View is used to edit a single document for any given Collection. [More details](#document-views). |
-| **`List`** | The List View is used to show a list of documents for any given Collection. |
+| **`edit`** | The Edit View is used to edit a single document for any given Collection. [More details](#document-views). |
+| **`list`** | The List View is used to show a list of documents for any given Collection. |
Note:
@@ -148,7 +152,7 @@ export const MyGlobalConfig: SanitizedGlobalConfig = {
admin: {
components: {
views: {
- Edit: MyCustomEditView, // highlight-line
+ edit: '/path/to/MyCustomEditView', // highlight-line
},
},
},
@@ -166,7 +170,7 @@ The following options are available:
| Property | Description |
| ---------- | ------------------------------------------------------------------- |
-| **`Edit`** | The Edit View is used to edit a single document for any given Global. [More details](#document-views). |
+| **`edit`** | The Edit View is used to edit a single document for any given Global. [More details](#document-views). |
Note:
@@ -187,9 +191,9 @@ export const MyCollectionOrGlobalConfig: SanitizedCollectionConfig = {
admin: {
components: {
views: {
- Edit: {
- API: {
- Component: MyCustomAPIView, // highlight-line
+ edit: {
+ api: {
+ Component: '/path/to/MyCustomAPIViewComponent', // highlight-line
},
},
},
@@ -209,15 +213,15 @@ The following options are available:
| Property | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
-| **`Default`** | The Default view is the primary view in which your document is edited. |
-| **`Versions`** | The Versions view is used to view the version history of a single document. [More details](../versions). |
-| **`Version`** | The Version view is used to view a single version of a single document for a given collection. [More details](../versions). |
-| **`API`** | The API view is used to display the REST API JSON response for a given document. |
-| **`LivePreview`** | The LivePreview view is used to display the Live Preview interface. [More details](../live-preview). |
+| **`default`** | The Default view is the primary view in which your document is edited. |
+| **`versions`** | The Versions view is used to view the version history of a single document. [More details](../versions). |
+| **`version`** | The Version view is used to view a single version of a single document for a given collection. [More details](../versions). |
+| **`api`** | The API view is used to display the REST API JSON response for a given document. |
+| **`livePreview`** | The LivePreview view is used to display the Live Preview interface. [More details](../live-preview). |
### Document Tabs
-Each Document View can be given a new tab in the Edit View, if desired. Tabs are highly configurable, from as simple as changing the label to swapping out the entire component, they can be modified in any way. To add or customize tabs in the Edit View, use the `Component.Tab` key:
+Each Document View can be given a new tab in the Edit View, if desired. Tabs are highly configurable, from as simple as changing the label to swapping out the entire component, they can be modified in any way. To add or customize tabs in the Edit View, use the `tab` key:
```ts
import type { SanitizedCollectionConfig } from 'payload'
@@ -227,17 +231,19 @@ export const MyCollection: SanitizedCollectionConfig = {
admin: {
components: {
views: {
- Edit: {
- MyCustomTab: {
- Component: MyCustomTab,
+ edit: {
+ myCustomTab: {
+ Component: '/path/to/MyCustomTab',
path: '/my-custom-tab',
- Tab: MyCustomTab // highlight-line
+ tab: {
+ Component: '/path/to/MyCustomTabComponent' // highlight-line
+ }
},
- AnotherCustomView: {
- Component: AnotherCustomView,
+ anotherCustomTab: {
+ Component: '/path/to/AnotherCustomView',
path: '/another-custom-view',
// highlight-start
- Tab: {
+ tab: {
label: 'Another Custom View',
href: '/another-custom-view',
}
@@ -261,14 +267,15 @@ Custom Views are just [Custom Components](./components) rendered at the page-lev
```ts
import type { SanitizedCollectionConfig } from 'payload'
-import { MyCustomView } from './MyCustomView'
export const MyCollectionConfig: SanitizedCollectionConfig = {
// ...
admin: {
components: {
views: {
- Edit: MyCustomView, // highlight-line
+ edit: {
+ Component: '/path/to/MyCustomView' // highlight-line
+ }
},
},
},
diff --git a/docs/configuration/collections.mdx b/docs/configuration/collections.mdx
index 4464d0c85..fc8667210 100644
--- a/docs/configuration/collections.mdx
+++ b/docs/configuration/collections.mdx
@@ -102,5 +102,5 @@ You can import types from Payload to help make writing your Collection configs e
The `CollectionConfig` type represents a raw Collection Config in its full form, where only the bare minimum properties are marked as required. The `SanitizedCollectionConfig` type represents a Collection Config after it has been fully sanitized. Generally, this is only used internally by Payload.
```ts
-import { CollectionConfig, SanitizedCollectionConfig } from 'payload'
+import type { CollectionConfig, SanitizedCollectionConfig } from 'payload'
```
diff --git a/docs/configuration/globals.mdx b/docs/configuration/globals.mdx
index 17c0bf844..8b95b4772 100644
--- a/docs/configuration/globals.mdx
+++ b/docs/configuration/globals.mdx
@@ -106,5 +106,5 @@ You can import types from Payload to help make writing your Global configs easie
The `GlobalConfig` type represents a raw Global Config in its full form, where only the bare minimum properties are marked as required. The `SanitizedGlobalConfig` type represents a Global Config after it has been fully sanitized. Generally, this is only used internally by Payload.
```ts
-import { GlobalConfig, SanitizedGlobalConfig } from 'payload'
+import type { GlobalConfig, SanitizedGlobalConfig } from 'payload'
```
diff --git a/docs/configuration/overview.mdx b/docs/configuration/overview.mdx
index 487e6bb5a..b60ab4f8e 100644
--- a/docs/configuration/overview.mdx
+++ b/docs/configuration/overview.mdx
@@ -246,5 +246,11 @@ You can import types from Payload to help make writing your config easier and ty
The `Config` type represents a raw Payload Config in its full form. Only the bare minimum properties are marked as required. The `SanitizedConfig` type represents a Payload Config after it has been fully sanitized. Generally, this is only used internally by Payload.
```ts
-import { Config, SanitizedConfig } from 'payload'
+import type { Config, SanitizedConfig } from 'payload'
```
+
+## Server vs. Client
+
+The Payload Config only lives on the server and is not allowed to contain any client-side code. That way, you can load up the Payload Config in any server environment or standalone script, without having to use Bundlers or Node.js loaders to handle importing client-only modules (e.g. scss files or React Components) without any errors.
+
+Behind the curtains, the Next.js-based Admin Panel generates a ClientConfig, which strips away any server-only code and enriches the config with React Components.
diff --git a/docs/local-api/outside-nextjs.mdx b/docs/local-api/outside-nextjs.mdx
index bb675cb5f..11073a5d9 100644
--- a/docs/local-api/outside-nextjs.mdx
+++ b/docs/local-api/outside-nextjs.mdx
@@ -2,11 +2,11 @@
title: Using Payload outside Next.js
label: Outside Next.js
order: 20
-desc: Payload can be used outside of Next.js within standalone scripts or in other frameworks like Remix, Sveltekit, Nuxt, and similar.
+desc: Payload can be used outside of Next.js within standalone scripts or in other frameworks like Remix, SvelteKit, Nuxt, and similar.
keywords: local api, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
---
-Payload can be used completely outside of Next.js which is helpful in cases like running scripts, using Payload in a separate backend service, or using Payload's Local API to fetch your data directly from your database in other frontend frameworks like Sveltekit, Remix, Nuxt, and similar.
+Payload can be used completely outside of Next.js which is helpful in cases like running scripts, using Payload in a separate backend service, or using Payload's Local API to fetch your data directly from your database in other frontend frameworks like SvelteKit, Remix, Nuxt, and similar.
Note:
@@ -16,38 +16,16 @@ Payload can be used completely outside of Next.js which is helpful in cases like
## Importing the Payload Config outside of Next.js
-Your Payload Config likely has imports which need to be handled properly, such as CSS imports and similar. If you were to try and import your config without any Node support for SCSS / CSS files, you'll see errors that arise accordingly.
+Payload provides a convenient way to run standalone scripts, which can be useful for tasks like seeding your database or performing one-off operations.
-This is especially relevant if you are importing your Payload Config outside of a bundler context, such as in standalone Node scripts.
-
-For these cases, you can use Payload's `importConfig` function to handle importing your config safely. It will handle everything you need to be able to load and use your Payload Config, without any client-side files present.
-
-Here's an example of a seed script that creates a few documents for local development / testing purposes, using Payload's `importConfig` function to safely import Payload, and the `getPayload` function to retrieve an initialized copy of Payload.
+In standalone scripts, can simply import the Payload Config and use it right away. If you need an initialized copy of Payload, you can then use the `getPayload` function. This can be useful for tasks like seeding your database or performing other one-off operations.
```ts
// We are importing `getPayload` because we don't need HMR
// for a standalone script. For usage of Payload inside Next.js,
// you should always use `import { getPayloadHMR } from '@payloadcms/next/utilities'` instead.
import { getPayload } from 'payload'
-
-// This is a helper function that will make sure we can safely load the Payload Config
-// and all of its client-side files, such as CSS, SCSS, etc.
-import { importConfig } from 'payload/node'
-
-import path from 'path'
-import { fileURLToPath } from 'node:url'
-import dotenv from 'dotenv'
-
-// In ESM, you can create the "dirname" variable
-// like this. We'll use this with `dotenv` to load our `.env` file, if necessary.
-const filename = fileURLToPath(import.meta.url)
-const dirname = path.dirname(filename)
-
-// If you don't need to load your .env file,
-// then you can skip this part!
-dotenv.config({
- path: path.resolve(dirname, '../.env'),
-})
+import config from '@payload-config'
const seed = async () => {
// Get a local copy of Payload by passing your config
@@ -71,6 +49,26 @@ const seed = async () => {
}
// Call the function here to run your seed script
-seed()
-
+await seed()
```
+
+You can then execute the script using `payload run`. Example: if you placed this standalone script in `src/seed.ts`, you would execute it like this:
+
+```sh
+payload run src/seed.ts
+```
+
+The `payload run` command does two things for you:
+
+1. It loads the environment variables the same way Next.js loads them, eliminating the need for additional dependencies like `dotenv`. The usage of `dotenv` is not recommended, as Next.js loads environment variables differently. By using `payload run`, you ensure consistent environment variable handling across your Payload and Next.js setup.
+2. It initializes swc, allowing direct execution of TypeScript files without requiring tools like tsx or ts-node.
+
+### Troubleshooting
+
+If you encounter import-related errors, try running the script in TSX mode:
+
+```sh
+payload run src/seed.ts --use-tsx
+```
+
+Note: Install tsx in your project first. Be aware that TSX mode is slower than the default swc mode, so only use it if necessary.
diff --git a/docs/migration-guide/overview.mdx b/docs/migration-guide/overview.mdx
index 00bd1348a..9b432a909 100644
--- a/docs/migration-guide/overview.mdx
+++ b/docs/migration-guide/overview.mdx
@@ -159,7 +159,6 @@ import {
useAllFormFields,
useAuth,
useClientFunctions,
- useComponentMap,
useConfig,
useDebounce,
useDebouncedCallback,
@@ -212,7 +211,6 @@ import {
ActionsProvider,
AuthProvider,
ClientFunctionProvider,
- ComponentMapProvider,
ConfigProvider,
DocumentEventsProvider,
DocumentInfoProvider,
@@ -299,14 +297,10 @@ import {
fieldBaseClass,
// TS Types
- ActionMap,
- CollectionComponentMap,
ColumnPreferences,
- ConfigComponentMapBase,
DocumentInfoContext,
DocumentInfoProps,
FieldType,
- FieldComponentProps,
FormProps,
RowLabelProps,
SelectFieldProps,
@@ -323,7 +317,6 @@ import {
AppHeader,
BlocksDrawer,
Column,
- ComponentMap,
DefaultBlockImage,
DeleteMany,
DocumentControls,
@@ -338,7 +331,6 @@ import {
FormLoadingOverlayToggle,
FormSubmit,
GenerateConfirmation,
- GlobalComponentMap,
HydrateClientUser,
ListControls,
ListSelection,
@@ -349,7 +341,8 @@ import {
PublishMany,
ReactSelect,
ReactSelectOption,
- ReducedBlock,
+ ClientField,
+ ClientBlock,
RenderFields,
SectionTitle,
Select,
diff --git a/eslint.config.js b/eslint.config.js
index 6692c62a6..a5fac2e30 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -28,6 +28,7 @@ export const rootParserOptions = {
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
EXPERIMENTAL_useProjectService: {
allowDefaultProjectForFiles: ['./src/*.ts', './src/*.tsx'],
+ maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 100,
},
sourceType: 'module',
ecmaVersion: 'latest',
diff --git a/examples/testing/README.md b/examples/testing/README.md
index 7f617cc21..69ffa7955 100644
--- a/examples/testing/README.md
+++ b/examples/testing/README.md
@@ -134,7 +134,7 @@ describe('Users', () => {
```json
"scripts": {
- "test": "NODE_OPTIONS=--experimental-vm-modules jest --forceExit --detectOpenHandles"
+ "test": "jest --forceExit --detectOpenHandles"
}
```
diff --git a/examples/testing/package.json b/examples/testing/package.json
index a3f197780..df60b4e9e 100644
--- a/examples/testing/package.json
+++ b/examples/testing/package.json
@@ -29,6 +29,6 @@
"build:server": "tsc",
"build": "yarn build:server && yarn build:payload",
"serve": "PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
- "test": "NODE_OPTIONS=--experimental-vm-modules jest --forceExit --detectOpenHandles"
+ "test": "jest --forceExit --detectOpenHandles"
}
}
diff --git a/jest.config.js b/jest.config.js
index e5f684682..1ece71836 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,8 +1,28 @@
+const esModules = [
+ // file-type and all dependencies: https://github.com/sindresorhus/file-type
+ 'file-type',
+ 'strtok3',
+ 'readable-web-to-node-stream',
+ 'token-types',
+ 'peek-readable',
+ 'find-up',
+ 'locate-path',
+ 'p-locate',
+ 'p-limit',
+ 'yocto-queue',
+ 'unicorn-magic',
+ 'path-exists',
+ 'qs-esm',
+].join('|')
+
/** @type {import('jest').Config} */
const baseJestConfig = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
- setupFiles: ['/test/jest.setup.env.js'],
setupFilesAfterEnv: ['/test/jest.setup.js'],
+ transformIgnorePatterns: [
+ `/node_modules/(?!.pnpm)(?!(${esModules})/)`,
+ `/node_modules/.pnpm/(?!(${esModules.replace(/\//g, '\\+')})@)`,
+ ],
moduleNameMapper: {
'\\.(css|scss)$': '/test/helpers/mocks/emptyModule.js',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
diff --git a/next.config.mjs b/next.config.mjs
index de0767232..25fc14150 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -1,6 +1,11 @@
import bundleAnalyzer from '@next/bundle-analyzer'
import withPayload from './packages/next/src/withPayload.js'
+import path from 'path'
+import { fileURLToPath } from 'url'
+
+const __filename = fileURLToPath(import.meta.url)
+const dirname = path.dirname(__filename)
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
@@ -15,6 +20,10 @@ export default withBundleAnalyzer(
typescript: {
ignoreBuildErrors: true,
},
+ env: {
+ PAYLOAD_CORE_DEV: 'true',
+ ROOT_DIR: path.resolve(dirname),
+ },
async redirects() {
return [
{
diff --git a/package.json b/package.json
index c64809ad1..60176d8b3 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"build:email-nodemailer": "turbo build --filter email-nodemailer",
"build:email-resend": "turbo build --filter email-resend",
"build:eslint-config": "turbo build --filter eslint-config",
+ "build:essentials:force": "pnpm clean:build && turbo build --filter=\"payload...\" --filter=\"@payloadcms/ui\" --filter=\"@payloadcms/next\" --filter=\"@payloadcms/db-mongodb\" --filter=\"@payloadcms/db-postgres\" --filter=\"@payloadcms/richtext-lexical\" --filter=\"@payloadcms/translations\" --filter=\"@payloadcms/plugin-cloud\" --filter=\"@payloadcms/graphql\" --no-cache --force",
"build:force": "pnpm run build:core:force",
"build:graphql": "turbo build --filter graphql",
"build:live-preview": "turbo build --filter live-preview",
@@ -52,10 +53,12 @@
"clean:all": "node ./scripts/delete-recursively.js '@node_modules' 'media/*' '**/dist/' '**/.cache/*' '**/.next/*' '**/.turbo/*' '**/tsconfig.tsbuildinfo' '**/payload*.tgz' '**/meta_*.json'",
"clean:build": "node ./scripts/delete-recursively.js 'media/' '**/dist/' '**/.cache/' '**/.next/' '**/.turbo/' '**/tsconfig.tsbuildinfo' '**/payload*.tgz' '**/meta_*.json'",
"clean:cache": "node ./scripts/delete-recursively.js node_modules/.cache! packages/payload/node_modules/.cache! .next/*",
- "dev": "cross-env NODE_OPTIONS=--no-deprecation node ./test/dev.js",
- "dev:generate-graphql-schema": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/generateGraphQLSchema.ts",
- "dev:generate-types": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/generateTypes.ts",
- "dev:postgres": "cross-env NODE_OPTIONS=--no-deprecation PAYLOAD_DATABASE=postgres node ./test/dev.js",
+ "dev": "pnpm runts ./test/dev.ts",
+ "runts": "node --no-deprecation --import @swc-node/register/esm-register",
+ "dev:generate-graphql-schema": "pnpm runts ./test/generateGraphQLSchema.ts",
+ "dev:generate-importmap": "pnpm runts ./test/generateImportMap.ts",
+ "dev:generate-types": "pnpm runts ./test/generateTypes.ts",
+ "dev:postgres": "pnpm runts ./test/dev.ts",
"devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev",
"docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start",
"docker:start": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml up -d",
@@ -67,20 +70,20 @@
"obliterate-playwright-cache-macos": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
"prepare": "husky",
"reinstall": "pnpm clean:all && pnpm install",
- "release:alpha": "tsx ./scripts/release.ts --bump prerelease --tag alpha",
- "release:beta": "tsx ./scripts/release.ts --bump prerelease --tag beta",
- "script:gen-templates": "tsx ./scripts/generate-template-variations.ts",
- "script:list-published": "tsx scripts/lib/getPackageRegistryVersions.ts",
- "script:pack": "tsx scripts/pack-all-to-dest.ts",
+ "release:alpha": "pnpm runts ./scripts/release.ts --bump prerelease --tag alpha",
+ "release:beta": "pnpm runts ./scripts/release.ts --bump prerelease --tag beta",
+ "script:gen-templates": "pnpm runts ./scripts/generate-template-variations.ts",
+ "script:list-published": "pnpm runts scripts/lib/getPackageRegistryVersions.ts",
+ "script:pack": "pnpm runts scripts/pack-all-to-dest.ts",
"pretest": "pnpm build",
"test": "pnpm test:int && pnpm test:components && pnpm test:e2e",
- "test:components": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" jest --config=jest.components.config.js",
- "test:e2e": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 tsx ./test/runE2E.ts",
+ "test:components": "cross-env NODE_OPTIONS=\" --no-deprecation\" jest --config=jest.components.config.js",
+ "test:e2e": "pnpm runts ./test/runE2E.ts",
"test:e2e:debug": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 PWDEBUG=1 DISABLE_LOGGING=true playwright test",
"test:e2e:headed": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 DISABLE_LOGGING=true playwright test --headed",
- "test:int": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
- "test:int:postgres": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
- "test:unit": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=jest.config.js --runInBand",
+ "test:int": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
+ "test:int:postgres": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
+ "test:unit": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=jest.config.js --runInBand",
"translateNewKeys": "pnpm --filter payload run translateNewKeys"
},
"lint-staged": {
@@ -100,8 +103,9 @@
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/eslint-plugin": "workspace:*",
"@payloadcms/live-preview-react": "workspace:*",
- "@playwright/test": "1.43.0",
- "@swc/cli": "0.3.12",
+ "@playwright/test": "1.46.0",
+ "@swc-node/register": "1.10.9",
+ "@swc/cli": "0.4.0",
"@swc/jest": "0.2.36",
"@types/fs-extra": "^11.0.2",
"@types/jest": "29.5.12",
@@ -134,8 +138,8 @@
"next": "15.0.0-canary.104",
"open": "^10.1.0",
"p-limit": "^5.0.0",
- "playwright": "1.43.0",
- "playwright-core": "1.43.0",
+ "playwright": "1.46.0",
+ "playwright-core": "1.46.0",
"prettier": "3.3.2",
"prompts": "2.4.2",
"react": "^19.0.0-rc-06d0b89e-20240801",
@@ -146,9 +150,8 @@
"shelljs": "0.8.5",
"slash": "3.0.0",
"sort-package-json": "^2.10.0",
- "swc-plugin-transform-remove-imports": "1.14.0",
+ "swc-plugin-transform-remove-imports": "1.15.0",
"tempy": "1.0.1",
- "tsx": "4.16.2",
"turbo": "^1.13.3",
"typescript": "5.5.4"
},
@@ -177,9 +180,6 @@
"react": "$react",
"react-dom": "$react-dom",
"typescript": "$typescript"
- },
- "patchedDependencies": {
- "playwright@1.43.0": "patches/playwright@1.43.0.patch"
}
},
"overrides": {
diff --git a/packages/create-payload-app/jest.config.js b/packages/create-payload-app/jest.config.js
index a73df5e61..b6bd5c7bd 100644
--- a/packages/create-payload-app/jest.config.js
+++ b/packages/create-payload-app/jest.config.js
@@ -22,7 +22,7 @@ const customJestConfig = {
},
testEnvironment: 'node',
testMatch: ['/**/*spec.ts'],
- testTimeout: 90000,
+ testTimeout: 160000,
transform: {
'^.+\\.(t|j)sx?$': ['@swc/jest'],
},
diff --git a/packages/create-payload-app/package.json b/packages/create-payload-app/package.json
index 579e5665f..80b647088 100644
--- a/packages/create-payload-app/package.json
+++ b/packages/create-payload-app/package.json
@@ -42,7 +42,7 @@
"build": "pnpm pack-template-files && pnpm typecheck && pnpm build:swc",
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
"clean": "rimraf {dist,*.tsbuildinfo}",
- "pack-template-files": "tsx src/scripts/pack-template-files.ts",
+ "pack-template-files": "node --no-deprecation --import @swc-node/register/esm-register src/scripts/pack-template-files.ts",
"prepublishOnly": "pnpm clean && pnpm build",
"test": "jest",
"typecheck": "tsc"
@@ -50,7 +50,7 @@
"dependencies": {
"@clack/prompts": "^0.7.0",
"@sindresorhus/slugify": "^1.1.0",
- "@swc/core": "^1.6.13",
+ "@swc/core": "1.7.10",
"arg": "^5.0.0",
"chalk": "^4.1.0",
"comment-json": "^4.2.3",
diff --git a/packages/create-payload-app/src/lib/wrap-next-config.ts b/packages/create-payload-app/src/lib/wrap-next-config.ts
index b2e1e208a..cef704077 100644
--- a/packages/create-payload-app/src/lib/wrap-next-config.ts
+++ b/packages/create-payload-app/src/lib/wrap-next-config.ts
@@ -1,6 +1,6 @@
import type { ExportDefaultExpression, ModuleItem } from '@swc/core'
-import swc from '@swc/core'
+import { parse } from '@swc/core'
import chalk from 'chalk'
import { Syntax, parseModule } from 'esprima-next'
import fs from 'fs'
@@ -281,10 +281,10 @@ async function compileTypeScriptFileToAST(
* https://github.com/swc-project/swc/issues/1366
*/
if (process.env.NODE_ENV === 'test') {
- parseOffset = (await swc.parse('')).span.end
+ parseOffset = (await parse('')).span.end
}
- const module = await swc.parse(fileContent, {
+ const module = await parse(fileContent, {
syntax: 'typescript',
})
diff --git a/packages/db-mongodb/src/types.ts b/packages/db-mongodb/src/types.ts
index 25ff7a821..ba1d34b1c 100644
--- a/packages/db-mongodb/src/types.ts
+++ b/packages/db-mongodb/src/types.ts
@@ -67,31 +67,6 @@ export type FieldGenerator = {
schema: TSchema
}
-/**
- * Field config types that need representation in the database
- */
-type FieldType =
- | 'array'
- | 'blocks'
- | 'checkbox'
- | 'code'
- | 'collapsible'
- | 'date'
- | 'email'
- | 'group'
- | 'json'
- | 'number'
- | 'point'
- | 'radio'
- | 'relationship'
- | 'richText'
- | 'row'
- | 'select'
- | 'tabs'
- | 'text'
- | 'textarea'
- | 'upload'
-
export type FieldGeneratorFunction = (
args: FieldGenerator,
) => void
diff --git a/packages/db-postgres/package.json b/packages/db-postgres/package.json
index 259f77a03..8c17ed32a 100644
--- a/packages/db-postgres/package.json
+++ b/packages/db-postgres/package.json
@@ -42,7 +42,7 @@
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepack": "pnpm clean && pnpm turbo build",
"prepublishOnly": "pnpm clean && pnpm turbo build",
- "renamePredefinedMigrations": "tsx ./scripts/renamePredefinedMigrations.ts"
+ "renamePredefinedMigrations": "node --no-deprecation --import @swc-node/register/esm-register ./scripts/renamePredefinedMigrations.ts"
},
"dependencies": {
"@payloadcms/drizzle": "workspace:*",
diff --git a/packages/graphql/bin.js b/packages/graphql/bin.js
index a6915f923..e9bec7ed3 100755
--- a/packages/graphql/bin.js
+++ b/packages/graphql/bin.js
@@ -1,5 +1,21 @@
#!/usr/bin/env node
-import { bin } from './dist/bin/index.js'
+import { register } from 'node:module'
+import path from 'node:path'
+import { fileURLToPath, pathToFileURL } from 'node:url'
-bin()
+// Allow disabling SWC for debugging
+if (process.env.DISABLE_SWC !== 'true') {
+ const filename = fileURLToPath(import.meta.url)
+ const dirname = path.dirname(filename)
+ const url = pathToFileURL(dirname).toString() + '/'
+
+ register('@swc-node/register/esm', url)
+}
+
+const start = async () => {
+ const { bin } = await import('./dist/bin/index.js')
+ await bin()
+}
+
+void start()
diff --git a/packages/graphql/package.json b/packages/graphql/package.json
index 0d9221557..0e0a72f8c 100644
--- a/packages/graphql/package.json
+++ b/packages/graphql/package.json
@@ -43,6 +43,7 @@
"dependencies": {
"graphql-scalars": "1.22.2",
"pluralize": "8.0.0",
+ "@swc-node/register": "1.10.9",
"ts-essentials": "7.0.3"
},
"devDependencies": {
diff --git a/packages/graphql/src/bin/index.ts b/packages/graphql/src/bin/index.ts
index a88c35c60..a5afe83a7 100755
--- a/packages/graphql/src/bin/index.ts
+++ b/packages/graphql/src/bin/index.ts
@@ -1,13 +1,13 @@
/* eslint-disable no-console */
import minimist from 'minimist'
-import { findConfig, importConfig, loadEnv } from 'payload/node'
+import { findConfig, loadEnv } from 'payload/node'
import { generateSchema } from './generateSchema.js'
export const bin = async () => {
loadEnv()
const configPath = findConfig()
- const config = await importConfig(configPath)
+ const config = await (await import(configPath)).default
const args = minimist(process.argv.slice(2))
const script = (typeof args._[0] === 'string' ? args._[0] : '').toLowerCase()
diff --git a/packages/next/package.json b/packages/next/package.json
index 5b7af8477..1bedf51ee 100644
--- a/packages/next/package.json
+++ b/packages/next/package.json
@@ -90,7 +90,7 @@
"esbuild": "0.23.0",
"esbuild-sass-plugin": "3.3.1",
"payload": "workspace:*",
- "swc-plugin-transform-remove-imports": "1.14.0"
+ "swc-plugin-transform-remove-imports": "1.15.0"
},
"peerDependencies": {
"graphql": "^16.8.1",
diff --git a/packages/next/src/elements/DocumentHeader/Tabs/Tab/index.tsx b/packages/next/src/elements/DocumentHeader/Tabs/Tab/index.tsx
index 121aabc66..fa4531496 100644
--- a/packages/next/src/elements/DocumentHeader/Tabs/Tab/index.tsx
+++ b/packages/next/src/elements/DocumentHeader/Tabs/Tab/index.tsx
@@ -1,5 +1,6 @@
import type { DocumentTabConfig, DocumentTabProps } from 'payload'
+import { RenderComponent, getCreateMappedComponent } from '@payloadcms/ui/shared'
import React, { Fragment } from 'react'
import { DocumentTabLink } from './TabLink.js'
@@ -7,22 +8,25 @@ import './index.scss'
export const baseClass = 'doc-tab'
-export const DocumentTab: React.FC = (props) => {
+export const DocumentTab: React.FC<
+ { readonly Pill_Component?: React.FC } & DocumentTabConfig & DocumentTabProps
+> = (props) => {
const {
Pill,
+ Pill_Component,
apiURL,
collectionConfig,
condition,
- config,
globalConfig,
href: tabHref,
i18n,
isActive: tabIsActive,
label,
newTab,
+ payload,
permissions,
} = props
-
+ const { config } = payload
const { routes } = config
let href = typeof tabHref === 'string' ? tabHref : ''
@@ -55,6 +59,17 @@ export const DocumentTab: React.FC = (prop
})
: label
+ const createMappedComponent = getCreateMappedComponent({
+ importMap: payload.importMap,
+ serverProps: {
+ i18n,
+ payload,
+ permissions,
+ },
+ })
+
+ const mappedPin = createMappedComponent(Pill, undefined, Pill_Component, 'Pill')
+
return (
= (prop
>
{labelToRender}
- {Pill && (
+ {mappedPin && (
-
+
)}
diff --git a/packages/next/src/elements/DocumentHeader/Tabs/getCustomViews.ts b/packages/next/src/elements/DocumentHeader/Tabs/getCustomViews.ts
index e79116964..3fd6a18c2 100644
--- a/packages/next/src/elements/DocumentHeader/Tabs/getCustomViews.ts
+++ b/packages/next/src/elements/DocumentHeader/Tabs/getCustomViews.ts
@@ -12,9 +12,9 @@ export const getCustomViews = (args: {
if (collectionConfig) {
const collectionViewsConfig =
- typeof collectionConfig?.admin?.components?.views?.Edit === 'object' &&
- typeof collectionConfig?.admin?.components?.views?.Edit !== 'function'
- ? collectionConfig?.admin?.components?.views?.Edit
+ typeof collectionConfig?.admin?.components?.views?.edit === 'object' &&
+ typeof collectionConfig?.admin?.components?.views?.edit !== 'function'
+ ? collectionConfig?.admin?.components?.views?.edit
: undefined
customViews = Object.entries(collectionViewsConfig || {}).reduce((prev, [key, view]) => {
@@ -28,9 +28,9 @@ export const getCustomViews = (args: {
if (globalConfig) {
const globalViewsConfig =
- typeof globalConfig?.admin?.components?.views?.Edit === 'object' &&
- typeof globalConfig?.admin?.components?.views?.Edit !== 'function'
- ? globalConfig?.admin?.components?.views?.Edit
+ typeof globalConfig?.admin?.components?.views?.edit === 'object' &&
+ typeof globalConfig?.admin?.components?.views?.edit !== 'function'
+ ? globalConfig?.admin?.components?.views?.edit
: undefined
customViews = Object.entries(globalViewsConfig || {}).reduce((prev, [key, view]) => {
diff --git a/packages/next/src/elements/DocumentHeader/Tabs/getViewConfig.ts b/packages/next/src/elements/DocumentHeader/Tabs/getViewConfig.ts
index 1b12d6342..2b0f6ef1a 100644
--- a/packages/next/src/elements/DocumentHeader/Tabs/getViewConfig.ts
+++ b/packages/next/src/elements/DocumentHeader/Tabs/getViewConfig.ts
@@ -9,9 +9,9 @@ export const getViewConfig = (args: {
if (collectionConfig) {
const collectionConfigViewsConfig =
- typeof collectionConfig?.admin?.components?.views?.Edit === 'object' &&
- typeof collectionConfig?.admin?.components?.views?.Edit !== 'function'
- ? collectionConfig?.admin?.components?.views?.Edit
+ typeof collectionConfig?.admin?.components?.views?.edit === 'object' &&
+ typeof collectionConfig?.admin?.components?.views?.edit !== 'function'
+ ? collectionConfig?.admin?.components?.views?.edit
: undefined
return collectionConfigViewsConfig?.[name]
@@ -19,9 +19,9 @@ export const getViewConfig = (args: {
if (globalConfig) {
const globalConfigViewsConfig =
- typeof globalConfig?.admin?.components?.views?.Edit === 'object' &&
- typeof globalConfig?.admin?.components?.views?.Edit !== 'function'
- ? globalConfig?.admin?.components?.views?.Edit
+ typeof globalConfig?.admin?.components?.views?.edit === 'object' &&
+ typeof globalConfig?.admin?.components?.views?.edit !== 'function'
+ ? globalConfig?.admin?.components?.views?.edit
: undefined
return globalConfigViewsConfig?.[name]
diff --git a/packages/next/src/elements/DocumentHeader/Tabs/index.tsx b/packages/next/src/elements/DocumentHeader/Tabs/index.tsx
index 2d40465f9..966d15cec 100644
--- a/packages/next/src/elements/DocumentHeader/Tabs/index.tsx
+++ b/packages/next/src/elements/DocumentHeader/Tabs/index.tsx
@@ -1,11 +1,12 @@
import type { I18n } from '@payloadcms/translations'
import type {
+ Payload,
Permissions,
SanitizedCollectionConfig,
- SanitizedConfig,
SanitizedGlobalConfig,
} from 'payload'
+import { RenderComponent, getCreateMappedComponent } from '@payloadcms/ui/shared'
import { isPlainObject } from 'payload'
import React from 'react'
@@ -20,12 +21,13 @@ const baseClass = 'doc-tabs'
export const DocumentTabs: React.FC<{
collectionConfig: SanitizedCollectionConfig
- config: SanitizedConfig
globalConfig: SanitizedGlobalConfig
i18n: I18n
+ payload: Payload
permissions: Permissions
}> = (props) => {
- const { collectionConfig, config, globalConfig, permissions } = props
+ const { collectionConfig, globalConfig, i18n, payload, permissions } = props
+ const { config } = payload
const customViews = getCustomViews({ collectionConfig, globalConfig })
@@ -46,10 +48,9 @@ export const DocumentTabs: React.FC<{
})
?.map(([name, tab], index) => {
const viewConfig = getViewConfig({ name, collectionConfig, globalConfig })
- const tabFromConfig = viewConfig && 'Tab' in viewConfig ? viewConfig.Tab : undefined
- const tabConfig = typeof tabFromConfig === 'object' ? tabFromConfig : undefined
+ const tabFromConfig = viewConfig && 'tab' in viewConfig ? viewConfig.tab : undefined
- const { condition } = tabConfig || {}
+ const { condition } = tabFromConfig || {}
const meetsCondition =
!condition ||
@@ -72,17 +73,39 @@ export const DocumentTabs: React.FC<{
return null
})}
{customViews?.map((CustomView, index) => {
- if ('Tab' in CustomView) {
- const { Tab, path } = CustomView
+ if ('tab' in CustomView) {
+ const { path, tab } = CustomView
- if (typeof Tab === 'object' && !isPlainObject(Tab)) {
- throw new Error(
- `Custom 'Tab' Component for path: "${path}" must be a React Server Component. To use client-side functionality, render your Client Component within a Server Component and pass it only props that are serializable. More info: https://react.dev/reference/react/use-server#serializable-parameters-and-return-values`,
+ if (tab.Component) {
+ const createMappedComponent = getCreateMappedComponent({
+ importMap: payload.importMap,
+ serverProps: {
+ i18n,
+ payload,
+ permissions,
+ ...props,
+ key: `tab-custom-${index}`,
+ path,
+ },
+ })
+
+ const mappedTab = createMappedComponent(
+ tab.Component,
+ undefined,
+ undefined,
+ 'tab.Component',
)
- }
- if (typeof Tab === 'function') {
- return
+ return (
+
+ )
}
return (
@@ -90,7 +113,7 @@ export const DocumentTabs: React.FC<{
key={`tab-custom-${index}`}
{...{
...props,
- ...Tab,
+ ...tab,
}}
/>
)
diff --git a/packages/next/src/elements/DocumentHeader/Tabs/tabs/index.tsx b/packages/next/src/elements/DocumentHeader/Tabs/tabs/index.tsx
index e2c414f71..a21a8fe17 100644
--- a/packages/next/src/elements/DocumentHeader/Tabs/tabs/index.tsx
+++ b/packages/next/src/elements/DocumentHeader/Tabs/tabs/index.tsx
@@ -1,15 +1,16 @@
import type { DocumentTabConfig } from 'payload'
+import type React from 'react'
import { VersionsPill } from './VersionsPill/index.js'
export const documentViewKeys = [
- 'API',
- 'Default',
- 'LivePreview',
- 'References',
- 'Relationships',
- 'Version',
- 'Versions',
+ 'api',
+ 'default',
+ 'livePreview',
+ 'references',
+ 'relationships',
+ 'version',
+ 'versions',
]
export type DocumentViewKey = (typeof documentViewKeys)[number]
@@ -17,10 +18,11 @@ export type DocumentViewKey = (typeof documentViewKeys)[number]
export const tabs: Record<
DocumentViewKey,
{
+ Pill_Component?: React.FC
order?: number // TODO: expose this to the globalConfig config
} & DocumentTabConfig
> = {
- API: {
+ api: {
condition: ({ collectionConfig, globalConfig }) =>
(collectionConfig && !collectionConfig?.admin?.hideAPIURL) ||
(globalConfig && !globalConfig?.admin?.hideAPIURL),
@@ -28,14 +30,14 @@ export const tabs: Record<
label: 'API',
order: 1000,
},
- Default: {
+ default: {
href: '',
// isActive: ({ href, location }) =>
// location.pathname === href || location.pathname === `${href}/create`,
label: ({ t }) => t('general:edit'),
order: 0,
},
- LivePreview: {
+ livePreview: {
condition: ({ collectionConfig, config, globalConfig }) => {
if (collectionConfig) {
return Boolean(
@@ -57,17 +59,17 @@ export const tabs: Record<
label: ({ t }) => t('general:livePreview'),
order: 100,
},
- References: {
+ references: {
condition: () => false,
},
- Relationships: {
+ relationships: {
condition: () => false,
},
- Version: {
+ version: {
condition: () => false,
},
- Versions: {
- Pill: VersionsPill,
+ versions: {
+ Pill_Component: VersionsPill,
condition: ({ collectionConfig, globalConfig, permissions }) =>
Boolean(
(collectionConfig?.versions &&
diff --git a/packages/next/src/elements/DocumentHeader/index.tsx b/packages/next/src/elements/DocumentHeader/index.tsx
index f2d3fd4e0..e2504ebcc 100644
--- a/packages/next/src/elements/DocumentHeader/index.tsx
+++ b/packages/next/src/elements/DocumentHeader/index.tsx
@@ -1,8 +1,8 @@
import type { I18n } from '@payloadcms/translations'
import type {
+ Payload,
Permissions,
SanitizedCollectionConfig,
- SanitizedConfig,
SanitizedGlobalConfig,
} from 'payload'
@@ -16,14 +16,14 @@ const baseClass = `doc-header`
export const DocumentHeader: React.FC<{
collectionConfig?: SanitizedCollectionConfig
- config: SanitizedConfig
customHeader?: React.ReactNode
globalConfig?: SanitizedGlobalConfig
hideTabs?: boolean
i18n: I18n
+ payload: Payload
permissions: Permissions
}> = (props) => {
- const { collectionConfig, config, customHeader, globalConfig, hideTabs, i18n, permissions } =
+ const { collectionConfig, customHeader, globalConfig, hideTabs, i18n, payload, permissions } =
props
return (
@@ -35,9 +35,9 @@ export const DocumentHeader: React.FC<{
{!hideTabs && (
)}
diff --git a/packages/next/src/elements/EmailAndUsername/index.tsx b/packages/next/src/elements/EmailAndUsername/index.tsx
index 0d295c95d..30e8c8e50 100644
--- a/packages/next/src/elements/EmailAndUsername/index.tsx
+++ b/packages/next/src/elements/EmailAndUsername/index.tsx
@@ -7,7 +7,7 @@ import { email, username } from 'payload/shared'
import React from 'react'
type Props = {
- loginWithUsername?: LoginWithUsernameOptions | false
+ readonly loginWithUsername?: LoginWithUsernameOptions | false
}
function EmailFieldComponent(props: Props) {
const { loginWithUsername } = props
@@ -21,10 +21,11 @@ function EmailFieldComponent(props: Props) {
return (
)
@@ -43,10 +44,11 @@ function UsernameFieldComponent(props: Props) {
if (showUsernameField) {
return (
)
@@ -70,25 +72,34 @@ export function RenderEmailAndUsernameFields(props: RenderEmailAndUsernameFields
return (
,
- cellComponentProps: null,
- fieldComponentProps: { type: 'email', autoComplete: 'off', readOnly },
- fieldIsPresentational: false,
- isFieldAffectingData: true,
+ admin: {
+ autoComplete: 'off',
+ components: {
+ Field: {
+ type: 'client',
+ Component: null,
+ RenderedComponent: ,
+ },
+ },
+ },
localized: false,
},
{
name: 'username',
type: 'text',
- CustomField: ,
- cellComponentProps: null,
- fieldComponentProps: { type: 'text', readOnly },
- fieldIsPresentational: false,
- isFieldAffectingData: true,
+ admin: {
+ components: {
+ Field: {
+ type: 'client',
+ Component: null,
+ RenderedComponent: ,
+ },
+ },
+ },
localized: false,
},
]}
diff --git a/packages/next/src/elements/Logo/index.tsx b/packages/next/src/elements/Logo/index.tsx
index b14688c00..e91eac0a1 100644
--- a/packages/next/src/elements/Logo/index.tsx
+++ b/packages/next/src/elements/Logo/index.tsx
@@ -1,6 +1,6 @@
import type { ServerProps } from 'payload'
-import { PayloadLogo, RenderCustomComponent } from '@payloadcms/ui/shared'
+import { PayloadLogo, RenderComponent, getCreateMappedComponent } from '@payloadcms/ui/shared'
import React from 'react'
export const Logo: React.FC = (props) => {
@@ -16,19 +16,20 @@ export const Logo: React.FC = (props) => {
} = {},
} = payload.config
- return (
-
- )
+ const createMappedComponent = getCreateMappedComponent({
+ importMap: payload.importMap,
+ serverProps: {
+ i18n,
+ locale,
+ params,
+ payload,
+ permissions,
+ searchParams,
+ user,
+ },
+ })
+
+ const mappedCustomLogo = createMappedComponent(CustomLogo, undefined, PayloadLogo, 'CustomLogo')
+
+ return
}
diff --git a/packages/next/src/elements/Nav/index.client.tsx b/packages/next/src/elements/Nav/index.client.tsx
index 2ee4b320a..92ff7ff96 100644
--- a/packages/next/src/elements/Nav/index.client.tsx
+++ b/packages/next/src/elements/Nav/index.client.tsx
@@ -25,9 +25,11 @@ export const DefaultNavClient: React.FC = () => {
const pathname = usePathname()
const {
- collections,
- globals,
- routes: { admin: adminRoute },
+ config: {
+ collections,
+ globals,
+ routes: { admin: adminRoute },
+ },
} = useConfig()
const { i18n } = useTranslation()
diff --git a/packages/next/src/elements/Nav/index.tsx b/packages/next/src/elements/Nav/index.tsx
index 898c1699f..74cd81176 100644
--- a/packages/next/src/elements/Nav/index.tsx
+++ b/packages/next/src/elements/Nav/index.tsx
@@ -1,6 +1,7 @@
import type { ServerProps } from 'payload'
import { Logout } from '@payloadcms/ui'
+import { RenderComponent, getCreateMappedComponent } from '@payloadcms/ui/shared'
import React from 'react'
import { NavHamburger } from './NavHamburger/index.js'
@@ -9,8 +10,6 @@ import './index.scss'
const baseClass = 'nav'
-import { WithServerSideProps } from '@payloadcms/ui/shared'
-
import { DefaultNavClient } from './index.client.js'
export type NavProps = ServerProps
@@ -28,48 +27,38 @@ export const DefaultNav: React.FC = (props) => {
},
} = payload.config
- const BeforeNavLinks = Array.isArray(beforeNavLinks)
- ? beforeNavLinks.map((Component, i) => (
-
- ))
- : null
+ const createMappedComponent = getCreateMappedComponent({
+ importMap: payload.importMap,
+ serverProps: {
+ i18n,
+ locale,
+ params,
+ payload,
+ permissions,
+ searchParams,
+ user,
+ },
+ })
- const AfterNavLinks = Array.isArray(afterNavLinks)
- ? afterNavLinks.map((Component, i) => (
-
- ))
- : null
+ const mappedBeforeNavLinks = createMappedComponent(
+ beforeNavLinks,
+ undefined,
+ undefined,
+ 'beforeNavLinks',
+ )
+ const mappedAfterNavLinks = createMappedComponent(
+ afterNavLinks,
+ undefined,
+ undefined,
+ 'afterNavLinks',
+ )
return (